1 /* 2 * Copyright (C) 2016 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.dialer.app.calllog; 17 18 import android.app.Notification; 19 import android.app.Notification.Builder; 20 import android.app.PendingIntent; 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.graphics.Bitmap; 25 import android.graphics.drawable.Icon; 26 import android.net.Uri; 27 import android.provider.CallLog.Calls; 28 import android.service.notification.StatusBarNotification; 29 import android.support.annotation.NonNull; 30 import android.support.annotation.Nullable; 31 import android.support.annotation.VisibleForTesting; 32 import android.support.annotation.WorkerThread; 33 import android.support.v4.os.BuildCompat; 34 import android.support.v4.os.UserManagerCompat; 35 import android.support.v4.util.Pair; 36 import android.telecom.PhoneAccount; 37 import android.telecom.PhoneAccountHandle; 38 import android.telecom.TelecomManager; 39 import android.telephony.PhoneNumberUtils; 40 import android.text.BidiFormatter; 41 import android.text.TextDirectionHeuristics; 42 import android.text.TextUtils; 43 import android.util.ArraySet; 44 import com.android.contacts.common.ContactsUtils; 45 import com.android.dialer.app.MainComponent; 46 import com.android.dialer.app.R; 47 import com.android.dialer.app.calllog.CallLogNotificationsQueryHelper.NewCall; 48 import com.android.dialer.app.contactinfo.ContactPhotoLoader; 49 import com.android.dialer.callintent.CallInitiationType; 50 import com.android.dialer.callintent.CallIntentBuilder; 51 import com.android.dialer.common.Assert; 52 import com.android.dialer.common.LogUtil; 53 import com.android.dialer.common.concurrent.DialerExecutor.Worker; 54 import com.android.dialer.compat.android.provider.VoicemailCompat; 55 import com.android.dialer.duo.DuoComponent; 56 import com.android.dialer.enrichedcall.FuzzyPhoneNumberMatcher; 57 import com.android.dialer.notification.DialerNotificationManager; 58 import com.android.dialer.notification.NotificationChannelId; 59 import com.android.dialer.notification.missedcalls.MissedCallConstants; 60 import com.android.dialer.notification.missedcalls.MissedCallNotificationCanceller; 61 import com.android.dialer.notification.missedcalls.MissedCallNotificationTags; 62 import com.android.dialer.phonenumbercache.ContactInfo; 63 import com.android.dialer.phonenumberutil.PhoneNumberHelper; 64 import com.android.dialer.precall.PreCall; 65 import com.android.dialer.theme.base.ThemeComponent; 66 import com.android.dialer.util.DialerUtils; 67 import com.android.dialer.util.IntentUtil; 68 import java.util.Iterator; 69 import java.util.List; 70 import java.util.Set; 71 72 /** Creates a notification for calls that the user missed (neither answered nor rejected). */ 73 public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> { 74 75 private final Context context; 76 private final CallLogNotificationsQueryHelper callLogNotificationsQueryHelper; 77 78 @VisibleForTesting MissedCallNotifier( Context context, CallLogNotificationsQueryHelper callLogNotificationsQueryHelper)79 MissedCallNotifier( 80 Context context, CallLogNotificationsQueryHelper callLogNotificationsQueryHelper) { 81 this.context = context; 82 this.callLogNotificationsQueryHelper = callLogNotificationsQueryHelper; 83 } 84 getInstance(Context context)85 public static MissedCallNotifier getInstance(Context context) { 86 return new MissedCallNotifier(context, CallLogNotificationsQueryHelper.getInstance(context)); 87 } 88 89 @Nullable 90 @Override doInBackground(@ullable Pair<Integer, String> input)91 public Void doInBackground(@Nullable Pair<Integer, String> input) throws Throwable { 92 updateMissedCallNotification(input.first, input.second); 93 return null; 94 } 95 96 /** 97 * Update missed call notifications from the call log. Accepts default information in case call 98 * log cannot be accessed. 99 * 100 * @param count the number of missed calls to display if call log cannot be accessed. May be 101 * {@link CallLogNotificationsService#UNKNOWN_MISSED_CALL_COUNT} if unknown. 102 * @param number the phone number of the most recent call to display if the call log cannot be 103 * accessed. May be null if unknown. 104 */ 105 @VisibleForTesting 106 @WorkerThread updateMissedCallNotification(int count, @Nullable String number)107 void updateMissedCallNotification(int count, @Nullable String number) { 108 LogUtil.enterBlock("MissedCallNotifier.updateMissedCallNotification"); 109 110 final int titleResId; 111 CharSequence expandedText; // The text in the notification's line 1 and 2. 112 113 List<NewCall> newCalls = callLogNotificationsQueryHelper.getNewMissedCalls(); 114 115 removeSelfManagedCalls(newCalls); 116 117 if ((newCalls != null && newCalls.isEmpty()) || count == 0) { 118 // No calls to notify about: clear the notification. 119 CallLogNotificationsQueryHelper.markAllMissedCallsInCallLogAsRead(context); 120 MissedCallNotificationCanceller.cancelAll(context); 121 return; 122 } 123 124 if (newCalls != null) { 125 if (count != CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT 126 && count != newCalls.size()) { 127 LogUtil.w( 128 "MissedCallNotifier.updateMissedCallNotification", 129 "Call count does not match call log count." 130 + " count: " 131 + count 132 + " newCalls.size(): " 133 + newCalls.size()); 134 } 135 count = newCalls.size(); 136 } 137 138 if (count == CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT) { 139 // If the intent did not contain a count, and we are unable to get a count from the 140 // call log, then no notification can be shown. 141 LogUtil.i("MissedCallNotifier.updateMissedCallNotification", "unknown missed call count"); 142 return; 143 } 144 145 Notification.Builder groupSummary = createNotificationBuilder(); 146 boolean useCallList = newCalls != null; 147 148 if (count == 1) { 149 LogUtil.i( 150 "MissedCallNotifier.updateMissedCallNotification", 151 "1 missed call, looking up contact info"); 152 NewCall call = 153 useCallList 154 ? newCalls.get(0) 155 : new NewCall( 156 null, 157 null, 158 number, 159 Calls.PRESENTATION_ALLOWED, 160 null, 161 null, 162 null, 163 null, 164 System.currentTimeMillis(), 165 VoicemailCompat.TRANSCRIPTION_NOT_STARTED); 166 167 // TODO: look up caller ID that is not in contacts. 168 ContactInfo contactInfo = 169 callLogNotificationsQueryHelper.getContactInfo( 170 call.number, call.numberPresentation, call.countryIso); 171 titleResId = 172 contactInfo.userType == ContactsUtils.USER_TYPE_WORK 173 ? R.string.notification_missedWorkCallTitle 174 : R.string.notification_missedCallTitle; 175 176 if (TextUtils.equals(contactInfo.name, contactInfo.formattedNumber) 177 || TextUtils.equals(contactInfo.name, contactInfo.number)) { 178 expandedText = 179 PhoneNumberUtils.createTtsSpannable( 180 BidiFormatter.getInstance() 181 .unicodeWrap(contactInfo.name, TextDirectionHeuristics.LTR)); 182 } else { 183 expandedText = contactInfo.name; 184 } 185 186 ContactPhotoLoader loader = new ContactPhotoLoader(context, contactInfo); 187 Bitmap photoIcon = loader.loadPhotoIcon(); 188 if (photoIcon != null) { 189 groupSummary.setLargeIcon(photoIcon); 190 } 191 } else { 192 titleResId = R.string.notification_missedCallsTitle; 193 expandedText = context.getString(R.string.notification_missedCallsMsg, count); 194 } 195 196 LogUtil.i("MissedCallNotifier.updateMissedCallNotification", "preparing notification"); 197 198 // Create a public viewable version of the notification, suitable for display when sensitive 199 // notification content is hidden. 200 Notification.Builder publicSummaryBuilder = createNotificationBuilder(); 201 publicSummaryBuilder 202 .setContentTitle(context.getText(titleResId)) 203 .setContentIntent(createCallLogPendingIntent()) 204 .setDeleteIntent( 205 CallLogNotificationsService.createCancelAllMissedCallsPendingIntent(context)); 206 207 // Create the notification summary suitable for display when sensitive information is showing. 208 groupSummary 209 .setContentTitle(context.getText(titleResId)) 210 .setContentText(expandedText) 211 .setContentIntent(createCallLogPendingIntent()) 212 .setDeleteIntent( 213 CallLogNotificationsService.createCancelAllMissedCallsPendingIntent(context)) 214 .setGroupSummary(useCallList) 215 .setOnlyAlertOnce(useCallList) 216 .setPublicVersion(publicSummaryBuilder.build()); 217 if (BuildCompat.isAtLeastO()) { 218 groupSummary.setChannelId(NotificationChannelId.MISSED_CALL); 219 } 220 221 Notification notification = groupSummary.build(); 222 configureLedOnNotification(notification); 223 224 LogUtil.i("MissedCallNotifier.updateMissedCallNotification", "adding missed call notification"); 225 DialerNotificationManager.notify( 226 context, 227 MissedCallConstants.GROUP_SUMMARY_NOTIFICATION_TAG, 228 MissedCallConstants.NOTIFICATION_ID, 229 notification); 230 231 if (useCallList) { 232 // Do not repost active notifications to prevent erasing post call notes. 233 Set<String> activeAndThrottledTags = new ArraySet<>(); 234 for (StatusBarNotification activeNotification : 235 DialerNotificationManager.getActiveNotifications(context)) { 236 activeAndThrottledTags.add(activeNotification.getTag()); 237 } 238 // Do not repost throttled notifications 239 for (StatusBarNotification throttledNotification : 240 DialerNotificationManager.getThrottledNotificationSet()) { 241 activeAndThrottledTags.add(throttledNotification.getTag()); 242 } 243 244 for (NewCall call : newCalls) { 245 String callTag = getNotificationTagForCall(call); 246 if (!activeAndThrottledTags.contains(callTag)) { 247 DialerNotificationManager.notify( 248 context, 249 callTag, 250 MissedCallConstants.NOTIFICATION_ID, 251 getNotificationForCall(call, null)); 252 } 253 } 254 } 255 } 256 257 /** 258 * Remove self-managed calls from {@code newCalls}. If a {@link PhoneAccount} declared it is 259 * {@link PhoneAccount#CAPABILITY_SELF_MANAGED}, it should handle the in call UI and notifications 260 * itself, but might still write to call log with {@link 261 * PhoneAccount#EXTRA_LOG_SELF_MANAGED_CALLS}. 262 */ removeSelfManagedCalls(@ullable List<NewCall> newCalls)263 private void removeSelfManagedCalls(@Nullable List<NewCall> newCalls) { 264 if (newCalls == null) { 265 return; 266 } 267 268 TelecomManager telecomManager = context.getSystemService(TelecomManager.class); 269 Iterator<NewCall> iterator = newCalls.iterator(); 270 while (iterator.hasNext()) { 271 NewCall call = iterator.next(); 272 if (call.accountComponentName == null || call.accountId == null) { 273 continue; 274 } 275 ComponentName componentName = ComponentName.unflattenFromString(call.accountComponentName); 276 if (componentName == null) { 277 continue; 278 } 279 PhoneAccountHandle phoneAccountHandle = new PhoneAccountHandle(componentName, call.accountId); 280 PhoneAccount phoneAccount = telecomManager.getPhoneAccount(phoneAccountHandle); 281 if (phoneAccount == null) { 282 continue; 283 } 284 if (DuoComponent.get(context).getDuo().isDuoAccount(phoneAccountHandle)) { 285 iterator.remove(); 286 continue; 287 } 288 if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)) { 289 LogUtil.i( 290 "MissedCallNotifier.removeSelfManagedCalls", 291 "ignoring self-managed call " + call.callsUri); 292 iterator.remove(); 293 } 294 } 295 } 296 getNotificationTagForCall(@onNull NewCall call)297 private static String getNotificationTagForCall(@NonNull NewCall call) { 298 return MissedCallNotificationTags.getNotificationTagForCallUri(call.callsUri); 299 } 300 301 @WorkerThread insertPostCallNotification(@onNull String number, @NonNull String note)302 public void insertPostCallNotification(@NonNull String number, @NonNull String note) { 303 Assert.isWorkerThread(); 304 LogUtil.enterBlock("MissedCallNotifier.insertPostCallNotification"); 305 List<NewCall> newCalls = callLogNotificationsQueryHelper.getNewMissedCalls(); 306 if (newCalls != null && !newCalls.isEmpty()) { 307 for (NewCall call : newCalls) { 308 if (FuzzyPhoneNumberMatcher.matches(call.number, number.replace("tel:", ""))) { 309 LogUtil.i("MissedCallNotifier.insertPostCallNotification", "Notification updated"); 310 // Update the first notification that matches our post call note sender. 311 DialerNotificationManager.notify( 312 context, 313 getNotificationTagForCall(call), 314 MissedCallConstants.NOTIFICATION_ID, 315 getNotificationForCall(call, note)); 316 return; 317 } 318 } 319 } 320 LogUtil.i("MissedCallNotifier.insertPostCallNotification", "notification not found"); 321 } 322 getNotificationForCall( @onNull NewCall call, @Nullable String postCallMessage)323 private Notification getNotificationForCall( 324 @NonNull NewCall call, @Nullable String postCallMessage) { 325 ContactInfo contactInfo = 326 callLogNotificationsQueryHelper.getContactInfo( 327 call.number, call.numberPresentation, call.countryIso); 328 329 // Create a public viewable version of the notification, suitable for display when sensitive 330 // notification content is hidden. 331 int titleResId = 332 contactInfo.userType == ContactsUtils.USER_TYPE_WORK 333 ? R.string.notification_missedWorkCallTitle 334 : R.string.notification_missedCallTitle; 335 Notification.Builder publicBuilder = 336 createNotificationBuilder(call).setContentTitle(context.getText(titleResId)); 337 338 Notification.Builder builder = createNotificationBuilder(call); 339 CharSequence expandedText; 340 if (TextUtils.equals(contactInfo.name, contactInfo.formattedNumber) 341 || TextUtils.equals(contactInfo.name, contactInfo.number)) { 342 expandedText = 343 PhoneNumberUtils.createTtsSpannable( 344 BidiFormatter.getInstance() 345 .unicodeWrap(contactInfo.name, TextDirectionHeuristics.LTR)); 346 } else { 347 expandedText = contactInfo.name; 348 } 349 350 if (postCallMessage != null) { 351 expandedText = 352 context.getString(R.string.post_call_notification_message, expandedText, postCallMessage); 353 } 354 355 ContactPhotoLoader loader = new ContactPhotoLoader(context, contactInfo); 356 Bitmap photoIcon = loader.loadPhotoIcon(); 357 if (photoIcon != null) { 358 builder.setLargeIcon(photoIcon); 359 } 360 // Create the notification suitable for display when sensitive information is showing. 361 builder 362 .setContentTitle(context.getText(titleResId)) 363 .setContentText(expandedText) 364 // Include a public version of the notification to be shown when the missed call 365 // notification is shown on the user's lock screen and they have chosen to hide 366 // sensitive notification information. 367 .setPublicVersion(publicBuilder.build()); 368 369 // Add additional actions when the user isn't locked 370 if (UserManagerCompat.isUserUnlocked(context)) { 371 if (!TextUtils.isEmpty(call.number) 372 && !TextUtils.equals(call.number, context.getString(R.string.handle_restricted))) { 373 builder.addAction( 374 new Notification.Action.Builder( 375 Icon.createWithResource(context, R.drawable.ic_phone_24dp), 376 context.getString(R.string.notification_missedCall_call_back), 377 createCallBackPendingIntent(call.number, call.callsUri)) 378 .build()); 379 380 if (!PhoneNumberHelper.isUriNumber(call.number)) { 381 builder.addAction( 382 new Notification.Action.Builder( 383 Icon.createWithResource(context, R.drawable.quantum_ic_message_white_24), 384 context.getString(R.string.notification_missedCall_message), 385 createSendSmsFromNotificationPendingIntent(call.number, call.callsUri)) 386 .build()); 387 } 388 } 389 } 390 391 Notification notification = builder.build(); 392 configureLedOnNotification(notification); 393 return notification; 394 } 395 createNotificationBuilder()396 private Notification.Builder createNotificationBuilder() { 397 return new Notification.Builder(context) 398 .setGroup(MissedCallConstants.GROUP_KEY) 399 .setSmallIcon(android.R.drawable.stat_notify_missed_call) 400 .setColor(ThemeComponent.get(context).theme().getColorPrimary()) 401 .setAutoCancel(true) 402 .setOnlyAlertOnce(true) 403 .setShowWhen(true) 404 .setDefaults(Notification.DEFAULT_VIBRATE); 405 } 406 createNotificationBuilder(@onNull NewCall call)407 private Notification.Builder createNotificationBuilder(@NonNull NewCall call) { 408 Builder builder = 409 createNotificationBuilder() 410 .setWhen(call.dateMs) 411 .setDeleteIntent( 412 CallLogNotificationsService.createCancelSingleMissedCallPendingIntent( 413 context, call.callsUri)) 414 .setContentIntent(createCallLogPendingIntent(call.callsUri)); 415 if (BuildCompat.isAtLeastO()) { 416 builder.setChannelId(NotificationChannelId.MISSED_CALL); 417 } 418 419 return builder; 420 } 421 422 /** Trigger an intent to make a call from a missed call number. */ 423 @WorkerThread callBackFromMissedCall(String number, Uri callUri)424 public void callBackFromMissedCall(String number, Uri callUri) { 425 closeSystemDialogs(context); 426 CallLogNotificationsQueryHelper.markSingleMissedCallInCallLogAsRead(context, callUri); 427 MissedCallNotificationCanceller.cancelSingle(context, callUri); 428 DialerUtils.startActivityWithErrorToast( 429 context, 430 PreCall.getIntent( 431 context, 432 new CallIntentBuilder(number, CallInitiationType.Type.MISSED_CALL_NOTIFICATION)) 433 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); 434 } 435 436 /** Trigger an intent to send an sms from a missed call number. */ sendSmsFromMissedCall(String number, Uri callUri)437 public void sendSmsFromMissedCall(String number, Uri callUri) { 438 closeSystemDialogs(context); 439 CallLogNotificationsQueryHelper.markSingleMissedCallInCallLogAsRead(context, callUri); 440 MissedCallNotificationCanceller.cancelSingle(context, callUri); 441 DialerUtils.startActivityWithErrorToast( 442 context, IntentUtil.getSendSmsIntent(number).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); 443 } 444 445 /** 446 * Creates a new pending intent that sends the user to the call log. 447 * 448 * @return The pending intent. 449 */ createCallLogPendingIntent()450 private PendingIntent createCallLogPendingIntent() { 451 return createCallLogPendingIntent(null); 452 } 453 454 /** 455 * Creates a new pending intent that sends the user to the call log. 456 * 457 * @return The pending intent. 458 * @param callUri Uri of the call to jump to. May be null 459 */ createCallLogPendingIntent(@ullable Uri callUri)460 private PendingIntent createCallLogPendingIntent(@Nullable Uri callUri) { 461 Intent contentIntent = MainComponent.getShowCallLogIntent(context); 462 463 // TODO (a bug): scroll to call 464 contentIntent.setData(callUri); 465 return PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT); 466 } 467 createCallBackPendingIntent(String number, @NonNull Uri callUri)468 private PendingIntent createCallBackPendingIntent(String number, @NonNull Uri callUri) { 469 Intent intent = new Intent(context, CallLogNotificationsService.class); 470 intent.setAction(CallLogNotificationsService.ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION); 471 intent.putExtra(MissedCallNotificationReceiver.EXTRA_NOTIFICATION_PHONE_NUMBER, number); 472 intent.setData(callUri); 473 // Use FLAG_UPDATE_CURRENT to make sure any previous pending intent is updated with the new 474 // extra. 475 return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); 476 } 477 createSendSmsFromNotificationPendingIntent( String number, @NonNull Uri callUri)478 private PendingIntent createSendSmsFromNotificationPendingIntent( 479 String number, @NonNull Uri callUri) { 480 Intent intent = new Intent(context, CallLogNotificationsActivity.class); 481 intent.setAction(CallLogNotificationsActivity.ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION); 482 intent.putExtra(CallLogNotificationsActivity.EXTRA_MISSED_CALL_NUMBER, number); 483 intent.setData(callUri); 484 // Use FLAG_UPDATE_CURRENT to make sure any previous pending intent is updated with the new 485 // extra. 486 return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); 487 } 488 489 /** Configures a notification to emit the blinky notification light. */ configureLedOnNotification(Notification notification)490 private void configureLedOnNotification(Notification notification) { 491 notification.flags |= Notification.FLAG_SHOW_LIGHTS; 492 notification.defaults |= Notification.DEFAULT_LIGHTS; 493 } 494 495 /** Closes open system dialogs and the notification shade. */ closeSystemDialogs(Context context)496 private void closeSystemDialogs(Context context) { 497 context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); 498 } 499 } 500