blob: a2fa7e4925a62b6970755e60f27d1d0e1b1a321a [file] [log] [blame]
Eric Erfanianccca3152017-02-22 16:32:36 -08001/*
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
17package com.android.incallui;
18
Eric Erfanian90508232017-03-24 09:31:16 -070019import static android.telecom.Call.Details.PROPERTY_HIGH_DEF_AUDIO;
Eric Erfanianccca3152017-02-22 16:32:36 -080020import static com.android.contacts.common.compat.CallCompat.Details.PROPERTY_ENTERPRISE_CALL;
21import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST;
22import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VIDEO_INCOMING_CALL;
23import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VOICE_INCOMING_CALL;
24import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_INCOMING_CALL;
25import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_VIDEO_UPGRADE_REQUEST;
26import static com.android.incallui.NotificationBroadcastReceiver.ACTION_HANG_UP_ONGOING_CALL;
yuegb26c1ae2017-09-18 16:59:16 -070027import static com.android.incallui.NotificationBroadcastReceiver.ACTION_TURN_OFF_SPEAKER;
28import static com.android.incallui.NotificationBroadcastReceiver.ACTION_TURN_ON_SPEAKER;
Eric Erfanianccca3152017-02-22 16:32:36 -080029
Eric Erfaniand5e47f62017-03-15 14:41:07 -070030import android.Manifest;
Eric Erfanianccca3152017-02-22 16:32:36 -080031import android.app.ActivityManager;
32import android.app.Notification;
Eric Erfanianccca3152017-02-22 16:32:36 -080033import android.app.PendingIntent;
34import android.content.Context;
35import android.content.Intent;
Eric Erfanian83b20212017-05-31 08:53:10 -070036import android.content.res.Resources;
Eric Erfanianccca3152017-02-22 16:32:36 -080037import android.graphics.Bitmap;
Eric Erfanianccca3152017-02-22 16:32:36 -080038import android.graphics.drawable.BitmapDrawable;
39import android.graphics.drawable.Drawable;
Eric Erfaniand5e47f62017-03-15 14:41:07 -070040import android.graphics.drawable.Icon;
Eric Erfanianccca3152017-02-22 16:32:36 -080041import android.media.AudioAttributes;
42import android.net.Uri;
43import android.os.Build.VERSION;
44import android.os.Build.VERSION_CODES;
wangqicf61ca02017-08-31 15:32:55 -070045import android.os.Trace;
Eric Erfanianccca3152017-02-22 16:32:36 -080046import android.support.annotation.ColorRes;
47import android.support.annotation.NonNull;
48import android.support.annotation.Nullable;
Eric Erfaniand5e47f62017-03-15 14:41:07 -070049import android.support.annotation.RequiresPermission;
Eric Erfanianccca3152017-02-22 16:32:36 -080050import android.support.annotation.StringRes;
51import android.support.annotation.VisibleForTesting;
Eric Erfaniand5e47f62017-03-15 14:41:07 -070052import android.support.v4.os.BuildCompat;
Eric Erfanianccca3152017-02-22 16:32:36 -080053import android.telecom.Call.Details;
yuegb26c1ae2017-09-18 16:59:16 -070054import android.telecom.CallAudioState;
Eric Erfanianccca3152017-02-22 16:32:36 -080055import android.telecom.PhoneAccount;
Eric Erfaniand5e47f62017-03-15 14:41:07 -070056import android.telecom.PhoneAccountHandle;
Eric Erfanianccca3152017-02-22 16:32:36 -080057import android.telecom.TelecomManager;
58import android.text.BidiFormatter;
59import android.text.Spannable;
60import android.text.SpannableString;
61import android.text.TextDirectionHeuristics;
62import android.text.TextUtils;
63import android.text.style.ForegroundColorSpan;
64import com.android.contacts.common.ContactsUtils;
65import com.android.contacts.common.ContactsUtils.UserType;
66import com.android.contacts.common.preference.ContactsPreferences;
Eric Erfanianccca3152017-02-22 16:32:36 -080067import com.android.contacts.common.util.ContactDisplayUtils;
Eric Erfanian2ca43182017-08-31 06:57:16 -070068import com.android.dialer.common.Assert;
Eric Erfanianccca3152017-02-22 16:32:36 -080069import com.android.dialer.common.LogUtil;
Eric Erfanian2ca43182017-08-31 06:57:16 -070070import com.android.dialer.configprovider.ConfigProviderBindings;
71import com.android.dialer.contactphoto.BitmapUtil;
Eric Erfaniand8046e52017-04-06 09:41:50 -070072import com.android.dialer.enrichedcall.EnrichedCallManager;
73import com.android.dialer.enrichedcall.Session;
Eric Erfanian2ca43182017-08-31 06:57:16 -070074import com.android.dialer.lettertile.LetterTileDrawable;
75import com.android.dialer.lettertile.LetterTileDrawable.ContactType;
Eric Erfaniand8046e52017-04-06 09:41:50 -070076import com.android.dialer.multimedia.MultimediaData;
Eric Erfanian2ca43182017-08-31 06:57:16 -070077import com.android.dialer.notification.DialerNotificationManager;
78import com.android.dialer.notification.NotificationChannelId;
Eric Erfanian90508232017-03-24 09:31:16 -070079import com.android.dialer.oem.MotorolaUtils;
Eric Erfanianccca3152017-02-22 16:32:36 -080080import com.android.dialer.util.DrawableConverter;
81import com.android.incallui.ContactInfoCache.ContactCacheEntry;
82import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
83import com.android.incallui.InCallPresenter.InCallState;
84import com.android.incallui.async.PausableExecutorImpl;
yuegb26c1ae2017-09-18 16:59:16 -070085import com.android.incallui.audiomode.AudioModeProvider;
Eric Erfanianccca3152017-02-22 16:32:36 -080086import com.android.incallui.call.CallList;
87import com.android.incallui.call.DialerCall;
Eric Erfanianccca3152017-02-22 16:32:36 -080088import com.android.incallui.call.DialerCallListener;
89import com.android.incallui.ringtone.DialerRingtoneManager;
90import com.android.incallui.ringtone.InCallTonePlayer;
91import com.android.incallui.ringtone.ToneGeneratorFactory;
Eric Erfanian90508232017-03-24 09:31:16 -070092import com.android.incallui.videotech.utils.SessionModificationState;
Eric Erfaniand5e47f62017-03-15 14:41:07 -070093import java.util.List;
94import java.util.Locale;
Eric Erfanianccca3152017-02-22 16:32:36 -080095import java.util.Objects;
96
97/** This class adds Notifications to the status bar for the in-call experience. */
Eric Erfaniand8046e52017-04-06 09:41:50 -070098public class StatusBarNotifier
yuegb26c1ae2017-09-18 16:59:16 -070099 implements InCallPresenter.InCallStateListener,
100 EnrichedCallManager.StateChangedListener,
101 AudioModeProvider.AudioModeListener {
Eric Erfanianccca3152017-02-22 16:32:36 -0800102
Eric Erfanian2ca43182017-08-31 06:57:16 -0700103 private static final String NOTIFICATION_TAG = "STATUS_BAR_NOTIFIER";
104 private static final int NOTIFICATION_ID = 1;
105
Eric Erfanianccca3152017-02-22 16:32:36 -0800106 // Notification types
107 // Indicates that no notification is currently showing.
108 private static final int NOTIFICATION_NONE = 0;
109 // Notification for an active call. This is non-interruptive, but cannot be dismissed.
Eric Erfanian10b34a52017-05-04 08:23:17 -0700110 private static final int NOTIFICATION_IN_CALL = 1;
Eric Erfanianccca3152017-02-22 16:32:36 -0800111 // Notification for incoming calls. This is interruptive and will show up as a HUN.
Eric Erfanian10b34a52017-05-04 08:23:17 -0700112 private static final int NOTIFICATION_INCOMING_CALL = 2;
113 // Notification for incoming calls in the case where there is already an active call.
114 // This is non-interruptive, but otherwise behaves the same as NOTIFICATION_INCOMING_CALL
115 private static final int NOTIFICATION_INCOMING_CALL_QUIET = 3;
Eric Erfanianccca3152017-02-22 16:32:36 -0800116
Eric Erfanianccca3152017-02-22 16:32:36 -0800117 private static final long[] VIBRATE_PATTERN = new long[] {0, 1000, 1000};
118
119 private final Context mContext;
120 private final ContactInfoCache mContactInfoCache;
Eric Erfanianccca3152017-02-22 16:32:36 -0800121 private final DialerRingtoneManager mDialerRingtoneManager;
122 @Nullable private ContactsPreferences mContactsPreferences;
123 private int mCurrentNotification = NOTIFICATION_NONE;
124 private int mCallState = DialerCall.State.INVALID;
125 private int mSavedIcon = 0;
126 private String mSavedContent = null;
127 private Bitmap mSavedLargeIcon;
128 private String mSavedContentTitle;
yuegb26c1ae2017-09-18 16:59:16 -0700129 private CallAudioState savedCallAudioState;
Eric Erfanianccca3152017-02-22 16:32:36 -0800130 private Uri mRingtone;
131 private StatusBarCallListener mStatusBarCallListener;
132
Eric Erfaniand8046e52017-04-06 09:41:50 -0700133 public StatusBarNotifier(@NonNull Context context, @NonNull ContactInfoCache contactInfoCache) {
Eric Erfanian2ca43182017-08-31 06:57:16 -0700134 mContext = Assert.isNotNull(context);
Eric Erfanianccca3152017-02-22 16:32:36 -0800135 mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext);
136 mContactInfoCache = contactInfoCache;
Eric Erfanianccca3152017-02-22 16:32:36 -0800137 mDialerRingtoneManager =
138 new DialerRingtoneManager(
139 new InCallTonePlayer(new ToneGeneratorFactory(), new PausableExecutorImpl()),
140 CallList.getInstance());
141 mCurrentNotification = NOTIFICATION_NONE;
yuegb26c1ae2017-09-18 16:59:16 -0700142 AudioModeProvider.getInstance().addListener(this);
Eric Erfanianccca3152017-02-22 16:32:36 -0800143 }
144
145 /**
146 * Should only be called from a irrecoverable state where it is necessary to dismiss all
147 * notifications.
148 */
Eric Erfanian2ca43182017-08-31 06:57:16 -0700149 static void clearAllCallNotifications(Context context) {
150 LogUtil.e(
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700151 "StatusBarNotifier.clearAllCallNotifications",
152 "something terrible happened, clear all InCall notifications");
Eric Erfanianccca3152017-02-22 16:32:36 -0800153
Eric Erfanian2ca43182017-08-31 06:57:16 -0700154 DialerNotificationManager.cancel(context, NOTIFICATION_TAG, NOTIFICATION_ID);
Eric Erfanianccca3152017-02-22 16:32:36 -0800155 }
156
157 private static int getWorkStringFromPersonalString(int resId) {
158 if (resId == R.string.notification_ongoing_call) {
159 return R.string.notification_ongoing_work_call;
Eric Erfanianccca3152017-02-22 16:32:36 -0800160 } 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 */
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
176 /** Creates notifications according to the state we receive from {@link InCallPresenter}. */
177 @Override
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700178 @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
Eric Erfanianccca3152017-02-22 16:32:36 -0800179 public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700180 LogUtil.d("StatusBarNotifier.onStateChange", "%s->%s", oldState, newState);
Eric Erfanianccca3152017-02-22 16:32:36 -0800181 updateNotification(callList);
182 }
183
Eric Erfaniand8046e52017-04-06 09:41:50 -0700184 @Override
185 public void onEnrichedCallStateChanged() {
186 LogUtil.enterBlock("StatusBarNotifier.onEnrichedCallStateChanged");
187 updateNotification(CallList.getInstance());
188 }
189
Eric Erfanianccca3152017-02-22 16:32:36 -0800190 /**
191 * Updates the phone app's status bar notification *and* launches the incoming call UI in response
192 * to a new incoming call.
193 *
194 * <p>If an incoming call is ringing (or call-waiting), the notification will also include a
195 * "fullScreenIntent" that will cause the InCallScreen to be launched, unless the current
196 * foreground activity is marked as "immersive".
197 *
198 * <p>(This is the mechanism that actually brings up the incoming call UI when we receive a "new
199 * ringing connection" event from the telephony layer.)
200 *
201 * <p>Also note that this method is safe to call even if the phone isn't actually ringing (or,
202 * more likely, if an incoming call *was* ringing briefly but then disconnected). In that case,
203 * we'll simply update or cancel the in-call notification based on the current phone state.
204 *
205 * @see #updateInCallNotification(CallList)
206 */
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700207 @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
Eric Erfaniand8046e52017-04-06 09:41:50 -0700208 public void updateNotification(CallList callList) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800209 updateInCallNotification(callList);
210 }
211
212 /**
213 * Take down the in-call notification.
214 *
215 * @see #updateInCallNotification(CallList)
216 */
217 private void cancelNotification() {
218 if (mStatusBarCallListener != null) {
219 setStatusBarCallListener(null);
220 }
221 if (mCurrentNotification != NOTIFICATION_NONE) {
Eric Erfanian8369df02017-05-03 10:27:13 -0700222 LogUtil.i("StatusBarNotifier.cancelNotification", "cancel");
Eric Erfanian2ca43182017-08-31 06:57:16 -0700223 DialerNotificationManager.cancel(mContext, NOTIFICATION_TAG, NOTIFICATION_ID);
Eric Erfanianccca3152017-02-22 16:32:36 -0800224 }
225 mCurrentNotification = NOTIFICATION_NONE;
226 }
227
228 /**
229 * Helper method for updateInCallNotification() and updateNotification(): Update the phone app's
230 * status bar notification based on the current telephony state, or cancels the notification if
231 * the phone is totally idle.
232 */
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700233 @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
Eric Erfanianccca3152017-02-22 16:32:36 -0800234 private void updateInCallNotification(CallList callList) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700235 LogUtil.d("StatusBarNotifier.updateInCallNotification", "");
Eric Erfanianccca3152017-02-22 16:32:36 -0800236
237 final DialerCall call = getCallToShow(callList);
238
239 if (call != null) {
240 showNotification(callList, call);
241 } else {
242 cancelNotification();
243 }
244 }
245
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700246 @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
Eric Erfanianccca3152017-02-22 16:32:36 -0800247 private void showNotification(final CallList callList, final DialerCall call) {
wangqicf61ca02017-08-31 15:32:55 -0700248 Trace.beginSection("StatusBarNotifier.showNotification");
Eric Erfanianccca3152017-02-22 16:32:36 -0800249 final boolean isIncoming =
250 (call.getState() == DialerCall.State.INCOMING
251 || call.getState() == DialerCall.State.CALL_WAITING);
252 setStatusBarCallListener(new StatusBarCallListener(call));
253
254 // we make a call to the contact info cache to query for supplemental data to what the
255 // call provides. This includes the contact name and photo.
256 // This callback will always get called immediately and synchronously with whatever data
257 // it has available, and may make a subsequent call later (same thread) if it had to
258 // call into the contacts provider for more data.
259 mContactInfoCache.findInfo(
260 call,
261 isIncoming,
262 new ContactInfoCacheCallback() {
263 @Override
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700264 @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
Eric Erfanianccca3152017-02-22 16:32:36 -0800265 public void onContactInfoComplete(String callId, ContactCacheEntry entry) {
266 DialerCall call = callList.getCallById(callId);
267 if (call != null) {
268 call.getLogState().contactLookupResult = entry.contactLookupResult;
269 buildAndSendNotification(callList, call, entry);
270 }
271 }
272
273 @Override
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700274 @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
Eric Erfanianccca3152017-02-22 16:32:36 -0800275 public void onImageLoadComplete(String callId, ContactCacheEntry entry) {
276 DialerCall call = callList.getCallById(callId);
277 if (call != null) {
278 buildAndSendNotification(callList, call, entry);
279 }
280 }
281 });
wangqicf61ca02017-08-31 15:32:55 -0700282 Trace.endSection();
Eric Erfanianccca3152017-02-22 16:32:36 -0800283 }
284
285 /** Sets up the main Ui for the notification */
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700286 @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
Eric Erfanianccca3152017-02-22 16:32:36 -0800287 private void buildAndSendNotification(
288 CallList callList, DialerCall originalCall, ContactCacheEntry contactInfo) {
wangqicf61ca02017-08-31 15:32:55 -0700289 Trace.beginSection("StatusBarNotifier.buildAndSendNotification");
Eric Erfanianccca3152017-02-22 16:32:36 -0800290 // This can get called to update an existing notification after contact information has come
291 // back. However, it can happen much later. Before we continue, we need to make sure that
292 // the call being passed in is still the one we want to show in the notification.
293 final DialerCall call = getCallToShow(callList);
294 if (call == null || !call.getId().equals(originalCall.getId())) {
wangqicf61ca02017-08-31 15:32:55 -0700295 Trace.endSection();
Eric Erfanianccca3152017-02-22 16:32:36 -0800296 return;
297 }
298
wangqicf61ca02017-08-31 15:32:55 -0700299 Trace.beginSection("prepare work");
Eric Erfanianccca3152017-02-22 16:32:36 -0800300 final int callState = call.getState();
yuegb26c1ae2017-09-18 16:59:16 -0700301 final CallAudioState callAudioState = AudioModeProvider.getInstance().getAudioState();
Eric Erfanianccca3152017-02-22 16:32:36 -0800302
303 // Check if data has changed; if nothing is different, don't issue another notification.
304 final int iconResId = getIconToDisplay(call);
Eric Erfanian83b20212017-05-31 08:53:10 -0700305 Bitmap largeIcon = getLargeIconToDisplay(mContext, contactInfo, call);
Eric Erfanianccca3152017-02-22 16:32:36 -0800306 final String content = getContentString(call, contactInfo.userType);
307 final String contentTitle = getContentTitle(contactInfo, call);
308
309 final boolean isVideoUpgradeRequest =
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700310 call.getVideoTech().getSessionModificationState()
Eric Erfanian90508232017-03-24 09:31:16 -0700311 == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
Eric Erfanianccca3152017-02-22 16:32:36 -0800312 final int notificationType;
313 if (callState == DialerCall.State.INCOMING
314 || callState == DialerCall.State.CALL_WAITING
315 || isVideoUpgradeRequest) {
Eric Erfanian2ca43182017-08-31 06:57:16 -0700316 if (ConfigProviderBindings.get(mContext)
317 .getBoolean("quiet_incoming_call_if_ui_showing", true)) {
318 notificationType =
319 InCallPresenter.getInstance().isShowingInCallUi()
320 ? NOTIFICATION_INCOMING_CALL_QUIET
321 : NOTIFICATION_INCOMING_CALL;
322 } else {
323 boolean alreadyActive =
324 callList.getActiveOrBackgroundCall() != null
325 && InCallPresenter.getInstance().isShowingInCallUi();
326 notificationType =
327 alreadyActive ? NOTIFICATION_INCOMING_CALL_QUIET : NOTIFICATION_INCOMING_CALL;
328 }
Eric Erfanianccca3152017-02-22 16:32:36 -0800329 } else {
330 notificationType = NOTIFICATION_IN_CALL;
331 }
wangqicf61ca02017-08-31 15:32:55 -0700332 Trace.endSection(); // prepare work
Eric Erfanianccca3152017-02-22 16:32:36 -0800333
334 if (!checkForChangeAndSaveData(
335 iconResId,
336 content,
337 largeIcon,
338 contentTitle,
339 callState,
340 notificationType,
yuegb26c1ae2017-09-18 16:59:16 -0700341 contactInfo.contactRingtoneUri,
342 callAudioState)) {
wangqicf61ca02017-08-31 15:32:55 -0700343 Trace.endSection();
Eric Erfanianccca3152017-02-22 16:32:36 -0800344 return;
345 }
346
347 if (largeIcon != null) {
348 largeIcon = getRoundedIcon(largeIcon);
349 }
350
351 // This builder is used for the notification shown when the device is locked and the user
352 // has set their notification settings to 'hide sensitive content'
353 // {@see Notification.Builder#setPublicVersion}.
354 Notification.Builder publicBuilder = new Notification.Builder(mContext);
355 publicBuilder
356 .setSmallIcon(iconResId)
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700357 .setColor(mContext.getResources().getColor(R.color.dialer_theme_color, mContext.getTheme()))
Eric Erfanianccca3152017-02-22 16:32:36 -0800358 // Hide work call state for the lock screen notification
359 .setContentTitle(getContentString(call, ContactsUtils.USER_TYPE_CURRENT));
360 setNotificationWhen(call, callState, publicBuilder);
361
362 // Builder for the notification shown when the device is unlocked or the user has set their
363 // notification settings to 'show all notification content'.
364 final Notification.Builder builder = getNotificationBuilder();
365 builder.setPublicVersion(publicBuilder.build());
366
367 // Set up the main intent to send the user to the in-call screen
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700368 builder.setContentIntent(createLaunchPendingIntent(false /* isFullScreen */));
Eric Erfanianccca3152017-02-22 16:32:36 -0800369
370 // Set the intent as a full screen intent as well if a call is incoming
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700371 PhoneAccountHandle accountHandle = call.getAccountHandle();
372 if (accountHandle == null) {
373 accountHandle = getAnyPhoneAccount();
374 }
Eric Erfanian10b34a52017-05-04 08:23:17 -0700375
376 LogUtil.i("StatusBarNotifier.buildAndSendNotification", "notificationType=" + notificationType);
377 switch (notificationType) {
378 case NOTIFICATION_INCOMING_CALL:
Eric Erfanian2ca43182017-08-31 06:57:16 -0700379 if (BuildCompat.isAtLeastO()) {
380 builder.setChannelId(NotificationChannelId.INCOMING_CALL);
381 }
Eric Erfanian10b34a52017-05-04 08:23:17 -0700382 configureFullScreenIntent(builder, createLaunchPendingIntent(true /* isFullScreen */));
383 // Set the notification category and bump the priority for incoming calls
384 builder.setCategory(Notification.CATEGORY_CALL);
385 // This will be ignored on O+ and handled by the channel
Eric Erfanian10b34a52017-05-04 08:23:17 -0700386 builder.setPriority(Notification.PRIORITY_MAX);
387 if (mCurrentNotification != NOTIFICATION_INCOMING_CALL) {
388 LogUtil.i(
389 "StatusBarNotifier.buildAndSendNotification",
390 "Canceling old notification so this one can be noisy");
391 // Moving from a non-interuptive notification (or none) to a noisy one. Cancel the old
392 // notification (if there is one) so the fullScreenIntent or HUN will show
Eric Erfanian2ca43182017-08-31 06:57:16 -0700393 DialerNotificationManager.cancel(mContext, NOTIFICATION_TAG, NOTIFICATION_ID);
Eric Erfanian10b34a52017-05-04 08:23:17 -0700394 }
395 break;
396 case NOTIFICATION_INCOMING_CALL_QUIET:
Eric Erfanian2ca43182017-08-31 06:57:16 -0700397 if (BuildCompat.isAtLeastO()) {
398 builder.setChannelId(NotificationChannelId.ONGOING_CALL);
399 }
Eric Erfanian10b34a52017-05-04 08:23:17 -0700400 break;
401 case NOTIFICATION_IN_CALL:
Eric Erfanian2ca43182017-08-31 06:57:16 -0700402 if (BuildCompat.isAtLeastO()) {
403 publicBuilder.setColorized(true);
404 builder.setColorized(true);
405 builder.setChannelId(NotificationChannelId.ONGOING_CALL);
406 }
407 break;
408 default:
Eric Erfanian10b34a52017-05-04 08:23:17 -0700409 break;
Eric Erfanianccca3152017-02-22 16:32:36 -0800410 }
411
412 // Set the content
413 builder.setContentText(content);
414 builder.setSmallIcon(iconResId);
415 builder.setContentTitle(contentTitle);
416 builder.setLargeIcon(largeIcon);
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700417 builder.setColor(
418 mContext.getResources().getColor(R.color.dialer_theme_color, mContext.getTheme()));
Eric Erfanianccca3152017-02-22 16:32:36 -0800419
420 if (isVideoUpgradeRequest) {
421 builder.setUsesChronometer(false);
422 addDismissUpgradeRequestAction(builder);
423 addAcceptUpgradeRequestAction(builder);
424 } else {
yuegb26c1ae2017-09-18 16:59:16 -0700425 createIncomingCallNotification(call, callState, callAudioState, builder);
Eric Erfanianccca3152017-02-22 16:32:36 -0800426 }
427
428 addPersonReference(builder, contactInfo, call);
429
wangqicf61ca02017-08-31 15:32:55 -0700430 Trace.beginSection("fire notification");
Eric Erfanianccca3152017-02-22 16:32:36 -0800431 // Fire off the notification
432 Notification notification = builder.build();
433
434 if (mDialerRingtoneManager.shouldPlayRingtone(callState, contactInfo.contactRingtoneUri)) {
435 notification.flags |= Notification.FLAG_INSISTENT;
436 notification.sound = contactInfo.contactRingtoneUri;
437 AudioAttributes.Builder audioAttributes = new AudioAttributes.Builder();
438 audioAttributes.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC);
439 audioAttributes.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE);
440 notification.audioAttributes = audioAttributes.build();
441 if (mDialerRingtoneManager.shouldVibrate(mContext.getContentResolver())) {
442 notification.vibrate = VIBRATE_PATTERN;
443 }
444 }
445 if (mDialerRingtoneManager.shouldPlayCallWaitingTone(callState)) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700446 LogUtil.v("StatusBarNotifier.buildAndSendNotification", "playing call waiting tone");
Eric Erfanianccca3152017-02-22 16:32:36 -0800447 mDialerRingtoneManager.playCallWaitingTone();
448 }
Eric Erfanianccca3152017-02-22 16:32:36 -0800449
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700450 LogUtil.i(
451 "StatusBarNotifier.buildAndSendNotification",
452 "displaying notification for " + notificationType);
453
Eric Erfanianccca3152017-02-22 16:32:36 -0800454 try {
Eric Erfanian2ca43182017-08-31 06:57:16 -0700455 DialerNotificationManager.notify(mContext, NOTIFICATION_TAG, NOTIFICATION_ID, notification);
Eric Erfanianccca3152017-02-22 16:32:36 -0800456 } catch (RuntimeException e) {
457 // TODO(b/34744003): Move the memory stats into silent feedback PSD.
458 ActivityManager activityManager = mContext.getSystemService(ActivityManager.class);
459 ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
460 activityManager.getMemoryInfo(memoryInfo);
461 throw new RuntimeException(
462 String.format(
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700463 Locale.US,
Eric Erfanianccca3152017-02-22 16:32:36 -0800464 "Error displaying notification with photo type: %d (low memory? %b, availMem: %d)",
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700465 contactInfo.photoType,
466 memoryInfo.lowMemory,
467 memoryInfo.availMem),
Eric Erfanianccca3152017-02-22 16:32:36 -0800468 e);
469 }
wangqicf61ca02017-08-31 15:32:55 -0700470 Trace.endSection();
Eric Erfanianccca3152017-02-22 16:32:36 -0800471 call.getLatencyReport().onNotificationShown();
472 mCurrentNotification = notificationType;
wangqicf61ca02017-08-31 15:32:55 -0700473 Trace.endSection();
Eric Erfanianccca3152017-02-22 16:32:36 -0800474 }
475
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700476 @Nullable
477 @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
478 private PhoneAccountHandle getAnyPhoneAccount() {
479 PhoneAccountHandle accountHandle;
480 TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class);
481 accountHandle = telecomManager.getDefaultOutgoingPhoneAccount(PhoneAccount.SCHEME_TEL);
482 if (accountHandle == null) {
483 List<PhoneAccountHandle> accountHandles = telecomManager.getCallCapablePhoneAccounts();
484 if (!accountHandles.isEmpty()) {
485 accountHandle = accountHandles.get(0);
486 }
487 }
488 return accountHandle;
489 }
490
Eric Erfanianccca3152017-02-22 16:32:36 -0800491 private void createIncomingCallNotification(
yuegb26c1ae2017-09-18 16:59:16 -0700492 DialerCall call, int state, CallAudioState callAudioState, Notification.Builder builder) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800493 setNotificationWhen(call, state, builder);
494
495 // Add hang up option for any active calls (active | onhold), outgoing calls (dialing).
496 if (state == DialerCall.State.ACTIVE
497 || state == DialerCall.State.ONHOLD
498 || DialerCall.State.isDialing(state)) {
499 addHangupAction(builder);
yuegb26c1ae2017-09-18 16:59:16 -0700500 addSpeakerAction(builder, callAudioState);
Eric Erfanianccca3152017-02-22 16:32:36 -0800501 } else if (state == DialerCall.State.INCOMING || state == DialerCall.State.CALL_WAITING) {
502 addDismissAction(builder);
503 if (call.isVideoCall()) {
504 addVideoCallAction(builder);
505 } else {
506 addAnswerAction(builder);
507 }
508 }
509 }
510
511 /**
512 * Sets the notification's when section as needed. For active calls, this is explicitly set as the
513 * duration of the call. For all other states, the notification will automatically show the time
514 * at which the notification was created.
515 */
516 private void setNotificationWhen(DialerCall call, int state, Notification.Builder builder) {
517 if (state == DialerCall.State.ACTIVE) {
518 builder.setUsesChronometer(true);
519 builder.setWhen(call.getConnectTimeMillis());
520 } else {
521 builder.setUsesChronometer(false);
522 }
523 }
524
525 /**
526 * Checks the new notification data and compares it against any notification that we are already
527 * displaying. If the data is exactly the same, we return false so that we do not issue a new
528 * notification for the exact same data.
529 */
530 private boolean checkForChangeAndSaveData(
531 int icon,
532 String content,
533 Bitmap largeIcon,
534 String contentTitle,
535 int state,
536 int notificationType,
yuegb26c1ae2017-09-18 16:59:16 -0700537 Uri ringtone,
538 CallAudioState callAudioState) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800539
540 // The two are different:
541 // if new title is not null, it should be different from saved version OR
542 // if new title is null, the saved version should not be null
543 final boolean contentTitleChanged =
544 (contentTitle != null && !contentTitle.equals(mSavedContentTitle))
545 || (contentTitle == null && mSavedContentTitle != null);
546
Eric Erfanian8369df02017-05-03 10:27:13 -0700547 boolean largeIconChanged =
548 mSavedLargeIcon == null ? largeIcon != null : !mSavedLargeIcon.sameAs(largeIcon);
549
Eric Erfanianccca3152017-02-22 16:32:36 -0800550 // any change means we are definitely updating
551 boolean retval =
552 (mSavedIcon != icon)
553 || !Objects.equals(mSavedContent, content)
554 || (mCallState != state)
Eric Erfanian8369df02017-05-03 10:27:13 -0700555 || largeIconChanged
Eric Erfanianccca3152017-02-22 16:32:36 -0800556 || contentTitleChanged
yuegb26c1ae2017-09-18 16:59:16 -0700557 || !Objects.equals(mRingtone, ringtone)
558 || !Objects.equals(savedCallAudioState, callAudioState);
Eric Erfanianccca3152017-02-22 16:32:36 -0800559
560 // If we aren't showing a notification right now or the notification type is changing,
561 // definitely do an update.
562 if (mCurrentNotification != notificationType) {
563 if (mCurrentNotification == NOTIFICATION_NONE) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700564 LogUtil.d(
565 "StatusBarNotifier.checkForChangeAndSaveData", "showing notification for first time.");
Eric Erfanianccca3152017-02-22 16:32:36 -0800566 }
567 retval = true;
568 }
569
570 mSavedIcon = icon;
571 mSavedContent = content;
572 mCallState = state;
573 mSavedLargeIcon = largeIcon;
574 mSavedContentTitle = contentTitle;
575 mRingtone = ringtone;
yuegb26c1ae2017-09-18 16:59:16 -0700576 savedCallAudioState = callAudioState;
Eric Erfanianccca3152017-02-22 16:32:36 -0800577
578 if (retval) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700579 LogUtil.d(
580 "StatusBarNotifier.checkForChangeAndSaveData", "data changed. Showing notification");
Eric Erfanianccca3152017-02-22 16:32:36 -0800581 }
582
583 return retval;
584 }
585
586 /** Returns the main string to use in the notification. */
587 @VisibleForTesting
588 @Nullable
589 String getContentTitle(ContactCacheEntry contactInfo, DialerCall call) {
Eric Erfanian2ca43182017-08-31 06:57:16 -0700590 if (call.isConferenceCall()) {
591 return CallerInfoUtils.getConferenceString(
592 mContext, call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE));
Eric Erfanianccca3152017-02-22 16:32:36 -0800593 }
594
595 String preferredName =
596 ContactDisplayUtils.getPreferredDisplayName(
597 contactInfo.namePrimary, contactInfo.nameAlternative, mContactsPreferences);
598 if (TextUtils.isEmpty(preferredName)) {
599 return TextUtils.isEmpty(contactInfo.number)
600 ? null
601 : BidiFormatter.getInstance()
602 .unicodeWrap(contactInfo.number, TextDirectionHeuristics.LTR);
603 }
604 return preferredName;
605 }
606
607 private void addPersonReference(
608 Notification.Builder builder, ContactCacheEntry contactInfo, DialerCall call) {
609 // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed.
610 // So, do not pass {@link Contacts#CONTENT_LOOKUP_URI} to NotificationManager to avoid
611 // NotificationManager using it.
612 if (contactInfo.lookupUri != null && contactInfo.userType != ContactsUtils.USER_TYPE_WORK) {
613 builder.addPerson(contactInfo.lookupUri.toString());
614 } else if (!TextUtils.isEmpty(call.getNumber())) {
615 builder.addPerson(Uri.fromParts(PhoneAccount.SCHEME_TEL, call.getNumber(), null).toString());
616 }
617 }
618
619 /** Gets a large icon from the contact info object to display in the notification. */
Eric Erfanian83b20212017-05-31 08:53:10 -0700620 private static Bitmap getLargeIconToDisplay(
621 Context context, ContactCacheEntry contactInfo, DialerCall call) {
622 Resources resources = context.getResources();
Eric Erfanianccca3152017-02-22 16:32:36 -0800623 Bitmap largeIcon = null;
Eric Erfanianccca3152017-02-22 16:32:36 -0800624 if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) {
625 largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap();
626 }
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700627 if (contactInfo.photo == null) {
Eric Erfanian83b20212017-05-31 08:53:10 -0700628 int width = (int) resources.getDimension(android.R.dimen.notification_large_icon_width);
629 int height = (int) resources.getDimension(android.R.dimen.notification_large_icon_height);
Eric Erfanian2ca43182017-08-31 06:57:16 -0700630 @ContactType
631 int contactType =
632 LetterTileDrawable.getContactTypeFromPrimitives(
633 CallerInfoUtils.isVoiceMailNumber(context, call),
634 call.isSpam(),
635 contactInfo.isBusiness,
636 call.getNumberPresentation(),
637 call.isConferenceCall() && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE));
Eric Erfanian83b20212017-05-31 08:53:10 -0700638 LetterTileDrawable lettertile = new LetterTileDrawable(resources);
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700639
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700640 lettertile.setCanonicalDialerLetterTileDetails(
641 contactInfo.namePrimary == null ? contactInfo.number : contactInfo.namePrimary,
642 contactInfo.lookupKey,
643 LetterTileDrawable.SHAPE_CIRCLE,
644 contactType);
645 largeIcon = lettertile.getBitmap(width, height);
646 }
647
Eric Erfanianccca3152017-02-22 16:32:36 -0800648 if (call.isSpam()) {
Eric Erfanian83b20212017-05-31 08:53:10 -0700649 Drawable drawable = resources.getDrawable(R.drawable.blocked_contact, context.getTheme());
Eric Erfanianccca3152017-02-22 16:32:36 -0800650 largeIcon = DrawableConverter.drawableToBitmap(drawable);
651 }
652 return largeIcon;
653 }
654
655 private Bitmap getRoundedIcon(Bitmap bitmap) {
656 if (bitmap == null) {
657 return null;
658 }
659 final int height =
660 (int) mContext.getResources().getDimension(android.R.dimen.notification_large_icon_height);
661 final int width =
662 (int) mContext.getResources().getDimension(android.R.dimen.notification_large_icon_width);
663 return BitmapUtil.getRoundedBitmap(bitmap, width, height);
664 }
665
666 /**
667 * Returns the appropriate icon res Id to display based on the call for which we want to display
668 * information.
669 */
670 private int getIconToDisplay(DialerCall call) {
671 // Even if both lines are in use, we only show a single item in
672 // the expanded Notifications UI. It's labeled "Ongoing call"
673 // (or "On hold" if there's only one call, and it's on hold.)
674 // Also, we don't have room to display caller-id info from two
675 // different calls. So if both lines are in use, display info
676 // from the foreground call. And if there's a ringing call,
677 // display that regardless of the state of the other calls.
678 if (call.getState() == DialerCall.State.ONHOLD) {
679 return R.drawable.ic_phone_paused_white_24dp;
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700680 } else if (call.getVideoTech().getSessionModificationState()
Eric Erfanian90508232017-03-24 09:31:16 -0700681 == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
Eric Erfaniand8046e52017-04-06 09:41:50 -0700682 return R.drawable.quantum_ic_videocam_white_24;
Eric Erfanian90508232017-03-24 09:31:16 -0700683 } else if (call.hasProperty(PROPERTY_HIGH_DEF_AUDIO)
684 && MotorolaUtils.shouldShowHdIconInNotification(mContext)) {
Eric Erfanian2ca43182017-08-31 06:57:16 -0700685 // Normally when a call is ongoing the status bar displays an icon of a phone. This is a
686 // helpful hint for users so they know how to get back to the call. For Sprint HD calls, we
687 // replace this icon with an icon of a phone with a HD badge. This is a carrier requirement.
Eric Erfanian90508232017-03-24 09:31:16 -0700688 return R.drawable.ic_hd_call;
Eric Erfanianccca3152017-02-22 16:32:36 -0800689 }
Eric Erfanian2ca43182017-08-31 06:57:16 -0700690 // If ReturnToCall is enabled, use the static icon. The animated one will show in the bubble.
691 if (ReturnToCallController.isEnabled(mContext)) {
692 return R.drawable.quantum_ic_call_vd_theme_24;
693 } else {
694 return R.drawable.on_going_call;
695 }
Eric Erfanianccca3152017-02-22 16:32:36 -0800696 }
697
698 /** Returns the message to use with the notification. */
699 private String getContentString(DialerCall call, @UserType long userType) {
700 boolean isIncomingOrWaiting =
701 call.getState() == DialerCall.State.INCOMING
702 || call.getState() == DialerCall.State.CALL_WAITING;
703
704 if (isIncomingOrWaiting
705 && call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED) {
706
707 if (!TextUtils.isEmpty(call.getChildNumber())) {
708 return mContext.getString(R.string.child_number, call.getChildNumber());
709 } else if (!TextUtils.isEmpty(call.getCallSubject()) && call.isCallSubjectSupported()) {
710 return call.getCallSubject();
711 }
712 }
713
714 int resId = R.string.notification_ongoing_call;
Eric Erfanian2ca43182017-08-31 06:57:16 -0700715 String wifiBrand = mContext.getString(R.string.notification_call_wifi_brand);
Eric Erfanianccca3152017-02-22 16:32:36 -0800716 if (call.hasProperty(Details.PROPERTY_WIFI)) {
Eric Erfanian2ca43182017-08-31 06:57:16 -0700717 resId = R.string.notification_ongoing_call_wifi_template;
Eric Erfanianccca3152017-02-22 16:32:36 -0800718 }
719
720 if (isIncomingOrWaiting) {
Eric Erfaniand8046e52017-04-06 09:41:50 -0700721 if (call.isSpam()) {
722 resId = R.string.notification_incoming_spam_call;
Eric Erfanian2ca43182017-08-31 06:57:16 -0700723 } else if (shouldShowEnrichedCallNotification(call.getEnrichedCallSession())) {
724 resId = getECIncomingCallText(call.getEnrichedCallSession());
Eric Erfaniand8046e52017-04-06 09:41:50 -0700725 } else if (call.hasProperty(Details.PROPERTY_WIFI)) {
Eric Erfanian2ca43182017-08-31 06:57:16 -0700726 resId = R.string.notification_incoming_call_wifi_template;
727
Eric Erfanianccca3152017-02-22 16:32:36 -0800728 } else {
Eric Erfaniand8046e52017-04-06 09:41:50 -0700729 resId = R.string.notification_incoming_call;
Eric Erfanianccca3152017-02-22 16:32:36 -0800730 }
731 } else if (call.getState() == DialerCall.State.ONHOLD) {
732 resId = R.string.notification_on_hold;
733 } else if (DialerCall.State.isDialing(call.getState())) {
734 resId = R.string.notification_dialing;
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700735 } else if (call.getVideoTech().getSessionModificationState()
Eric Erfanian90508232017-03-24 09:31:16 -0700736 == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800737 resId = R.string.notification_requesting_video_call;
738 }
739
740 // Is the call placed through work connection service.
741 boolean isWorkCall = call.hasProperty(PROPERTY_ENTERPRISE_CALL);
742 if (userType == ContactsUtils.USER_TYPE_WORK || isWorkCall) {
743 resId = getWorkStringFromPersonalString(resId);
Eric Erfanian2ca43182017-08-31 06:57:16 -0700744 wifiBrand = mContext.getString(R.string.notification_call_wifi_work_brand);
745 }
746
747 if (resId == R.string.notification_incoming_call_wifi_template
748 || resId == R.string.notification_ongoing_call_wifi_template) {
749 // TODO(b/64525903): Potentially apply this template logic everywhere.
750 return mContext.getString(resId, wifiBrand);
Eric Erfanianccca3152017-02-22 16:32:36 -0800751 }
752
753 return mContext.getString(resId);
754 }
755
Eric Erfanian2ca43182017-08-31 06:57:16 -0700756 private boolean shouldShowEnrichedCallNotification(Session session) {
757 if (session == null) {
758 return false;
759 }
760 return session.getMultimediaData().hasData() || session.getMultimediaData().isImportant();
761 }
762
Eric Erfaniand8046e52017-04-06 09:41:50 -0700763 private int getECIncomingCallText(Session session) {
764 int resId;
765 MultimediaData data = session.getMultimediaData();
766 boolean hasImage = data.hasImageData();
767 boolean hasSubject = !TextUtils.isEmpty(data.getText());
768 boolean hasMap = data.getLocation() != null;
769 if (data.isImportant()) {
770 if (hasMap) {
771 if (hasImage) {
772 if (hasSubject) {
773 resId = R.string.important_notification_incoming_call_with_photo_message_location;
774 } else {
775 resId = R.string.important_notification_incoming_call_with_photo_location;
776 }
777 } else if (hasSubject) {
778 resId = R.string.important_notification_incoming_call_with_message_location;
779 } else {
780 resId = R.string.important_notification_incoming_call_with_location;
781 }
782 } else if (hasImage) {
783 if (hasSubject) {
784 resId = R.string.important_notification_incoming_call_with_photo_message;
785 } else {
786 resId = R.string.important_notification_incoming_call_with_photo;
787 }
Eric Erfanian2ca43182017-08-31 06:57:16 -0700788 } else if (hasSubject) {
Eric Erfaniand8046e52017-04-06 09:41:50 -0700789 resId = R.string.important_notification_incoming_call_with_message;
Eric Erfanian2ca43182017-08-31 06:57:16 -0700790 } else {
791 resId = R.string.important_notification_incoming_call;
Eric Erfaniand8046e52017-04-06 09:41:50 -0700792 }
793 if (mContext.getString(resId).length() > 50) {
794 resId = R.string.important_notification_incoming_call_attachments;
795 }
796 } else {
797 if (hasMap) {
798 if (hasImage) {
799 if (hasSubject) {
800 resId = R.string.notification_incoming_call_with_photo_message_location;
801 } else {
802 resId = R.string.notification_incoming_call_with_photo_location;
803 }
804 } else if (hasSubject) {
805 resId = R.string.notification_incoming_call_with_message_location;
806 } else {
807 resId = R.string.notification_incoming_call_with_location;
808 }
809 } else if (hasImage) {
810 if (hasSubject) {
811 resId = R.string.notification_incoming_call_with_photo_message;
812 } else {
813 resId = R.string.notification_incoming_call_with_photo;
814 }
815 } else {
816 resId = R.string.notification_incoming_call_with_message;
817 }
818 }
819 if (mContext.getString(resId).length() > 50) {
820 resId = R.string.notification_incoming_call_attachments;
821 }
822 return resId;
823 }
824
Eric Erfanianccca3152017-02-22 16:32:36 -0800825 /** Gets the most relevant call to display in the notification. */
826 private DialerCall getCallToShow(CallList callList) {
827 if (callList == null) {
828 return null;
829 }
830 DialerCall call = callList.getIncomingCall();
831 if (call == null) {
832 call = callList.getOutgoingCall();
833 }
834 if (call == null) {
835 call = callList.getVideoUpgradeRequestCall();
836 }
837 if (call == null) {
838 call = callList.getActiveOrBackgroundCall();
839 }
840 return call;
841 }
842
843 private Spannable getActionText(@StringRes int stringRes, @ColorRes int colorRes) {
844 Spannable spannable = new SpannableString(mContext.getText(stringRes));
845 if (VERSION.SDK_INT >= VERSION_CODES.N_MR1) {
846 // This will only work for cases where the Notification.Builder has a fullscreen intent set
847 // Notification.Builder that does not have a full screen intent will take the color of the
848 // app and the following leads to a no-op.
849 spannable.setSpan(
850 new ForegroundColorSpan(mContext.getColor(colorRes)), 0, spannable.length(), 0);
851 }
852 return spannable;
853 }
854
855 private void addAnswerAction(Notification.Builder builder) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700856 LogUtil.d(
857 "StatusBarNotifier.addAnswerAction",
858 "will show \"answer\" action in the incoming call Notification");
Eric Erfanianccca3152017-02-22 16:32:36 -0800859 PendingIntent answerVoicePendingIntent =
860 createNotificationPendingIntent(mContext, ACTION_ANSWER_VOICE_INCOMING_CALL);
861 builder.addAction(
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700862 new Notification.Action.Builder(
Eric Erfanian2ca43182017-08-31 06:57:16 -0700863 Icon.createWithResource(mContext, R.drawable.quantum_ic_call_white_24),
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700864 getActionText(
865 R.string.notification_action_answer, R.color.notification_action_accept),
866 answerVoicePendingIntent)
867 .build());
Eric Erfanianccca3152017-02-22 16:32:36 -0800868 }
869
870 private void addDismissAction(Notification.Builder builder) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700871 LogUtil.d(
872 "StatusBarNotifier.addDismissAction",
873 "will show \"decline\" action in the incoming call Notification");
Eric Erfanianccca3152017-02-22 16:32:36 -0800874 PendingIntent declinePendingIntent =
875 createNotificationPendingIntent(mContext, ACTION_DECLINE_INCOMING_CALL);
876 builder.addAction(
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700877 new Notification.Action.Builder(
Eric Erfaniand8046e52017-04-06 09:41:50 -0700878 Icon.createWithResource(mContext, R.drawable.quantum_ic_close_white_24),
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700879 getActionText(
880 R.string.notification_action_dismiss, R.color.notification_action_dismiss),
881 declinePendingIntent)
882 .build());
Eric Erfanianccca3152017-02-22 16:32:36 -0800883 }
884
885 private void addHangupAction(Notification.Builder builder) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700886 LogUtil.d(
887 "StatusBarNotifier.addHangupAction",
888 "will show \"hang-up\" action in the ongoing active call Notification");
Eric Erfanianccca3152017-02-22 16:32:36 -0800889 PendingIntent hangupPendingIntent =
890 createNotificationPendingIntent(mContext, ACTION_HANG_UP_ONGOING_CALL);
891 builder.addAction(
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700892 new Notification.Action.Builder(
Eric Erfanian2ca43182017-08-31 06:57:16 -0700893 Icon.createWithResource(mContext, R.drawable.quantum_ic_call_end_white_24),
Eric Erfanian10b34a52017-05-04 08:23:17 -0700894 mContext.getText(R.string.notification_action_end_call),
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700895 hangupPendingIntent)
896 .build());
Eric Erfanianccca3152017-02-22 16:32:36 -0800897 }
898
yuegb26c1ae2017-09-18 16:59:16 -0700899 private void addSpeakerAction(Notification.Builder builder, CallAudioState callAudioState) {
900 if ((callAudioState.getSupportedRouteMask() & CallAudioState.ROUTE_BLUETOOTH)
901 == CallAudioState.ROUTE_BLUETOOTH) {
902 // Don't add speaker button if bluetooth is connected
903 return;
904 }
905 if (callAudioState.getRoute() == CallAudioState.ROUTE_SPEAKER) {
906 addSpeakerOffAction(builder);
907 } else if ((callAudioState.getRoute() & CallAudioState.ROUTE_WIRED_OR_EARPIECE) != 0) {
908 addSpeakerOnAction(builder);
909 }
910 }
911
912 private void addSpeakerOnAction(Notification.Builder builder) {
913 LogUtil.d(
914 "StatusBarNotifier.addSpeakerOnAction",
915 "will show \"Speaker on\" action in the ongoing active call Notification");
916 PendingIntent speakerOnPendingIntent =
917 createNotificationPendingIntent(mContext, ACTION_TURN_ON_SPEAKER);
918 builder.addAction(
919 new Notification.Action.Builder(
920 Icon.createWithResource(mContext, R.drawable.quantum_ic_volume_up_white_24),
921 mContext.getText(R.string.notification_action_speaker_on),
922 speakerOnPendingIntent)
923 .build());
924 }
925
926 private void addSpeakerOffAction(Notification.Builder builder) {
927 LogUtil.d(
928 "StatusBarNotifier.addSpeakerOffAction",
929 "will show \"Speaker off\" action in the ongoing active call Notification");
930 PendingIntent speakerOffPendingIntent =
931 createNotificationPendingIntent(mContext, ACTION_TURN_OFF_SPEAKER);
932 builder.addAction(
933 new Notification.Action.Builder(
934 Icon.createWithResource(mContext, R.drawable.quantum_ic_phone_in_talk_white_24),
935 mContext.getText(R.string.notification_action_speaker_off),
936 speakerOffPendingIntent)
937 .build());
938 }
939
Eric Erfanianccca3152017-02-22 16:32:36 -0800940 private void addVideoCallAction(Notification.Builder builder) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700941 LogUtil.i(
942 "StatusBarNotifier.addVideoCallAction",
943 "will show \"video\" action in the incoming call Notification");
Eric Erfanianccca3152017-02-22 16:32:36 -0800944 PendingIntent answerVideoPendingIntent =
945 createNotificationPendingIntent(mContext, ACTION_ANSWER_VIDEO_INCOMING_CALL);
946 builder.addAction(
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700947 new Notification.Action.Builder(
Eric Erfaniand8046e52017-04-06 09:41:50 -0700948 Icon.createWithResource(mContext, R.drawable.quantum_ic_videocam_white_24),
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700949 getActionText(
950 R.string.notification_action_answer_video,
951 R.color.notification_action_answer_video),
952 answerVideoPendingIntent)
953 .build());
Eric Erfanianccca3152017-02-22 16:32:36 -0800954 }
955
956 private void addAcceptUpgradeRequestAction(Notification.Builder builder) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700957 LogUtil.i(
958 "StatusBarNotifier.addAcceptUpgradeRequestAction",
959 "will show \"accept upgrade\" action in the incoming call Notification");
Eric Erfanianccca3152017-02-22 16:32:36 -0800960 PendingIntent acceptVideoPendingIntent =
961 createNotificationPendingIntent(mContext, ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST);
962 builder.addAction(
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700963 new Notification.Action.Builder(
Eric Erfaniand8046e52017-04-06 09:41:50 -0700964 Icon.createWithResource(mContext, R.drawable.quantum_ic_videocam_white_24),
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700965 getActionText(
966 R.string.notification_action_accept, R.color.notification_action_accept),
967 acceptVideoPendingIntent)
968 .build());
Eric Erfanianccca3152017-02-22 16:32:36 -0800969 }
970
971 private void addDismissUpgradeRequestAction(Notification.Builder builder) {
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700972 LogUtil.i(
973 "StatusBarNotifier.addDismissUpgradeRequestAction",
974 "will show \"dismiss upgrade\" action in the incoming call Notification");
Eric Erfanianccca3152017-02-22 16:32:36 -0800975 PendingIntent declineVideoPendingIntent =
976 createNotificationPendingIntent(mContext, ACTION_DECLINE_VIDEO_UPGRADE_REQUEST);
977 builder.addAction(
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700978 new Notification.Action.Builder(
Eric Erfaniand8046e52017-04-06 09:41:50 -0700979 Icon.createWithResource(mContext, R.drawable.quantum_ic_videocam_white_24),
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700980 getActionText(
981 R.string.notification_action_dismiss, R.color.notification_action_dismiss),
982 declineVideoPendingIntent)
983 .build());
Eric Erfanianccca3152017-02-22 16:32:36 -0800984 }
985
986 /** Adds fullscreen intent to the builder. */
Eric Erfanian10b34a52017-05-04 08:23:17 -0700987 private void configureFullScreenIntent(Notification.Builder builder, PendingIntent intent) {
Eric Erfanianccca3152017-02-22 16:32:36 -0800988 // Ok, we actually want to launch the incoming call
989 // UI at this point (in addition to simply posting a notification
990 // to the status bar). Setting fullScreenIntent will cause
991 // the InCallScreen to be launched immediately *unless* the
992 // current foreground activity is marked as "immersive".
Eric Erfaniand5e47f62017-03-15 14:41:07 -0700993 LogUtil.d("StatusBarNotifier.configureFullScreenIntent", "setting fullScreenIntent: " + intent);
Eric Erfanianccca3152017-02-22 16:32:36 -0800994 builder.setFullScreenIntent(intent, true);
Eric Erfanianccca3152017-02-22 16:32:36 -0800995 }
996
997 private Notification.Builder getNotificationBuilder() {
998 final Notification.Builder builder = new Notification.Builder(mContext);
999 builder.setOngoing(true);
Eric Erfaniand5e47f62017-03-15 14:41:07 -07001000 builder.setOnlyAlertOnce(true);
Eric Erfanian10b34a52017-05-04 08:23:17 -07001001 // This will be ignored on O+ and handled by the channel
Eric Erfanian2ca43182017-08-31 06:57:16 -07001002 // noinspection deprecation
Eric Erfanian10b34a52017-05-04 08:23:17 -07001003 builder.setPriority(Notification.PRIORITY_HIGH);
Eric Erfanianccca3152017-02-22 16:32:36 -08001004
1005 return builder;
1006 }
1007
Eric Erfaniand5e47f62017-03-15 14:41:07 -07001008 private PendingIntent createLaunchPendingIntent(boolean isFullScreen) {
Eric Erfanianccca3152017-02-22 16:32:36 -08001009 Intent intent =
1010 InCallActivity.getIntent(
Eric Erfaniand5e47f62017-03-15 14:41:07 -07001011 mContext, false /* showDialpad */, false /* newOutgoingCall */, isFullScreen);
Eric Erfanianccca3152017-02-22 16:32:36 -08001012
Eric Erfanian2ca43182017-08-31 06:57:16 -07001013 int requestCode = InCallActivity.PENDING_INTENT_REQUEST_CODE_NON_FULL_SCREEN;
Eric Erfanianccca3152017-02-22 16:32:36 -08001014 if (isFullScreen) {
1015 // Use a unique request code so that the pending intent isn't clobbered by the
1016 // non-full screen pending intent.
Eric Erfanian2ca43182017-08-31 06:57:16 -07001017 requestCode = InCallActivity.PENDING_INTENT_REQUEST_CODE_FULL_SCREEN;
Eric Erfanianccca3152017-02-22 16:32:36 -08001018 }
1019
1020 // PendingIntent that can be used to launch the InCallActivity. The
1021 // system fires off this intent if the user pulls down the windowshade
1022 // and clicks the notification's expanded view. It's also used to
1023 // launch the InCallActivity immediately when when there's an incoming
1024 // call (see the "fullScreenIntent" field below).
1025 return PendingIntent.getActivity(mContext, requestCode, intent, 0);
1026 }
1027
1028 private void setStatusBarCallListener(StatusBarCallListener listener) {
1029 if (mStatusBarCallListener != null) {
1030 mStatusBarCallListener.cleanup();
1031 }
1032 mStatusBarCallListener = listener;
1033 }
1034
yuegb26c1ae2017-09-18 16:59:16 -07001035 @Override
1036 public void onAudioStateChanged(CallAudioState audioState) {
1037 if (CallList.getInstance().getActiveOrBackgroundCall() == null) {
1038 // We only care about speaker mode when in call
1039 return;
1040 }
1041
1042 updateNotification(CallList.getInstance());
1043 }
1044
Eric Erfanianccca3152017-02-22 16:32:36 -08001045 private class StatusBarCallListener implements DialerCallListener {
1046
1047 private DialerCall mDialerCall;
1048
1049 StatusBarCallListener(DialerCall dialerCall) {
1050 mDialerCall = dialerCall;
1051 mDialerCall.addListener(this);
1052 }
1053
1054 void cleanup() {
1055 mDialerCall.removeListener(this);
1056 }
1057
1058 @Override
1059 public void onDialerCallDisconnect() {}
1060
1061 @Override
1062 public void onDialerCallUpdate() {
1063 if (CallList.getInstance().getIncomingCall() == null) {
1064 mDialerRingtoneManager.stopCallWaitingTone();
1065 }
1066 }
1067
1068 @Override
1069 public void onDialerCallChildNumberChange() {}
1070
1071 @Override
1072 public void onDialerCallLastForwardedNumberChange() {}
1073
1074 @Override
1075 public void onDialerCallUpgradeToVideo() {}
1076
1077 @Override
1078 public void onWiFiToLteHandover() {}
1079
1080 @Override
1081 public void onHandoverToWifiFailure() {}
1082
Eric Erfanianc857f902017-05-15 14:05:33 -07001083 @Override
1084 public void onInternationalCallOnWifi() {}
1085
Eric Erfanian2ca43182017-08-31 06:57:16 -07001086 @Override
1087 public void onEnrichedCallSessionUpdate() {}
1088
Eric Erfanianccca3152017-02-22 16:32:36 -08001089 /**
1090 * Responds to changes in the session modification state for the call by dismissing the status
1091 * bar notification as required.
1092 */
1093 @Override
Eric Erfaniand5e47f62017-03-15 14:41:07 -07001094 public void onDialerCallSessionModificationStateChange() {
1095 if (mDialerCall.getVideoTech().getSessionModificationState()
Eric Erfanian90508232017-03-24 09:31:16 -07001096 == SessionModificationState.NO_REQUEST) {
Eric Erfanianccca3152017-02-22 16:32:36 -08001097 cleanup();
1098 updateNotification(CallList.getInstance());
1099 }
1100 }
1101 }
1102}