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