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