blob: 10c4a6490c9baeeb0a62f430c1e3e6eb5c1f4064 [file] [log] [blame]
Eric Erfanianccca3152017-02-22 16:32:36 -08001/*
2 * Copyright (C) 2017 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
17package com.android.incallui;
18
19import android.annotation.TargetApi;
20import android.app.Notification;
Eric Erfanianccca3152017-02-22 16:32:36 -080021import android.app.PendingIntent;
22import android.content.Context;
23import android.content.Intent;
24import android.graphics.Bitmap;
25import android.graphics.BitmapFactory;
26import android.graphics.drawable.BitmapDrawable;
27import android.net.Uri;
28import android.os.Build.VERSION_CODES;
29import android.support.annotation.NonNull;
30import android.support.annotation.Nullable;
Eric Erfanianea7890c2017-06-19 12:40:59 -070031import android.support.v4.os.BuildCompat;
Eric Erfanianccca3152017-02-22 16:32:36 -080032import android.telecom.Call;
33import android.telecom.PhoneAccount;
34import android.telecom.VideoProfile;
35import android.text.BidiFormatter;
36import android.text.TextDirectionHeuristics;
37import android.text.TextUtils;
38import android.util.ArrayMap;
39import com.android.contacts.common.ContactsUtils;
40import com.android.contacts.common.compat.CallCompat;
41import com.android.contacts.common.preference.ContactsPreferences;
Eric Erfanianccca3152017-02-22 16:32:36 -080042import com.android.contacts.common.util.ContactDisplayUtils;
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -070043import com.android.dialer.common.Assert;
44import com.android.dialer.contactphoto.BitmapUtil;
45import com.android.dialer.notification.DialerNotificationManager;
Eric Erfanianea7890c2017-06-19 12:40:59 -070046import com.android.dialer.notification.NotificationChannelId;
twyena4745bd2017-12-12 18:40:11 -080047import com.android.dialer.telecom.TelecomCallUtil;
calderwoodraa93df432018-05-23 12:59:03 -070048import com.android.dialer.theme.base.ThemeComponent;
Eric Erfanianccca3152017-02-22 16:32:36 -080049import com.android.incallui.call.DialerCall;
50import com.android.incallui.call.DialerCallDelegate;
51import com.android.incallui.call.ExternalCallList;
52import com.android.incallui.latencyreport.LatencyReport;
Eric Erfanianccca3152017-02-22 16:32:36 -080053import java.util.Map;
54
55/**
56 * Handles the display of notifications for "external calls".
57 *
58 * <p>External calls are a representation of a call which is in progress on the user's other device
59 * (e.g. another phone, or a watch).
60 */
61public class ExternalCallNotifier implements ExternalCallList.ExternalCallListener {
62
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -070063 /**
64 * Common tag for all external call notifications. Unlike other grouped notifications in Dialer,
65 * external call notifications are uniquely identified by ID.
66 */
Eric Erfanianea7890c2017-06-19 12:40:59 -070067 private static final String NOTIFICATION_TAG = "EXTERNAL_CALL";
Eric Erfanianccca3152017-02-22 16:32:36 -080068
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -070069 private static final int GROUP_SUMMARY_NOTIFICATION_ID = -1;
70 private static final String GROUP_SUMMARY_NOTIFICATION_TAG = "GroupSummary_ExternalCall";
71 /**
72 * Key used to associate all external call notifications and the summary as belonging to a single
73 * group.
74 */
75 private static final String GROUP_KEY = "ExternalCallGroup";
Eric Erfanianea7890c2017-06-19 12:40:59 -070076
linyuh183cb712017-12-27 17:02:37 -080077 private final Context context;
78 private final ContactInfoCache contactInfoCache;
79 private Map<Call, NotificationInfo> notifications = new ArrayMap<>();
80 private int nextUniqueNotificationId;
81 private ContactsPreferences contactsPreferences;
Eric Erfanianccca3152017-02-22 16:32:36 -080082
83 /** Initializes a new instance of the external call notifier. */
84 public ExternalCallNotifier(
85 @NonNull Context context, @NonNull ContactInfoCache contactInfoCache) {
linyuh183cb712017-12-27 17:02:37 -080086 this.context = context;
87 contactsPreferences = ContactsPreferencesFactory.newContactsPreferences(this.context);
88 this.contactInfoCache = contactInfoCache;
Eric Erfanianccca3152017-02-22 16:32:36 -080089 }
90
91 /**
92 * Handles the addition of a new external call by showing a new notification. Triggered by {@link
93 * CallList#onCallAdded(android.telecom.Call)}.
94 */
95 @Override
96 public void onExternalCallAdded(android.telecom.Call call) {
97 Log.i(this, "onExternalCallAdded " + call);
linyuh183cb712017-12-27 17:02:37 -080098 Assert.checkArgument(!notifications.containsKey(call));
99 NotificationInfo info = new NotificationInfo(call, nextUniqueNotificationId++);
100 notifications.put(call, info);
Eric Erfanianccca3152017-02-22 16:32:36 -0800101
102 showNotifcation(info);
103 }
104
105 /**
106 * Handles the removal of an external call by hiding its associated notification. Triggered by
107 * {@link CallList#onCallRemoved(android.telecom.Call)}.
108 */
109 @Override
110 public void onExternalCallRemoved(android.telecom.Call call) {
111 Log.i(this, "onExternalCallRemoved " + call);
112
113 dismissNotification(call);
114 }
115
116 /** Handles updates to an external call. */
117 @Override
118 public void onExternalCallUpdated(Call call) {
linyuh183cb712017-12-27 17:02:37 -0800119 Assert.checkArgument(notifications.containsKey(call));
120 postNotification(notifications.get(call));
Eric Erfanianccca3152017-02-22 16:32:36 -0800121 }
122
123 @Override
124 public void onExternalCallPulled(Call call) {
125 // no-op; if an external call is pulled, it will be removed via onExternalCallRemoved.
126 }
127
128 /**
129 * Initiates a call pull given a notification ID.
130 *
131 * @param notificationId The notification ID associated with the external call which is to be
132 * pulled.
133 */
134 @TargetApi(VERSION_CODES.N_MR1)
135 public void pullExternalCall(int notificationId) {
linyuh183cb712017-12-27 17:02:37 -0800136 for (NotificationInfo info : notifications.values()) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800137 if (info.getNotificationId() == notificationId
138 && CallCompat.canPullExternalCall(info.getCall())) {
139 info.getCall().pullExternalCall();
140 return;
141 }
142 }
143 }
144
145 /**
146 * Shows a notification for a new external call. Performs a contact cache lookup to find any
147 * associated photo and information for the call.
148 */
149 private void showNotifcation(final NotificationInfo info) {
150 // We make a call to the contact info cache to query for supplemental data to what the
151 // call provides. This includes the contact name and photo.
152 // This callback will always get called immediately and synchronously with whatever data
153 // it has available, and may make a subsequent call later (same thread) if it had to
154 // call into the contacts provider for more data.
155 DialerCall dialerCall =
156 new DialerCall(
linyuh183cb712017-12-27 17:02:37 -0800157 context,
Eric Erfanianccca3152017-02-22 16:32:36 -0800158 new DialerCallDelegateStub(),
159 info.getCall(),
160 new LatencyReport(),
161 false /* registerCallback */);
162
linyuh183cb712017-12-27 17:02:37 -0800163 contactInfoCache.findInfo(
Eric Erfanianccca3152017-02-22 16:32:36 -0800164 dialerCall,
165 false /* isIncoming */,
166 new ContactInfoCache.ContactInfoCacheCallback() {
167 @Override
168 public void onContactInfoComplete(
169 String callId, ContactInfoCache.ContactCacheEntry entry) {
170
171 // Ensure notification still exists as the external call could have been
172 // removed during async contact info lookup.
linyuh183cb712017-12-27 17:02:37 -0800173 if (notifications.containsKey(info.getCall())) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800174 saveContactInfo(info, entry);
175 }
176 }
177
178 @Override
179 public void onImageLoadComplete(String callId, ContactInfoCache.ContactCacheEntry entry) {
180
181 // Ensure notification still exists as the external call could have been
182 // removed during async contact info lookup.
linyuh183cb712017-12-27 17:02:37 -0800183 if (notifications.containsKey(info.getCall())) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800184 savePhoto(info, entry);
185 }
186 }
187 });
188 }
189
190 /** Dismisses a notification for an external call. */
191 private void dismissNotification(Call call) {
linyuh183cb712017-12-27 17:02:37 -0800192 Assert.checkArgument(notifications.containsKey(call));
Eric Erfanianccca3152017-02-22 16:32:36 -0800193
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -0700194 // This will also dismiss the group summary if there are no more external call notifications.
195 DialerNotificationManager.cancel(
linyuh183cb712017-12-27 17:02:37 -0800196 context, NOTIFICATION_TAG, notifications.get(call).getNotificationId());
Eric Erfanianccca3152017-02-22 16:32:36 -0800197
linyuh183cb712017-12-27 17:02:37 -0800198 notifications.remove(call);
Eric Erfanianccca3152017-02-22 16:32:36 -0800199 }
200
201 /**
202 * Attempts to build a large icon to use for the notification based on the contact info and post
203 * the updated notification to the notification manager.
204 */
205 private void savePhoto(NotificationInfo info, ContactInfoCache.ContactCacheEntry entry) {
linyuh183cb712017-12-27 17:02:37 -0800206 Bitmap largeIcon = getLargeIconToDisplay(context, entry, info.getCall());
Eric Erfanianccca3152017-02-22 16:32:36 -0800207 if (largeIcon != null) {
linyuh183cb712017-12-27 17:02:37 -0800208 largeIcon = getRoundedIcon(context, largeIcon);
Eric Erfanianccca3152017-02-22 16:32:36 -0800209 }
210 info.setLargeIcon(largeIcon);
211 postNotification(info);
212 }
213
214 /**
215 * Builds and stores the contact information the notification will display and posts the updated
216 * notification to the notification manager.
217 */
218 private void saveContactInfo(NotificationInfo info, ContactInfoCache.ContactCacheEntry entry) {
linyuh183cb712017-12-27 17:02:37 -0800219 info.setContentTitle(getContentTitle(context, contactsPreferences, entry, info.getCall()));
Eric Erfanianccca3152017-02-22 16:32:36 -0800220 info.setPersonReference(getPersonReference(entry, info.getCall()));
221 postNotification(info);
222 }
223
224 /** Rebuild an existing or show a new notification given {@link NotificationInfo}. */
225 private void postNotification(NotificationInfo info) {
linyuh183cb712017-12-27 17:02:37 -0800226 Notification.Builder builder = new Notification.Builder(context);
Eric Erfanianccca3152017-02-22 16:32:36 -0800227 // Set notification as ongoing since calls are long-running versus a point-in-time notice.
228 builder.setOngoing(true);
229 // Make the notification prioritized over the other normal notifications.
230 builder.setPriority(Notification.PRIORITY_HIGH);
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -0700231 builder.setGroup(GROUP_KEY);
Eric Erfanianccca3152017-02-22 16:32:36 -0800232
233 boolean isVideoCall = VideoProfile.isVideo(info.getCall().getDetails().getVideoState());
234 // Set the content ("Ongoing call on another device")
235 builder.setContentText(
linyuh183cb712017-12-27 17:02:37 -0800236 context.getString(
Eric Erfanianccca3152017-02-22 16:32:36 -0800237 isVideoCall
238 ? R.string.notification_external_video_call
239 : R.string.notification_external_call));
240 builder.setSmallIcon(R.drawable.quantum_ic_call_white_24);
241 builder.setContentTitle(info.getContentTitle());
242 builder.setLargeIcon(info.getLargeIcon());
calderwoodraa93df432018-05-23 12:59:03 -0700243 builder.setColor(ThemeComponent.get(context).theme().getColorPrimary());
Eric Erfanianccca3152017-02-22 16:32:36 -0800244 builder.addPerson(info.getPersonReference());
Eric Erfanianea7890c2017-06-19 12:40:59 -0700245 if (BuildCompat.isAtLeastO()) {
246 builder.setChannelId(NotificationChannelId.DEFAULT);
247 }
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700248
Eric Erfanianccca3152017-02-22 16:32:36 -0800249 // Where the external call supports being transferred to the local device, add an action
250 // to the notification to initiate the call pull process.
251 if (CallCompat.canPullExternalCall(info.getCall())) {
252
253 Intent intent =
254 new Intent(
255 NotificationBroadcastReceiver.ACTION_PULL_EXTERNAL_CALL,
256 null,
linyuh183cb712017-12-27 17:02:37 -0800257 context,
Eric Erfanianccca3152017-02-22 16:32:36 -0800258 NotificationBroadcastReceiver.class);
259 intent.putExtra(
260 NotificationBroadcastReceiver.EXTRA_NOTIFICATION_ID, info.getNotificationId());
261 builder.addAction(
262 new Notification.Action.Builder(
263 R.drawable.quantum_ic_call_white_24,
linyuh183cb712017-12-27 17:02:37 -0800264 context.getString(
Eric Erfanianccca3152017-02-22 16:32:36 -0800265 isVideoCall
266 ? R.string.notification_take_video_call
267 : R.string.notification_take_call),
linyuh183cb712017-12-27 17:02:37 -0800268 PendingIntent.getBroadcast(context, info.getNotificationId(), intent, 0))
Eric Erfanianccca3152017-02-22 16:32:36 -0800269 .build());
270 }
271
272 /**
273 * This builder is used for the notification shown when the device is locked and the user has
274 * set their notification settings to 'hide sensitive content' {@see
275 * Notification.Builder#setPublicVersion}.
276 */
linyuh183cb712017-12-27 17:02:37 -0800277 Notification.Builder publicBuilder = new Notification.Builder(context);
Eric Erfanianccca3152017-02-22 16:32:36 -0800278 publicBuilder.setSmallIcon(R.drawable.quantum_ic_call_white_24);
calderwoodraa93df432018-05-23 12:59:03 -0700279 publicBuilder.setColor(ThemeComponent.get(context).theme().getColorPrimary());
Eric Erfanianea7890c2017-06-19 12:40:59 -0700280 if (BuildCompat.isAtLeastO()) {
281 publicBuilder.setChannelId(NotificationChannelId.DEFAULT);
282 }
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700283
Eric Erfanianccca3152017-02-22 16:32:36 -0800284 builder.setPublicVersion(publicBuilder.build());
285 Notification notification = builder.build();
286
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -0700287 DialerNotificationManager.notify(
linyuh183cb712017-12-27 17:02:37 -0800288 context, NOTIFICATION_TAG, info.getNotificationId(), notification);
Eric Erfanianccca3152017-02-22 16:32:36 -0800289
linyuh183cb712017-12-27 17:02:37 -0800290 showGroupSummaryNotification(context);
Eric Erfanianccca3152017-02-22 16:32:36 -0800291 }
292
293 /**
294 * Finds a large icon to display in a notification for a call. For conference calls, a conference
295 * call icon is used, otherwise if contact info is specified, the user's contact photo or avatar
296 * is used.
297 *
298 * @param context The context.
299 * @param contactInfo The contact cache info.
300 * @param call The call.
301 * @return The large icon to use for the notification.
302 */
303 private @Nullable Bitmap getLargeIconToDisplay(
304 Context context, ContactInfoCache.ContactCacheEntry contactInfo, android.telecom.Call call) {
305
306 Bitmap largeIcon = null;
307 if (call.getDetails().hasProperty(android.telecom.Call.Details.PROPERTY_CONFERENCE)
308 && !call.getDetails()
309 .hasProperty(android.telecom.Call.Details.PROPERTY_GENERIC_CONFERENCE)) {
310
Eric Erfanian83b20212017-05-31 08:53:10 -0700311 largeIcon =
312 BitmapFactory.decodeResource(
313 context.getResources(), R.drawable.quantum_ic_group_vd_theme_24);
Eric Erfanianccca3152017-02-22 16:32:36 -0800314 }
315 if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) {
316 largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap();
317 }
318 return largeIcon;
319 }
320
321 /**
322 * Given a bitmap, returns a rounded version of the icon suitable for display in a notification.
323 *
324 * @param context The context.
325 * @param bitmap The bitmap to round.
326 * @return The rounded bitmap.
327 */
328 private @Nullable Bitmap getRoundedIcon(Context context, @Nullable Bitmap bitmap) {
329 if (bitmap == null) {
330 return null;
331 }
332 final int height =
333 (int) context.getResources().getDimension(android.R.dimen.notification_large_icon_height);
334 final int width =
335 (int) context.getResources().getDimension(android.R.dimen.notification_large_icon_width);
336 return BitmapUtil.getRoundedBitmap(bitmap, width, height);
337 }
338
339 /**
340 * Builds a notification content title for a call. If the call is a conference call, it is
341 * identified as such. Otherwise an attempt is made to show an associated contact name or phone
342 * number.
343 *
344 * @param context The context.
345 * @param contactsPreferences Contacts preferences, used to determine the preferred formatting for
346 * contact names.
347 * @param contactInfo The contact info which was looked up in the contact cache.
348 * @param call The call to generate a title for.
349 * @return The content title.
350 */
351 private @Nullable String getContentTitle(
352 Context context,
353 @Nullable ContactsPreferences contactsPreferences,
354 ContactInfoCache.ContactCacheEntry contactInfo,
355 android.telecom.Call call) {
356
Eric Erfanian91ce7d22017-06-05 13:35:02 -0700357 if (call.getDetails().hasProperty(android.telecom.Call.Details.PROPERTY_CONFERENCE)) {
358 return CallerInfoUtils.getConferenceString(
359 context,
360 call.getDetails().hasProperty(android.telecom.Call.Details.PROPERTY_GENERIC_CONFERENCE));
Eric Erfanianccca3152017-02-22 16:32:36 -0800361 }
362
363 String preferredName =
364 ContactDisplayUtils.getPreferredDisplayName(
365 contactInfo.namePrimary, contactInfo.nameAlternative, contactsPreferences);
366 if (TextUtils.isEmpty(preferredName)) {
367 return TextUtils.isEmpty(contactInfo.number)
368 ? null
369 : BidiFormatter.getInstance()
370 .unicodeWrap(contactInfo.number, TextDirectionHeuristics.LTR);
371 }
372 return preferredName;
373 }
374
375 /**
376 * Gets a "person reference" for a notification, used by the system to determine whether the
377 * notification should be allowed past notification interruption filters.
378 *
379 * @param contactInfo The contact info from cache.
380 * @param call The call.
381 * @return the person reference.
382 */
383 private String getPersonReference(ContactInfoCache.ContactCacheEntry contactInfo, Call call) {
384
385 String number = TelecomCallUtil.getNumber(call);
386 // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed.
387 // So, do not pass {@link Contacts#CONTENT_LOOKUP_URI} to NotificationManager to avoid
388 // NotificationManager using it.
389 if (contactInfo.lookupUri != null && contactInfo.userType != ContactsUtils.USER_TYPE_WORK) {
390 return contactInfo.lookupUri.toString();
391 } else if (!TextUtils.isEmpty(number)) {
392 return Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null).toString();
393 }
394 return "";
395 }
396
397 private static class DialerCallDelegateStub implements DialerCallDelegate {
398
399 @Override
400 public DialerCall getDialerCallFromTelecomCall(Call telecomCall) {
401 return null;
402 }
403 }
404
405 /** Represents a call and associated cached notification data. */
406 private static class NotificationInfo {
407
linyuh183cb712017-12-27 17:02:37 -0800408 @NonNull private final Call call;
409 private final int notificationId;
410 @Nullable private String contentTitle;
411 @Nullable private Bitmap largeIcon;
412 @Nullable private String personReference;
Eric Erfanianccca3152017-02-22 16:32:36 -0800413
414 public NotificationInfo(@NonNull Call call, int notificationId) {
linyuh183cb712017-12-27 17:02:37 -0800415 this.call = call;
416 this.notificationId = notificationId;
Eric Erfanianccca3152017-02-22 16:32:36 -0800417 }
418
419 public Call getCall() {
linyuh183cb712017-12-27 17:02:37 -0800420 return call;
Eric Erfanianccca3152017-02-22 16:32:36 -0800421 }
422
423 public int getNotificationId() {
linyuh183cb712017-12-27 17:02:37 -0800424 return notificationId;
Eric Erfanianccca3152017-02-22 16:32:36 -0800425 }
426
427 public @Nullable String getContentTitle() {
linyuh183cb712017-12-27 17:02:37 -0800428 return contentTitle;
Eric Erfanianccca3152017-02-22 16:32:36 -0800429 }
430
431 public void setContentTitle(@Nullable String contentTitle) {
linyuh183cb712017-12-27 17:02:37 -0800432 this.contentTitle = contentTitle;
Eric Erfanianccca3152017-02-22 16:32:36 -0800433 }
434
435 public @Nullable Bitmap getLargeIcon() {
linyuh183cb712017-12-27 17:02:37 -0800436 return largeIcon;
Eric Erfanianccca3152017-02-22 16:32:36 -0800437 }
438
439 public void setLargeIcon(@Nullable Bitmap largeIcon) {
linyuh183cb712017-12-27 17:02:37 -0800440 this.largeIcon = largeIcon;
Eric Erfanianccca3152017-02-22 16:32:36 -0800441 }
442
443 public @Nullable String getPersonReference() {
linyuh183cb712017-12-27 17:02:37 -0800444 return personReference;
Eric Erfanianccca3152017-02-22 16:32:36 -0800445 }
446
447 public void setPersonReference(@Nullable String personReference) {
linyuh183cb712017-12-27 17:02:37 -0800448 this.personReference = personReference;
Eric Erfanianccca3152017-02-22 16:32:36 -0800449 }
450 }
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -0700451
452 private static void showGroupSummaryNotification(@NonNull Context context) {
453 Notification.Builder summary = new Notification.Builder(context);
454 // Set notification as ongoing since calls are long-running versus a point-in-time notice.
455 summary.setOngoing(true);
456 // Make the notification prioritized over the other normal notifications.
457 summary.setPriority(Notification.PRIORITY_HIGH);
458 summary.setGroup(GROUP_KEY);
459 summary.setGroupSummary(true);
460 summary.setSmallIcon(R.drawable.quantum_ic_call_white_24);
461 if (BuildCompat.isAtLeastO()) {
462 summary.setChannelId(NotificationChannelId.DEFAULT);
463 }
464 DialerNotificationManager.notify(
465 context, GROUP_SUMMARY_NOTIFICATION_TAG, GROUP_SUMMARY_NOTIFICATION_ID, summary.build());
466 }
Eric Erfanianccca3152017-02-22 16:32:36 -0800467}