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