/* * 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.Notification.Builder; 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.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.lettertiles.LetterTileDrawable; 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.enrichedcall.EnrichedCallComponent; import com.android.dialer.enrichedcall.EnrichedCallManager; import com.android.dialer.enrichedcall.Session; import com.android.dialer.multimedia.MultimediaData; import com.android.dialer.notification.NotificationChannelManager; import com.android.dialer.notification.NotificationChannelManager.Channel; 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 { // 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 = R.id.notification_ongoing_call; // Notification for incoming calls. This is interruptive and will show up as a HUN. private static final int NOTIFICATION_INCOMING_CALL = R.id.notification_incoming_call; 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; private boolean mShowFullScreenIntent; 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) { LogUtil.i( "StatusBarNotifier.clearAllCallNotifications", "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); } private static void setColorized(@NonNull Builder builder) { if (BuildCompat.isAtLeastO()) { builder.setColorized(true); } } /** 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.d("StatusBarNotifier.cancelNotification", "cancel");
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.
*/
@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(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) {
notificationType = NOTIFICATION_INCOMING_CALL;
} else {
notificationType = NOTIFICATION_IN_CALL;
}
if (!checkForChangeAndSaveData(
iconResId,
content,
largeIcon,
contentTitle,
callState,
notificationType,
contactInfo.contactRingtoneUri,
InCallPresenter.getInstance().shouldShowFullScreenNotification())) {
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));
setColorized(publicBuilder);
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();
}
if (notificationType == NOTIFICATION_INCOMING_CALL) {
NotificationChannelManager.applyChannel(
builder, mContext, Channel.INCOMING_CALL, accountHandle);
if (InCallPresenter.getInstance().shouldShowFullScreenNotification()) {
configureFullScreenIntent(
builder, createLaunchPendingIntent(true /* isFullScreen */), callList, call);
}
// Set the notification category and bump the priority for incoming calls
builder.setCategory(Notification.CATEGORY_CALL);
builder.setPriority(Notification.PRIORITY_MAX);
} else {
NotificationChannelManager.applyChannel(
builder, mContext, Channel.ONGOING_CALL, accountHandle);
}
// 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()));
setColorized(builder);
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();
}
if (mCurrentNotification != notificationType && mCurrentNotification != NOTIFICATION_NONE) {
LogUtil.i(
"StatusBarNotifier.buildAndSendNotification",
"previous notification already showing - cancelling " + mCurrentNotification);
mNotificationManager.cancel(mCurrentNotification);
}
LogUtil.i(
"StatusBarNotifier.buildAndSendNotification",
"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(
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