blob: 3f54689e5eeaef5967774f4ab51b141c61a4a912 [file] [log] [blame]
/*
* Copyright (C) 2019 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.server.telecom.ui;
import android.annotation.NonNull;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.TaskStackBuilder;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Binder;
import android.os.UserHandle;
import android.provider.CallLog;
import android.telecom.DisconnectCause;
import android.telecom.Log;
import android.telecom.PhoneAccount;
import android.telephony.PhoneNumberUtils;
import android.telephony.TelephonyManager;
import android.text.BidiFormatter;
import android.text.TextDirectionHeuristics;
import android.text.TextUtils;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallState;
import com.android.server.telecom.CallsManager;
import com.android.server.telecom.CallsManagerListenerBase;
import com.android.server.telecom.Constants;
import com.android.server.telecom.R;
import com.android.server.telecom.TelecomBroadcastIntentProcessor;
import com.android.server.telecom.components.TelecomBroadcastReceiver;
import java.util.Locale;
/**
* Handles notifications generated by Telecom for the case that a call was disconnected in order to
* connect another "higher priority" emergency call and gives the user the choice to call or
* message that user back after, similar to the missed call notifier.
*/
public class DisconnectedCallNotifier extends CallsManagerListenerBase {
public interface Factory {
DisconnectedCallNotifier create(Context context, CallsManager manager);
}
public static class Default implements Factory {
@Override
public DisconnectedCallNotifier create(Context context, CallsManager manager) {
return new DisconnectedCallNotifier(context, manager);
}
}
private static class CallInfo {
public final UserHandle userHandle;
public final Uri handle;
public final long endTimeMs;
public final Bitmap callerInfoIcon;
public final Drawable callerInfoPhoto;
public final String callerInfoName;
public final boolean isEmergency;
public CallInfo(UserHandle userHandle, Uri handle, long endTimeMs, Bitmap callerInfoIcon,
Drawable callerInfoPhoto, String callerInfoName, boolean isEmergency) {
this.userHandle = userHandle;
this.handle = handle;
this.endTimeMs = endTimeMs;
this.callerInfoIcon = callerInfoIcon;
this.callerInfoPhoto = callerInfoPhoto;
this.callerInfoName = callerInfoName;
this.isEmergency = isEmergency;
}
@Override
public String toString() {
return "CallInfo{" +
"userHandle=" + userHandle +
", handle=" + handle +
", isEmergency=" + isEmergency +
", endTimeMs=" + endTimeMs +
", callerInfoIcon=" + callerInfoIcon +
", callerInfoPhoto=" + callerInfoPhoto +
", callerInfoName='" + callerInfoName + '\'' +
'}';
}
}
private static final String NOTIFICATION_TAG =
DisconnectedCallNotifier.class.getSimpleName();
private static final int DISCONNECTED_CALL_NOTIFICATION_ID = 1;
private final Context mContext;
private final CallsManager mCallsManager;
private final NotificationManager mNotificationManager;
// The pending info to display to the user after they have ended the emergency call.
private CallInfo mPendingCallNotification;
public DisconnectedCallNotifier(Context context, CallsManager callsManager) {
mContext = context;
mNotificationManager =
(NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
mCallsManager = callsManager;
}
@Override
public void onCallRemoved(Call call) {
// Wait until the emergency call is ended before showing the notification.
if (mCallsManager.getCalls().isEmpty() && mPendingCallNotification != null) {
showDisconnectedNotification(mPendingCallNotification);
mPendingCallNotification = null;
}
}
@Override
public void onCallStateChanged(Call call, int oldState, int newState) {
DisconnectCause cause = call.getDisconnectCause();
if (cause == null) {
Log.w(this, "onCallStateChanged: unexpected null disconnect cause.");
return;
}
// Call disconnected in favor of an emergency call. Place the call into a pending queue.
if ((newState == CallState.DISCONNECTED) && (cause.getCode() == DisconnectCause.LOCAL) &&
DisconnectCause.REASON_EMERGENCY_CALL_PLACED.equals(cause.getReason())) {
// Clear any existing notification.
clearNotification(mCallsManager.getCurrentUserHandle());
UserHandle userHandle = call.getTargetPhoneAccount() != null ?
call.getTargetPhoneAccount().getUserHandle() : call.getInitiatingUser();
// As a last resort, use the current user to display the notification.
if (userHandle == null) userHandle = mCallsManager.getCurrentUserHandle();
mPendingCallNotification = new CallInfo(userHandle, call.getHandle(),
call.getCreationTimeMillis() + call.getAgeMillis(), call.getPhotoIcon(),
call.getPhoto(), call.getName(), call.isEmergencyCall());
}
}
private void showDisconnectedNotification(@NonNull CallInfo call) {
Log.i(this, "showDisconnectedNotification: userHandle=%d", call.userHandle.getIdentifier());
final int titleResId = R.string.notification_disconnectedCall_title;
final CharSequence expandedText = call.isEmergency
? mContext.getText(R.string.notification_disconnectedCall_generic_body)
: mContext.getString(R.string.notification_disconnectedCall_body,
getNameForCallNotification(call));
// Create a public viewable version of the notification, suitable for display when sensitive
// notification content is hidden.
// We use user's context here to make sure notification is badged if it is a managed user.
Context contextForUser = getContextForUser(call.userHandle);
Notification.Builder publicBuilder = new Notification.Builder(contextForUser,
NotificationChannelManager.CHANNEL_ID_DISCONNECTED_CALLS);
publicBuilder.setSmallIcon(android.R.drawable.stat_notify_error)
.setColor(mContext.getResources().getColor(R.color.theme_color, null /*theme*/))
// Set when the call was disconnected.
.setWhen(call.endTimeMs)
.setShowWhen(true)
// Show "Phone" for notification title.
.setContentTitle(mContext.getText(R.string.userCallActivityLabel))
// Notification details shows that there are disconnected call(s), but does not
// reveal the caller information.
.setContentText(mContext.getText(titleResId))
.setAutoCancel(true);
if (!call.isEmergency) {
publicBuilder.setContentIntent(createCallLogPendingIntent(call.userHandle));
}
// Create the notification suitable for display when sensitive information is showing.
Notification.Builder builder = new Notification.Builder(contextForUser,
NotificationChannelManager.CHANNEL_ID_DISCONNECTED_CALLS);
builder.setSmallIcon(android.R.drawable.stat_notify_error)
.setColor(mContext.getResources().getColor(R.color.theme_color, null /*theme*/))
.setWhen(call.endTimeMs)
.setShowWhen(true)
.setContentTitle(mContext.getText(titleResId))
//Only show expanded text for sensitive information
.setStyle(new Notification.BigTextStyle().bigText(expandedText))
.setAutoCancel(true)
// Include a public version of the notification to be shown when the call
// notification is shown on the user's lock screen and they have chosen to hide
// sensitive notification information.
.setPublicVersion(publicBuilder.build())
.setChannelId(NotificationChannelManager.CHANNEL_ID_DISCONNECTED_CALLS);
if (!call.isEmergency) {
builder.setContentIntent(createCallLogPendingIntent(call.userHandle));
}
String handle = call.handle != null ? call.handle.getSchemeSpecificPart() : null;
if (!TextUtils.isEmpty(handle)
&& !TextUtils.equals(handle, mContext.getString(R.string.handle_restricted))
&& !call.isEmergency) {
builder.addAction(new Notification.Action.Builder(
Icon.createWithResource(contextForUser, R.drawable.ic_phone_24dp),
// Reuse missed call "Call back"
mContext.getString(R.string.notification_missedCall_call_back),
createCallBackPendingIntent(call.handle, call.userHandle)).build());
if (canRespondViaSms(call)) {
builder.addAction(new Notification.Action.Builder(
Icon.createWithResource(contextForUser, R.drawable.ic_message_24dp),
// Reuse missed call "Call back"
mContext.getString(R.string.notification_missedCall_message),
createSendSmsFromNotificationPendingIntent(call.handle,
call.userHandle)).build());
}
}
if (call.callerInfoIcon != null) {
builder.setLargeIcon(call.callerInfoIcon);
} else {
if (call.callerInfoPhoto instanceof BitmapDrawable) {
builder.setLargeIcon(((BitmapDrawable) call.callerInfoPhoto).getBitmap());
}
}
Notification notification = builder.build();
Log.i(this, "Adding missed call notification for %s.", Log.pii(call.handle));
long token = Binder.clearCallingIdentity();
try {
// TODO: Only support one notification right now, so if multiple are hung up, we only
// show the last one. Support multiple in the future.
mNotificationManager.notifyAsUser(NOTIFICATION_TAG, DISCONNECTED_CALL_NOTIFICATION_ID,
notification, call.userHandle);
} finally {
Binder.restoreCallingIdentity(token);
}
}
/**
* Returns the name to use in the call notification.
*/
private String getNameForCallNotification(@NonNull CallInfo call) {
String number = call.handle != null ? call.handle.getSchemeSpecificPart() : null;
if (!TextUtils.isEmpty(number)) {
String formattedNumber = PhoneNumberUtils.formatNumber(number,
getCurrentCountryIso(mContext));
// The formatted number will be null if there was a problem formatting it, but we can
// default to using the unformatted number instead (e.g. a SIP URI may not be able to
// be formatted.
if (!TextUtils.isEmpty(formattedNumber)) {
number = formattedNumber;
}
}
if (!TextUtils.isEmpty(call.callerInfoName) && TextUtils.isGraphic(call.callerInfoName)) {
return call.callerInfoName;
}
if (!TextUtils.isEmpty(number)) {
// A handle should always be displayed LTR using {@link BidiFormatter} regardless of the
// content of the rest of the notification.
// TODO: Does this apply to SIP addresses?
BidiFormatter bidiFormatter = BidiFormatter.getInstance();
return bidiFormatter.unicodeWrap(number, TextDirectionHeuristics.LTR);
} else {
// Use "unknown" if the call is unidentifiable.
return mContext.getString(R.string.unknown);
}
}
/**
* @return The ISO 3166-1 two letters country code of the country the user is in based on the
* network location. If the network location does not exist, fall back to the locale
* setting.
*/
private String getCurrentCountryIso(Context context) {
// Without framework function calls, this seems to be the most accurate location service
// we can rely on.
final TelephonyManager telephonyManager =
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
String countryIso = telephonyManager.getNetworkCountryIso().toUpperCase();
if (countryIso == null) {
countryIso = Locale.getDefault().getCountry();
Log.w(this, "No CountryDetector; falling back to countryIso based on locale: "
+ countryIso);
}
return countryIso;
}
private Context getContextForUser(UserHandle user) {
try {
return mContext.createPackageContextAsUser(mContext.getPackageName(), 0, user);
} catch (PackageManager.NameNotFoundException e) {
// Default to mContext, not finding the package system is running as is unlikely.
return mContext;
}
}
/**
* Creates an intent to be invoked when the user opts to "call back" from the disconnected call
* notification.
*
* @param handle The handle to call back.
*/
private PendingIntent createCallBackPendingIntent(Uri handle, UserHandle userHandle) {
return createTelecomPendingIntent(
TelecomBroadcastIntentProcessor.ACTION_DISCONNECTED_CALL_BACK_FROM_NOTIFICATION,
handle, userHandle);
}
/**
* Creates generic pending intent from the specified parameters to be received by
* {@link TelecomBroadcastIntentProcessor}.
*
* @param action The intent action.
* @param data The intent data.
*/
private PendingIntent createTelecomPendingIntent(String action, Uri data,
UserHandle userHandle) {
Intent intent = new Intent(action, data, mContext, TelecomBroadcastReceiver.class);
intent.putExtra(TelecomBroadcastIntentProcessor.EXTRA_USERHANDLE, userHandle);
return PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
private boolean canRespondViaSms(@NonNull CallInfo call) {
// Only allow respond-via-sms for "tel:" calls.
return call.handle != null &&
PhoneAccount.SCHEME_TEL.equals(call.handle.getScheme());
}
/**
* Creates a new pending intent that sends the user to the call log.
*
* @return The pending intent.
*/
private PendingIntent createCallLogPendingIntent(UserHandle userHandle) {
Intent intent = new Intent(Intent.ACTION_VIEW, null);
intent.setType(CallLog.Calls.CONTENT_TYPE);
TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(mContext);
taskStackBuilder.addNextIntent(intent);
return taskStackBuilder.getPendingIntent(0, 0, null, userHandle);
}
/**
* Creates an intent to be invoked when the user opts to "send sms" from the missed call
* notification.
*/
private PendingIntent createSendSmsFromNotificationPendingIntent(Uri handle,
UserHandle userHandle) {
return createTelecomPendingIntent(
TelecomBroadcastIntentProcessor.ACTION_DISCONNECTED_SEND_SMS_FROM_NOTIFICATION,
Uri.fromParts(Constants.SCHEME_SMSTO, handle.getSchemeSpecificPart(), null),
userHandle);
}
/**
* Clear any of the active notifications.
* @param userHandle The user to clear the notifications for.
*/
public void clearNotification(UserHandle userHandle) {
long token = Binder.clearCallingIdentity();
try {
mNotificationManager.cancelAsUser(NOTIFICATION_TAG, DISCONNECTED_CALL_NOTIFICATION_ID,
userHandle);
} finally {
Binder.restoreCallingIdentity(token);
}
}
}