Home | History | Annotate | Download | only in telecom
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.car.dialer.telecom;
     17 
     18 import android.content.ContentResolver;
     19 import android.content.Context;
     20 import android.content.CursorLoader;
     21 import android.content.Loader;
     22 import android.database.Cursor;
     23 import android.net.Uri;
     24 import android.provider.BaseColumns;
     25 import android.provider.CallLog;
     26 import android.provider.ContactsContract;
     27 import android.text.TextUtils;
     28 import android.util.Log;
     29 
     30 import java.util.ArrayList;
     31 import java.util.HashMap;
     32 import java.util.List;
     33 
     34 /**
     35  * Manage loading different types of call logs.
     36  * Currently supports:
     37  *     All calls
     38  *     Missed calls
     39  *     speed dial calls
     40  */
     41 public class PhoneLoader {
     42     private static final String TAG = "Em.PhoneLoader";
     43 
     44     /** CALL_TYPE_ALL and _MISSED's values are assigned to be consistent with the Dialer **/
     45     public final static int CALL_TYPE_ALL = -1;
     46     public final static int CALL_TYPE_MISSED = CallLog.Calls.MISSED_TYPE;
     47     /** Starred and frequent **/
     48     public final static int CALL_TYPE_SPEED_DIAL = 2;
     49 
     50     private static final int NUM_LOGS_TO_DISPLAY = 100;
     51     private static final String[] EMPTY_STRING_ARRAY = new String[0];
     52 
     53     public static final int INCOMING_TYPE = 1;
     54     public static final int OUTGOING_TYPE = 2;
     55     public static final int MISSED_TYPE = 3;
     56     public static final int VOICEMAIL_TYPE = 4;
     57 
     58     private static HashMap<String, String> sNumberCache;
     59 
     60     /**
     61      * Hybrid Factory for creating a Contact Loader that also immediately starts its execution.
     62      * Note: NOT to be used wit LoaderManagers.
     63      */
     64     public static CursorLoader registerCallObserver(int type,
     65             Context context, Loader.OnLoadCompleteListener<Cursor> listener) {
     66         if (Log.isLoggable(TAG, Log.DEBUG)) {
     67             Log.d(TAG, "registerCallObserver: type: " + type + ", listener: " + listener);
     68         }
     69 
     70         switch(type) {
     71             case CALL_TYPE_ALL:
     72             case CALL_TYPE_MISSED:
     73                 return fetchCallLog(type, context, listener);
     74             case CALL_TYPE_SPEED_DIAL:
     75                 CursorLoader loader = newStrequentContactLoader(context);
     76                 loader.registerListener(0, listener);
     77                 loader.startLoading();
     78                 return loader;
     79             default:
     80                 throw new UnsupportedOperationException("Unknown CALL_TYPE " + type + ".");
     81         }
     82     }
     83 
     84     /**
     85      * Factory method for creating a Loader that will fetch strequent contacts from the phone.
     86      */
     87     public static CursorLoader newStrequentContactLoader(Context context) {
     88         Uri uri = ContactsContract.Contacts.CONTENT_STREQUENT_URI.buildUpon()
     89                 .appendQueryParameter(ContactsContract.STREQUENT_PHONE_ONLY, "true")
     90                 .appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true").build();
     91 
     92         return new CursorLoader(context, uri, null, null, null, null);
     93     }
     94 
     95     // TODO(mcrico): Separate into a factory method and move configuration to registerCallObserver
     96     private static CursorLoader fetchCallLog(int callType,
     97             Context context, Loader.OnLoadCompleteListener<Cursor> listener) {
     98         if (Log.isLoggable(TAG, Log.DEBUG)) {
     99             Log.d(TAG, "fetchCallLog");
    100         }
    101 
    102         // We need to check for NULL explicitly otherwise entries with where READ is NULL
    103         // may not match either the query or its negation.
    104         // We consider the calls that are not yet consumed (i.e. IS_READ = 0) as "new".
    105         StringBuilder where = new StringBuilder();
    106         List<String> selectionArgs = new ArrayList<String>();
    107 
    108         if (callType > CALL_TYPE_ALL) {
    109             // add a filter for call type
    110             where.append(String.format("(%s = ?)", CallLog.Calls.TYPE));
    111             selectionArgs.add(Integer.toString(callType));
    112         }
    113         String selection = where.length() > 0 ? where.toString() : null;
    114 
    115         if (Log.isLoggable(TAG, Log.DEBUG)) {
    116             Log.d(TAG, "accessingCallLog");
    117         }
    118 
    119         Uri uri = CallLog.Calls.CONTENT_URI.buildUpon()
    120                 .appendQueryParameter(CallLog.Calls.LIMIT_PARAM_KEY,
    121                         Integer.toString(NUM_LOGS_TO_DISPLAY))
    122                 .build();
    123         CursorLoader loader = new CursorLoader(context, uri, null, selection,
    124                 selectionArgs.toArray(EMPTY_STRING_ARRAY), CallLog.Calls.DEFAULT_SORT_ORDER);
    125         loader.registerListener(0, listener);
    126         loader.startLoading();
    127         return loader;
    128     }
    129 
    130     /**
    131      * @return The column index of the contact id. It should be {@link BaseColumns#_ID}. However,
    132      *         if that fails use {@link android.provider.ContactsContract.RawContacts#CONTACT_ID}.
    133      *         If that also fails, we use the first column in the table.
    134      */
    135     public static int getIdColumnIndex(Cursor cursor) {
    136         int ret = cursor.getColumnIndex(BaseColumns._ID);
    137         if (ret == -1) {
    138             if (Log.isLoggable(TAG, Log.INFO)) {
    139                 Log.i(TAG, "Falling back to contact_id instead of _id");
    140             }
    141 
    142             // Some versions of the ContactsProvider on LG don't have an _id column but instead
    143             // use contact_id. If the lookup for _id fails, we fallback to contact_id.
    144             ret = cursor.getColumnIndexOrThrow(ContactsContract.RawContacts.CONTACT_ID);
    145         }
    146         if (ret == -1) {
    147             Log.e(TAG, "Neither _id or contact_id exist! Falling back to column 0. " +
    148                     "There is no guarantee that this will work!");
    149             ret = 0;
    150         }
    151         return ret;
    152     }
    153 
    154     /**
    155      * @return The column index of the number.
    156      *         Will return a valid column for call log or contacts queries.
    157      */
    158     public static int getNumberColumnIndex(Cursor cursor) {
    159         int numberColumn = cursor.getColumnIndex(CallLog.Calls.NUMBER);
    160         if (numberColumn == -1) {
    161             numberColumn = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
    162         }
    163         return numberColumn;
    164     }
    165 
    166 
    167     /**
    168      * @return The column index of the number type.
    169      *         Will return a valid column for call log or contacts queries.
    170      */
    171     public static int getTypeColumnIndex(Cursor cursor) {
    172         int typeColumn = cursor.getColumnIndex(CallLog.Calls.TYPE);
    173         if (typeColumn == -1) {
    174             typeColumn = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.TYPE);
    175         }
    176         return typeColumn;
    177     }
    178 
    179     /**
    180      * @return The column index of the name.
    181      *         Will return a valid column for call log or contacts queries.
    182      */
    183     public static int getNameColumnIndex(Cursor cursor) {
    184         int typeColumn = cursor.getColumnIndex(CallLog.Calls.CACHED_NAME);
    185         if (typeColumn == -1) {
    186             typeColumn = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME);
    187         }
    188         return typeColumn;
    189     }
    190 
    191     /**
    192      * @return The phone number for the contact. Most phones will simply get the value in the
    193      *         column returned by {@link #getNumberColumnIndex(Cursor)}. However, some devices
    194      *         such as the Galaxy S6 return null for those columns. In those cases, we use the
    195      *         contact id (which we hopefully do have) to look up just the phone number for that
    196      *         specific contact.
    197      */
    198     public static String getPhoneNumber(Cursor cursor, ContentResolver cr) {
    199         int columnIndex = getNumberColumnIndex(cursor);
    200         String number = cursor.getString(columnIndex);
    201         if (number == null) {
    202             Log.w(TAG, "Phone number is null. Using fallback method.");
    203             int idColumnIndex = getIdColumnIndex(cursor);
    204             String idColumnName = cursor.getColumnName(idColumnIndex);
    205             String contactId = cursor.getString(idColumnIndex);
    206             getNumberFromContactId(cr, idColumnName, contactId);
    207         }
    208         return number;
    209     }
    210 
    211     /**
    212      * Return the phone number for the given contact id.
    213      * @param columnName On some phones, we have to use non-standard columns for the primary key.
    214      * @param id The value in the columnName for the desired contact.
    215      * @return The phone number for the given contact or empty string if there was an error.
    216      */
    217     public static String getNumberFromContactId(ContentResolver cr, String columnName, String id) {
    218         if (TextUtils.isEmpty(id)) {
    219             Log.e(TAG, "You must specify a valid id to get a contact's phone number.");
    220             return "";
    221         }
    222         if (sNumberCache == null) {
    223             sNumberCache = new HashMap<>();
    224         } else if (sNumberCache.containsKey(id)) {
    225             return sNumberCache.get(id);
    226         }
    227 
    228         Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
    229         Cursor phoneNumberCursor = cr.query(uri,
    230                 new String[] {ContactsContract.CommonDataKinds.Phone.NUMBER},
    231                 columnName + " = ?" , new String[] {id}, null);
    232 
    233         if (!phoneNumberCursor.moveToFirst()) {
    234             Log.e(TAG, "Unable to move phone number cursor to the first item.");
    235             return "";
    236         }
    237         String number = phoneNumberCursor.getString(0);
    238         phoneNumberCursor.close();
    239         return number;
    240     }
    241 }
    242