/* * 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 android.telecom.Call.Details.PROPERTY_HIGH_DEF_AUDIO; 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.Manifest; import android.app.ActivityManager; import android.app.Notification; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; 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.RequiresPermission; import android.support.annotation.StringRes; import android.support.annotation.VisibleForTesting; import android.support.v4.os.BuildCompat; import android.telecom.Call.Details; import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; 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.ContactDisplayUtils; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.configprovider.ConfigProviderBindings; import com.android.dialer.contactphoto.BitmapUtil; import com.android.dialer.enrichedcall.EnrichedCallManager; import com.android.dialer.enrichedcall.Session; import com.android.dialer.lettertile.LetterTileDrawable; import com.android.dialer.lettertile.LetterTileDrawable.ContactType; import com.android.dialer.multimedia.MultimediaData; import com.android.dialer.notification.DialerNotificationManager; import com.android.dialer.notification.NotificationChannelId; import com.android.dialer.oem.MotorolaUtils; 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.DialerCallListener; import com.android.incallui.ringtone.DialerRingtoneManager; import com.android.incallui.ringtone.InCallTonePlayer; import com.android.incallui.ringtone.ToneGeneratorFactory; import com.android.incallui.videotech.utils.SessionModificationState; import java.util.List; import java.util.Locale; import java.util.Objects; /** This class adds Notifications to the status bar for the in-call experience. */ public class StatusBarNotifier implements InCallPresenter.InCallStateListener, EnrichedCallManager.StateChangedListener { private static final String NOTIFICATION_TAG = "STATUS_BAR_NOTIFIER"; private static final int NOTIFICATION_ID = 1; // 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; // Notification for incoming calls in the case where there is already an active call. // This is non-interruptive, but otherwise behaves the same as NOTIFICATION_INCOMING_CALL private static final int NOTIFICATION_INCOMING_CALL_QUIET = 3; private static final long[] VIBRATE_PATTERN = new long[] {0, 1000, 1000}; private final Context mContext; private final ContactInfoCache mContactInfoCache; 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) { mContext = Assert.isNotNull(context); mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext); mContactInfoCache = contactInfoCache; 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 context) { LogUtil.e( "StatusBarNotifier.clearAllCallNotifications", "something terrible happened, clear all InCall notifications"); DialerNotificationManager.cancel(context, NOTIFICATION_TAG, NOTIFICATION_ID); } 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_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 @RequiresPermission(Manifest.permission.READ_PHONE_STATE) public void onStateChange(InCallState oldState, InCallState newState, CallList callList) { LogUtil.d("StatusBarNotifier.onStateChange", "%s->%s", oldState, newState); updateNotification(callList); } @Override public void onEnrichedCallStateChanged() { LogUtil.enterBlock("StatusBarNotifier.onEnrichedCallStateChanged"); updateNotification(CallList.getInstance()); } /** * Updates the phone app's status bar notification *and* launches the incoming call UI in response * to a new incoming call. * *
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". * *
(This is the mechanism that actually brings up the incoming call UI when we receive a "new * ringing connection" event from the telephony layer.) * *
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)
*/
@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
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) {
LogUtil.i("StatusBarNotifier.cancelNotification", "cancel");
DialerNotificationManager.cancel(mContext, NOTIFICATION_TAG, NOTIFICATION_ID);
}
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.
*/
@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
private void updateInCallNotification(CallList callList) {
LogUtil.d("StatusBarNotifier.updateInCallNotification", "");
final DialerCall call = getCallToShow(callList);
if (call != null) {
showNotification(callList, call);
} else {
cancelNotification();
}
}
@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
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
@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
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
@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
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 */
@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
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(mContext, contactInfo, call);
final String content = getContentString(call, contactInfo.userType);
final String contentTitle = getContentTitle(contactInfo, call);
final boolean isVideoUpgradeRequest =
call.getVideoTech().getSessionModificationState()
== SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
final int notificationType;
if (callState == DialerCall.State.INCOMING
|| callState == DialerCall.State.CALL_WAITING
|| isVideoUpgradeRequest) {
if (ConfigProviderBindings.get(mContext)
.getBoolean("quiet_incoming_call_if_ui_showing", true)) {
notificationType =
InCallPresenter.getInstance().isShowingInCallUi()
? NOTIFICATION_INCOMING_CALL_QUIET
: NOTIFICATION_INCOMING_CALL;
} else {
boolean alreadyActive =
callList.getActiveOrBackgroundCall() != null
&& InCallPresenter.getInstance().isShowingInCallUi();
notificationType =
alreadyActive ? NOTIFICATION_INCOMING_CALL_QUIET : 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, mContext.getTheme()))
// 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 */));
// Set the intent as a full screen intent as well if a call is incoming
PhoneAccountHandle accountHandle = call.getAccountHandle();
if (accountHandle == null) {
accountHandle = getAnyPhoneAccount();
}
LogUtil.i("StatusBarNotifier.buildAndSendNotification", "notificationType=" + notificationType);
switch (notificationType) {
case NOTIFICATION_INCOMING_CALL:
if (BuildCompat.isAtLeastO()) {
builder.setChannelId(NotificationChannelId.INCOMING_CALL);
}
configureFullScreenIntent(builder, createLaunchPendingIntent(true /* isFullScreen */));
// Set the notification category and bump the priority for incoming calls
builder.setCategory(Notification.CATEGORY_CALL);
// This will be ignored on O+ and handled by the channel
builder.setPriority(Notification.PRIORITY_MAX);
if (mCurrentNotification != NOTIFICATION_INCOMING_CALL) {
LogUtil.i(
"StatusBarNotifier.buildAndSendNotification",
"Canceling old notification so this one can be noisy");
// Moving from a non-interuptive notification (or none) to a noisy one. Cancel the old
// notification (if there is one) so the fullScreenIntent or HUN will show
DialerNotificationManager.cancel(mContext, NOTIFICATION_TAG, NOTIFICATION_ID);
}
break;
case NOTIFICATION_INCOMING_CALL_QUIET:
if (BuildCompat.isAtLeastO()) {
builder.setChannelId(NotificationChannelId.ONGOING_CALL);
}
break;
case NOTIFICATION_IN_CALL:
if (BuildCompat.isAtLeastO()) {
publicBuilder.setColorized(true);
builder.setColorized(true);
builder.setChannelId(NotificationChannelId.ONGOING_CALL);
}
break;
default: // fall out
}
// 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, mContext.getTheme()));
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)) {
LogUtil.v("StatusBarNotifier.buildAndSendNotification", "playing call waiting tone");
mDialerRingtoneManager.playCallWaitingTone();
}
LogUtil.i(
"StatusBarNotifier.buildAndSendNotification",
"displaying notification for " + notificationType);
try {
DialerNotificationManager.notify(mContext, NOTIFICATION_TAG, NOTIFICATION_ID, 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(
Locale.US,
"Error displaying notification with photo type: %d (low memory? %b, availMem: %d)",
contactInfo.photoType,
memoryInfo.lowMemory,
memoryInfo.availMem),
e);
}
call.getLatencyReport().onNotificationShown();
mCurrentNotification = notificationType;
}
@Nullable
@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
private PhoneAccountHandle getAnyPhoneAccount() {
PhoneAccountHandle accountHandle;
TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class);
accountHandle = telecomManager.getDefaultOutgoingPhoneAccount(PhoneAccount.SCHEME_TEL);
if (accountHandle == null) {
List