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