blob: 160e2502fc3ea75a04c280f95fa04c14bb471814 [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;
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -070041import com.android.dialer.common.Assert;
42import com.android.dialer.contactphoto.BitmapUtil;
twyen5578d922018-06-25 12:36:26 -070043import com.android.dialer.contacts.ContactsComponent;
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -070044import com.android.dialer.notification.DialerNotificationManager;
Eric Erfanianea7890c2017-06-19 12:40:59 -070045import com.android.dialer.notification.NotificationChannelId;
twyena4745bd2017-12-12 18:40:11 -080046import com.android.dialer.telecom.TelecomCallUtil;
calderwoodraa93df432018-05-23 12:59:03 -070047import com.android.dialer.theme.base.ThemeComponent;
Eric Erfanianccca3152017-02-22 16:32:36 -080048import com.android.incallui.call.DialerCall;
49import com.android.incallui.call.DialerCallDelegate;
50import com.android.incallui.call.ExternalCallList;
51import com.android.incallui.latencyreport.LatencyReport;
Eric Erfanianccca3152017-02-22 16:32:36 -080052import java.util.Map;
53
54/**
55 * Handles the display of notifications for "external calls".
56 *
57 * <p>External calls are a representation of a call which is in progress on the user's other device
58 * (e.g. another phone, or a watch).
59 */
60public class ExternalCallNotifier implements ExternalCallList.ExternalCallListener {
61
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -070062 /**
63 * Common tag for all external call notifications. Unlike other grouped notifications in Dialer,
64 * external call notifications are uniquely identified by ID.
65 */
Eric Erfanianea7890c2017-06-19 12:40:59 -070066 private static final String NOTIFICATION_TAG = "EXTERNAL_CALL";
Eric Erfanianccca3152017-02-22 16:32:36 -080067
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -070068 private static final int GROUP_SUMMARY_NOTIFICATION_ID = -1;
69 private static final String GROUP_SUMMARY_NOTIFICATION_TAG = "GroupSummary_ExternalCall";
70 /**
71 * Key used to associate all external call notifications and the summary as belonging to a single
72 * group.
73 */
74 private static final String GROUP_KEY = "ExternalCallGroup";
Eric Erfanianea7890c2017-06-19 12:40:59 -070075
linyuh183cb712017-12-27 17:02:37 -080076 private final Context context;
77 private final ContactInfoCache contactInfoCache;
78 private Map<Call, NotificationInfo> notifications = new ArrayMap<>();
79 private int nextUniqueNotificationId;
Eric Erfanianccca3152017-02-22 16:32:36 -080080
81 /** Initializes a new instance of the external call notifier. */
82 public ExternalCallNotifier(
83 @NonNull Context context, @NonNull ContactInfoCache contactInfoCache) {
linyuh183cb712017-12-27 17:02:37 -080084 this.context = context;
linyuh183cb712017-12-27 17:02:37 -080085 this.contactInfoCache = contactInfoCache;
Eric Erfanianccca3152017-02-22 16:32:36 -080086 }
87
88 /**
89 * Handles the addition of a new external call by showing a new notification. Triggered by {@link
90 * CallList#onCallAdded(android.telecom.Call)}.
91 */
92 @Override
93 public void onExternalCallAdded(android.telecom.Call call) {
94 Log.i(this, "onExternalCallAdded " + call);
linyuh183cb712017-12-27 17:02:37 -080095 Assert.checkArgument(!notifications.containsKey(call));
96 NotificationInfo info = new NotificationInfo(call, nextUniqueNotificationId++);
97 notifications.put(call, info);
Eric Erfanianccca3152017-02-22 16:32:36 -080098
99 showNotifcation(info);
100 }
101
102 /**
103 * Handles the removal of an external call by hiding its associated notification. Triggered by
104 * {@link CallList#onCallRemoved(android.telecom.Call)}.
105 */
106 @Override
107 public void onExternalCallRemoved(android.telecom.Call call) {
108 Log.i(this, "onExternalCallRemoved " + call);
109
110 dismissNotification(call);
111 }
112
113 /** Handles updates to an external call. */
114 @Override
115 public void onExternalCallUpdated(Call call) {
linyuh183cb712017-12-27 17:02:37 -0800116 Assert.checkArgument(notifications.containsKey(call));
117 postNotification(notifications.get(call));
Eric Erfanianccca3152017-02-22 16:32:36 -0800118 }
119
120 @Override
121 public void onExternalCallPulled(Call call) {
122 // no-op; if an external call is pulled, it will be removed via onExternalCallRemoved.
123 }
124
125 /**
126 * Initiates a call pull given a notification ID.
127 *
128 * @param notificationId The notification ID associated with the external call which is to be
129 * pulled.
130 */
131 @TargetApi(VERSION_CODES.N_MR1)
132 public void pullExternalCall(int notificationId) {
linyuh183cb712017-12-27 17:02:37 -0800133 for (NotificationInfo info : notifications.values()) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800134 if (info.getNotificationId() == notificationId
135 && CallCompat.canPullExternalCall(info.getCall())) {
136 info.getCall().pullExternalCall();
137 return;
138 }
139 }
140 }
141
142 /**
143 * Shows a notification for a new external call. Performs a contact cache lookup to find any
144 * associated photo and information for the call.
145 */
146 private void showNotifcation(final NotificationInfo info) {
147 // We make a call to the contact info cache to query for supplemental data to what the
148 // call provides. This includes the contact name and photo.
149 // This callback will always get called immediately and synchronously with whatever data
150 // it has available, and may make a subsequent call later (same thread) if it had to
151 // call into the contacts provider for more data.
152 DialerCall dialerCall =
153 new DialerCall(
linyuh183cb712017-12-27 17:02:37 -0800154 context,
Eric Erfanianccca3152017-02-22 16:32:36 -0800155 new DialerCallDelegateStub(),
156 info.getCall(),
157 new LatencyReport(),
158 false /* registerCallback */);
159
linyuh183cb712017-12-27 17:02:37 -0800160 contactInfoCache.findInfo(
Eric Erfanianccca3152017-02-22 16:32:36 -0800161 dialerCall,
162 false /* isIncoming */,
163 new ContactInfoCache.ContactInfoCacheCallback() {
164 @Override
165 public void onContactInfoComplete(
166 String callId, ContactInfoCache.ContactCacheEntry entry) {
167
168 // Ensure notification still exists as the external call could have been
169 // removed during async contact info lookup.
linyuh183cb712017-12-27 17:02:37 -0800170 if (notifications.containsKey(info.getCall())) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800171 saveContactInfo(info, entry);
172 }
173 }
174
175 @Override
176 public void onImageLoadComplete(String callId, ContactInfoCache.ContactCacheEntry entry) {
177
178 // Ensure notification still exists as the external call could have been
179 // removed during async contact info lookup.
linyuh183cb712017-12-27 17:02:37 -0800180 if (notifications.containsKey(info.getCall())) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800181 savePhoto(info, entry);
182 }
183 }
184 });
185 }
186
187 /** Dismisses a notification for an external call. */
188 private void dismissNotification(Call call) {
linyuh183cb712017-12-27 17:02:37 -0800189 Assert.checkArgument(notifications.containsKey(call));
Eric Erfanianccca3152017-02-22 16:32:36 -0800190
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -0700191 // This will also dismiss the group summary if there are no more external call notifications.
192 DialerNotificationManager.cancel(
linyuh183cb712017-12-27 17:02:37 -0800193 context, NOTIFICATION_TAG, notifications.get(call).getNotificationId());
Eric Erfanianccca3152017-02-22 16:32:36 -0800194
linyuh183cb712017-12-27 17:02:37 -0800195 notifications.remove(call);
Eric Erfanianccca3152017-02-22 16:32:36 -0800196 }
197
198 /**
199 * Attempts to build a large icon to use for the notification based on the contact info and post
200 * the updated notification to the notification manager.
201 */
202 private void savePhoto(NotificationInfo info, ContactInfoCache.ContactCacheEntry entry) {
linyuh183cb712017-12-27 17:02:37 -0800203 Bitmap largeIcon = getLargeIconToDisplay(context, entry, info.getCall());
Eric Erfanianccca3152017-02-22 16:32:36 -0800204 if (largeIcon != null) {
linyuh183cb712017-12-27 17:02:37 -0800205 largeIcon = getRoundedIcon(context, largeIcon);
Eric Erfanianccca3152017-02-22 16:32:36 -0800206 }
207 info.setLargeIcon(largeIcon);
208 postNotification(info);
209 }
210
211 /**
212 * Builds and stores the contact information the notification will display and posts the updated
213 * notification to the notification manager.
214 */
215 private void saveContactInfo(NotificationInfo info, ContactInfoCache.ContactCacheEntry entry) {
twyen5578d922018-06-25 12:36:26 -0700216 info.setContentTitle(getContentTitle(context, entry, info.getCall()));
Eric Erfanianccca3152017-02-22 16:32:36 -0800217 info.setPersonReference(getPersonReference(entry, info.getCall()));
218 postNotification(info);
219 }
220
221 /** Rebuild an existing or show a new notification given {@link NotificationInfo}. */
222 private void postNotification(NotificationInfo info) {
linyuh183cb712017-12-27 17:02:37 -0800223 Notification.Builder builder = new Notification.Builder(context);
Eric Erfanianccca3152017-02-22 16:32:36 -0800224 // Set notification as ongoing since calls are long-running versus a point-in-time notice.
225 builder.setOngoing(true);
226 // Make the notification prioritized over the other normal notifications.
227 builder.setPriority(Notification.PRIORITY_HIGH);
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -0700228 builder.setGroup(GROUP_KEY);
Eric Erfanianccca3152017-02-22 16:32:36 -0800229
230 boolean isVideoCall = VideoProfile.isVideo(info.getCall().getDetails().getVideoState());
231 // Set the content ("Ongoing call on another device")
232 builder.setContentText(
linyuh183cb712017-12-27 17:02:37 -0800233 context.getString(
Eric Erfanianccca3152017-02-22 16:32:36 -0800234 isVideoCall
235 ? R.string.notification_external_video_call
236 : R.string.notification_external_call));
237 builder.setSmallIcon(R.drawable.quantum_ic_call_white_24);
238 builder.setContentTitle(info.getContentTitle());
239 builder.setLargeIcon(info.getLargeIcon());
calderwoodraa93df432018-05-23 12:59:03 -0700240 builder.setColor(ThemeComponent.get(context).theme().getColorPrimary());
Eric Erfanianccca3152017-02-22 16:32:36 -0800241 builder.addPerson(info.getPersonReference());
Eric Erfanianea7890c2017-06-19 12:40:59 -0700242 if (BuildCompat.isAtLeastO()) {
243 builder.setChannelId(NotificationChannelId.DEFAULT);
244 }
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700245
Eric Erfanianccca3152017-02-22 16:32:36 -0800246 // Where the external call supports being transferred to the local device, add an action
247 // to the notification to initiate the call pull process.
248 if (CallCompat.canPullExternalCall(info.getCall())) {
249
250 Intent intent =
251 new Intent(
252 NotificationBroadcastReceiver.ACTION_PULL_EXTERNAL_CALL,
253 null,
linyuh183cb712017-12-27 17:02:37 -0800254 context,
Eric Erfanianccca3152017-02-22 16:32:36 -0800255 NotificationBroadcastReceiver.class);
256 intent.putExtra(
257 NotificationBroadcastReceiver.EXTRA_NOTIFICATION_ID, info.getNotificationId());
258 builder.addAction(
259 new Notification.Action.Builder(
260 R.drawable.quantum_ic_call_white_24,
linyuh183cb712017-12-27 17:02:37 -0800261 context.getString(
Eric Erfanianccca3152017-02-22 16:32:36 -0800262 isVideoCall
263 ? R.string.notification_take_video_call
264 : R.string.notification_take_call),
linyuh183cb712017-12-27 17:02:37 -0800265 PendingIntent.getBroadcast(context, info.getNotificationId(), intent, 0))
Eric Erfanianccca3152017-02-22 16:32:36 -0800266 .build());
267 }
268
269 /**
270 * This builder is used for the notification shown when the device is locked and the user has
271 * set their notification settings to 'hide sensitive content' {@see
272 * Notification.Builder#setPublicVersion}.
273 */
linyuh183cb712017-12-27 17:02:37 -0800274 Notification.Builder publicBuilder = new Notification.Builder(context);
Eric Erfanianccca3152017-02-22 16:32:36 -0800275 publicBuilder.setSmallIcon(R.drawable.quantum_ic_call_white_24);
calderwoodraa93df432018-05-23 12:59:03 -0700276 publicBuilder.setColor(ThemeComponent.get(context).theme().getColorPrimary());
Eric Erfanianea7890c2017-06-19 12:40:59 -0700277 if (BuildCompat.isAtLeastO()) {
278 publicBuilder.setChannelId(NotificationChannelId.DEFAULT);
279 }
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700280
Eric Erfanianccca3152017-02-22 16:32:36 -0800281 builder.setPublicVersion(publicBuilder.build());
282 Notification notification = builder.build();
283
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -0700284 DialerNotificationManager.notify(
linyuh183cb712017-12-27 17:02:37 -0800285 context, NOTIFICATION_TAG, info.getNotificationId(), notification);
Eric Erfanianccca3152017-02-22 16:32:36 -0800286
linyuh183cb712017-12-27 17:02:37 -0800287 showGroupSummaryNotification(context);
Eric Erfanianccca3152017-02-22 16:32:36 -0800288 }
289
290 /**
291 * Finds a large icon to display in a notification for a call. For conference calls, a conference
292 * call icon is used, otherwise if contact info is specified, the user's contact photo or avatar
293 * is used.
294 *
295 * @param context The context.
296 * @param contactInfo The contact cache info.
297 * @param call The call.
298 * @return The large icon to use for the notification.
299 */
300 private @Nullable Bitmap getLargeIconToDisplay(
301 Context context, ContactInfoCache.ContactCacheEntry contactInfo, android.telecom.Call call) {
302
303 Bitmap largeIcon = null;
304 if (call.getDetails().hasProperty(android.telecom.Call.Details.PROPERTY_CONFERENCE)
305 && !call.getDetails()
306 .hasProperty(android.telecom.Call.Details.PROPERTY_GENERIC_CONFERENCE)) {
307
Eric Erfanian83b20212017-05-31 08:53:10 -0700308 largeIcon =
309 BitmapFactory.decodeResource(
310 context.getResources(), R.drawable.quantum_ic_group_vd_theme_24);
Eric Erfanianccca3152017-02-22 16:32:36 -0800311 }
312 if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) {
313 largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap();
314 }
315 return largeIcon;
316 }
317
318 /**
319 * Given a bitmap, returns a rounded version of the icon suitable for display in a notification.
320 *
321 * @param context The context.
322 * @param bitmap The bitmap to round.
323 * @return The rounded bitmap.
324 */
325 private @Nullable Bitmap getRoundedIcon(Context context, @Nullable Bitmap bitmap) {
326 if (bitmap == null) {
327 return null;
328 }
329 final int height =
330 (int) context.getResources().getDimension(android.R.dimen.notification_large_icon_height);
331 final int width =
332 (int) context.getResources().getDimension(android.R.dimen.notification_large_icon_width);
333 return BitmapUtil.getRoundedBitmap(bitmap, width, height);
334 }
335
336 /**
337 * Builds a notification content title for a call. If the call is a conference call, it is
338 * identified as such. Otherwise an attempt is made to show an associated contact name or phone
339 * number.
340 *
341 * @param context The context.
Eric Erfanianccca3152017-02-22 16:32:36 -0800342 * @param contactInfo The contact info which was looked up in the contact cache.
343 * @param call The call to generate a title for.
344 * @return The content title.
345 */
346 private @Nullable String getContentTitle(
twyen5578d922018-06-25 12:36:26 -0700347 Context context, ContactInfoCache.ContactCacheEntry contactInfo, android.telecom.Call call) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800348
Eric Erfanian91ce7d22017-06-05 13:35:02 -0700349 if (call.getDetails().hasProperty(android.telecom.Call.Details.PROPERTY_CONFERENCE)) {
350 return CallerInfoUtils.getConferenceString(
351 context,
352 call.getDetails().hasProperty(android.telecom.Call.Details.PROPERTY_GENERIC_CONFERENCE));
Eric Erfanianccca3152017-02-22 16:32:36 -0800353 }
354
355 String preferredName =
twyen5578d922018-06-25 12:36:26 -0700356 ContactsComponent.get(context)
357 .contactDisplayPreferences()
358 .getDisplayName(contactInfo.namePrimary, contactInfo.nameAlternative);
Eric Erfanianccca3152017-02-22 16:32:36 -0800359 if (TextUtils.isEmpty(preferredName)) {
360 return TextUtils.isEmpty(contactInfo.number)
361 ? null
362 : BidiFormatter.getInstance()
363 .unicodeWrap(contactInfo.number, TextDirectionHeuristics.LTR);
364 }
365 return preferredName;
366 }
367
368 /**
369 * Gets a "person reference" for a notification, used by the system to determine whether the
370 * notification should be allowed past notification interruption filters.
371 *
372 * @param contactInfo The contact info from cache.
373 * @param call The call.
374 * @return the person reference.
375 */
376 private String getPersonReference(ContactInfoCache.ContactCacheEntry contactInfo, Call call) {
377
378 String number = TelecomCallUtil.getNumber(call);
379 // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed.
380 // So, do not pass {@link Contacts#CONTENT_LOOKUP_URI} to NotificationManager to avoid
381 // NotificationManager using it.
382 if (contactInfo.lookupUri != null && contactInfo.userType != ContactsUtils.USER_TYPE_WORK) {
383 return contactInfo.lookupUri.toString();
384 } else if (!TextUtils.isEmpty(number)) {
385 return Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null).toString();
386 }
387 return "";
388 }
389
390 private static class DialerCallDelegateStub implements DialerCallDelegate {
391
392 @Override
393 public DialerCall getDialerCallFromTelecomCall(Call telecomCall) {
394 return null;
395 }
396 }
397
398 /** Represents a call and associated cached notification data. */
399 private static class NotificationInfo {
400
linyuh183cb712017-12-27 17:02:37 -0800401 @NonNull private final Call call;
402 private final int notificationId;
403 @Nullable private String contentTitle;
404 @Nullable private Bitmap largeIcon;
405 @Nullable private String personReference;
Eric Erfanianccca3152017-02-22 16:32:36 -0800406
407 public NotificationInfo(@NonNull Call call, int notificationId) {
linyuh183cb712017-12-27 17:02:37 -0800408 this.call = call;
409 this.notificationId = notificationId;
Eric Erfanianccca3152017-02-22 16:32:36 -0800410 }
411
412 public Call getCall() {
linyuh183cb712017-12-27 17:02:37 -0800413 return call;
Eric Erfanianccca3152017-02-22 16:32:36 -0800414 }
415
416 public int getNotificationId() {
linyuh183cb712017-12-27 17:02:37 -0800417 return notificationId;
Eric Erfanianccca3152017-02-22 16:32:36 -0800418 }
419
420 public @Nullable String getContentTitle() {
linyuh183cb712017-12-27 17:02:37 -0800421 return contentTitle;
Eric Erfanianccca3152017-02-22 16:32:36 -0800422 }
423
424 public void setContentTitle(@Nullable String contentTitle) {
linyuh183cb712017-12-27 17:02:37 -0800425 this.contentTitle = contentTitle;
Eric Erfanianccca3152017-02-22 16:32:36 -0800426 }
427
428 public @Nullable Bitmap getLargeIcon() {
linyuh183cb712017-12-27 17:02:37 -0800429 return largeIcon;
Eric Erfanianccca3152017-02-22 16:32:36 -0800430 }
431
432 public void setLargeIcon(@Nullable Bitmap largeIcon) {
linyuh183cb712017-12-27 17:02:37 -0800433 this.largeIcon = largeIcon;
Eric Erfanianccca3152017-02-22 16:32:36 -0800434 }
435
436 public @Nullable String getPersonReference() {
linyuh183cb712017-12-27 17:02:37 -0800437 return personReference;
Eric Erfanianccca3152017-02-22 16:32:36 -0800438 }
439
440 public void setPersonReference(@Nullable String personReference) {
linyuh183cb712017-12-27 17:02:37 -0800441 this.personReference = personReference;
Eric Erfanianccca3152017-02-22 16:32:36 -0800442 }
443 }
Eric Erfanianfc0eb8c2017-08-31 06:57:16 -0700444
445 private static void showGroupSummaryNotification(@NonNull Context context) {
446 Notification.Builder summary = new Notification.Builder(context);
447 // Set notification as ongoing since calls are long-running versus a point-in-time notice.
448 summary.setOngoing(true);
449 // Make the notification prioritized over the other normal notifications.
450 summary.setPriority(Notification.PRIORITY_HIGH);
451 summary.setGroup(GROUP_KEY);
452 summary.setGroupSummary(true);
453 summary.setSmallIcon(R.drawable.quantum_ic_call_white_24);
454 if (BuildCompat.isAtLeastO()) {
455 summary.setChannelId(NotificationChannelId.DEFAULT);
456 }
457 DialerNotificationManager.notify(
458 context, GROUP_SUMMARY_NOTIFICATION_TAG, GROUP_SUMMARY_NOTIFICATION_ID, summary.build());
459 }
Eric Erfanianccca3152017-02-22 16:32:36 -0800460}