• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.internal.annotations.VisibleForTesting;
45 import com.android.server.telecom.Call;
46 import com.android.server.telecom.CallState;
47 import com.android.server.telecom.CallsManager;
48 import com.android.server.telecom.CallsManagerListenerBase;
49 import com.android.server.telecom.Constants;
50 import com.android.server.telecom.R;
51 import com.android.server.telecom.TelecomBroadcastIntentProcessor;
52 import com.android.server.telecom.components.TelecomBroadcastReceiver;
53 
54 import java.util.Locale;
55 
56 /**
57  * Handles notifications generated by Telecom for the case that a call was disconnected in order to
58  * connect another "higher priority" emergency call and gives the user the choice to call or
59  * message that user back after, similar to the missed call notifier.
60  */
61 public class DisconnectedCallNotifier extends CallsManagerListenerBase {
62 
63     public interface Factory {
create(Context context, CallsManager manager)64         DisconnectedCallNotifier create(Context context, CallsManager manager);
65     }
66 
67     public static class Default implements Factory {
68 
69         @Override
create(Context context, CallsManager manager)70         public DisconnectedCallNotifier create(Context context, CallsManager manager) {
71             return new DisconnectedCallNotifier(context, manager);
72         }
73     }
74 
75     private static class CallInfo {
76         public final UserHandle userHandle;
77         public final Uri handle;
78         public final long endTimeMs;
79         public final Bitmap callerInfoIcon;
80         public final Drawable callerInfoPhoto;
81         public final String callerInfoName;
82         public final boolean isEmergency;
83 
CallInfo(UserHandle userHandle, Uri handle, long endTimeMs, Bitmap callerInfoIcon, Drawable callerInfoPhoto, String callerInfoName, boolean isEmergency)84         public CallInfo(UserHandle userHandle, Uri handle, long endTimeMs, Bitmap callerInfoIcon,
85                 Drawable callerInfoPhoto, String callerInfoName, boolean isEmergency) {
86             this.userHandle = userHandle;
87             this.handle = handle;
88             this.endTimeMs = endTimeMs;
89             this.callerInfoIcon = callerInfoIcon;
90             this.callerInfoPhoto = callerInfoPhoto;
91             this.callerInfoName = callerInfoName;
92             this.isEmergency = isEmergency;
93         }
94 
95         @Override
toString()96         public String toString() {
97             return "CallInfo{" +
98                     "userHandle=" + userHandle +
99                     ", handle=" + handle +
100                     ", isEmergency=" + isEmergency +
101                     ", endTimeMs=" + endTimeMs +
102                     ", callerInfoIcon=" + callerInfoIcon +
103                     ", callerInfoPhoto=" + callerInfoPhoto +
104                     ", callerInfoName='" + callerInfoName + '\'' +
105                     '}';
106         }
107     }
108 
109     private static final String NOTIFICATION_TAG =
110             DisconnectedCallNotifier.class.getSimpleName();
111     private static final int DISCONNECTED_CALL_NOTIFICATION_ID = 1;
112 
113     private final Context mContext;
114     private final CallsManager mCallsManager;
115     private final NotificationManager mNotificationManager;
116     // The pending info to display to the user after they have ended the emergency call.
117     private CallInfo mPendingCallNotification;
118 
DisconnectedCallNotifier(Context context, CallsManager callsManager)119     public DisconnectedCallNotifier(Context context, CallsManager callsManager) {
120         mContext = context;
121         mNotificationManager =
122                 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
123         mCallsManager = callsManager;
124     }
125 
126     @Override
onCallRemoved(Call call)127     public void onCallRemoved(Call call) {
128         // Wait until the emergency call is ended before showing the notification.
129         if (mCallsManager.getCalls().isEmpty() && mPendingCallNotification != null) {
130             showDisconnectedNotification(mPendingCallNotification);
131             mPendingCallNotification = null;
132         }
133     }
134 
135     @Override
onCallStateChanged(Call call, int oldState, int newState)136     public void onCallStateChanged(Call call, int oldState, int newState) {
137         DisconnectCause cause = call.getDisconnectCause();
138         if (cause == null) {
139             Log.w(this, "onCallStateChanged: unexpected null disconnect cause.");
140             return;
141         }
142         // Call disconnected in favor of an emergency call. Place the call into a pending queue.
143         if ((newState == CallState.DISCONNECTED) && (cause.getCode() == DisconnectCause.LOCAL) &&
144                 DisconnectCause.REASON_EMERGENCY_CALL_PLACED.equals(cause.getReason())) {
145             // Clear any existing notification.
146             clearNotification(mCallsManager.getCurrentUserHandle());
147             UserHandle userHandle = call.getAssociatedUser();
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      */
289     @VisibleForTesting
getCurrentCountryIso(Context context)290     public String getCurrentCountryIso(Context context) {
291         // Without framework function calls, this seems to be the most accurate location service
292         // we can rely on.
293         final TelephonyManager telephonyManager =
294                 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
295         String countryIso;
296         try {
297             countryIso = telephonyManager.getNetworkCountryIso().toUpperCase();
298         } catch (UnsupportedOperationException ignored) {
299             countryIso = null;
300         }
301 
302         if (countryIso == null) {
303             countryIso = Locale.getDefault().getCountry();
304             Log.w(this, "No CountryDetector; falling back to countryIso based on locale: "
305                     + countryIso);
306         }
307         return countryIso;
308     }
309 
getContextForUser(UserHandle user)310     private Context getContextForUser(UserHandle user) {
311         try {
312             return mContext.createPackageContextAsUser(mContext.getPackageName(), 0, user);
313         } catch (PackageManager.NameNotFoundException e) {
314             // Default to mContext, not finding the package system is running as is unlikely.
315             return mContext;
316         }
317     }
318 
319     /**
320      * Creates an intent to be invoked when the user opts to "call back" from the disconnected call
321      * notification.
322      *
323      * @param handle The handle to call back.
324      */
createCallBackPendingIntent(Uri handle, UserHandle userHandle)325     private PendingIntent createCallBackPendingIntent(Uri handle, UserHandle userHandle) {
326         return createTelecomPendingIntent(
327                 TelecomBroadcastIntentProcessor.ACTION_DISCONNECTED_CALL_BACK_FROM_NOTIFICATION,
328                 handle, userHandle);
329     }
330 
331     /**
332      * Creates generic pending intent from the specified parameters to be received by
333      * {@link TelecomBroadcastIntentProcessor}.
334      *
335      * @param action The intent action.
336      * @param data The intent data.
337      */
createTelecomPendingIntent(String action, Uri data, UserHandle userHandle)338     private PendingIntent createTelecomPendingIntent(String action, Uri data,
339             UserHandle userHandle) {
340         Intent intent = new Intent(action, data, mContext, TelecomBroadcastReceiver.class);
341         intent.putExtra(TelecomBroadcastIntentProcessor.EXTRA_USERHANDLE, userHandle);
342         return PendingIntent.getBroadcast(mContext, 0, intent,
343                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
344     }
345 
canRespondViaSms(@onNull CallInfo call)346     private boolean canRespondViaSms(@NonNull CallInfo call) {
347         // Only allow respond-via-sms for "tel:" calls.
348         return call.handle != null &&
349                 PhoneAccount.SCHEME_TEL.equals(call.handle.getScheme());
350     }
351 
352     /**
353      * Creates a new pending intent that sends the user to the call log.
354      *
355      * @return The pending intent.
356      */
createCallLogPendingIntent(UserHandle userHandle)357     private PendingIntent createCallLogPendingIntent(UserHandle userHandle) {
358         Intent intent = new Intent(Intent.ACTION_VIEW, null);
359         intent.setType(CallLog.Calls.CONTENT_TYPE);
360 
361         TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(mContext);
362         taskStackBuilder.addNextIntent(intent);
363 
364         return taskStackBuilder.getPendingIntent(0, PendingIntent.FLAG_IMMUTABLE, null, userHandle);
365     }
366 
367     /**
368      * Creates an intent to be invoked when the user opts to "send sms" from the missed call
369      * notification.
370      */
createSendSmsFromNotificationPendingIntent(Uri handle, UserHandle userHandle)371     private PendingIntent createSendSmsFromNotificationPendingIntent(Uri handle,
372             UserHandle userHandle) {
373         return createTelecomPendingIntent(
374                 TelecomBroadcastIntentProcessor.ACTION_DISCONNECTED_SEND_SMS_FROM_NOTIFICATION,
375                 Uri.fromParts(Constants.SCHEME_SMSTO, handle.getSchemeSpecificPart(), null),
376                 userHandle);
377     }
378 
379     /**
380      * Clear any of the active notifications.
381      * @param userHandle The user to clear the notifications for.
382      */
clearNotification(UserHandle userHandle)383     public void clearNotification(UserHandle userHandle) {
384         long token = Binder.clearCallingIdentity();
385         try {
386             mNotificationManager.cancelAsUser(NOTIFICATION_TAG, DISCONNECTED_CALL_NOTIFICATION_ID,
387                     userHandle);
388         } finally {
389             Binder.restoreCallingIdentity(token);
390         }
391     }
392 }
393