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.NotificationManager; 21 import android.app.PendingIntent; 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.UserManagerCompat; 34 import android.support.v4.util.Pair; 35 import android.text.BidiFormatter; 36 import android.text.TextDirectionHeuristics; 37 import android.text.TextUtils; 38 import com.android.contacts.common.ContactsUtils; 39 import com.android.contacts.common.compat.PhoneNumberUtilsCompat; 40 import com.android.dialer.app.DialtactsActivity; 41 import com.android.dialer.app.R; 42 import com.android.dialer.app.calllog.CallLogNotificationsQueryHelper.NewCall; 43 import com.android.dialer.app.contactinfo.ContactPhotoLoader; 44 import com.android.dialer.app.list.DialtactsPagerAdapter; 45 import com.android.dialer.callintent.CallInitiationType; 46 import com.android.dialer.callintent.CallIntentBuilder; 47 import com.android.dialer.common.LogUtil; 48 import com.android.dialer.common.concurrent.DialerExecutor.Worker; 49 import com.android.dialer.notification.NotificationChannelManager; 50 import com.android.dialer.notification.NotificationChannelManager.Channel; 51 import com.android.dialer.phonenumbercache.ContactInfo; 52 import com.android.dialer.phonenumberutil.PhoneNumberHelper; 53 import com.android.dialer.util.DialerUtils; 54 import com.android.dialer.util.IntentUtil; 55 import java.util.HashSet; 56 import java.util.List; 57 import java.util.Set; 58 59 /** Creates a notification for calls that the user missed (neither answered nor rejected). */ 60 public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> { 61 62 /** The tag used to identify notifications from this class. */ 63 static final String NOTIFICATION_TAG = "MissedCallNotifier"; 64 /** The identifier of the notification of new missed calls. */ 65 private static final int NOTIFICATION_ID = R.id.notification_missed_call; 66 67 private final Context context; 68 private final CallLogNotificationsQueryHelper callLogNotificationsQueryHelper; 69 70 @VisibleForTesting 71 MissedCallNotifier( 72 Context context, CallLogNotificationsQueryHelper callLogNotificationsQueryHelper) { 73 this.context = context; 74 this.callLogNotificationsQueryHelper = callLogNotificationsQueryHelper; 75 } 76 77 static MissedCallNotifier getIstance(Context context) { 78 return new MissedCallNotifier(context, CallLogNotificationsQueryHelper.getInstance(context)); 79 } 80 81 @Nullable 82 @Override 83 public Void doInBackground(@Nullable Pair<Integer, String> input) throws Throwable { 84 updateMissedCallNotification(input.first, input.second); 85 return null; 86 } 87 88 /** 89 * Update missed call notifications from the call log. Accepts default information in case call 90 * log cannot be accessed. 91 * 92 * @param count the number of missed calls to display if call log cannot be accessed. May be 93 * {@link CallLogNotificationsService#UNKNOWN_MISSED_CALL_COUNT} if unknown. 94 * @param number the phone number of the most recent call to display if the call log cannot be 95 * accessed. May be null if unknown. 96 */ 97 @VisibleForTesting 98 @WorkerThread 99 void updateMissedCallNotification(int count, @Nullable String number) { 100 final int titleResId; 101 CharSequence expandedText; // The text in the notification's line 1 and 2. 102 103 List<NewCall> newCalls = callLogNotificationsQueryHelper.getNewMissedCalls(); 104 105 if ((newCalls != null && newCalls.isEmpty()) || count == 0) { 106 // No calls to notify about: clear the notification. 107 CallLogNotificationsQueryHelper.removeMissedCallNotifications(context, null); 108 return; 109 } 110 111 if (newCalls != null) { 112 if (count != CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT 113 && count != newCalls.size()) { 114 LogUtil.w( 115 "MissedCallNotifier.updateMissedCallNotification", 116 "Call count does not match call log count." 117 + " count: " 118 + count 119 + " newCalls.size(): " 120 + newCalls.size()); 121 } 122 count = newCalls.size(); 123 } 124 125 if (count == CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT) { 126 // If the intent did not contain a count, and we are unable to get a count from the 127 // call log, then no notification can be shown. 128 return; 129 } 130 131 Notification.Builder groupSummary = createNotificationBuilder(); 132 boolean useCallList = newCalls != null; 133 134 if (count == 1) { 135 NewCall call = 136 useCallList 137 ? newCalls.get(0) 138 : new NewCall( 139 null, 140 null, 141 number, 142 Calls.PRESENTATION_ALLOWED, 143 null, 144 null, 145 null, 146 null, 147 System.currentTimeMillis()); 148 149 //TODO: look up caller ID that is not in contacts. 150 ContactInfo contactInfo = 151 callLogNotificationsQueryHelper.getContactInfo( 152 call.number, call.numberPresentation, call.countryIso); 153 titleResId = 154 contactInfo.userType == ContactsUtils.USER_TYPE_WORK 155 ? R.string.notification_missedWorkCallTitle 156 : R.string.notification_missedCallTitle; 157 158 if (TextUtils.equals(contactInfo.name, contactInfo.formattedNumber) 159 || TextUtils.equals(contactInfo.name, contactInfo.number)) { 160 expandedText = 161 PhoneNumberUtilsCompat.createTtsSpannable( 162 BidiFormatter.getInstance() 163 .unicodeWrap(contactInfo.name, TextDirectionHeuristics.LTR)); 164 } else { 165 expandedText = contactInfo.name; 166 } 167 168 ContactPhotoLoader loader = new ContactPhotoLoader(context, contactInfo); 169 Bitmap photoIcon = loader.loadPhotoIcon(); 170 if (photoIcon != null) { 171 groupSummary.setLargeIcon(photoIcon); 172 } 173 } else { 174 titleResId = R.string.notification_missedCallsTitle; 175 expandedText = context.getString(R.string.notification_missedCallsMsg, count); 176 } 177 178 // Create a public viewable version of the notification, suitable for display when sensitive 179 // notification content is hidden. 180 Notification.Builder publicSummaryBuilder = createNotificationBuilder(); 181 publicSummaryBuilder 182 .setContentTitle(context.getText(titleResId)) 183 .setContentIntent(createCallLogPendingIntent()) 184 .setDeleteIntent(createClearMissedCallsPendingIntent(null)); 185 186 // Create the notification summary suitable for display when sensitive information is showing. 187 groupSummary 188 .setContentTitle(context.getText(titleResId)) 189 .setContentText(expandedText) 190 .setContentIntent(createCallLogPendingIntent()) 191 .setDeleteIntent(createClearMissedCallsPendingIntent(null)) 192 .setGroupSummary(useCallList) 193 .setOnlyAlertOnce(useCallList) 194 .setPublicVersion(publicSummaryBuilder.build()); 195 196 NotificationChannelManager.applyChannel(groupSummary, context, Channel.MISSED_CALL, null); 197 198 Notification notification = groupSummary.build(); 199 configureLedOnNotification(notification); 200 201 LogUtil.i("MissedCallNotifier.updateMissedCallNotification", "adding missed call notification"); 202 getNotificationMgr().notify(NOTIFICATION_TAG, NOTIFICATION_ID, notification); 203 204 if (useCallList) { 205 // Do not repost active notifications to prevent erasing post call notes. 206 NotificationManager manager = getNotificationMgr(); 207 Set<String> activeTags = new HashSet<>(); 208 for (StatusBarNotification activeNotification : manager.getActiveNotifications()) { 209 activeTags.add(activeNotification.getTag()); 210 } 211 212 for (NewCall call : newCalls) { 213 String callTag = call.callsUri.toString(); 214 if (!activeTags.contains(callTag)) { 215 manager.notify(callTag, NOTIFICATION_ID, getNotificationForCall(call, null)); 216 } 217 } 218 } 219 } 220 221 public void insertPostCallNotification(@NonNull String number, @NonNull String note) { 222 List<NewCall> newCalls = callLogNotificationsQueryHelper.getNewMissedCalls(); 223 if (newCalls != null && !newCalls.isEmpty()) { 224 for (NewCall call : newCalls) { 225 if (call.number.equals(number.replace("tel:", ""))) { 226 // Update the first notification that matches our post call note sender. 227 getNotificationMgr() 228 .notify( 229 call.callsUri.toString(), NOTIFICATION_ID, getNotificationForCall(call, note)); 230 break; 231 } 232 } 233 } 234 } 235 236 private Notification getNotificationForCall( 237 @NonNull NewCall call, @Nullable String postCallMessage) { 238 ContactInfo contactInfo = 239 callLogNotificationsQueryHelper.getContactInfo( 240 call.number, call.numberPresentation, call.countryIso); 241 242 // Create a public viewable version of the notification, suitable for display when sensitive 243 // notification content is hidden. 244 int titleResId = 245 contactInfo.userType == ContactsUtils.USER_TYPE_WORK 246 ? R.string.notification_missedWorkCallTitle 247 : R.string.notification_missedCallTitle; 248 Notification.Builder publicBuilder = 249 createNotificationBuilder(call).setContentTitle(context.getText(titleResId)); 250 251 Notification.Builder builder = createNotificationBuilder(call); 252 CharSequence expandedText; 253 if (TextUtils.equals(contactInfo.name, contactInfo.formattedNumber) 254 || TextUtils.equals(contactInfo.name, contactInfo.number)) { 255 expandedText = 256 PhoneNumberUtilsCompat.createTtsSpannable( 257 BidiFormatter.getInstance() 258 .unicodeWrap(contactInfo.name, TextDirectionHeuristics.LTR)); 259 } else { 260 expandedText = contactInfo.name; 261 } 262 263 if (postCallMessage != null) { 264 expandedText = 265 context.getString(R.string.post_call_notification_message, expandedText, postCallMessage); 266 } 267 268 ContactPhotoLoader loader = new ContactPhotoLoader(context, contactInfo); 269 Bitmap photoIcon = loader.loadPhotoIcon(); 270 if (photoIcon != null) { 271 builder.setLargeIcon(photoIcon); 272 } 273 // Create the notification suitable for display when sensitive information is showing. 274 builder 275 .setContentTitle(context.getText(titleResId)) 276 .setContentText(expandedText) 277 // Include a public version of the notification to be shown when the missed call 278 // notification is shown on the user's lock screen and they have chosen to hide 279 // sensitive notification information. 280 .setPublicVersion(publicBuilder.build()); 281 282 // Add additional actions when the user isn't locked 283 if (UserManagerCompat.isUserUnlocked(context)) { 284 if (!TextUtils.isEmpty(call.number) 285 && !TextUtils.equals(call.number, context.getString(R.string.handle_restricted))) { 286 builder.addAction( 287 new Notification.Action.Builder( 288 Icon.createWithResource(context, R.drawable.ic_phone_24dp), 289 context.getString(R.string.notification_missedCall_call_back), 290 createCallBackPendingIntent(call.number, call.callsUri)) 291 .build()); 292 293 if (!PhoneNumberHelper.isUriNumber(call.number)) { 294 builder.addAction( 295 new Notification.Action.Builder( 296 Icon.createWithResource(context, R.drawable.quantum_ic_message_white_24), 297 context.getString(R.string.notification_missedCall_message), 298 createSendSmsFromNotificationPendingIntent(call.number, call.callsUri)) 299 .build()); 300 } 301 } 302 } 303 304 Notification notification = builder.build(); 305 configureLedOnNotification(notification); 306 return notification; 307 } 308 309 private Notification.Builder createNotificationBuilder() { 310 return new Notification.Builder(context) 311 .setGroup(NOTIFICATION_TAG) 312 .setSmallIcon(android.R.drawable.stat_notify_missed_call) 313 .setColor(context.getResources().getColor(R.color.dialer_theme_color, null)) 314 .setAutoCancel(true) 315 .setOnlyAlertOnce(true) 316 .setShowWhen(true) 317 .setDefaults(Notification.DEFAULT_VIBRATE); 318 } 319 320 private Notification.Builder createNotificationBuilder(@NonNull NewCall call) { 321 Builder builder = 322 createNotificationBuilder() 323 .setWhen(call.dateMs) 324 .setDeleteIntent(createClearMissedCallsPendingIntent(call.callsUri)) 325 .setContentIntent(createCallLogPendingIntent(call.callsUri)); 326 327 NotificationChannelManager.applyChannel(builder, context, Channel.MISSED_CALL, null); 328 return builder; 329 } 330 331 /** Trigger an intent to make a call from a missed call number. */ 332 @WorkerThread 333 public void callBackFromMissedCall(String number, Uri callUri) { 334 closeSystemDialogs(context); 335 CallLogNotificationsQueryHelper.removeMissedCallNotifications(context, callUri); 336 DialerUtils.startActivityWithErrorToast( 337 context, 338 new CallIntentBuilder(number, CallInitiationType.Type.MISSED_CALL_NOTIFICATION) 339 .build() 340 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); 341 } 342 343 /** Trigger an intent to send an sms from a missed call number. */ 344 public void sendSmsFromMissedCall(String number, Uri callUri) { 345 closeSystemDialogs(context); 346 CallLogNotificationsQueryHelper.removeMissedCallNotifications(context, callUri); 347 DialerUtils.startActivityWithErrorToast( 348 context, IntentUtil.getSendSmsIntent(number).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); 349 } 350 351 /** 352 * Creates a new pending intent that sends the user to the call log. 353 * 354 * @return The pending intent. 355 */ 356 private PendingIntent createCallLogPendingIntent() { 357 return createCallLogPendingIntent(null); 358 } 359 360 /** 361 * Creates a new pending intent that sends the user to the call log. 362 * 363 * @return The pending intent. 364 * @param callUri Uri of the call to jump to. May be null 365 */ 366 private PendingIntent createCallLogPendingIntent(@Nullable Uri callUri) { 367 Intent contentIntent = 368 DialtactsActivity.getShowTabIntent(context, DialtactsPagerAdapter.TAB_INDEX_HISTORY); 369 // TODO (b/35486204): scroll to call 370 contentIntent.setData(callUri); 371 return PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT); 372 } 373 374 /** Creates a pending intent that marks all new missed calls as old. */ 375 private PendingIntent createClearMissedCallsPendingIntent(@Nullable Uri callUri) { 376 Intent intent = new Intent(context, CallLogNotificationsService.class); 377 intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_MISSED_CALLS_AS_OLD); 378 intent.setData(callUri); 379 return PendingIntent.getService(context, 0, intent, 0); 380 } 381 382 private PendingIntent createCallBackPendingIntent(String number, @NonNull Uri callUri) { 383 Intent intent = new Intent(context, CallLogNotificationsService.class); 384 intent.setAction(CallLogNotificationsService.ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION); 385 intent.putExtra(MissedCallNotificationReceiver.EXTRA_NOTIFICATION_PHONE_NUMBER, number); 386 intent.setData(callUri); 387 // Use FLAG_UPDATE_CURRENT to make sure any previous pending intent is updated with the new 388 // extra. 389 return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); 390 } 391 392 private PendingIntent createSendSmsFromNotificationPendingIntent( 393 String number, @NonNull Uri callUri) { 394 Intent intent = new Intent(context, CallLogNotificationsActivity.class); 395 intent.setAction(CallLogNotificationsActivity.ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION); 396 intent.putExtra(CallLogNotificationsActivity.EXTRA_MISSED_CALL_NUMBER, number); 397 intent.setData(callUri); 398 // Use FLAG_UPDATE_CURRENT to make sure any previous pending intent is updated with the new 399 // extra. 400 return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); 401 } 402 403 /** Configures a notification to emit the blinky notification light. */ 404 private void configureLedOnNotification(Notification notification) { 405 notification.flags |= Notification.FLAG_SHOW_LIGHTS; 406 notification.defaults |= Notification.DEFAULT_LIGHTS; 407 } 408 409 /** Closes open system dialogs and the notification shade. */ 410 private void closeSystemDialogs(Context context) { 411 context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); 412 } 413 414 private NotificationManager getNotificationMgr() { 415 return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 416 } 417 } 418