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