diff options
Diffstat (limited to 'InCallUI/src/com/android/incallui/ExternalCallNotifier.java')
-rw-r--r-- | InCallUI/src/com/android/incallui/ExternalCallNotifier.java | 406 |
1 files changed, 406 insertions, 0 deletions
diff --git a/InCallUI/src/com/android/incallui/ExternalCallNotifier.java b/InCallUI/src/com/android/incallui/ExternalCallNotifier.java new file mode 100644 index 000000000..40a2e02bf --- /dev/null +++ b/InCallUI/src/com/android/incallui/ExternalCallNotifier.java @@ -0,0 +1,406 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.incallui; + +import com.google.common.base.Preconditions; + +import com.android.contacts.common.ContactsUtils; +import com.android.contacts.common.compat.CallSdkCompat; +import com.android.contacts.common.preference.ContactsPreferences; +import com.android.contacts.common.util.BitmapUtil; +import com.android.contacts.common.util.ContactDisplayUtils; +import com.android.dialer.R; +import com.android.incallui.util.TelecomCallUtil; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.net.Uri; +import android.support.annotation.Nullable; +import android.telecom.Call; +import android.telecom.PhoneAccount; +import android.text.BidiFormatter; +import android.text.TextDirectionHeuristics; +import android.text.TextUtils; +import android.util.ArrayMap; + +import java.util.Map; + +/** + * Handles the display of notifications for "external calls". + * + * External calls are a representation of a call which is in progress on the user's other device + * (e.g. another phone, or a watch). + */ +public class ExternalCallNotifier implements ExternalCallList.ExternalCallListener { + + /** + * Tag used with the notification manager to uniquely identify external call notifications. + */ + private static final String NOTIFICATION_TAG = "EXTERNAL_CALL"; + + /** + * Represents a call and associated cached notification data. + */ + private static class NotificationInfo { + private final Call mCall; + private final int mNotificationId; + @Nullable private String mContentTitle; + @Nullable private Bitmap mLargeIcon; + @Nullable private String mPersonReference; + + public NotificationInfo(Call call, int notificationId) { + Preconditions.checkNotNull(call); + mCall = call; + mNotificationId = notificationId; + } + + public Call getCall() { + return mCall; + } + + public int getNotificationId() { + return mNotificationId; + } + + public @Nullable String getContentTitle() { + return mContentTitle; + } + + public @Nullable Bitmap getLargeIcon() { + return mLargeIcon; + } + + public @Nullable String getPersonReference() { + return mPersonReference; + } + + public void setContentTitle(@Nullable String contentTitle) { + mContentTitle = contentTitle; + } + + public void setLargeIcon(@Nullable Bitmap largeIcon) { + mLargeIcon = largeIcon; + } + + public void setPersonReference(@Nullable String personReference) { + mPersonReference = personReference; + } + } + + private final Context mContext; + private final ContactInfoCache mContactInfoCache; + private Map<Call, NotificationInfo> mNotifications = new ArrayMap<>(); + private int mNextUniqueNotificationId; + private ContactsPreferences mContactsPreferences; + + /** + * Initializes a new instance of the external call notifier. + */ + public ExternalCallNotifier(Context context, ContactInfoCache contactInfoCache) { + mContext = Preconditions.checkNotNull(context); + mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext); + mContactInfoCache = Preconditions.checkNotNull(contactInfoCache); + } + + /** + * Handles the addition of a new external call by showing a new notification. + * Triggered by {@link CallList#onCallAdded(android.telecom.Call)}. + */ + @Override + public void onExternalCallAdded(android.telecom.Call call) { + Log.i(this, "onExternalCallAdded " + call); + Preconditions.checkArgument(!mNotifications.containsKey(call)); + NotificationInfo info = new NotificationInfo(call, mNextUniqueNotificationId++); + mNotifications.put(call, info); + + showNotifcation(info); + } + + /** + * Handles the removal of an external call by hiding its associated notification. + * Triggered by {@link CallList#onCallRemoved(android.telecom.Call)}. + */ + @Override + public void onExternalCallRemoved(android.telecom.Call call) { + Log.i(this, "onExternalCallRemoved " + call); + + dismissNotification(call); + } + + /** + * Handles updates to an external call. + */ + @Override + public void onExternalCallUpdated(Call call) { + Preconditions.checkArgument(mNotifications.containsKey(call)); + postNotification(mNotifications.get(call)); + } + + /** + * Initiates a call pull given a notification ID. + * + * @param notificationId The notification ID associated with the external call which is to be + * pulled. + */ + public void pullExternalCall(int notificationId) { + for (NotificationInfo info : mNotifications.values()) { + if (info.getNotificationId() == notificationId) { + CallSdkCompat.pullExternalCall(info.getCall()); + return; + } + } + } + + /** + * Shows a notification for a new external call. Performs a contact cache lookup to find any + * associated photo and information for the call. + */ + private void showNotifcation(final NotificationInfo info) { + // We make a call to the contact info cache to query for supplemental data to what the + // call provides. This includes the contact name and photo. + // This callback will always get called immediately and synchronously with whatever data + // it has available, and may make a subsequent call later (same thread) if it had to + // call into the contacts provider for more data. + com.android.incallui.Call incallCall = new com.android.incallui.Call(info.getCall(), + false /* registerCallback */); + + mContactInfoCache.findInfo(incallCall, false /* isIncoming */, + new ContactInfoCache.ContactInfoCacheCallback() { + @Override + public void onContactInfoComplete(String callId, + ContactInfoCache.ContactCacheEntry entry) { + + // Ensure notification still exists as the external call could have been + // removed during async contact info lookup. + if (mNotifications.containsKey(info.getCall())) { + saveContactInfo(info, entry); + } + } + + @Override + public void onImageLoadComplete(String callId, + ContactInfoCache.ContactCacheEntry entry) { + + // Ensure notification still exists as the external call could have been + // removed during async contact info lookup. + if (mNotifications.containsKey(info.getCall())) { + savePhoto(info, entry); + } + } + + @Override + public void onContactInteractionsInfoComplete(String callId, + ContactInfoCache.ContactCacheEntry entry) { + } + }); + } + + /** + * Dismisses a notification for an external call. + */ + private void dismissNotification(Call call) { + Preconditions.checkArgument(mNotifications.containsKey(call)); + + NotificationManager notificationManager = + (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(NOTIFICATION_TAG, mNotifications.get(call).getNotificationId()); + + mNotifications.remove(call); + } + + /** + * Attempts to build a large icon to use for the notification based on the contact info and + * post the updated notification to the notification manager. + */ + private void savePhoto(NotificationInfo info, ContactInfoCache.ContactCacheEntry entry) { + Bitmap largeIcon = getLargeIconToDisplay(mContext, entry, info.getCall()); + if (largeIcon != null) { + largeIcon = getRoundedIcon(mContext, largeIcon); + } + info.setLargeIcon(largeIcon); + postNotification(info); + } + + /** + * Builds and stores the contact information the notification will display and posts the updated + * notification to the notification manager. + */ + private void saveContactInfo(NotificationInfo info, ContactInfoCache.ContactCacheEntry entry) { + info.setContentTitle(getContentTitle(mContext, mContactsPreferences, + entry, info.getCall())); + info.setPersonReference(getPersonReference(entry, info.getCall())); + postNotification(info); + } + + /** + * Rebuild an existing or show a new notification given {@link NotificationInfo}. + */ + private void postNotification(NotificationInfo info) { + Log.i(this, "postNotification : " + info.getContentTitle()); + Notification.Builder builder = new Notification.Builder(mContext); + // Set notification as ongoing since calls are long-running versus a point-in-time notice. + builder.setOngoing(true); + // Make the notification prioritized over the other normal notifications. + builder.setPriority(Notification.PRIORITY_HIGH); + // Set the content ("Ongoing call on another device") + builder.setContentText(mContext.getString(R.string.notification_external_call)); + builder.setSmallIcon(R.drawable.ic_call_white_24dp); + builder.setContentTitle(info.getContentTitle()); + builder.setLargeIcon(info.getLargeIcon()); + builder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color)); + builder.addPerson(info.getPersonReference()); + + // Where the external call supports being transferred to the local device, add an action + // to the notification to initiate the call pull process. + if ((info.getCall().getDetails().getCallCapabilities() + & CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL) + == CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL) { + + Intent intent = new Intent( + NotificationBroadcastReceiver.ACTION_PULL_EXTERNAL_CALL, null, mContext, + NotificationBroadcastReceiver.class); + intent.putExtra(NotificationBroadcastReceiver.EXTRA_NOTIFICATION_ID, + info.getNotificationId()); + + builder.addAction(new Notification.Action.Builder(R.drawable.ic_call_white_24dp, + mContext.getText(R.string.notification_transfer_call), + PendingIntent.getBroadcast(mContext, 0, intent, 0)).build()); + } + + /** + * This builder is used for the notification shown when the device is locked and the user + * has set their notification settings to 'hide sensitive content' + * {@see Notification.Builder#setPublicVersion}. + */ + Notification.Builder publicBuilder = new Notification.Builder(mContext); + publicBuilder.setSmallIcon(R.drawable.ic_call_white_24dp); + publicBuilder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color)); + + builder.setPublicVersion(publicBuilder.build()); + Notification notification = builder.build(); + + NotificationManager notificationManager = + (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(NOTIFICATION_TAG, info.getNotificationId(), notification); + } + + /** + * Finds a large icon to display in a notification for a call. For conference calls, a + * conference call icon is used, otherwise if contact info is specified, the user's contact + * photo or avatar is used. + * + * @param context The context. + * @param contactInfo The contact cache info. + * @param call The call. + * @return The large icon to use for the notification. + */ + private @Nullable Bitmap getLargeIconToDisplay(Context context, + ContactInfoCache.ContactCacheEntry contactInfo, android.telecom.Call call) { + + Bitmap largeIcon = null; + if (call.getDetails().hasProperty(android.telecom.Call.Details.PROPERTY_CONFERENCE) && + !call.getDetails() + .hasProperty(android.telecom.Call.Details.PROPERTY_GENERIC_CONFERENCE)) { + + largeIcon = BitmapFactory.decodeResource(context.getResources(), + R.drawable.img_conference); + } + if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) { + largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap(); + } + return largeIcon; + } + + /** + * Given a bitmap, returns a rounded version of the icon suitable for display in a notification. + * + * @param context The context. + * @param bitmap The bitmap to round. + * @return The rounded bitmap. + */ + private @Nullable Bitmap getRoundedIcon(Context context, @Nullable Bitmap bitmap) { + if (bitmap == null) { + return null; + } + final int height = (int) context.getResources().getDimension( + android.R.dimen.notification_large_icon_height); + final int width = (int) context.getResources().getDimension( + android.R.dimen.notification_large_icon_width); + return BitmapUtil.getRoundedBitmap(bitmap, width, height); + } + + /** + * Builds a notification content title for a call. If the call is a conference call, it is + * identified as such. Otherwise an attempt is made to show an associated contact name or + * phone number. + * + * @param context The context. + * @param contactsPreferences Contacts preferences, used to determine the preferred formatting + * for contact names. + * @param contactInfo The contact info which was looked up in the contact cache. + * @param call The call to generate a title for. + * @return The content title. + */ + private @Nullable String getContentTitle(Context context, + @Nullable ContactsPreferences contactsPreferences, + ContactInfoCache.ContactCacheEntry contactInfo, android.telecom.Call call) { + + if (call.getDetails().hasProperty(android.telecom.Call.Details.PROPERTY_CONFERENCE) && + !call.getDetails() + .hasProperty(android.telecom.Call.Details.PROPERTY_GENERIC_CONFERENCE)) { + + return context.getResources().getString(R.string.card_title_conf_call); + } + + String preferredName = ContactDisplayUtils.getPreferredDisplayName(contactInfo.namePrimary, + contactInfo.nameAlternative, contactsPreferences); + if (TextUtils.isEmpty(preferredName)) { + return TextUtils.isEmpty(contactInfo.number) ? null : BidiFormatter.getInstance() + .unicodeWrap(contactInfo.number, TextDirectionHeuristics.LTR); + } + return preferredName; + } + + /** + * Gets a "person reference" for a notification, used by the system to determine whether the + * notification should be allowed past notification interruption filters. + * + * @param contactInfo The contact info from cache. + * @param call The call. + * @return the person reference. + */ + private String getPersonReference(ContactInfoCache.ContactCacheEntry contactInfo, + Call call) { + + String number = TelecomCallUtil.getNumber(call); + // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed. + // So, do not pass {@link Contacts#CONTENT_LOOKUP_URI} to NotificationManager to avoid + // NotificationManager using it. + if (contactInfo.lookupUri != null && contactInfo.userType != ContactsUtils.USER_TYPE_WORK) { + return contactInfo.lookupUri.toString(); + } else if (!TextUtils.isEmpty(number)) { + return Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null).toString(); + } + return ""; + } +} |