• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
NotificationInfo(Call call, int notificationId)71         public NotificationInfo(Call call, int notificationId) {
72             Preconditions.checkNotNull(call);
73             mCall = call;
74             mNotificationId = notificationId;
75         }
76 
getCall()77         public Call getCall() {
78             return mCall;
79         }
80 
getNotificationId()81         public int getNotificationId() {
82             return mNotificationId;
83         }
84 
getContentTitle()85         public @Nullable String getContentTitle() {
86             return mContentTitle;
87         }
88 
getLargeIcon()89         public @Nullable Bitmap getLargeIcon() {
90             return mLargeIcon;
91         }
92 
getPersonReference()93         public @Nullable String getPersonReference() {
94             return mPersonReference;
95         }
96 
setContentTitle(@ullable String contentTitle)97         public void setContentTitle(@Nullable String contentTitle) {
98             mContentTitle = contentTitle;
99         }
100 
setLargeIcon(@ullable Bitmap largeIcon)101         public void setLargeIcon(@Nullable Bitmap largeIcon) {
102             mLargeIcon = largeIcon;
103         }
104 
setPersonReference(@ullable String personReference)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      */
ExternalCallNotifier(Context context, ContactInfoCache contactInfoCache)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
onExternalCallAdded(android.telecom.Call call)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
onExternalCallRemoved(android.telecom.Call call)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
onExternalCallUpdated(Call call)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      */
pullExternalCall(int notificationId)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      */
showNotifcation(final NotificationInfo info)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      */
dismissNotification(Call call)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      */
savePhoto(NotificationInfo info, ContactInfoCache.ContactCacheEntry entry)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      */
saveContactInfo(NotificationInfo info, ContactInfoCache.ContactCacheEntry entry)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      */
postNotification(NotificationInfo info)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      */
getLargeIconToDisplay(Context context, ContactInfoCache.ContactCacheEntry contactInfo, android.telecom.Call call)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      */
getRoundedIcon(Context context, @Nullable Bitmap bitmap)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      */
getContentTitle(Context context, @Nullable ContactsPreferences contactsPreferences, ContactInfoCache.ContactCacheEntry contactInfo, android.telecom.Call call)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      */
getPersonReference(ContactInfoCache.ContactCacheEntry contactInfo, Call call)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