Home | History | Annotate | Download | only in incallui
      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