1 /* 2 * Copyright (C) 2013 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 17 package com.android.dialer.calllog; 18 19 import com.google.common.base.Strings; 20 21 import android.Manifest; 22 import android.content.ContentResolver; 23 import android.content.ContentUris; 24 import android.content.Context; 25 import android.database.Cursor; 26 import android.net.Uri; 27 import android.provider.CallLog.Calls; 28 import android.provider.ContactsContract.PhoneLookup; 29 import android.support.annotation.NonNull; 30 import android.support.annotation.Nullable; 31 import android.telephony.PhoneNumberUtils; 32 import android.text.TextUtils; 33 import android.util.Log; 34 35 import com.android.contacts.common.GeoUtil; 36 import com.android.contacts.common.util.PermissionsUtil; 37 import com.android.dialer.R; 38 import com.android.dialer.util.TelecomUtil; 39 40 import java.util.ArrayList; 41 import java.util.List; 42 43 /** 44 * Helper class operating on call log notifications. 45 */ 46 public class CallLogNotificationsHelper { 47 private static final String TAG = "CallLogNotifHelper"; 48 private static CallLogNotificationsHelper sInstance; 49 50 /** Returns the singleton instance of the {@link CallLogNotificationsHelper}. */ getInstance(Context context)51 public static CallLogNotificationsHelper getInstance(Context context) { 52 if (sInstance == null) { 53 ContentResolver contentResolver = context.getContentResolver(); 54 String countryIso = GeoUtil.getCurrentCountryIso(context); 55 sInstance = new CallLogNotificationsHelper(context, 56 createNewCallsQuery(context, contentResolver), 57 createNameLookupQuery(context, contentResolver), 58 new ContactInfoHelper(context, countryIso), 59 countryIso); 60 } 61 return sInstance; 62 } 63 64 private final Context mContext; 65 private final NewCallsQuery mNewCallsQuery; 66 private final NameLookupQuery mNameLookupQuery; 67 private final ContactInfoHelper mContactInfoHelper; 68 private final String mCurrentCountryIso; 69 CallLogNotificationsHelper(Context context, NewCallsQuery newCallsQuery, NameLookupQuery nameLookupQuery, ContactInfoHelper contactInfoHelper, String countryIso)70 CallLogNotificationsHelper(Context context, NewCallsQuery newCallsQuery, 71 NameLookupQuery nameLookupQuery, ContactInfoHelper contactInfoHelper, 72 String countryIso) { 73 mContext = context; 74 mNewCallsQuery = newCallsQuery; 75 mNameLookupQuery = nameLookupQuery; 76 mContactInfoHelper = contactInfoHelper; 77 mCurrentCountryIso = countryIso; 78 } 79 80 /** 81 * Get all voicemails with the "new" flag set to 1. 82 * 83 * @return A list of NewCall objects where each object represents a new voicemail. 84 */ 85 @Nullable getNewVoicemails()86 public List<NewCall> getNewVoicemails() { 87 return mNewCallsQuery.query(Calls.VOICEMAIL_TYPE); 88 } 89 90 /** 91 * Get all missed calls with the "new" flag set to 1. 92 * 93 * @return A list of NewCall objects where each object represents a new missed call. 94 */ 95 @Nullable getNewMissedCalls()96 public List<NewCall> getNewMissedCalls() { 97 return mNewCallsQuery.query(Calls.MISSED_TYPE); 98 } 99 100 /** 101 * Given a number and number information (presentation and country ISO), get the best name 102 * for display. If the name is empty but we have a special presentation, display that. 103 * Otherwise attempt to look it up in the database or the cache. 104 * If that fails, fall back to displaying the number. 105 */ getName(@ullable String number, int numberPresentation, @Nullable String countryIso)106 public String getName(@Nullable String number, int numberPresentation, 107 @Nullable String countryIso) { 108 return getContactInfo(number, numberPresentation, countryIso).name; 109 } 110 111 /** 112 * Given a number and number information (presentation and country ISO), get 113 * {@link ContactInfo}. If the name is empty but we have a special presentation, display that. 114 * Otherwise attempt to look it up in the cache. 115 * If that fails, fall back to displaying the number. 116 */ getContactInfo(@ullable String number, int numberPresentation, @Nullable String countryIso)117 public ContactInfo getContactInfo(@Nullable String number, int numberPresentation, 118 @Nullable String countryIso) { 119 if (countryIso == null) { 120 countryIso = mCurrentCountryIso; 121 } 122 123 number = Strings.nullToEmpty(number); 124 ContactInfo contactInfo = new ContactInfo(); 125 contactInfo.number = number; 126 contactInfo.formattedNumber = PhoneNumberUtils.formatNumber(number, countryIso); 127 // contactInfo.normalizedNumber is not PhoneNumberUtils.normalizeNumber. Read ContactInfo. 128 contactInfo.normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso); 129 130 // 1. Special number representation. 131 contactInfo.name = PhoneNumberDisplayUtil.getDisplayName( 132 mContext, 133 number, 134 numberPresentation, 135 false).toString(); 136 if (!TextUtils.isEmpty(contactInfo.name)) { 137 return contactInfo; 138 } 139 140 // 2. Look it up in the cache. 141 ContactInfo cachedContactInfo = mContactInfoHelper.lookupNumber(number, countryIso); 142 143 if (cachedContactInfo != null && !TextUtils.isEmpty(cachedContactInfo.name)) { 144 return cachedContactInfo; 145 } 146 147 if (!TextUtils.isEmpty(contactInfo.formattedNumber)) { 148 // 3. If we cannot lookup the contact, use the formatted number instead. 149 contactInfo.name = contactInfo.formattedNumber; 150 } else if (!TextUtils.isEmpty(number)) { 151 // 4. If number can't be formatted, use number. 152 contactInfo.name = number; 153 } else { 154 // 5. Otherwise, it's unknown number. 155 contactInfo.name = mContext.getResources().getString(R.string.unknown); 156 } 157 return contactInfo; 158 } 159 160 /** Removes the missed call notifications. */ removeMissedCallNotifications(Context context)161 public static void removeMissedCallNotifications(Context context) { 162 TelecomUtil.cancelMissedCallsNotification(context); 163 } 164 165 /** Update the voice mail notifications. */ updateVoicemailNotifications(Context context)166 public static void updateVoicemailNotifications(Context context) { 167 CallLogNotificationsService.updateVoicemailNotifications(context, null); 168 } 169 170 /** Information about a new voicemail. */ 171 public static final class NewCall { 172 public final Uri callsUri; 173 public final Uri voicemailUri; 174 public final String number; 175 public final int numberPresentation; 176 public final String accountComponentName; 177 public final String accountId; 178 public final String transcription; 179 public final String countryIso; 180 public final long dateMs; 181 NewCall( Uri callsUri, Uri voicemailUri, String number, int numberPresentation, String accountComponentName, String accountId, String transcription, String countryIso, long dateMs)182 public NewCall( 183 Uri callsUri, 184 Uri voicemailUri, 185 String number, 186 int numberPresentation, 187 String accountComponentName, 188 String accountId, 189 String transcription, 190 String countryIso, 191 long dateMs) { 192 this.callsUri = callsUri; 193 this.voicemailUri = voicemailUri; 194 this.number = number; 195 this.numberPresentation = numberPresentation; 196 this.accountComponentName = accountComponentName; 197 this.accountId = accountId; 198 this.transcription = transcription; 199 this.countryIso = countryIso; 200 this.dateMs = dateMs; 201 } 202 } 203 204 /** Allows determining the new calls for which a notification should be generated. */ 205 public interface NewCallsQuery { 206 /** 207 * Returns the new calls of a certain type for which a notification should be generated. 208 */ 209 @Nullable query(int type)210 public List<NewCall> query(int type); 211 } 212 213 /** Create a new instance of {@link NewCallsQuery}. */ createNewCallsQuery(Context context, ContentResolver contentResolver)214 public static NewCallsQuery createNewCallsQuery(Context context, 215 ContentResolver contentResolver) { 216 217 return new DefaultNewCallsQuery(context.getApplicationContext(), contentResolver); 218 } 219 220 /** 221 * Default implementation of {@link NewCallsQuery} that looks up the list of new calls to 222 * notify about in the call log. 223 */ 224 private static final class DefaultNewCallsQuery implements NewCallsQuery { 225 private static final String[] PROJECTION = { 226 Calls._ID, 227 Calls.NUMBER, 228 Calls.VOICEMAIL_URI, 229 Calls.NUMBER_PRESENTATION, 230 Calls.PHONE_ACCOUNT_COMPONENT_NAME, 231 Calls.PHONE_ACCOUNT_ID, 232 Calls.TRANSCRIPTION, 233 Calls.COUNTRY_ISO, 234 Calls.DATE 235 }; 236 private static final int ID_COLUMN_INDEX = 0; 237 private static final int NUMBER_COLUMN_INDEX = 1; 238 private static final int VOICEMAIL_URI_COLUMN_INDEX = 2; 239 private static final int NUMBER_PRESENTATION_COLUMN_INDEX = 3; 240 private static final int PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX = 4; 241 private static final int PHONE_ACCOUNT_ID_COLUMN_INDEX = 5; 242 private static final int TRANSCRIPTION_COLUMN_INDEX = 6; 243 private static final int COUNTRY_ISO_COLUMN_INDEX = 7; 244 private static final int DATE_COLUMN_INDEX = 8; 245 246 private final ContentResolver mContentResolver; 247 private final Context mContext; 248 DefaultNewCallsQuery(Context context, ContentResolver contentResolver)249 private DefaultNewCallsQuery(Context context, ContentResolver contentResolver) { 250 mContext = context; 251 mContentResolver = contentResolver; 252 } 253 254 @Override 255 @Nullable query(int type)256 public List<NewCall> query(int type) { 257 if (!PermissionsUtil.hasPermission(mContext, Manifest.permission.READ_CALL_LOG)) { 258 Log.w(TAG, "No READ_CALL_LOG permission, returning null for calls lookup."); 259 return null; 260 } 261 final String selection = String.format("%s = 1 AND %s = ?", Calls.NEW, Calls.TYPE); 262 final String[] selectionArgs = new String[]{ Integer.toString(type) }; 263 try (Cursor cursor = mContentResolver.query(Calls.CONTENT_URI_WITH_VOICEMAIL, 264 PROJECTION, selection, selectionArgs, Calls.DEFAULT_SORT_ORDER)) { 265 if (cursor == null) { 266 return null; 267 } 268 List<NewCall> newCalls = new ArrayList<>(); 269 while (cursor.moveToNext()) { 270 newCalls.add(createNewCallsFromCursor(cursor)); 271 } 272 return newCalls; 273 } catch (RuntimeException e) { 274 Log.w(TAG, "Exception when querying Contacts Provider for calls lookup"); 275 return null; 276 } 277 } 278 279 /** Returns an instance of {@link NewCall} created by using the values of the cursor. */ createNewCallsFromCursor(Cursor cursor)280 private NewCall createNewCallsFromCursor(Cursor cursor) { 281 String voicemailUriString = cursor.getString(VOICEMAIL_URI_COLUMN_INDEX); 282 Uri callsUri = ContentUris.withAppendedId( 283 Calls.CONTENT_URI_WITH_VOICEMAIL, cursor.getLong(ID_COLUMN_INDEX)); 284 Uri voicemailUri = voicemailUriString == null ? null : Uri.parse(voicemailUriString); 285 return new NewCall( 286 callsUri, 287 voicemailUri, 288 cursor.getString(NUMBER_COLUMN_INDEX), 289 cursor.getInt(NUMBER_PRESENTATION_COLUMN_INDEX), 290 cursor.getString(PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX), 291 cursor.getString(PHONE_ACCOUNT_ID_COLUMN_INDEX), 292 cursor.getString(TRANSCRIPTION_COLUMN_INDEX), 293 cursor.getString(COUNTRY_ISO_COLUMN_INDEX), 294 cursor.getLong(DATE_COLUMN_INDEX)); 295 } 296 } 297 298 /** Allows determining the name associated with a given phone number. */ 299 public interface NameLookupQuery { 300 /** 301 * Returns the name associated with the given number in the contacts database, or null if 302 * the number does not correspond to any of the contacts. 303 * <p> 304 * If there are multiple contacts with the same phone number, it will return the name of one 305 * of the matching contacts. 306 */ 307 @Nullable query(@ullable String number)308 public String query(@Nullable String number); 309 } 310 311 /** Create a new instance of {@link NameLookupQuery}. */ createNameLookupQuery(Context context, ContentResolver contentResolver)312 public static NameLookupQuery createNameLookupQuery(Context context, 313 ContentResolver contentResolver) { 314 return new DefaultNameLookupQuery(context.getApplicationContext(), contentResolver); 315 } 316 317 /** 318 * Default implementation of {@link NameLookupQuery} that looks up the name of a contact in the 319 * contacts database. 320 */ 321 private static final class DefaultNameLookupQuery implements NameLookupQuery { 322 private static final String[] PROJECTION = { PhoneLookup.DISPLAY_NAME }; 323 private static final int DISPLAY_NAME_COLUMN_INDEX = 0; 324 325 private final ContentResolver mContentResolver; 326 private final Context mContext; 327 DefaultNameLookupQuery(Context context, ContentResolver contentResolver)328 private DefaultNameLookupQuery(Context context, ContentResolver contentResolver) { 329 mContext = context; 330 mContentResolver = contentResolver; 331 } 332 333 @Override 334 @Nullable query(@ullable String number)335 public String query(@Nullable String number) { 336 if (!PermissionsUtil.hasPermission(mContext, Manifest.permission.READ_CONTACTS)) { 337 Log.w(TAG, "No READ_CONTACTS permission, returning null for name lookup."); 338 return null; 339 } 340 try (Cursor cursor = mContentResolver.query( 341 Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)), 342 PROJECTION, null, null, null)) { 343 if (cursor == null || !cursor.moveToFirst()) { 344 return null; 345 } 346 return cursor.getString(DISPLAY_NAME_COLUMN_INDEX); 347 } catch (RuntimeException e) { 348 Log.w(TAG, "Exception when querying Contacts Provider for name lookup"); 349 return null; 350 } 351 } 352 } 353 } 354