• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2013 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 static android.telecom.Call.Details.PROPERTY_HIGH_DEF_AUDIO;
20 import static com.android.contacts.common.compat.CallCompat.Details.PROPERTY_ENTERPRISE_CALL;
21 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST;
22 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VIDEO_INCOMING_CALL;
23 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VOICE_INCOMING_CALL;
24 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_INCOMING_CALL;
25 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_VIDEO_UPGRADE_REQUEST;
26 import static com.android.incallui.NotificationBroadcastReceiver.ACTION_HANG_UP_ONGOING_CALL;
27 
28 import android.Manifest;
29 import android.app.ActivityManager;
30 import android.app.Notification;
31 import android.app.Notification.Builder;
32 import android.app.NotificationManager;
33 import android.app.PendingIntent;
34 import android.content.Context;
35 import android.content.Intent;
36 import android.content.res.Resources;
37 import android.graphics.Bitmap;
38 import android.graphics.drawable.BitmapDrawable;
39 import android.graphics.drawable.Drawable;
40 import android.graphics.drawable.Icon;
41 import android.media.AudioAttributes;
42 import android.net.Uri;
43 import android.os.Build.VERSION;
44 import android.os.Build.VERSION_CODES;
45 import android.support.annotation.ColorRes;
46 import android.support.annotation.NonNull;
47 import android.support.annotation.Nullable;
48 import android.support.annotation.RequiresPermission;
49 import android.support.annotation.StringRes;
50 import android.support.annotation.VisibleForTesting;
51 import android.support.v4.os.BuildCompat;
52 import android.telecom.Call.Details;
53 import android.telecom.PhoneAccount;
54 import android.telecom.PhoneAccountHandle;
55 import android.telecom.TelecomManager;
56 import android.text.BidiFormatter;
57 import android.text.Spannable;
58 import android.text.SpannableString;
59 import android.text.TextDirectionHeuristics;
60 import android.text.TextUtils;
61 import android.text.style.ForegroundColorSpan;
62 import com.android.contacts.common.ContactsUtils;
63 import com.android.contacts.common.ContactsUtils.UserType;
64 import com.android.contacts.common.lettertiles.LetterTileDrawable;
65 import com.android.contacts.common.preference.ContactsPreferences;
66 import com.android.contacts.common.util.BitmapUtil;
67 import com.android.contacts.common.util.ContactDisplayUtils;
68 import com.android.dialer.common.LogUtil;
69 import com.android.dialer.enrichedcall.EnrichedCallComponent;
70 import com.android.dialer.enrichedcall.EnrichedCallManager;
71 import com.android.dialer.enrichedcall.Session;
72 import com.android.dialer.multimedia.MultimediaData;
73 import com.android.dialer.notification.NotificationChannelManager;
74 import com.android.dialer.notification.NotificationChannelManager.Channel;
75 import com.android.dialer.oem.MotorolaUtils;
76 import com.android.dialer.util.DrawableConverter;
77 import com.android.incallui.ContactInfoCache.ContactCacheEntry;
78 import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
79 import com.android.incallui.InCallPresenter.InCallState;
80 import com.android.incallui.async.PausableExecutorImpl;
81 import com.android.incallui.call.CallList;
82 import com.android.incallui.call.DialerCall;
83 import com.android.incallui.call.DialerCallListener;
84 import com.android.incallui.ringtone.DialerRingtoneManager;
85 import com.android.incallui.ringtone.InCallTonePlayer;
86 import com.android.incallui.ringtone.ToneGeneratorFactory;
87 import com.android.incallui.videotech.utils.SessionModificationState;
88 import java.util.List;
89 import java.util.Locale;
90 import java.util.Objects;
91 
92 /** This class adds Notifications to the status bar for the in-call experience. */
93 public class StatusBarNotifier
94     implements InCallPresenter.InCallStateListener, EnrichedCallManager.StateChangedListener {
95 
96   // Notification types
97   // Indicates that no notification is currently showing.
98   private static final int NOTIFICATION_NONE = 0;
99   // Notification for an active call. This is non-interruptive, but cannot be dismissed.
100   private static final int NOTIFICATION_IN_CALL = 1;
101   // Notification for incoming calls. This is interruptive and will show up as a HUN.
102   private static final int NOTIFICATION_INCOMING_CALL = 2;
103   // Notification for incoming calls in the case where there is already an active call.
104   // This is non-interruptive, but otherwise behaves the same as NOTIFICATION_INCOMING_CALL
105   private static final int NOTIFICATION_INCOMING_CALL_QUIET = 3;
106 
107   private static final int PENDING_INTENT_REQUEST_CODE_NON_FULL_SCREEN = 0;
108   private static final int PENDING_INTENT_REQUEST_CODE_FULL_SCREEN = 1;
109 
110   private static final long[] VIBRATE_PATTERN = new long[] {0, 1000, 1000};
111 
112   private final Context mContext;
113   private final ContactInfoCache mContactInfoCache;
114   private final NotificationManager mNotificationManager;
115   private final DialerRingtoneManager mDialerRingtoneManager;
116   @Nullable private ContactsPreferences mContactsPreferences;
117   private int mCurrentNotification = NOTIFICATION_NONE;
118   private int mCallState = DialerCall.State.INVALID;
119   private int mSavedIcon = 0;
120   private String mSavedContent = null;
121   private Bitmap mSavedLargeIcon;
122   private String mSavedContentTitle;
123   private Uri mRingtone;
124   private StatusBarCallListener mStatusBarCallListener;
125 
StatusBarNotifier(@onNull Context context, @NonNull ContactInfoCache contactInfoCache)126   public StatusBarNotifier(@NonNull Context context, @NonNull ContactInfoCache contactInfoCache) {
127     Objects.requireNonNull(context);
128     mContext = context;
129     mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext);
130     mContactInfoCache = contactInfoCache;
131     mNotificationManager = context.getSystemService(NotificationManager.class);
132     mDialerRingtoneManager =
133         new DialerRingtoneManager(
134             new InCallTonePlayer(new ToneGeneratorFactory(), new PausableExecutorImpl()),
135             CallList.getInstance());
136     mCurrentNotification = NOTIFICATION_NONE;
137   }
138 
139   /**
140    * Should only be called from a irrecoverable state where it is necessary to dismiss all
141    * notifications.
142    */
clearAllCallNotifications(Context backupContext)143   static void clearAllCallNotifications(Context backupContext) {
144     LogUtil.i(
145         "StatusBarNotifier.clearAllCallNotifications",
146         "something terrible happened, clear all InCall notifications");
147 
148     NotificationManager notificationManager =
149         backupContext.getSystemService(NotificationManager.class);
150     notificationManager.cancel(R.id.notification_ongoing_call);
151   }
152 
getWorkStringFromPersonalString(int resId)153   private static int getWorkStringFromPersonalString(int resId) {
154     if (resId == R.string.notification_ongoing_call) {
155       return R.string.notification_ongoing_work_call;
156     } else if (resId == R.string.notification_ongoing_call_wifi) {
157       return R.string.notification_ongoing_work_call_wifi;
158     } else if (resId == R.string.notification_incoming_call_wifi) {
159       return R.string.notification_incoming_work_call_wifi;
160     } else if (resId == R.string.notification_incoming_call) {
161       return R.string.notification_incoming_work_call;
162     } else {
163       return resId;
164     }
165   }
166 
167   /**
168    * Returns PendingIntent for answering a phone call. This will typically be used from Notification
169    * context.
170    */
createNotificationPendingIntent(Context context, String action)171   private static PendingIntent createNotificationPendingIntent(Context context, String action) {
172     final Intent intent = new Intent(action, null, context, NotificationBroadcastReceiver.class);
173     return PendingIntent.getBroadcast(context, 0, intent, 0);
174   }
175 
setColorized(@onNull Builder builder)176   private static void setColorized(@NonNull Builder builder) {
177     if (BuildCompat.isAtLeastO()) {
178       builder.setColorized(true);
179     }
180   }
181 
182   /** Creates notifications according to the state we receive from {@link InCallPresenter}. */
183   @Override
184   @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
onStateChange(InCallState oldState, InCallState newState, CallList callList)185   public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
186     LogUtil.d("StatusBarNotifier.onStateChange", "%s->%s", oldState, newState);
187     updateNotification(callList);
188   }
189 
190   @Override
onEnrichedCallStateChanged()191   public void onEnrichedCallStateChanged() {
192     LogUtil.enterBlock("StatusBarNotifier.onEnrichedCallStateChanged");
193     updateNotification(CallList.getInstance());
194   }
195 
196   /**
197    * Updates the phone app's status bar notification *and* launches the incoming call UI in response
198    * to a new incoming call.
199    *
200    * <p>If an incoming call is ringing (or call-waiting), the notification will also include a
201    * "fullScreenIntent" that will cause the InCallScreen to be launched, unless the current
202    * foreground activity is marked as "immersive".
203    *
204    * <p>(This is the mechanism that actually brings up the incoming call UI when we receive a "new
205    * ringing connection" event from the telephony layer.)
206    *
207    * <p>Also note that this method is safe to call even if the phone isn't actually ringing (or,
208    * more likely, if an incoming call *was* ringing briefly but then disconnected). In that case,
209    * we'll simply update or cancel the in-call notification based on the current phone state.
210    *
211    * @see #updateInCallNotification(CallList)
212    */
213   @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
updateNotification(CallList callList)214   public void updateNotification(CallList callList) {
215     updateInCallNotification(callList);
216   }
217 
218   /**
219    * Take down the in-call notification.
220    *
221    * @see #updateInCallNotification(CallList)
222    */
cancelNotification()223   private void cancelNotification() {
224     if (mStatusBarCallListener != null) {
225       setStatusBarCallListener(null);
226     }
227     if (mCurrentNotification != NOTIFICATION_NONE) {
228       LogUtil.i("StatusBarNotifier.cancelNotification", "cancel");
229       mNotificationManager.cancel(R.id.notification_ongoing_call);
230     }
231     mCurrentNotification = NOTIFICATION_NONE;
232   }
233 
234   /**
235    * Helper method for updateInCallNotification() and updateNotification(): Update the phone app's
236    * status bar notification based on the current telephony state, or cancels the notification if
237    * the phone is totally idle.
238    */
239   @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
updateInCallNotification(CallList callList)240   private void updateInCallNotification(CallList callList) {
241     LogUtil.d("StatusBarNotifier.updateInCallNotification", "");
242 
243     final DialerCall call = getCallToShow(callList);
244 
245     if (call != null) {
246       showNotification(callList, call);
247     } else {
248       cancelNotification();
249     }
250   }
251 
252   @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
showNotification(final CallList callList, final DialerCall call)253   private void showNotification(final CallList callList, final DialerCall call) {
254     final boolean isIncoming =
255         (call.getState() == DialerCall.State.INCOMING
256             || call.getState() == DialerCall.State.CALL_WAITING);
257     setStatusBarCallListener(new StatusBarCallListener(call));
258 
259     // we make a call to the contact info cache to query for supplemental data to what the
260     // call provides.  This includes the contact name and photo.
261     // This callback will always get called immediately and synchronously with whatever data
262     // it has available, and may make a subsequent call later (same thread) if it had to
263     // call into the contacts provider for more data.
264     mContactInfoCache.findInfo(
265         call,
266         isIncoming,
267         new ContactInfoCacheCallback() {
268           @Override
269           @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
270           public void onContactInfoComplete(String callId, ContactCacheEntry entry) {
271             DialerCall call = callList.getCallById(callId);
272             if (call != null) {
273               call.getLogState().contactLookupResult = entry.contactLookupResult;
274               buildAndSendNotification(callList, call, entry);
275             }
276           }
277 
278           @Override
279           @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
280           public void onImageLoadComplete(String callId, ContactCacheEntry entry) {
281             DialerCall call = callList.getCallById(callId);
282             if (call != null) {
283               buildAndSendNotification(callList, call, entry);
284             }
285           }
286         });
287   }
288 
289   /** Sets up the main Ui for the notification */
290   @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
buildAndSendNotification( CallList callList, DialerCall originalCall, ContactCacheEntry contactInfo)291   private void buildAndSendNotification(
292       CallList callList, DialerCall originalCall, ContactCacheEntry contactInfo) {
293     // This can get called to update an existing notification after contact information has come
294     // back. However, it can happen much later. Before we continue, we need to make sure that
295     // the call being passed in is still the one we want to show in the notification.
296     final DialerCall call = getCallToShow(callList);
297     if (call == null || !call.getId().equals(originalCall.getId())) {
298       return;
299     }
300 
301     final int callState = call.getState();
302 
303     // Check if data has changed; if nothing is different, don't issue another notification.
304     final int iconResId = getIconToDisplay(call);
305     Bitmap largeIcon = getLargeIconToDisplay(mContext, contactInfo, call);
306     final String content = getContentString(call, contactInfo.userType);
307     final String contentTitle = getContentTitle(contactInfo, call);
308 
309     final boolean isVideoUpgradeRequest =
310         call.getVideoTech().getSessionModificationState()
311             == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
312     final int notificationType;
313     if (callState == DialerCall.State.INCOMING
314         || callState == DialerCall.State.CALL_WAITING
315         || isVideoUpgradeRequest) {
316       boolean alreadyActive =
317           callList.getActiveOrBackgroundCall() != null
318               && InCallPresenter.getInstance().isShowingInCallUi();
319       notificationType =
320           alreadyActive ? NOTIFICATION_INCOMING_CALL_QUIET : NOTIFICATION_INCOMING_CALL;
321     } else {
322       notificationType = NOTIFICATION_IN_CALL;
323     }
324 
325     if (!checkForChangeAndSaveData(
326         iconResId,
327         content,
328         largeIcon,
329         contentTitle,
330         callState,
331         notificationType,
332         contactInfo.contactRingtoneUri)) {
333       return;
334     }
335 
336     if (largeIcon != null) {
337       largeIcon = getRoundedIcon(largeIcon);
338     }
339 
340     // This builder is used for the notification shown when the device is locked and the user
341     // has set their notification settings to 'hide sensitive content'
342     // {@see Notification.Builder#setPublicVersion}.
343     Notification.Builder publicBuilder = new Notification.Builder(mContext);
344     publicBuilder
345         .setSmallIcon(iconResId)
346         .setColor(mContext.getResources().getColor(R.color.dialer_theme_color, mContext.getTheme()))
347         // Hide work call state for the lock screen notification
348         .setContentTitle(getContentString(call, ContactsUtils.USER_TYPE_CURRENT));
349     setNotificationWhen(call, callState, publicBuilder);
350 
351     // Builder for the notification shown when the device is unlocked or the user has set their
352     // notification settings to 'show all notification content'.
353     final Notification.Builder builder = getNotificationBuilder();
354     builder.setPublicVersion(publicBuilder.build());
355 
356     // Set up the main intent to send the user to the in-call screen
357     builder.setContentIntent(createLaunchPendingIntent(false /* isFullScreen */));
358 
359     // Set the intent as a full screen intent as well if a call is incoming
360     PhoneAccountHandle accountHandle = call.getAccountHandle();
361     if (accountHandle == null) {
362       accountHandle = getAnyPhoneAccount();
363     }
364 
365     LogUtil.i("StatusBarNotifier.buildAndSendNotification", "notificationType=" + notificationType);
366     switch (notificationType) {
367       case NOTIFICATION_INCOMING_CALL:
368         NotificationChannelManager.applyChannel(
369             builder, mContext, Channel.INCOMING_CALL, accountHandle);
370         configureFullScreenIntent(builder, createLaunchPendingIntent(true /* isFullScreen */));
371         // Set the notification category and bump the priority for incoming calls
372         builder.setCategory(Notification.CATEGORY_CALL);
373         // This will be ignored on O+ and handled by the channel
374         //noinspection deprecation
375         builder.setPriority(Notification.PRIORITY_MAX);
376         if (mCurrentNotification != NOTIFICATION_INCOMING_CALL) {
377           LogUtil.i(
378               "StatusBarNotifier.buildAndSendNotification",
379               "Canceling old notification so this one can be noisy");
380           // Moving from a non-interuptive notification (or none) to a noisy one. Cancel the old
381           // notification (if there is one) so the fullScreenIntent or HUN will show
382           mNotificationManager.cancel(R.id.notification_ongoing_call);
383         }
384         break;
385       case NOTIFICATION_INCOMING_CALL_QUIET:
386         NotificationChannelManager.applyChannel(
387             builder, mContext, Channel.ONGOING_CALL, accountHandle);
388         break;
389       case NOTIFICATION_IN_CALL:
390         setColorized(publicBuilder);
391         setColorized(builder);
392         NotificationChannelManager.applyChannel(
393             builder, mContext, Channel.ONGOING_CALL, accountHandle);
394         break;
395     }
396 
397     // Set the content
398     builder.setContentText(content);
399     builder.setSmallIcon(iconResId);
400     builder.setContentTitle(contentTitle);
401     builder.setLargeIcon(largeIcon);
402     builder.setColor(
403         mContext.getResources().getColor(R.color.dialer_theme_color, mContext.getTheme()));
404 
405     if (isVideoUpgradeRequest) {
406       builder.setUsesChronometer(false);
407       addDismissUpgradeRequestAction(builder);
408       addAcceptUpgradeRequestAction(builder);
409     } else {
410       createIncomingCallNotification(call, callState, builder);
411     }
412 
413     addPersonReference(builder, contactInfo, call);
414 
415     // Fire off the notification
416     Notification notification = builder.build();
417 
418     if (mDialerRingtoneManager.shouldPlayRingtone(callState, contactInfo.contactRingtoneUri)) {
419       notification.flags |= Notification.FLAG_INSISTENT;
420       notification.sound = contactInfo.contactRingtoneUri;
421       AudioAttributes.Builder audioAttributes = new AudioAttributes.Builder();
422       audioAttributes.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC);
423       audioAttributes.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE);
424       notification.audioAttributes = audioAttributes.build();
425       if (mDialerRingtoneManager.shouldVibrate(mContext.getContentResolver())) {
426         notification.vibrate = VIBRATE_PATTERN;
427       }
428     }
429     if (mDialerRingtoneManager.shouldPlayCallWaitingTone(callState)) {
430       LogUtil.v("StatusBarNotifier.buildAndSendNotification", "playing call waiting tone");
431       mDialerRingtoneManager.playCallWaitingTone();
432     }
433 
434     LogUtil.i(
435         "StatusBarNotifier.buildAndSendNotification",
436         "displaying notification for " + notificationType);
437 
438     try {
439       mNotificationManager.notify(R.id.notification_ongoing_call, notification);
440     } catch (RuntimeException e) {
441       // TODO(b/34744003): Move the memory stats into silent feedback PSD.
442       ActivityManager activityManager = mContext.getSystemService(ActivityManager.class);
443       ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
444       activityManager.getMemoryInfo(memoryInfo);
445       throw new RuntimeException(
446           String.format(
447               Locale.US,
448               "Error displaying notification with photo type: %d (low memory? %b, availMem: %d)",
449               contactInfo.photoType,
450               memoryInfo.lowMemory,
451               memoryInfo.availMem),
452           e);
453     }
454     call.getLatencyReport().onNotificationShown();
455     mCurrentNotification = notificationType;
456   }
457 
458   @Nullable
459   @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
getAnyPhoneAccount()460   private PhoneAccountHandle getAnyPhoneAccount() {
461     PhoneAccountHandle accountHandle;
462     TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class);
463     accountHandle = telecomManager.getDefaultOutgoingPhoneAccount(PhoneAccount.SCHEME_TEL);
464     if (accountHandle == null) {
465       List<PhoneAccountHandle> accountHandles = telecomManager.getCallCapablePhoneAccounts();
466       if (!accountHandles.isEmpty()) {
467         accountHandle = accountHandles.get(0);
468       }
469     }
470     return accountHandle;
471   }
472 
createIncomingCallNotification( DialerCall call, int state, Notification.Builder builder)473   private void createIncomingCallNotification(
474       DialerCall call, int state, Notification.Builder builder) {
475     setNotificationWhen(call, state, builder);
476 
477     // Add hang up option for any active calls (active | onhold), outgoing calls (dialing).
478     if (state == DialerCall.State.ACTIVE
479         || state == DialerCall.State.ONHOLD
480         || DialerCall.State.isDialing(state)) {
481       addHangupAction(builder);
482     } else if (state == DialerCall.State.INCOMING || state == DialerCall.State.CALL_WAITING) {
483       addDismissAction(builder);
484       if (call.isVideoCall()) {
485         addVideoCallAction(builder);
486       } else {
487         addAnswerAction(builder);
488       }
489     }
490   }
491 
492   /**
493    * Sets the notification's when section as needed. For active calls, this is explicitly set as the
494    * duration of the call. For all other states, the notification will automatically show the time
495    * at which the notification was created.
496    */
setNotificationWhen(DialerCall call, int state, Notification.Builder builder)497   private void setNotificationWhen(DialerCall call, int state, Notification.Builder builder) {
498     if (state == DialerCall.State.ACTIVE) {
499       builder.setUsesChronometer(true);
500       builder.setWhen(call.getConnectTimeMillis());
501     } else {
502       builder.setUsesChronometer(false);
503     }
504   }
505 
506   /**
507    * Checks the new notification data and compares it against any notification that we are already
508    * displaying. If the data is exactly the same, we return false so that we do not issue a new
509    * notification for the exact same data.
510    */
checkForChangeAndSaveData( int icon, String content, Bitmap largeIcon, String contentTitle, int state, int notificationType, Uri ringtone)511   private boolean checkForChangeAndSaveData(
512       int icon,
513       String content,
514       Bitmap largeIcon,
515       String contentTitle,
516       int state,
517       int notificationType,
518       Uri ringtone) {
519 
520     // The two are different:
521     // if new title is not null, it should be different from saved version OR
522     // if new title is null, the saved version should not be null
523     final boolean contentTitleChanged =
524         (contentTitle != null && !contentTitle.equals(mSavedContentTitle))
525             || (contentTitle == null && mSavedContentTitle != null);
526 
527     boolean largeIconChanged =
528         mSavedLargeIcon == null ? largeIcon != null : !mSavedLargeIcon.sameAs(largeIcon);
529 
530     // any change means we are definitely updating
531     boolean retval =
532         (mSavedIcon != icon)
533             || !Objects.equals(mSavedContent, content)
534             || (mCallState != state)
535             || largeIconChanged
536             || contentTitleChanged
537             || !Objects.equals(mRingtone, ringtone);
538 
539     // If we aren't showing a notification right now or the notification type is changing,
540     // definitely do an update.
541     if (mCurrentNotification != notificationType) {
542       if (mCurrentNotification == NOTIFICATION_NONE) {
543         LogUtil.d(
544             "StatusBarNotifier.checkForChangeAndSaveData", "showing notification for first time.");
545       }
546       retval = true;
547     }
548 
549     mSavedIcon = icon;
550     mSavedContent = content;
551     mCallState = state;
552     mSavedLargeIcon = largeIcon;
553     mSavedContentTitle = contentTitle;
554     mRingtone = ringtone;
555 
556     if (retval) {
557       LogUtil.d(
558           "StatusBarNotifier.checkForChangeAndSaveData", "data changed.  Showing notification");
559     }
560 
561     return retval;
562   }
563 
564   /** Returns the main string to use in the notification. */
565   @VisibleForTesting
566   @Nullable
getContentTitle(ContactCacheEntry contactInfo, DialerCall call)567   String getContentTitle(ContactCacheEntry contactInfo, DialerCall call) {
568     if (call.isConferenceCall() && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)) {
569       return mContext.getResources().getString(R.string.conference_call_name);
570     }
571 
572     String preferredName =
573         ContactDisplayUtils.getPreferredDisplayName(
574             contactInfo.namePrimary, contactInfo.nameAlternative, mContactsPreferences);
575     if (TextUtils.isEmpty(preferredName)) {
576       return TextUtils.isEmpty(contactInfo.number)
577           ? null
578           : BidiFormatter.getInstance()
579               .unicodeWrap(contactInfo.number, TextDirectionHeuristics.LTR);
580     }
581     return preferredName;
582   }
583 
addPersonReference( Notification.Builder builder, ContactCacheEntry contactInfo, DialerCall call)584   private void addPersonReference(
585       Notification.Builder builder, ContactCacheEntry contactInfo, DialerCall call) {
586     // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed.
587     // So, do not pass {@link Contacts#CONTENT_LOOKUP_URI} to NotificationManager to avoid
588     // NotificationManager using it.
589     if (contactInfo.lookupUri != null && contactInfo.userType != ContactsUtils.USER_TYPE_WORK) {
590       builder.addPerson(contactInfo.lookupUri.toString());
591     } else if (!TextUtils.isEmpty(call.getNumber())) {
592       builder.addPerson(Uri.fromParts(PhoneAccount.SCHEME_TEL, call.getNumber(), null).toString());
593     }
594   }
595 
596   /** Gets a large icon from the contact info object to display in the notification. */
getLargeIconToDisplay( Context context, ContactCacheEntry contactInfo, DialerCall call)597   private static Bitmap getLargeIconToDisplay(
598       Context context, ContactCacheEntry contactInfo, DialerCall call) {
599     Resources resources = context.getResources();
600     Bitmap largeIcon = null;
601     if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) {
602       largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap();
603     }
604     if (contactInfo.photo == null) {
605       int width = (int) resources.getDimension(android.R.dimen.notification_large_icon_width);
606       int height = (int) resources.getDimension(android.R.dimen.notification_large_icon_height);
607       int contactType = LetterTileDrawable.TYPE_DEFAULT;
608       LetterTileDrawable lettertile = new LetterTileDrawable(resources);
609 
610       // TODO: Deduplicate across Dialer. b/36195917
611       if (CallerInfoUtils.isVoiceMailNumber(context, call)) {
612         contactType = LetterTileDrawable.TYPE_VOICEMAIL;
613       } else if (contactInfo.isBusiness) {
614         contactType = LetterTileDrawable.TYPE_BUSINESS;
615       } else if (call.getNumberPresentation() == TelecomManager.PRESENTATION_RESTRICTED) {
616         contactType = LetterTileDrawable.TYPE_GENERIC_AVATAR;
617       } else if (call.isConferenceCall()
618           && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)) {
619         contactType = LetterTileDrawable.TYPE_CONFERENCE;
620       }
621       lettertile.setCanonicalDialerLetterTileDetails(
622           contactInfo.namePrimary == null ? contactInfo.number : contactInfo.namePrimary,
623           contactInfo.lookupKey,
624           LetterTileDrawable.SHAPE_CIRCLE,
625           contactType);
626       largeIcon = lettertile.getBitmap(width, height);
627     }
628 
629     if (call.isSpam()) {
630       Drawable drawable = resources.getDrawable(R.drawable.blocked_contact, context.getTheme());
631       largeIcon = DrawableConverter.drawableToBitmap(drawable);
632     }
633     return largeIcon;
634   }
635 
getRoundedIcon(Bitmap bitmap)636   private Bitmap getRoundedIcon(Bitmap bitmap) {
637     if (bitmap == null) {
638       return null;
639     }
640     final int height =
641         (int) mContext.getResources().getDimension(android.R.dimen.notification_large_icon_height);
642     final int width =
643         (int) mContext.getResources().getDimension(android.R.dimen.notification_large_icon_width);
644     return BitmapUtil.getRoundedBitmap(bitmap, width, height);
645   }
646 
647   /**
648    * Returns the appropriate icon res Id to display based on the call for which we want to display
649    * information.
650    */
getIconToDisplay(DialerCall call)651   private int getIconToDisplay(DialerCall call) {
652     // Even if both lines are in use, we only show a single item in
653     // the expanded Notifications UI.  It's labeled "Ongoing call"
654     // (or "On hold" if there's only one call, and it's on hold.)
655     // Also, we don't have room to display caller-id info from two
656     // different calls.  So if both lines are in use, display info
657     // from the foreground call.  And if there's a ringing call,
658     // display that regardless of the state of the other calls.
659     if (call.getState() == DialerCall.State.ONHOLD) {
660       return R.drawable.ic_phone_paused_white_24dp;
661     } else if (call.getVideoTech().getSessionModificationState()
662         == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
663       return R.drawable.quantum_ic_videocam_white_24;
664     } else if (call.hasProperty(PROPERTY_HIGH_DEF_AUDIO)
665         && MotorolaUtils.shouldShowHdIconInNotification(mContext)) {
666       // Normally when a call is ongoing the status bar displays an icon of a phone with animated
667       // lines. This is a helpful hint for users so they know how to get back to the call.
668       // For Sprint HD calls, we replace this icon with an icon of a phone with a HD badge.
669       // This is a carrier requirement.
670       return R.drawable.ic_hd_call;
671     }
672     return R.anim.on_going_call;
673   }
674 
675   /** Returns the message to use with the notification. */
getContentString(DialerCall call, @UserType long userType)676   private String getContentString(DialerCall call, @UserType long userType) {
677     boolean isIncomingOrWaiting =
678         call.getState() == DialerCall.State.INCOMING
679             || call.getState() == DialerCall.State.CALL_WAITING;
680 
681     if (isIncomingOrWaiting
682         && call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED) {
683 
684       if (!TextUtils.isEmpty(call.getChildNumber())) {
685         return mContext.getString(R.string.child_number, call.getChildNumber());
686       } else if (!TextUtils.isEmpty(call.getCallSubject()) && call.isCallSubjectSupported()) {
687         return call.getCallSubject();
688       }
689     }
690 
691     int resId = R.string.notification_ongoing_call;
692     if (call.hasProperty(Details.PROPERTY_WIFI)) {
693       resId = R.string.notification_ongoing_call_wifi;
694     }
695 
696     if (isIncomingOrWaiting) {
697       EnrichedCallManager manager = EnrichedCallComponent.get(mContext).getEnrichedCallManager();
698       Session session = null;
699       if (call.getNumber() != null) {
700         session =
701             manager.getSession(
702                 call.getUniqueCallId(),
703                 call.getNumber(),
704                 manager.createIncomingCallComposerFilter());
705       }
706 
707       if (call.isSpam()) {
708         resId = R.string.notification_incoming_spam_call;
709       } else if (session != null) {
710         resId = getECIncomingCallText(session);
711       } else if (call.hasProperty(Details.PROPERTY_WIFI)) {
712         resId = R.string.notification_incoming_call_wifi;
713       } else {
714         resId = R.string.notification_incoming_call;
715       }
716     } else if (call.getState() == DialerCall.State.ONHOLD) {
717       resId = R.string.notification_on_hold;
718     } else if (DialerCall.State.isDialing(call.getState())) {
719       resId = R.string.notification_dialing;
720     } else if (call.getVideoTech().getSessionModificationState()
721         == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
722       resId = R.string.notification_requesting_video_call;
723     }
724 
725     // Is the call placed through work connection service.
726     boolean isWorkCall = call.hasProperty(PROPERTY_ENTERPRISE_CALL);
727     if (userType == ContactsUtils.USER_TYPE_WORK || isWorkCall) {
728       resId = getWorkStringFromPersonalString(resId);
729     }
730 
731     return mContext.getString(resId);
732   }
733 
getECIncomingCallText(Session session)734   private int getECIncomingCallText(Session session) {
735     int resId;
736     MultimediaData data = session.getMultimediaData();
737     boolean hasImage = data.hasImageData();
738     boolean hasSubject = !TextUtils.isEmpty(data.getText());
739     boolean hasMap = data.getLocation() != null;
740     if (data.isImportant()) {
741       if (hasMap) {
742         if (hasImage) {
743           if (hasSubject) {
744             resId = R.string.important_notification_incoming_call_with_photo_message_location;
745           } else {
746             resId = R.string.important_notification_incoming_call_with_photo_location;
747           }
748         } else if (hasSubject) {
749           resId = R.string.important_notification_incoming_call_with_message_location;
750         } else {
751           resId = R.string.important_notification_incoming_call_with_location;
752         }
753       } else if (hasImage) {
754         if (hasSubject) {
755           resId = R.string.important_notification_incoming_call_with_photo_message;
756         } else {
757           resId = R.string.important_notification_incoming_call_with_photo;
758         }
759       } else {
760         resId = R.string.important_notification_incoming_call_with_message;
761       }
762       if (mContext.getString(resId).length() > 50) {
763         resId = R.string.important_notification_incoming_call_attachments;
764       }
765     } else {
766       if (hasMap) {
767         if (hasImage) {
768           if (hasSubject) {
769             resId = R.string.notification_incoming_call_with_photo_message_location;
770           } else {
771             resId = R.string.notification_incoming_call_with_photo_location;
772           }
773         } else if (hasSubject) {
774           resId = R.string.notification_incoming_call_with_message_location;
775         } else {
776           resId = R.string.notification_incoming_call_with_location;
777         }
778       } else if (hasImage) {
779         if (hasSubject) {
780           resId = R.string.notification_incoming_call_with_photo_message;
781         } else {
782           resId = R.string.notification_incoming_call_with_photo;
783         }
784       } else {
785         resId = R.string.notification_incoming_call_with_message;
786       }
787     }
788     if (mContext.getString(resId).length() > 50) {
789       resId = R.string.notification_incoming_call_attachments;
790     }
791     return resId;
792   }
793 
794   /** Gets the most relevant call to display in the notification. */
getCallToShow(CallList callList)795   private DialerCall getCallToShow(CallList callList) {
796     if (callList == null) {
797       return null;
798     }
799     DialerCall call = callList.getIncomingCall();
800     if (call == null) {
801       call = callList.getOutgoingCall();
802     }
803     if (call == null) {
804       call = callList.getVideoUpgradeRequestCall();
805     }
806     if (call == null) {
807       call = callList.getActiveOrBackgroundCall();
808     }
809     return call;
810   }
811 
getActionText(@tringRes int stringRes, @ColorRes int colorRes)812   private Spannable getActionText(@StringRes int stringRes, @ColorRes int colorRes) {
813     Spannable spannable = new SpannableString(mContext.getText(stringRes));
814     if (VERSION.SDK_INT >= VERSION_CODES.N_MR1) {
815       // This will only work for cases where the Notification.Builder has a fullscreen intent set
816       // Notification.Builder that does not have a full screen intent will take the color of the
817       // app and the following leads to a no-op.
818       spannable.setSpan(
819           new ForegroundColorSpan(mContext.getColor(colorRes)), 0, spannable.length(), 0);
820     }
821     return spannable;
822   }
823 
addAnswerAction(Notification.Builder builder)824   private void addAnswerAction(Notification.Builder builder) {
825     LogUtil.d(
826         "StatusBarNotifier.addAnswerAction",
827         "will show \"answer\" action in the incoming call Notification");
828     PendingIntent answerVoicePendingIntent =
829         createNotificationPendingIntent(mContext, ACTION_ANSWER_VOICE_INCOMING_CALL);
830     // We put animation resources in "anim" folder instead of "drawable", which causes Android
831     // Studio to complain.
832     // TODO: Move "anim" resources to "drawable" as recommended in AnimationDrawable doc?
833     //noinspection ResourceType
834     builder.addAction(
835         new Notification.Action.Builder(
836                 Icon.createWithResource(mContext, R.anim.on_going_call),
837                 getActionText(
838                     R.string.notification_action_answer, R.color.notification_action_accept),
839                 answerVoicePendingIntent)
840             .build());
841   }
842 
addDismissAction(Notification.Builder builder)843   private void addDismissAction(Notification.Builder builder) {
844     LogUtil.d(
845         "StatusBarNotifier.addDismissAction",
846         "will show \"decline\" action in the incoming call Notification");
847     PendingIntent declinePendingIntent =
848         createNotificationPendingIntent(mContext, ACTION_DECLINE_INCOMING_CALL);
849     builder.addAction(
850         new Notification.Action.Builder(
851                 Icon.createWithResource(mContext, R.drawable.quantum_ic_close_white_24),
852                 getActionText(
853                     R.string.notification_action_dismiss, R.color.notification_action_dismiss),
854                 declinePendingIntent)
855             .build());
856   }
857 
addHangupAction(Notification.Builder builder)858   private void addHangupAction(Notification.Builder builder) {
859     LogUtil.d(
860         "StatusBarNotifier.addHangupAction",
861         "will show \"hang-up\" action in the ongoing active call Notification");
862     PendingIntent hangupPendingIntent =
863         createNotificationPendingIntent(mContext, ACTION_HANG_UP_ONGOING_CALL);
864     builder.addAction(
865         new Notification.Action.Builder(
866                 Icon.createWithResource(mContext, R.drawable.ic_call_end_white_24dp),
867                 mContext.getText(R.string.notification_action_end_call),
868                 hangupPendingIntent)
869             .build());
870   }
871 
addVideoCallAction(Notification.Builder builder)872   private void addVideoCallAction(Notification.Builder builder) {
873     LogUtil.i(
874         "StatusBarNotifier.addVideoCallAction",
875         "will show \"video\" action in the incoming call Notification");
876     PendingIntent answerVideoPendingIntent =
877         createNotificationPendingIntent(mContext, ACTION_ANSWER_VIDEO_INCOMING_CALL);
878     builder.addAction(
879         new Notification.Action.Builder(
880                 Icon.createWithResource(mContext, R.drawable.quantum_ic_videocam_white_24),
881                 getActionText(
882                     R.string.notification_action_answer_video,
883                     R.color.notification_action_answer_video),
884                 answerVideoPendingIntent)
885             .build());
886   }
887 
addAcceptUpgradeRequestAction(Notification.Builder builder)888   private void addAcceptUpgradeRequestAction(Notification.Builder builder) {
889     LogUtil.i(
890         "StatusBarNotifier.addAcceptUpgradeRequestAction",
891         "will show \"accept upgrade\" action in the incoming call Notification");
892     PendingIntent acceptVideoPendingIntent =
893         createNotificationPendingIntent(mContext, ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST);
894     builder.addAction(
895         new Notification.Action.Builder(
896                 Icon.createWithResource(mContext, R.drawable.quantum_ic_videocam_white_24),
897                 getActionText(
898                     R.string.notification_action_accept, R.color.notification_action_accept),
899                 acceptVideoPendingIntent)
900             .build());
901   }
902 
addDismissUpgradeRequestAction(Notification.Builder builder)903   private void addDismissUpgradeRequestAction(Notification.Builder builder) {
904     LogUtil.i(
905         "StatusBarNotifier.addDismissUpgradeRequestAction",
906         "will show \"dismiss upgrade\" action in the incoming call Notification");
907     PendingIntent declineVideoPendingIntent =
908         createNotificationPendingIntent(mContext, ACTION_DECLINE_VIDEO_UPGRADE_REQUEST);
909     builder.addAction(
910         new Notification.Action.Builder(
911                 Icon.createWithResource(mContext, R.drawable.quantum_ic_videocam_white_24),
912                 getActionText(
913                     R.string.notification_action_dismiss, R.color.notification_action_dismiss),
914                 declineVideoPendingIntent)
915             .build());
916   }
917 
918   /** Adds fullscreen intent to the builder. */
configureFullScreenIntent(Notification.Builder builder, PendingIntent intent)919   private void configureFullScreenIntent(Notification.Builder builder, PendingIntent intent) {
920     // Ok, we actually want to launch the incoming call
921     // UI at this point (in addition to simply posting a notification
922     // to the status bar).  Setting fullScreenIntent will cause
923     // the InCallScreen to be launched immediately *unless* the
924     // current foreground activity is marked as "immersive".
925     LogUtil.d("StatusBarNotifier.configureFullScreenIntent", "setting fullScreenIntent: " + intent);
926     builder.setFullScreenIntent(intent, true);
927   }
928 
getNotificationBuilder()929   private Notification.Builder getNotificationBuilder() {
930     final Notification.Builder builder = new Notification.Builder(mContext);
931     builder.setOngoing(true);
932     builder.setOnlyAlertOnce(true);
933     // This will be ignored on O+ and handled by the channel
934     //noinspection deprecation
935     builder.setPriority(Notification.PRIORITY_HIGH);
936 
937     return builder;
938   }
939 
createLaunchPendingIntent(boolean isFullScreen)940   private PendingIntent createLaunchPendingIntent(boolean isFullScreen) {
941     Intent intent =
942         InCallActivity.getIntent(
943             mContext, false /* showDialpad */, false /* newOutgoingCall */, isFullScreen);
944 
945     int requestCode = PENDING_INTENT_REQUEST_CODE_NON_FULL_SCREEN;
946     if (isFullScreen) {
947       // Use a unique request code so that the pending intent isn't clobbered by the
948       // non-full screen pending intent.
949       requestCode = PENDING_INTENT_REQUEST_CODE_FULL_SCREEN;
950     }
951 
952     // PendingIntent that can be used to launch the InCallActivity.  The
953     // system fires off this intent if the user pulls down the windowshade
954     // and clicks the notification's expanded view.  It's also used to
955     // launch the InCallActivity immediately when when there's an incoming
956     // call (see the "fullScreenIntent" field below).
957     return PendingIntent.getActivity(mContext, requestCode, intent, 0);
958   }
959 
setStatusBarCallListener(StatusBarCallListener listener)960   private void setStatusBarCallListener(StatusBarCallListener listener) {
961     if (mStatusBarCallListener != null) {
962       mStatusBarCallListener.cleanup();
963     }
964     mStatusBarCallListener = listener;
965   }
966 
967   private class StatusBarCallListener implements DialerCallListener {
968 
969     private DialerCall mDialerCall;
970 
StatusBarCallListener(DialerCall dialerCall)971     StatusBarCallListener(DialerCall dialerCall) {
972       mDialerCall = dialerCall;
973       mDialerCall.addListener(this);
974     }
975 
cleanup()976     void cleanup() {
977       mDialerCall.removeListener(this);
978     }
979 
980     @Override
onDialerCallDisconnect()981     public void onDialerCallDisconnect() {}
982 
983     @Override
onDialerCallUpdate()984     public void onDialerCallUpdate() {
985       if (CallList.getInstance().getIncomingCall() == null) {
986         mDialerRingtoneManager.stopCallWaitingTone();
987       }
988     }
989 
990     @Override
onDialerCallChildNumberChange()991     public void onDialerCallChildNumberChange() {}
992 
993     @Override
onDialerCallLastForwardedNumberChange()994     public void onDialerCallLastForwardedNumberChange() {}
995 
996     @Override
onDialerCallUpgradeToVideo()997     public void onDialerCallUpgradeToVideo() {}
998 
999     @Override
onWiFiToLteHandover()1000     public void onWiFiToLteHandover() {}
1001 
1002     @Override
onHandoverToWifiFailure()1003     public void onHandoverToWifiFailure() {}
1004 
1005     @Override
onInternationalCallOnWifi()1006     public void onInternationalCallOnWifi() {}
1007 
1008     /**
1009      * Responds to changes in the session modification state for the call by dismissing the status
1010      * bar notification as required.
1011      */
1012     @Override
onDialerCallSessionModificationStateChange()1013     public void onDialerCallSessionModificationStateChange() {
1014       if (mDialerCall.getVideoTech().getSessionModificationState()
1015           == SessionModificationState.NO_REQUEST) {
1016         cleanup();
1017         updateNotification(CallList.getInstance());
1018       }
1019     }
1020   }
1021 }
1022