1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.telecom.ui; 18 19 import android.annotation.NonNull; 20 import android.app.Notification; 21 import android.app.NotificationManager; 22 import android.app.PendingIntent; 23 import android.app.TaskStackBuilder; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.pm.PackageManager; 27 import android.graphics.Bitmap; 28 import android.graphics.drawable.BitmapDrawable; 29 import android.graphics.drawable.Drawable; 30 import android.graphics.drawable.Icon; 31 import android.net.Uri; 32 import android.os.Binder; 33 import android.os.UserHandle; 34 import android.provider.CallLog; 35 import android.telecom.DisconnectCause; 36 import android.telecom.Log; 37 import android.telecom.PhoneAccount; 38 import android.telephony.PhoneNumberUtils; 39 import android.telephony.TelephonyManager; 40 import android.text.BidiFormatter; 41 import android.text.TextDirectionHeuristics; 42 import android.text.TextUtils; 43 44 import com.android.server.telecom.Call; 45 import com.android.server.telecom.CallState; 46 import com.android.server.telecom.CallsManager; 47 import com.android.server.telecom.CallsManagerListenerBase; 48 import com.android.server.telecom.Constants; 49 import com.android.server.telecom.R; 50 import com.android.server.telecom.TelecomBroadcastIntentProcessor; 51 import com.android.server.telecom.components.TelecomBroadcastReceiver; 52 53 import java.util.Locale; 54 55 /** 56 * Handles notifications generated by Telecom for the case that a call was disconnected in order to 57 * connect another "higher priority" emergency call and gives the user the choice to call or 58 * message that user back after, similar to the missed call notifier. 59 */ 60 public class DisconnectedCallNotifier extends CallsManagerListenerBase { 61 62 public interface Factory { create(Context context, CallsManager manager)63 DisconnectedCallNotifier create(Context context, CallsManager manager); 64 } 65 66 public static class Default implements Factory { 67 68 @Override create(Context context, CallsManager manager)69 public DisconnectedCallNotifier create(Context context, CallsManager manager) { 70 return new DisconnectedCallNotifier(context, manager); 71 } 72 } 73 74 private static class CallInfo { 75 public final UserHandle userHandle; 76 public final Uri handle; 77 public final long endTimeMs; 78 public final Bitmap callerInfoIcon; 79 public final Drawable callerInfoPhoto; 80 public final String callerInfoName; 81 public final boolean isEmergency; 82 CallInfo(UserHandle userHandle, Uri handle, long endTimeMs, Bitmap callerInfoIcon, Drawable callerInfoPhoto, String callerInfoName, boolean isEmergency)83 public CallInfo(UserHandle userHandle, Uri handle, long endTimeMs, Bitmap callerInfoIcon, 84 Drawable callerInfoPhoto, String callerInfoName, boolean isEmergency) { 85 this.userHandle = userHandle; 86 this.handle = handle; 87 this.endTimeMs = endTimeMs; 88 this.callerInfoIcon = callerInfoIcon; 89 this.callerInfoPhoto = callerInfoPhoto; 90 this.callerInfoName = callerInfoName; 91 this.isEmergency = isEmergency; 92 } 93 94 @Override toString()95 public String toString() { 96 return "CallInfo{" + 97 "userHandle=" + userHandle + 98 ", handle=" + handle + 99 ", isEmergency=" + isEmergency + 100 ", endTimeMs=" + endTimeMs + 101 ", callerInfoIcon=" + callerInfoIcon + 102 ", callerInfoPhoto=" + callerInfoPhoto + 103 ", callerInfoName='" + callerInfoName + '\'' + 104 '}'; 105 } 106 } 107 108 private static final String NOTIFICATION_TAG = 109 DisconnectedCallNotifier.class.getSimpleName(); 110 private static final int DISCONNECTED_CALL_NOTIFICATION_ID = 1; 111 112 private final Context mContext; 113 private final CallsManager mCallsManager; 114 private final NotificationManager mNotificationManager; 115 // The pending info to display to the user after they have ended the emergency call. 116 private CallInfo mPendingCallNotification; 117 DisconnectedCallNotifier(Context context, CallsManager callsManager)118 public DisconnectedCallNotifier(Context context, CallsManager callsManager) { 119 mContext = context; 120 mNotificationManager = 121 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); 122 mCallsManager = callsManager; 123 } 124 125 @Override onCallRemoved(Call call)126 public void onCallRemoved(Call call) { 127 // Wait until the emergency call is ended before showing the notification. 128 if (mCallsManager.getCalls().isEmpty() && mPendingCallNotification != null) { 129 showDisconnectedNotification(mPendingCallNotification); 130 mPendingCallNotification = null; 131 } 132 } 133 134 @Override onCallStateChanged(Call call, int oldState, int newState)135 public void onCallStateChanged(Call call, int oldState, int newState) { 136 DisconnectCause cause = call.getDisconnectCause(); 137 if (cause == null) { 138 Log.w(this, "onCallStateChanged: unexpected null disconnect cause."); 139 return; 140 } 141 // Call disconnected in favor of an emergency call. Place the call into a pending queue. 142 if ((newState == CallState.DISCONNECTED) && (cause.getCode() == DisconnectCause.LOCAL) && 143 DisconnectCause.REASON_EMERGENCY_CALL_PLACED.equals(cause.getReason())) { 144 // Clear any existing notification. 145 clearNotification(mCallsManager.getCurrentUserHandle()); 146 UserHandle userHandle = call.getTargetPhoneAccount() != null ? 147 call.getTargetPhoneAccount().getUserHandle() : call.getInitiatingUser(); 148 // As a last resort, use the current user to display the notification. 149 if (userHandle == null) userHandle = mCallsManager.getCurrentUserHandle(); 150 mPendingCallNotification = new CallInfo(userHandle, call.getHandle(), 151 call.getCreationTimeMillis() + call.getAgeMillis(), call.getPhotoIcon(), 152 call.getPhoto(), call.getName(), call.isEmergencyCall()); 153 } 154 } 155 showDisconnectedNotification(@onNull CallInfo call)156 private void showDisconnectedNotification(@NonNull CallInfo call) { 157 Log.i(this, "showDisconnectedNotification: userHandle=%d", call.userHandle.getIdentifier()); 158 159 final int titleResId = R.string.notification_disconnectedCall_title; 160 final CharSequence expandedText = call.isEmergency 161 ? mContext.getText(R.string.notification_disconnectedCall_generic_body) 162 : mContext.getString(R.string.notification_disconnectedCall_body, 163 getNameForCallNotification(call)); 164 165 // Create a public viewable version of the notification, suitable for display when sensitive 166 // notification content is hidden. 167 // We use user's context here to make sure notification is badged if it is a managed user. 168 Context contextForUser = getContextForUser(call.userHandle); 169 Notification.Builder publicBuilder = new Notification.Builder(contextForUser, 170 NotificationChannelManager.CHANNEL_ID_DISCONNECTED_CALLS); 171 publicBuilder.setSmallIcon(android.R.drawable.stat_notify_error) 172 .setColor(mContext.getResources().getColor(R.color.theme_color, null /*theme*/)) 173 // Set when the call was disconnected. 174 .setWhen(call.endTimeMs) 175 .setShowWhen(true) 176 // Show "Phone" for notification title. 177 .setContentTitle(mContext.getText(R.string.userCallActivityLabel)) 178 // Notification details shows that there are disconnected call(s), but does not 179 // reveal the caller information. 180 .setContentText(mContext.getText(titleResId)) 181 .setAutoCancel(true); 182 183 if (!call.isEmergency) { 184 publicBuilder.setContentIntent(createCallLogPendingIntent(call.userHandle)); 185 } 186 187 // Create the notification suitable for display when sensitive information is showing. 188 Notification.Builder builder = new Notification.Builder(contextForUser, 189 NotificationChannelManager.CHANNEL_ID_DISCONNECTED_CALLS); 190 builder.setSmallIcon(android.R.drawable.stat_notify_error) 191 .setColor(mContext.getResources().getColor(R.color.theme_color, null /*theme*/)) 192 .setWhen(call.endTimeMs) 193 .setShowWhen(true) 194 .setContentTitle(mContext.getText(titleResId)) 195 //Only show expanded text for sensitive information 196 .setStyle(new Notification.BigTextStyle().bigText(expandedText)) 197 .setAutoCancel(true) 198 // Include a public version of the notification to be shown when the call 199 // notification is shown on the user's lock screen and they have chosen to hide 200 // sensitive notification information. 201 .setPublicVersion(publicBuilder.build()) 202 .setChannelId(NotificationChannelManager.CHANNEL_ID_DISCONNECTED_CALLS); 203 204 if (!call.isEmergency) { 205 builder.setContentIntent(createCallLogPendingIntent(call.userHandle)); 206 } 207 208 String handle = call.handle != null ? call.handle.getSchemeSpecificPart() : null; 209 210 if (!TextUtils.isEmpty(handle) 211 && !TextUtils.equals(handle, mContext.getString(R.string.handle_restricted)) 212 && !call.isEmergency) { 213 builder.addAction(new Notification.Action.Builder( 214 Icon.createWithResource(contextForUser, R.drawable.ic_phone_24dp), 215 // Reuse missed call "Call back" 216 mContext.getString(R.string.notification_missedCall_call_back), 217 createCallBackPendingIntent(call.handle, call.userHandle)).build()); 218 219 if (canRespondViaSms(call)) { 220 builder.addAction(new Notification.Action.Builder( 221 Icon.createWithResource(contextForUser, R.drawable.ic_message_24dp), 222 // Reuse missed call "Call back" 223 mContext.getString(R.string.notification_missedCall_message), 224 createSendSmsFromNotificationPendingIntent(call.handle, 225 call.userHandle)).build()); 226 } 227 } 228 229 if (call.callerInfoIcon != null) { 230 builder.setLargeIcon(call.callerInfoIcon); 231 } else { 232 if (call.callerInfoPhoto instanceof BitmapDrawable) { 233 builder.setLargeIcon(((BitmapDrawable) call.callerInfoPhoto).getBitmap()); 234 } 235 } 236 237 Notification notification = builder.build(); 238 239 Log.i(this, "Adding missed call notification for %s.", Log.pii(call.handle)); 240 long token = Binder.clearCallingIdentity(); 241 try { 242 // TODO: Only support one notification right now, so if multiple are hung up, we only 243 // show the last one. Support multiple in the future. 244 mNotificationManager.notifyAsUser(NOTIFICATION_TAG, DISCONNECTED_CALL_NOTIFICATION_ID, 245 notification, call.userHandle); 246 } finally { 247 Binder.restoreCallingIdentity(token); 248 } 249 } 250 251 /** 252 * Returns the name to use in the call notification. 253 */ getNameForCallNotification(@onNull CallInfo call)254 private String getNameForCallNotification(@NonNull CallInfo call) { 255 String number = call.handle != null ? call.handle.getSchemeSpecificPart() : null; 256 257 if (!TextUtils.isEmpty(number)) { 258 String formattedNumber = PhoneNumberUtils.formatNumber(number, 259 getCurrentCountryIso(mContext)); 260 261 // The formatted number will be null if there was a problem formatting it, but we can 262 // default to using the unformatted number instead (e.g. a SIP URI may not be able to 263 // be formatted. 264 if (!TextUtils.isEmpty(formattedNumber)) { 265 number = formattedNumber; 266 } 267 } 268 269 if (!TextUtils.isEmpty(call.callerInfoName) && TextUtils.isGraphic(call.callerInfoName)) { 270 return call.callerInfoName; 271 } 272 if (!TextUtils.isEmpty(number)) { 273 // A handle should always be displayed LTR using {@link BidiFormatter} regardless of the 274 // content of the rest of the notification. 275 // TODO: Does this apply to SIP addresses? 276 BidiFormatter bidiFormatter = BidiFormatter.getInstance(); 277 return bidiFormatter.unicodeWrap(number, TextDirectionHeuristics.LTR); 278 } else { 279 // Use "unknown" if the call is unidentifiable. 280 return mContext.getString(R.string.unknown); 281 } 282 } 283 284 /** 285 * @return The ISO 3166-1 two letters country code of the country the user is in based on the 286 * network location. If the network location does not exist, fall back to the locale 287 * setting. 288 */ getCurrentCountryIso(Context context)289 private String getCurrentCountryIso(Context context) { 290 // Without framework function calls, this seems to be the most accurate location service 291 // we can rely on. 292 final TelephonyManager telephonyManager = 293 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 294 String countryIso = telephonyManager.getNetworkCountryIso().toUpperCase(); 295 296 if (countryIso == null) { 297 countryIso = Locale.getDefault().getCountry(); 298 Log.w(this, "No CountryDetector; falling back to countryIso based on locale: " 299 + countryIso); 300 } 301 return countryIso; 302 } 303 getContextForUser(UserHandle user)304 private Context getContextForUser(UserHandle user) { 305 try { 306 return mContext.createPackageContextAsUser(mContext.getPackageName(), 0, user); 307 } catch (PackageManager.NameNotFoundException e) { 308 // Default to mContext, not finding the package system is running as is unlikely. 309 return mContext; 310 } 311 } 312 313 /** 314 * Creates an intent to be invoked when the user opts to "call back" from the disconnected call 315 * notification. 316 * 317 * @param handle The handle to call back. 318 */ createCallBackPendingIntent(Uri handle, UserHandle userHandle)319 private PendingIntent createCallBackPendingIntent(Uri handle, UserHandle userHandle) { 320 return createTelecomPendingIntent( 321 TelecomBroadcastIntentProcessor.ACTION_DISCONNECTED_CALL_BACK_FROM_NOTIFICATION, 322 handle, userHandle); 323 } 324 325 /** 326 * Creates generic pending intent from the specified parameters to be received by 327 * {@link TelecomBroadcastIntentProcessor}. 328 * 329 * @param action The intent action. 330 * @param data The intent data. 331 */ createTelecomPendingIntent(String action, Uri data, UserHandle userHandle)332 private PendingIntent createTelecomPendingIntent(String action, Uri data, 333 UserHandle userHandle) { 334 Intent intent = new Intent(action, data, mContext, TelecomBroadcastReceiver.class); 335 intent.putExtra(TelecomBroadcastIntentProcessor.EXTRA_USERHANDLE, userHandle); 336 return PendingIntent.getBroadcast(mContext, 0, intent, 337 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); 338 } 339 canRespondViaSms(@onNull CallInfo call)340 private boolean canRespondViaSms(@NonNull CallInfo call) { 341 // Only allow respond-via-sms for "tel:" calls. 342 return call.handle != null && 343 PhoneAccount.SCHEME_TEL.equals(call.handle.getScheme()); 344 } 345 346 /** 347 * Creates a new pending intent that sends the user to the call log. 348 * 349 * @return The pending intent. 350 */ createCallLogPendingIntent(UserHandle userHandle)351 private PendingIntent createCallLogPendingIntent(UserHandle userHandle) { 352 Intent intent = new Intent(Intent.ACTION_VIEW, null); 353 intent.setType(CallLog.Calls.CONTENT_TYPE); 354 355 TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(mContext); 356 taskStackBuilder.addNextIntent(intent); 357 358 return taskStackBuilder.getPendingIntent(0, PendingIntent.FLAG_IMMUTABLE, null, userHandle); 359 } 360 361 /** 362 * Creates an intent to be invoked when the user opts to "send sms" from the missed call 363 * notification. 364 */ createSendSmsFromNotificationPendingIntent(Uri handle, UserHandle userHandle)365 private PendingIntent createSendSmsFromNotificationPendingIntent(Uri handle, 366 UserHandle userHandle) { 367 return createTelecomPendingIntent( 368 TelecomBroadcastIntentProcessor.ACTION_DISCONNECTED_SEND_SMS_FROM_NOTIFICATION, 369 Uri.fromParts(Constants.SCHEME_SMSTO, handle.getSchemeSpecificPart(), null), 370 userHandle); 371 } 372 373 /** 374 * Clear any of the active notifications. 375 * @param userHandle The user to clear the notifications for. 376 */ clearNotification(UserHandle userHandle)377 public void clearNotification(UserHandle userHandle) { 378 long token = Binder.clearCallingIdentity(); 379 try { 380 mNotificationManager.cancelAsUser(NOTIFICATION_TAG, DISCONNECTED_CALL_NOTIFICATION_ID, 381 userHandle); 382 } finally { 383 Binder.restoreCallingIdentity(token); 384 } 385 } 386 } 387