| /* |
| * 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.getAssociatedUser(); |
| // 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 | PendingIntent.FLAG_IMMUTABLE); |
| } |
| |
| 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, PendingIntent.FLAG_IMMUTABLE, 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); |
| } |
| } |
| } |