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 17 package com.android.incallui; 18 19 import com.google.common.base.Preconditions; 20 21 import com.android.contacts.common.ContactsUtils; 22 import com.android.contacts.common.compat.CallSdkCompat; 23 import com.android.contacts.common.preference.ContactsPreferences; 24 import com.android.contacts.common.util.BitmapUtil; 25 import com.android.contacts.common.util.ContactDisplayUtils; 26 import com.android.dialer.R; 27 import com.android.incallui.util.TelecomCallUtil; 28 29 import android.app.Notification; 30 import android.app.NotificationManager; 31 import android.app.PendingIntent; 32 import android.content.Context; 33 import android.content.Intent; 34 import android.graphics.Bitmap; 35 import android.graphics.BitmapFactory; 36 import android.graphics.drawable.BitmapDrawable; 37 import android.net.Uri; 38 import android.support.annotation.Nullable; 39 import android.telecom.Call; 40 import android.telecom.PhoneAccount; 41 import android.text.BidiFormatter; 42 import android.text.TextDirectionHeuristics; 43 import android.text.TextUtils; 44 import android.util.ArrayMap; 45 46 import java.util.Map; 47 48 /** 49 * Handles the display of notifications for "external calls". 50 * 51 * External calls are a representation of a call which is in progress on the user's other device 52 * (e.g. another phone, or a watch). 53 */ 54 public class ExternalCallNotifier implements ExternalCallList.ExternalCallListener { 55 56 /** 57 * Tag used with the notification manager to uniquely identify external call notifications. 58 */ 59 private static final String NOTIFICATION_TAG = "EXTERNAL_CALL"; 60 61 /** 62 * Represents a call and associated cached notification data. 63 */ 64 private static class NotificationInfo { 65 private final Call mCall; 66 private final int mNotificationId; 67 @Nullable private String mContentTitle; 68 @Nullable private Bitmap mLargeIcon; 69 @Nullable private String mPersonReference; 70 71 public NotificationInfo(Call call, int notificationId) { 72 Preconditions.checkNotNull(call); 73 mCall = call; 74 mNotificationId = notificationId; 75 } 76 77 public Call getCall() { 78 return mCall; 79 } 80 81 public int getNotificationId() { 82 return mNotificationId; 83 } 84 85 public @Nullable String getContentTitle() { 86 return mContentTitle; 87 } 88 89 public @Nullable Bitmap getLargeIcon() { 90 return mLargeIcon; 91 } 92 93 public @Nullable String getPersonReference() { 94 return mPersonReference; 95 } 96 97 public void setContentTitle(@Nullable String contentTitle) { 98 mContentTitle = contentTitle; 99 } 100 101 public void setLargeIcon(@Nullable Bitmap largeIcon) { 102 mLargeIcon = largeIcon; 103 } 104 105 public void setPersonReference(@Nullable String personReference) { 106 mPersonReference = personReference; 107 } 108 } 109 110 private final Context mContext; 111 private final ContactInfoCache mContactInfoCache; 112 private Map<Call, NotificationInfo> mNotifications = new ArrayMap<>(); 113 private int mNextUniqueNotificationId; 114 private ContactsPreferences mContactsPreferences; 115 116 /** 117 * Initializes a new instance of the external call notifier. 118 */ 119 public ExternalCallNotifier(Context context, ContactInfoCache contactInfoCache) { 120 mContext = Preconditions.checkNotNull(context); 121 mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext); 122 mContactInfoCache = Preconditions.checkNotNull(contactInfoCache); 123 } 124 125 /** 126 * Handles the addition of a new external call by showing a new notification. 127 * Triggered by {@link CallList#onCallAdded(android.telecom.Call)}. 128 */ 129 @Override 130 public void onExternalCallAdded(android.telecom.Call call) { 131 Log.i(this, "onExternalCallAdded " + call); 132 Preconditions.checkArgument(!mNotifications.containsKey(call)); 133 NotificationInfo info = new NotificationInfo(call, mNextUniqueNotificationId++); 134 mNotifications.put(call, info); 135 136 showNotifcation(info); 137 } 138 139 /** 140 * Handles the removal of an external call by hiding its associated notification. 141 * Triggered by {@link CallList#onCallRemoved(android.telecom.Call)}. 142 */ 143 @Override 144 public void onExternalCallRemoved(android.telecom.Call call) { 145 Log.i(this, "onExternalCallRemoved " + call); 146 147 dismissNotification(call); 148 } 149 150 /** 151 * Handles updates to an external call. 152 */ 153 @Override 154 public void onExternalCallUpdated(Call call) { 155 Preconditions.checkArgument(mNotifications.containsKey(call)); 156 postNotification(mNotifications.get(call)); 157 } 158 159 /** 160 * Initiates a call pull given a notification ID. 161 * 162 * @param notificationId The notification ID associated with the external call which is to be 163 * pulled. 164 */ 165 public void pullExternalCall(int notificationId) { 166 for (NotificationInfo info : mNotifications.values()) { 167 if (info.getNotificationId() == notificationId) { 168 CallSdkCompat.pullExternalCall(info.getCall()); 169 return; 170 } 171 } 172 } 173 174 /** 175 * Shows a notification for a new external call. Performs a contact cache lookup to find any 176 * associated photo and information for the call. 177 */ 178 private void showNotifcation(final NotificationInfo info) { 179 // We make a call to the contact info cache to query for supplemental data to what the 180 // call provides. This includes the contact name and photo. 181 // This callback will always get called immediately and synchronously with whatever data 182 // it has available, and may make a subsequent call later (same thread) if it had to 183 // call into the contacts provider for more data. 184 com.android.incallui.Call incallCall = new com.android.incallui.Call(info.getCall(), 185 false /* registerCallback */); 186 187 mContactInfoCache.findInfo(incallCall, false /* isIncoming */, 188 new ContactInfoCache.ContactInfoCacheCallback() { 189 @Override 190 public void onContactInfoComplete(String callId, 191 ContactInfoCache.ContactCacheEntry entry) { 192 193 // Ensure notification still exists as the external call could have been 194 // removed during async contact info lookup. 195 if (mNotifications.containsKey(info.getCall())) { 196 saveContactInfo(info, entry); 197 } 198 } 199 200 @Override 201 public void onImageLoadComplete(String callId, 202 ContactInfoCache.ContactCacheEntry entry) { 203 204 // Ensure notification still exists as the external call could have been 205 // removed during async contact info lookup. 206 if (mNotifications.containsKey(info.getCall())) { 207 savePhoto(info, entry); 208 } 209 } 210 211 @Override 212 public void onContactInteractionsInfoComplete(String callId, 213 ContactInfoCache.ContactCacheEntry entry) { 214 } 215 }); 216 } 217 218 /** 219 * Dismisses a notification for an external call. 220 */ 221 private void dismissNotification(Call call) { 222 Preconditions.checkArgument(mNotifications.containsKey(call)); 223 224 NotificationManager notificationManager = 225 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); 226 notificationManager.cancel(NOTIFICATION_TAG, mNotifications.get(call).getNotificationId()); 227 228 mNotifications.remove(call); 229 } 230 231 /** 232 * Attempts to build a large icon to use for the notification based on the contact info and 233 * post the updated notification to the notification manager. 234 */ 235 private void savePhoto(NotificationInfo info, ContactInfoCache.ContactCacheEntry entry) { 236 Bitmap largeIcon = getLargeIconToDisplay(mContext, entry, info.getCall()); 237 if (largeIcon != null) { 238 largeIcon = getRoundedIcon(mContext, largeIcon); 239 } 240 info.setLargeIcon(largeIcon); 241 postNotification(info); 242 } 243 244 /** 245 * Builds and stores the contact information the notification will display and posts the updated 246 * notification to the notification manager. 247 */ 248 private void saveContactInfo(NotificationInfo info, ContactInfoCache.ContactCacheEntry entry) { 249 info.setContentTitle(getContentTitle(mContext, mContactsPreferences, 250 entry, info.getCall())); 251 info.setPersonReference(getPersonReference(entry, info.getCall())); 252 postNotification(info); 253 } 254 255 /** 256 * Rebuild an existing or show a new notification given {@link NotificationInfo}. 257 */ 258 private void postNotification(NotificationInfo info) { 259 Log.i(this, "postNotification : " + info.getContentTitle()); 260 Notification.Builder builder = new Notification.Builder(mContext); 261 // Set notification as ongoing since calls are long-running versus a point-in-time notice. 262 builder.setOngoing(true); 263 // Make the notification prioritized over the other normal notifications. 264 builder.setPriority(Notification.PRIORITY_HIGH); 265 // Set the content ("Ongoing call on another device") 266 builder.setContentText(mContext.getString(R.string.notification_external_call)); 267 builder.setSmallIcon(R.drawable.ic_call_white_24dp); 268 builder.setContentTitle(info.getContentTitle()); 269 builder.setLargeIcon(info.getLargeIcon()); 270 builder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color)); 271 builder.addPerson(info.getPersonReference()); 272 273 // Where the external call supports being transferred to the local device, add an action 274 // to the notification to initiate the call pull process. 275 if ((info.getCall().getDetails().getCallCapabilities() 276 & CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL) 277 == CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL) { 278 279 Intent intent = new Intent( 280 NotificationBroadcastReceiver.ACTION_PULL_EXTERNAL_CALL, null, mContext, 281 NotificationBroadcastReceiver.class); 282 intent.putExtra(NotificationBroadcastReceiver.EXTRA_NOTIFICATION_ID, 283 info.getNotificationId()); 284 285 builder.addAction(new Notification.Action.Builder(R.drawable.ic_call_white_24dp, 286 mContext.getText(R.string.notification_transfer_call), 287 PendingIntent.getBroadcast(mContext, 0, intent, 0)).build()); 288 } 289 290 /** 291 * This builder is used for the notification shown when the device is locked and the user 292 * has set their notification settings to 'hide sensitive content' 293 * {@see Notification.Builder#setPublicVersion}. 294 */ 295 Notification.Builder publicBuilder = new Notification.Builder(mContext); 296 publicBuilder.setSmallIcon(R.drawable.ic_call_white_24dp); 297 publicBuilder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color)); 298 299 builder.setPublicVersion(publicBuilder.build()); 300 Notification notification = builder.build(); 301 302 NotificationManager notificationManager = 303 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); 304 notificationManager.notify(NOTIFICATION_TAG, info.getNotificationId(), notification); 305 } 306 307 /** 308 * Finds a large icon to display in a notification for a call. For conference calls, a 309 * conference call icon is used, otherwise if contact info is specified, the user's contact 310 * photo or avatar is used. 311 * 312 * @param context The context. 313 * @param contactInfo The contact cache info. 314 * @param call The call. 315 * @return The large icon to use for the notification. 316 */ 317 private @Nullable Bitmap getLargeIconToDisplay(Context context, 318 ContactInfoCache.ContactCacheEntry contactInfo, android.telecom.Call call) { 319 320 Bitmap largeIcon = null; 321 if (call.getDetails().hasProperty(android.telecom.Call.Details.PROPERTY_CONFERENCE) && 322 !call.getDetails() 323 .hasProperty(android.telecom.Call.Details.PROPERTY_GENERIC_CONFERENCE)) { 324 325 largeIcon = BitmapFactory.decodeResource(context.getResources(), 326 R.drawable.img_conference); 327 } 328 if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) { 329 largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap(); 330 } 331 return largeIcon; 332 } 333 334 /** 335 * Given a bitmap, returns a rounded version of the icon suitable for display in a notification. 336 * 337 * @param context The context. 338 * @param bitmap The bitmap to round. 339 * @return The rounded bitmap. 340 */ 341 private @Nullable Bitmap getRoundedIcon(Context context, @Nullable Bitmap bitmap) { 342 if (bitmap == null) { 343 return null; 344 } 345 final int height = (int) context.getResources().getDimension( 346 android.R.dimen.notification_large_icon_height); 347 final int width = (int) context.getResources().getDimension( 348 android.R.dimen.notification_large_icon_width); 349 return BitmapUtil.getRoundedBitmap(bitmap, width, height); 350 } 351 352 /** 353 * Builds a notification content title for a call. If the call is a conference call, it is 354 * identified as such. Otherwise an attempt is made to show an associated contact name or 355 * phone number. 356 * 357 * @param context The context. 358 * @param contactsPreferences Contacts preferences, used to determine the preferred formatting 359 * for contact names. 360 * @param contactInfo The contact info which was looked up in the contact cache. 361 * @param call The call to generate a title for. 362 * @return The content title. 363 */ 364 private @Nullable String getContentTitle(Context context, 365 @Nullable ContactsPreferences contactsPreferences, 366 ContactInfoCache.ContactCacheEntry contactInfo, android.telecom.Call call) { 367 368 if (call.getDetails().hasProperty(android.telecom.Call.Details.PROPERTY_CONFERENCE) && 369 !call.getDetails() 370 .hasProperty(android.telecom.Call.Details.PROPERTY_GENERIC_CONFERENCE)) { 371 372 return context.getResources().getString(R.string.card_title_conf_call); 373 } 374 375 String preferredName = ContactDisplayUtils.getPreferredDisplayName(contactInfo.namePrimary, 376 contactInfo.nameAlternative, contactsPreferences); 377 if (TextUtils.isEmpty(preferredName)) { 378 return TextUtils.isEmpty(contactInfo.number) ? null : BidiFormatter.getInstance() 379 .unicodeWrap(contactInfo.number, TextDirectionHeuristics.LTR); 380 } 381 return preferredName; 382 } 383 384 /** 385 * Gets a "person reference" for a notification, used by the system to determine whether the 386 * notification should be allowed past notification interruption filters. 387 * 388 * @param contactInfo The contact info from cache. 389 * @param call The call. 390 * @return the person reference. 391 */ 392 private String getPersonReference(ContactInfoCache.ContactCacheEntry contactInfo, 393 Call call) { 394 395 String number = TelecomCallUtil.getNumber(call); 396 // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed. 397 // So, do not pass {@link Contacts#CONTENT_LOOKUP_URI} to NotificationManager to avoid 398 // NotificationManager using it. 399 if (contactInfo.lookupUri != null && contactInfo.userType != ContactsUtils.USER_TYPE_WORK) { 400 return contactInfo.lookupUri.toString(); 401 } else if (!TextUtils.isEmpty(number)) { 402 return Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null).toString(); 403 } 404 return ""; 405 } 406 } 407