Update dialer sources.
Test: Built package and system image.
This change clobbers the old source, and is an export
from an internal Google repository.
The internal repository was forked form Android in March,
and this change includes modifications since then, to
near the v8 release.
Since the fork, we've moved code from monolithic to independent modules. In addition,
we've switched to Blaze/Bazel as the build sysetm. This export, however, still uses make.
New dependencies have been added:
- Dagger
- Auto-Value
- Glide
- Libshortcutbadger
Going forward, development will still be in Google3, and the Gerrit release
will become an automated export, with the next drop happening in ~ two weeks.
Android.mk includes local modifications from ToT.
Abridged changelog:
Bug fixes
● Not able to mute, add a call when using Phone app in multiwindow mode
● Double tap on keypad triggering multiple key and tones
● Reported spam numbers not showing as spam in the call log
● Crash when user tries to block number while Phone app is not set as default
● Crash when user picks a number from search auto-complete list
Visual Voicemail (VVM) improvements
● Share Voicemail audio via standard exporting mechanisms that support file attachment
(email, MMS, etc.)
● Make phone number, email and web sites in VVM transcript clickable
● Set PIN before declining VVM Terms of Service {Carrier}
● Set client type for outbound visual voicemail SMS {Carrier}
New incoming call and incall UI on older devices
(Android M)
● Updated Phone app icon
● New incall UI (large buttons, button labels)
● New and animated Answer/Reject gestures
Accessibility
● Add custom answer/decline call buttons on answer screen for touch exploration
accessibility services
● Increase size of touch target
● Add verbal feedback when a Voicemail fails to load
● Fix pressing of Phone buttons while in a phone call using Switch Access
● Fix selecting and opening contacts in talkback mode
● Split focus for ‘Learn More’ link in caller id & spam to help distinguish similar text
Other
● Backup & Restore for App Preferences
● Prompt user to enable Wi-Fi calling if the call ends due to out of service and Wi-Fi is
connected
● Rename “Dialpad” to “Keypad”
● Show "Private number" for restricted calls
● Delete unused items (vcard, add contact, call history) from Phone menu
Change-Id: I2a7e53532a24c21bf308bf0a6d178d7ddbca4958
diff --git a/java/com/android/incallui/StatusBarNotifier.java b/java/com/android/incallui/StatusBarNotifier.java
new file mode 100644
index 0000000..c722675
--- /dev/null
+++ b/java/com/android/incallui/StatusBarNotifier.java
@@ -0,0 +1,842 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui;
+
+import static com.android.contacts.common.compat.CallCompat.Details.PROPERTY_ENTERPRISE_CALL;
+import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST;
+import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VIDEO_INCOMING_CALL;
+import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VOICE_INCOMING_CALL;
+import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_INCOMING_CALL;
+import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_VIDEO_UPGRADE_REQUEST;
+import static com.android.incallui.NotificationBroadcastReceiver.ACTION_HANG_UP_ONGOING_CALL;
+
+import android.app.ActivityManager;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.AudioAttributes;
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.ColorRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
+import android.support.annotation.VisibleForTesting;
+import android.telecom.Call.Details;
+import android.telecom.PhoneAccount;
+import android.telecom.TelecomManager;
+import android.text.BidiFormatter;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextDirectionHeuristics;
+import android.text.TextUtils;
+import android.text.style.ForegroundColorSpan;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.ContactsUtils.UserType;
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.common.util.BitmapUtil;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.util.DrawableConverter;
+import com.android.incallui.ContactInfoCache.ContactCacheEntry;
+import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
+import com.android.incallui.InCallPresenter.InCallState;
+import com.android.incallui.async.PausableExecutorImpl;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.SessionModificationState;
+import com.android.incallui.call.DialerCallListener;
+import com.android.incallui.ringtone.DialerRingtoneManager;
+import com.android.incallui.ringtone.InCallTonePlayer;
+import com.android.incallui.ringtone.ToneGeneratorFactory;
+import java.util.Objects;
+
+/** This class adds Notifications to the status bar for the in-call experience. */
+public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
+
+ // Notification types
+ // Indicates that no notification is currently showing.
+ private static final int NOTIFICATION_NONE = 0;
+ // Notification for an active call. This is non-interruptive, but cannot be dismissed.
+ private static final int NOTIFICATION_IN_CALL = 1;
+ // Notification for incoming calls. This is interruptive and will show up as a HUN.
+ private static final int NOTIFICATION_INCOMING_CALL = 2;
+
+ private static final int PENDING_INTENT_REQUEST_CODE_NON_FULL_SCREEN = 0;
+ private static final int PENDING_INTENT_REQUEST_CODE_FULL_SCREEN = 1;
+
+ private static final long[] VIBRATE_PATTERN = new long[] {0, 1000, 1000};
+
+ private final Context mContext;
+ private final ContactInfoCache mContactInfoCache;
+ private final NotificationManager mNotificationManager;
+ private final DialerRingtoneManager mDialerRingtoneManager;
+ @Nullable private ContactsPreferences mContactsPreferences;
+ private int mCurrentNotification = NOTIFICATION_NONE;
+ private int mCallState = DialerCall.State.INVALID;
+ private int mSavedIcon = 0;
+ private String mSavedContent = null;
+ private Bitmap mSavedLargeIcon;
+ private String mSavedContentTitle;
+ private Uri mRingtone;
+ private StatusBarCallListener mStatusBarCallListener;
+
+ public StatusBarNotifier(@NonNull Context context, @NonNull ContactInfoCache contactInfoCache) {
+ Objects.requireNonNull(context);
+ mContext = context;
+ mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext);
+ mContactInfoCache = contactInfoCache;
+ mNotificationManager = context.getSystemService(NotificationManager.class);
+ mDialerRingtoneManager =
+ new DialerRingtoneManager(
+ new InCallTonePlayer(new ToneGeneratorFactory(), new PausableExecutorImpl()),
+ CallList.getInstance());
+ mCurrentNotification = NOTIFICATION_NONE;
+ }
+
+ /**
+ * Should only be called from a irrecoverable state where it is necessary to dismiss all
+ * notifications.
+ */
+ static void clearAllCallNotifications(Context backupContext) {
+ Log.i(
+ StatusBarNotifier.class.getSimpleName(),
+ "Something terrible happened. Clear all InCall notifications");
+
+ NotificationManager notificationManager =
+ backupContext.getSystemService(NotificationManager.class);
+ notificationManager.cancel(NOTIFICATION_IN_CALL);
+ notificationManager.cancel(NOTIFICATION_INCOMING_CALL);
+ }
+
+ private static int getWorkStringFromPersonalString(int resId) {
+ if (resId == R.string.notification_ongoing_call) {
+ return R.string.notification_ongoing_work_call;
+ } else if (resId == R.string.notification_ongoing_call_wifi) {
+ return R.string.notification_ongoing_work_call_wifi;
+ } else if (resId == R.string.notification_incoming_call_wifi) {
+ return R.string.notification_incoming_work_call_wifi;
+ } else if (resId == R.string.notification_incoming_call) {
+ return R.string.notification_incoming_work_call;
+ } else {
+ return resId;
+ }
+ }
+
+ /**
+ * Returns PendingIntent for answering a phone call. This will typically be used from Notification
+ * context.
+ */
+ private static PendingIntent createNotificationPendingIntent(Context context, String action) {
+ final Intent intent = new Intent(action, null, context, NotificationBroadcastReceiver.class);
+ return PendingIntent.getBroadcast(context, 0, intent, 0);
+ }
+
+ /** Creates notifications according to the state we receive from {@link InCallPresenter}. */
+ @Override
+ public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
+ Log.d(this, "onStateChange");
+ updateNotification(callList);
+ }
+
+ /**
+ * Updates the phone app's status bar notification *and* launches the incoming call UI in response
+ * to a new incoming call.
+ *
+ * <p>If an incoming call is ringing (or call-waiting), the notification will also include a
+ * "fullScreenIntent" that will cause the InCallScreen to be launched, unless the current
+ * foreground activity is marked as "immersive".
+ *
+ * <p>(This is the mechanism that actually brings up the incoming call UI when we receive a "new
+ * ringing connection" event from the telephony layer.)
+ *
+ * <p>Also note that this method is safe to call even if the phone isn't actually ringing (or,
+ * more likely, if an incoming call *was* ringing briefly but then disconnected). In that case,
+ * we'll simply update or cancel the in-call notification based on the current phone state.
+ *
+ * @see #updateInCallNotification(CallList)
+ */
+ public void updateNotification(CallList callList) {
+ updateInCallNotification(callList);
+ }
+
+ /**
+ * Take down the in-call notification.
+ *
+ * @see #updateInCallNotification(CallList)
+ */
+ private void cancelNotification() {
+ if (mStatusBarCallListener != null) {
+ setStatusBarCallListener(null);
+ }
+ if (mCurrentNotification != NOTIFICATION_NONE) {
+ Log.d(this, "cancelInCall()...");
+ mNotificationManager.cancel(mCurrentNotification);
+ }
+ mCurrentNotification = NOTIFICATION_NONE;
+ }
+
+ /**
+ * Helper method for updateInCallNotification() and updateNotification(): Update the phone app's
+ * status bar notification based on the current telephony state, or cancels the notification if
+ * the phone is totally idle.
+ */
+ private void updateInCallNotification(CallList callList) {
+ Log.d(this, "updateInCallNotification...");
+
+ final DialerCall call = getCallToShow(callList);
+
+ if (call != null) {
+ showNotification(callList, call);
+ } else {
+ cancelNotification();
+ }
+ }
+
+ private void showNotification(final CallList callList, final DialerCall call) {
+ final boolean isIncoming =
+ (call.getState() == DialerCall.State.INCOMING
+ || call.getState() == DialerCall.State.CALL_WAITING);
+ setStatusBarCallListener(new StatusBarCallListener(call));
+
+ // we make a call to the contact info cache to query for supplemental data to what the
+ // call provides. This includes the contact name and photo.
+ // This callback will always get called immediately and synchronously with whatever data
+ // it has available, and may make a subsequent call later (same thread) if it had to
+ // call into the contacts provider for more data.
+ mContactInfoCache.findInfo(
+ call,
+ isIncoming,
+ new ContactInfoCacheCallback() {
+ @Override
+ public void onContactInfoComplete(String callId, ContactCacheEntry entry) {
+ DialerCall call = callList.getCallById(callId);
+ if (call != null) {
+ call.getLogState().contactLookupResult = entry.contactLookupResult;
+ buildAndSendNotification(callList, call, entry);
+ }
+ }
+
+ @Override
+ public void onImageLoadComplete(String callId, ContactCacheEntry entry) {
+ DialerCall call = callList.getCallById(callId);
+ if (call != null) {
+ buildAndSendNotification(callList, call, entry);
+ }
+ }
+ });
+ }
+
+ /** Sets up the main Ui for the notification */
+ private void buildAndSendNotification(
+ CallList callList, DialerCall originalCall, ContactCacheEntry contactInfo) {
+ // This can get called to update an existing notification after contact information has come
+ // back. However, it can happen much later. Before we continue, we need to make sure that
+ // the call being passed in is still the one we want to show in the notification.
+ final DialerCall call = getCallToShow(callList);
+ if (call == null || !call.getId().equals(originalCall.getId())) {
+ return;
+ }
+
+ final int callState = call.getState();
+
+ // Check if data has changed; if nothing is different, don't issue another notification.
+ final int iconResId = getIconToDisplay(call);
+ Bitmap largeIcon = getLargeIconToDisplay(contactInfo, call);
+ final String content = getContentString(call, contactInfo.userType);
+ final String contentTitle = getContentTitle(contactInfo, call);
+
+ final boolean isVideoUpgradeRequest =
+ call.getSessionModificationState()
+ == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
+ final int notificationType;
+ if (callState == DialerCall.State.INCOMING
+ || callState == DialerCall.State.CALL_WAITING
+ || isVideoUpgradeRequest) {
+ notificationType = NOTIFICATION_INCOMING_CALL;
+ } else {
+ notificationType = NOTIFICATION_IN_CALL;
+ }
+
+ if (!checkForChangeAndSaveData(
+ iconResId,
+ content,
+ largeIcon,
+ contentTitle,
+ callState,
+ notificationType,
+ contactInfo.contactRingtoneUri)) {
+ return;
+ }
+
+ if (largeIcon != null) {
+ largeIcon = getRoundedIcon(largeIcon);
+ }
+
+ // This builder is used for the notification shown when the device is locked and the user
+ // has set their notification settings to 'hide sensitive content'
+ // {@see Notification.Builder#setPublicVersion}.
+ Notification.Builder publicBuilder = new Notification.Builder(mContext);
+ publicBuilder
+ .setSmallIcon(iconResId)
+ .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
+ // Hide work call state for the lock screen notification
+ .setContentTitle(getContentString(call, ContactsUtils.USER_TYPE_CURRENT));
+ setNotificationWhen(call, callState, publicBuilder);
+
+ // Builder for the notification shown when the device is unlocked or the user has set their
+ // notification settings to 'show all notification content'.
+ final Notification.Builder builder = getNotificationBuilder();
+ builder.setPublicVersion(publicBuilder.build());
+
+ // Set up the main intent to send the user to the in-call screen
+ builder.setContentIntent(
+ createLaunchPendingIntent(false /* isFullScreen */, call.isVideoCall()));
+
+ // Set the intent as a full screen intent as well if a call is incoming
+ if (notificationType == NOTIFICATION_INCOMING_CALL) {
+ if (!InCallPresenter.getInstance().isActivityStarted()) {
+ configureFullScreenIntent(
+ builder,
+ createLaunchPendingIntent(true /* isFullScreen */, call.isVideoCall()),
+ callList,
+ call);
+ } else {
+ // If the incall screen is already up, we don't want to show HUN but regular notification
+ // should still be shown. In order to do that the previous one with full screen intent
+ // needs to be cancelled.
+ LogUtil.d(
+ "StatusBarNotifier.buildAndSendNotification",
+ "cancel previous incoming call notification");
+ mNotificationManager.cancel(NOTIFICATION_INCOMING_CALL);
+ }
+ // Set the notification category for incoming calls
+ builder.setCategory(Notification.CATEGORY_CALL);
+ }
+
+ // Set the content
+ builder.setContentText(content);
+ builder.setSmallIcon(iconResId);
+ builder.setContentTitle(contentTitle);
+ builder.setLargeIcon(largeIcon);
+ builder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color));
+
+ if (isVideoUpgradeRequest) {
+ builder.setUsesChronometer(false);
+ addDismissUpgradeRequestAction(builder);
+ addAcceptUpgradeRequestAction(builder);
+ } else {
+ createIncomingCallNotification(call, callState, builder);
+ }
+
+ addPersonReference(builder, contactInfo, call);
+
+ // Fire off the notification
+ Notification notification = builder.build();
+
+ if (mDialerRingtoneManager.shouldPlayRingtone(callState, contactInfo.contactRingtoneUri)) {
+ notification.flags |= Notification.FLAG_INSISTENT;
+ notification.sound = contactInfo.contactRingtoneUri;
+ AudioAttributes.Builder audioAttributes = new AudioAttributes.Builder();
+ audioAttributes.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC);
+ audioAttributes.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE);
+ notification.audioAttributes = audioAttributes.build();
+ if (mDialerRingtoneManager.shouldVibrate(mContext.getContentResolver())) {
+ notification.vibrate = VIBRATE_PATTERN;
+ }
+ }
+ if (mDialerRingtoneManager.shouldPlayCallWaitingTone(callState)) {
+ Log.v(this, "Playing call waiting tone");
+ mDialerRingtoneManager.playCallWaitingTone();
+ }
+ if (mCurrentNotification != notificationType && mCurrentNotification != NOTIFICATION_NONE) {
+ Log.i(this, "Previous notification already showing - cancelling " + mCurrentNotification);
+ mNotificationManager.cancel(mCurrentNotification);
+ }
+
+ Log.i(this, "Displaying notification for " + notificationType);
+ try {
+ mNotificationManager.notify(notificationType, notification);
+ } catch (RuntimeException e) {
+ // TODO(b/34744003): Move the memory stats into silent feedback PSD.
+ ActivityManager activityManager = mContext.getSystemService(ActivityManager.class);
+ ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
+ activityManager.getMemoryInfo(memoryInfo);
+ throw new RuntimeException(
+ String.format(
+ "Error displaying notification with photo type: %d (low memory? %b, availMem: %d)",
+ contactInfo.photoType, memoryInfo.lowMemory, memoryInfo.availMem),
+ e);
+ }
+ call.getLatencyReport().onNotificationShown();
+ mCurrentNotification = notificationType;
+ }
+
+ private void createIncomingCallNotification(
+ DialerCall call, int state, Notification.Builder builder) {
+ setNotificationWhen(call, state, builder);
+
+ // Add hang up option for any active calls (active | onhold), outgoing calls (dialing).
+ if (state == DialerCall.State.ACTIVE
+ || state == DialerCall.State.ONHOLD
+ || DialerCall.State.isDialing(state)) {
+ addHangupAction(builder);
+ } else if (state == DialerCall.State.INCOMING || state == DialerCall.State.CALL_WAITING) {
+ addDismissAction(builder);
+ if (call.isVideoCall()) {
+ addVideoCallAction(builder);
+ } else {
+ addAnswerAction(builder);
+ }
+ }
+ }
+
+ /**
+ * Sets the notification's when section as needed. For active calls, this is explicitly set as the
+ * duration of the call. For all other states, the notification will automatically show the time
+ * at which the notification was created.
+ */
+ private void setNotificationWhen(DialerCall call, int state, Notification.Builder builder) {
+ if (state == DialerCall.State.ACTIVE) {
+ builder.setUsesChronometer(true);
+ builder.setWhen(call.getConnectTimeMillis());
+ } else {
+ builder.setUsesChronometer(false);
+ }
+ }
+
+ /**
+ * Checks the new notification data and compares it against any notification that we are already
+ * displaying. If the data is exactly the same, we return false so that we do not issue a new
+ * notification for the exact same data.
+ */
+ private boolean checkForChangeAndSaveData(
+ int icon,
+ String content,
+ Bitmap largeIcon,
+ String contentTitle,
+ int state,
+ int notificationType,
+ Uri ringtone) {
+
+ // The two are different:
+ // if new title is not null, it should be different from saved version OR
+ // if new title is null, the saved version should not be null
+ final boolean contentTitleChanged =
+ (contentTitle != null && !contentTitle.equals(mSavedContentTitle))
+ || (contentTitle == null && mSavedContentTitle != null);
+
+ // any change means we are definitely updating
+ boolean retval =
+ (mSavedIcon != icon)
+ || !Objects.equals(mSavedContent, content)
+ || (mCallState != state)
+ || (mSavedLargeIcon != largeIcon)
+ || contentTitleChanged
+ || !Objects.equals(mRingtone, ringtone);
+
+ // If we aren't showing a notification right now or the notification type is changing,
+ // definitely do an update.
+ if (mCurrentNotification != notificationType) {
+ if (mCurrentNotification == NOTIFICATION_NONE) {
+ Log.d(this, "Showing notification for first time.");
+ }
+ retval = true;
+ }
+
+ mSavedIcon = icon;
+ mSavedContent = content;
+ mCallState = state;
+ mSavedLargeIcon = largeIcon;
+ mSavedContentTitle = contentTitle;
+ mRingtone = ringtone;
+
+ if (retval) {
+ Log.d(this, "Data changed. Showing notification");
+ }
+
+ return retval;
+ }
+
+ /** Returns the main string to use in the notification. */
+ @VisibleForTesting
+ @Nullable
+ String getContentTitle(ContactCacheEntry contactInfo, DialerCall call) {
+ if (call.isConferenceCall() && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)) {
+ return mContext.getResources().getString(R.string.conference_call_name);
+ }
+
+ String preferredName =
+ ContactDisplayUtils.getPreferredDisplayName(
+ contactInfo.namePrimary, contactInfo.nameAlternative, mContactsPreferences);
+ if (TextUtils.isEmpty(preferredName)) {
+ return TextUtils.isEmpty(contactInfo.number)
+ ? null
+ : BidiFormatter.getInstance()
+ .unicodeWrap(contactInfo.number, TextDirectionHeuristics.LTR);
+ }
+ return preferredName;
+ }
+
+ private void addPersonReference(
+ Notification.Builder builder, ContactCacheEntry contactInfo, DialerCall call) {
+ // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed.
+ // So, do not pass {@link Contacts#CONTENT_LOOKUP_URI} to NotificationManager to avoid
+ // NotificationManager using it.
+ if (contactInfo.lookupUri != null && contactInfo.userType != ContactsUtils.USER_TYPE_WORK) {
+ builder.addPerson(contactInfo.lookupUri.toString());
+ } else if (!TextUtils.isEmpty(call.getNumber())) {
+ builder.addPerson(Uri.fromParts(PhoneAccount.SCHEME_TEL, call.getNumber(), null).toString());
+ }
+ }
+
+ /** Gets a large icon from the contact info object to display in the notification. */
+ private Bitmap getLargeIconToDisplay(ContactCacheEntry contactInfo, DialerCall call) {
+ Bitmap largeIcon = null;
+ if (call.isConferenceCall() && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)) {
+ largeIcon = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.img_conference);
+ }
+ if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) {
+ largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap();
+ }
+ if (call.isSpam()) {
+ Drawable drawable = mContext.getResources().getDrawable(R.drawable.blocked_contact);
+ largeIcon = DrawableConverter.drawableToBitmap(drawable);
+ }
+ return largeIcon;
+ }
+
+ private Bitmap getRoundedIcon(Bitmap bitmap) {
+ if (bitmap == null) {
+ return null;
+ }
+ final int height =
+ (int) mContext.getResources().getDimension(android.R.dimen.notification_large_icon_height);
+ final int width =
+ (int) mContext.getResources().getDimension(android.R.dimen.notification_large_icon_width);
+ return BitmapUtil.getRoundedBitmap(bitmap, width, height);
+ }
+
+ /**
+ * Returns the appropriate icon res Id to display based on the call for which we want to display
+ * information.
+ */
+ private int getIconToDisplay(DialerCall call) {
+ // Even if both lines are in use, we only show a single item in
+ // the expanded Notifications UI. It's labeled "Ongoing call"
+ // (or "On hold" if there's only one call, and it's on hold.)
+ // Also, we don't have room to display caller-id info from two
+ // different calls. So if both lines are in use, display info
+ // from the foreground call. And if there's a ringing call,
+ // display that regardless of the state of the other calls.
+ if (call.getState() == DialerCall.State.ONHOLD) {
+ return R.drawable.ic_phone_paused_white_24dp;
+ } else if (call.getSessionModificationState()
+ == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ return R.drawable.ic_videocam;
+ }
+ return R.anim.on_going_call;
+ }
+
+ /** Returns the message to use with the notification. */
+ private String getContentString(DialerCall call, @UserType long userType) {
+ boolean isIncomingOrWaiting =
+ call.getState() == DialerCall.State.INCOMING
+ || call.getState() == DialerCall.State.CALL_WAITING;
+
+ if (isIncomingOrWaiting
+ && call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED) {
+
+ if (!TextUtils.isEmpty(call.getChildNumber())) {
+ return mContext.getString(R.string.child_number, call.getChildNumber());
+ } else if (!TextUtils.isEmpty(call.getCallSubject()) && call.isCallSubjectSupported()) {
+ return call.getCallSubject();
+ }
+ }
+
+ int resId = R.string.notification_ongoing_call;
+ if (call.hasProperty(Details.PROPERTY_WIFI)) {
+ resId = R.string.notification_ongoing_call_wifi;
+ }
+
+ if (isIncomingOrWaiting) {
+ if (call.hasProperty(Details.PROPERTY_WIFI)) {
+ resId = R.string.notification_incoming_call_wifi;
+ } else {
+ if (call.isSpam()) {
+ resId = R.string.notification_incoming_spam_call;
+ } else {
+ resId = R.string.notification_incoming_call;
+ }
+ }
+ } else if (call.getState() == DialerCall.State.ONHOLD) {
+ resId = R.string.notification_on_hold;
+ } else if (DialerCall.State.isDialing(call.getState())) {
+ resId = R.string.notification_dialing;
+ } else if (call.getSessionModificationState()
+ == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ resId = R.string.notification_requesting_video_call;
+ }
+
+ // Is the call placed through work connection service.
+ boolean isWorkCall = call.hasProperty(PROPERTY_ENTERPRISE_CALL);
+ if (userType == ContactsUtils.USER_TYPE_WORK || isWorkCall) {
+ resId = getWorkStringFromPersonalString(resId);
+ }
+
+ return mContext.getString(resId);
+ }
+
+ /** Gets the most relevant call to display in the notification. */
+ private DialerCall getCallToShow(CallList callList) {
+ if (callList == null) {
+ return null;
+ }
+ DialerCall call = callList.getIncomingCall();
+ if (call == null) {
+ call = callList.getOutgoingCall();
+ }
+ if (call == null) {
+ call = callList.getVideoUpgradeRequestCall();
+ }
+ if (call == null) {
+ call = callList.getActiveOrBackgroundCall();
+ }
+ return call;
+ }
+
+ private Spannable getActionText(@StringRes int stringRes, @ColorRes int colorRes) {
+ Spannable spannable = new SpannableString(mContext.getText(stringRes));
+ if (VERSION.SDK_INT >= VERSION_CODES.N_MR1) {
+ // This will only work for cases where the Notification.Builder has a fullscreen intent set
+ // Notification.Builder that does not have a full screen intent will take the color of the
+ // app and the following leads to a no-op.
+ spannable.setSpan(
+ new ForegroundColorSpan(mContext.getColor(colorRes)), 0, spannable.length(), 0);
+ }
+ return spannable;
+ }
+
+ private void addAnswerAction(Notification.Builder builder) {
+ Log.d(this, "Will show \"answer\" action in the incoming call Notification");
+ PendingIntent answerVoicePendingIntent =
+ createNotificationPendingIntent(mContext, ACTION_ANSWER_VOICE_INCOMING_CALL);
+ builder.addAction(
+ R.anim.on_going_call,
+ getActionText(R.string.notification_action_answer, R.color.notification_action_accept),
+ answerVoicePendingIntent);
+ }
+
+ private void addDismissAction(Notification.Builder builder) {
+ Log.d(this, "Will show \"decline\" action in the incoming call Notification");
+ PendingIntent declinePendingIntent =
+ createNotificationPendingIntent(mContext, ACTION_DECLINE_INCOMING_CALL);
+ builder.addAction(
+ R.drawable.ic_close_dk,
+ getActionText(R.string.notification_action_dismiss, R.color.notification_action_dismiss),
+ declinePendingIntent);
+ }
+
+ private void addHangupAction(Notification.Builder builder) {
+ Log.d(this, "Will show \"hang-up\" action in the ongoing active call Notification");
+ PendingIntent hangupPendingIntent =
+ createNotificationPendingIntent(mContext, ACTION_HANG_UP_ONGOING_CALL);
+ builder.addAction(
+ R.drawable.ic_call_end_white_24dp,
+ getActionText(R.string.notification_action_end_call, R.color.notification_action_end_call),
+ hangupPendingIntent);
+ }
+
+ private void addVideoCallAction(Notification.Builder builder) {
+ Log.i(this, "Will show \"video\" action in the incoming call Notification");
+ PendingIntent answerVideoPendingIntent =
+ createNotificationPendingIntent(mContext, ACTION_ANSWER_VIDEO_INCOMING_CALL);
+ builder.addAction(
+ R.drawable.ic_videocam,
+ getActionText(
+ R.string.notification_action_answer_video, R.color.notification_action_answer_video),
+ answerVideoPendingIntent);
+ }
+
+ private void addAcceptUpgradeRequestAction(Notification.Builder builder) {
+ Log.i(this, "Will show \"accept upgrade\" action in the incoming call Notification");
+ PendingIntent acceptVideoPendingIntent =
+ createNotificationPendingIntent(mContext, ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST);
+ builder.addAction(
+ R.drawable.ic_videocam,
+ getActionText(R.string.notification_action_accept, R.color.notification_action_accept),
+ acceptVideoPendingIntent);
+ }
+
+ private void addDismissUpgradeRequestAction(Notification.Builder builder) {
+ Log.i(this, "Will show \"dismiss upgrade\" action in the incoming call Notification");
+ PendingIntent declineVideoPendingIntent =
+ createNotificationPendingIntent(mContext, ACTION_DECLINE_VIDEO_UPGRADE_REQUEST);
+ builder.addAction(
+ R.drawable.ic_videocam,
+ getActionText(R.string.notification_action_dismiss, R.color.notification_action_dismiss),
+ declineVideoPendingIntent);
+ }
+
+ /** Adds fullscreen intent to the builder. */
+ private void configureFullScreenIntent(
+ Notification.Builder builder, PendingIntent intent, CallList callList, DialerCall call) {
+ // Ok, we actually want to launch the incoming call
+ // UI at this point (in addition to simply posting a notification
+ // to the status bar). Setting fullScreenIntent will cause
+ // the InCallScreen to be launched immediately *unless* the
+ // current foreground activity is marked as "immersive".
+ Log.d(this, "- Setting fullScreenIntent: " + intent);
+ builder.setFullScreenIntent(intent, true);
+
+ // Ugly hack alert:
+ //
+ // The NotificationManager has the (undocumented) behavior
+ // that it will *ignore* the fullScreenIntent field if you
+ // post a new Notification that matches the ID of one that's
+ // already active. Unfortunately this is exactly what happens
+ // when you get an incoming call-waiting call: the
+ // "ongoing call" notification is already visible, so the
+ // InCallScreen won't get launched in this case!
+ // (The result: if you bail out of the in-call UI while on a
+ // call and then get a call-waiting call, the incoming call UI
+ // won't come up automatically.)
+ //
+ // The workaround is to just notice this exact case (this is a
+ // call-waiting call *and* the InCallScreen is not in the
+ // foreground) and manually cancel the in-call notification
+ // before (re)posting it.
+ //
+ // TODO: there should be a cleaner way of avoiding this
+ // problem (see discussion in bug 3184149.)
+
+ // If a call is onhold during an incoming call, the call actually comes in as
+ // INCOMING. For that case *and* traditional call-waiting, we want to
+ // cancel the notification.
+ boolean isCallWaiting =
+ (call.getState() == DialerCall.State.CALL_WAITING
+ || (call.getState() == DialerCall.State.INCOMING
+ && callList.getBackgroundCall() != null));
+
+ if (isCallWaiting) {
+ Log.i(this, "updateInCallNotification: call-waiting! force relaunch...");
+ // Cancel the IN_CALL_NOTIFICATION immediately before
+ // (re)posting it; this seems to force the
+ // NotificationManager to launch the fullScreenIntent.
+ mNotificationManager.cancel(NOTIFICATION_IN_CALL);
+ }
+ }
+
+ private Notification.Builder getNotificationBuilder() {
+ final Notification.Builder builder = new Notification.Builder(mContext);
+ builder.setOngoing(true);
+
+ // Make the notification prioritized over the other normal notifications.
+ builder.setPriority(Notification.PRIORITY_HIGH);
+
+ return builder;
+ }
+
+ private PendingIntent createLaunchPendingIntent(boolean isFullScreen, boolean isVideoCall) {
+ Intent intent =
+ InCallActivity.getIntent(
+ mContext,
+ false /* showDialpad */,
+ false /* newOutgoingCall */,
+ isVideoCall,
+ isFullScreen);
+
+ int requestCode = PENDING_INTENT_REQUEST_CODE_NON_FULL_SCREEN;
+ if (isFullScreen) {
+ // Use a unique request code so that the pending intent isn't clobbered by the
+ // non-full screen pending intent.
+ requestCode = PENDING_INTENT_REQUEST_CODE_FULL_SCREEN;
+ }
+
+ // PendingIntent that can be used to launch the InCallActivity. The
+ // system fires off this intent if the user pulls down the windowshade
+ // and clicks the notification's expanded view. It's also used to
+ // launch the InCallActivity immediately when when there's an incoming
+ // call (see the "fullScreenIntent" field below).
+ return PendingIntent.getActivity(mContext, requestCode, intent, 0);
+ }
+
+ private void setStatusBarCallListener(StatusBarCallListener listener) {
+ if (mStatusBarCallListener != null) {
+ mStatusBarCallListener.cleanup();
+ }
+ mStatusBarCallListener = listener;
+ }
+
+ private class StatusBarCallListener implements DialerCallListener {
+
+ private DialerCall mDialerCall;
+
+ StatusBarCallListener(DialerCall dialerCall) {
+ mDialerCall = dialerCall;
+ mDialerCall.addListener(this);
+ }
+
+ void cleanup() {
+ mDialerCall.removeListener(this);
+ }
+
+ @Override
+ public void onDialerCallDisconnect() {}
+
+ @Override
+ public void onDialerCallUpdate() {
+ if (CallList.getInstance().getIncomingCall() == null) {
+ mDialerRingtoneManager.stopCallWaitingTone();
+ }
+ }
+
+ @Override
+ public void onDialerCallChildNumberChange() {}
+
+ @Override
+ public void onDialerCallLastForwardedNumberChange() {}
+
+ @Override
+ public void onDialerCallUpgradeToVideo() {}
+
+ @Override
+ public void onWiFiToLteHandover() {}
+
+ @Override
+ public void onHandoverToWifiFailure() {}
+
+ /**
+ * Responds to changes in the session modification state for the call by dismissing the status
+ * bar notification as required.
+ */
+ @Override
+ public void onDialerCallSessionModificationStateChange(@SessionModificationState int state) {
+ if (state == DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST) {
+ cleanup();
+ updateNotification(CallList.getInstance());
+ }
+ }
+ }
+}