Home | History | Annotate | Download | only in ui
      1 /*
      2  * Copyright (C) 2017 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.app.Notification;
     20 import android.app.NotificationManager;
     21 import android.app.PendingIntent;
     22 import android.content.Context;
     23 import android.content.Intent;
     24 import android.os.Bundle;
     25 import android.telecom.Log;
     26 import android.telecom.PhoneAccountHandle;
     27 import android.telecom.TelecomManager;
     28 import android.telecom.VideoProfile;
     29 import android.text.Spannable;
     30 import android.text.SpannableString;
     31 import android.text.TextUtils;
     32 import android.text.style.ForegroundColorSpan;
     33 import android.util.ArraySet;
     34 
     35 import com.android.internal.annotations.VisibleForTesting;
     36 import com.android.server.telecom.Call;
     37 import com.android.server.telecom.CallState;
     38 import com.android.server.telecom.CallsManagerListenerBase;
     39 import com.android.server.telecom.HandoverState;
     40 import com.android.server.telecom.R;
     41 import com.android.server.telecom.TelecomBroadcastIntentProcessor;
     42 import com.android.server.telecom.components.TelecomBroadcastReceiver;
     43 
     44 import java.util.Optional;
     45 import java.util.Set;
     46 
     47 /**
     48  * Manages the display of an incoming call UX when a new ringing self-managed call is added, and
     49  * there is an ongoing call in another {@link android.telecom.PhoneAccount}.
     50  */
     51 public class IncomingCallNotifier extends CallsManagerListenerBase {
     52 
     53     public interface IncomingCallNotifierFactory {
     54         IncomingCallNotifier make(Context context, CallsManagerProxy mCallsManagerProxy);
     55     }
     56 
     57     /**
     58      * Eliminates strict dependency between this class and CallsManager.
     59      */
     60     public interface CallsManagerProxy {
     61         boolean hasCallsForOtherPhoneAccount(PhoneAccountHandle phoneAccountHandle);
     62         int getNumCallsForOtherPhoneAccount(PhoneAccountHandle phoneAccountHandle);
     63         Call getActiveCall();
     64     }
     65 
     66     // Notification for incoming calls. This is interruptive and will show up as a HUN.
     67     @VisibleForTesting
     68     public static final int NOTIFICATION_INCOMING_CALL = 1;
     69     @VisibleForTesting
     70     public static final String NOTIFICATION_TAG = IncomingCallNotifier.class.getSimpleName();
     71 
     72 
     73     public final Call.ListenerBase mCallListener = new Call.ListenerBase() {
     74         @Override
     75         public void onCallerInfoChanged(Call call) {
     76             if (mIncomingCall != call) {
     77                 return;
     78             }
     79             showIncomingCallNotification(mIncomingCall);
     80         }
     81     };
     82 
     83     private final Context mContext;
     84     private final NotificationManager mNotificationManager;
     85     private final Set<Call> mCalls = new ArraySet<>();
     86     private CallsManagerProxy mCallsManagerProxy;
     87 
     88     // The current incoming call we are displaying UX for.
     89     private Call mIncomingCall;
     90 
     91     public IncomingCallNotifier(Context context) {
     92         mContext = context;
     93         mNotificationManager =
     94                 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
     95     }
     96 
     97     public void setCallsManagerProxy(CallsManagerProxy callsManagerProxy) {
     98         mCallsManagerProxy = callsManagerProxy;
     99     }
    100 
    101     public Call getIncomingCall() {
    102         return mIncomingCall;
    103     }
    104 
    105     @Override
    106     public void onCallAdded(Call call) {
    107         if (!mCalls.contains(call)) {
    108             mCalls.add(call);
    109         }
    110 
    111         updateIncomingCall();
    112     }
    113 
    114     @Override
    115     public void onCallRemoved(Call call) {
    116         if (mCalls.contains(call)) {
    117             mCalls.remove(call);
    118         }
    119 
    120         updateIncomingCall();
    121     }
    122 
    123     @Override
    124     public void onCallStateChanged(Call call, int oldState, int newState) {
    125         updateIncomingCall();
    126     }
    127 
    128     /**
    129      * Determines which call is the active ringing call at this time and triggers the display of the
    130      * UI.
    131      */
    132     private void updateIncomingCall() {
    133         Optional<Call> incomingCallOp = mCalls.stream()
    134                 .filter(call -> call.isSelfManaged() && call.isIncoming() &&
    135                         call.getState() == CallState.RINGING &&
    136                         call.getHandoverState() == HandoverState.HANDOVER_NONE)
    137                 .findFirst();
    138         Call incomingCall = incomingCallOp.orElse(null);
    139         if (incomingCall != null && mCallsManagerProxy != null &&
    140                 !mCallsManagerProxy.hasCallsForOtherPhoneAccount(
    141                         incomingCallOp.get().getTargetPhoneAccount())) {
    142             // If there is no calls in any other ConnectionService, we can rely on the
    143             // third-party app to display its own incoming call UI.
    144             incomingCall = null;
    145         }
    146 
    147         Log.i(this, "updateIncomingCall: foundIncomingcall = %s", incomingCall);
    148 
    149         boolean hadIncomingCall = mIncomingCall != null;
    150         boolean hasIncomingCall = incomingCall != null;
    151         if (incomingCall != mIncomingCall) {
    152             Call previousIncomingCall = mIncomingCall;
    153             mIncomingCall = incomingCall;
    154 
    155             if (hasIncomingCall && !hadIncomingCall) {
    156                 mIncomingCall.addListener(mCallListener);
    157                 showIncomingCallNotification(mIncomingCall);
    158             } else if (hadIncomingCall && !hasIncomingCall) {
    159                 previousIncomingCall.removeListener(mCallListener);
    160                 hideIncomingCallNotification();
    161             }
    162         }
    163     }
    164 
    165     private void showIncomingCallNotification(Call call) {
    166         Log.i(this, "showIncomingCallNotification showCall = %s", call);
    167 
    168         Notification.Builder builder = getNotificationBuilder(call,
    169                 mCallsManagerProxy.getActiveCall());
    170         mNotificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_INCOMING_CALL, builder.build());
    171     }
    172 
    173     private void hideIncomingCallNotification() {
    174         Log.i(this, "hideIncomingCallNotification");
    175         mNotificationManager.cancel(NOTIFICATION_TAG, NOTIFICATION_INCOMING_CALL);
    176     }
    177 
    178     private String getNotificationName(Call call) {
    179         String name = "";
    180         if (call.getCallerDisplayNamePresentation() == TelecomManager.PRESENTATION_ALLOWED) {
    181             name = call.getCallerDisplayName();
    182         }
    183         if (TextUtils.isEmpty(name)) {
    184             name = call.getName();
    185         }
    186 
    187         if (TextUtils.isEmpty(name)) {
    188             name = call.getPhoneNumber();
    189         }
    190         return name;
    191     }
    192 
    193     private Notification.Builder getNotificationBuilder(Call incomingCall, Call ongoingCall) {
    194         // Change the notification app name to "Android System" to sufficiently distinguish this
    195         // from the phone app's name.
    196         Bundle extras = new Bundle();
    197         extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, mContext.getString(
    198                 com.android.internal.R.string.android_system_label));
    199 
    200         Intent answerIntent = new Intent(
    201                 TelecomBroadcastIntentProcessor.ACTION_ANSWER_FROM_NOTIFICATION, null, mContext,
    202                 TelecomBroadcastReceiver.class);
    203         Intent rejectIntent = new Intent(
    204                 TelecomBroadcastIntentProcessor.ACTION_REJECT_FROM_NOTIFICATION, null, mContext,
    205                 TelecomBroadcastReceiver.class);
    206 
    207         String nameOrNumber = getNotificationName(incomingCall);
    208         CharSequence viaApp = incomingCall.getTargetPhoneAccountLabel();
    209         boolean isIncomingVideo = VideoProfile.isVideo(incomingCall.getVideoState());
    210         boolean isOngoingVideo = ongoingCall != null ?
    211                 VideoProfile.isVideo(ongoingCall.getVideoState()) : false;
    212         int numOtherCalls = ongoingCall != null ?
    213                 mCallsManagerProxy.getNumCallsForOtherPhoneAccount(
    214                         incomingCall.getTargetPhoneAccount()) : 1;
    215 
    216         // Build the "IncomingApp call from John Smith" message.
    217         CharSequence incomingCallText;
    218         if (isIncomingVideo) {
    219             incomingCallText = mContext.getString(R.string.notification_incoming_video_call, viaApp,
    220                     nameOrNumber);
    221         } else {
    222             incomingCallText = mContext.getString(R.string.notification_incoming_call, viaApp,
    223                     nameOrNumber);
    224         }
    225 
    226         // Build the "Answering will end your OtherApp call" line.
    227         CharSequence disconnectText;
    228         if (ongoingCall != null && ongoingCall.isSelfManaged()) {
    229             CharSequence ongoingApp = ongoingCall.getTargetPhoneAccountLabel();
    230             // For an ongoing self-managed call, we use a message like:
    231             // "Answering will end your OtherApp call".
    232             if (numOtherCalls > 1) {
    233                 // Multiple ongoing calls in the other app, so don't bother specifing whether it is
    234                 // a video call or audio call.
    235                 disconnectText = mContext.getString(R.string.answering_ends_other_calls,
    236                         ongoingApp);
    237             } else if (isOngoingVideo) {
    238                 disconnectText = mContext.getString(R.string.answering_ends_other_video_call,
    239                         ongoingApp);
    240             } else {
    241                 disconnectText = mContext.getString(R.string.answering_ends_other_call, ongoingApp);
    242             }
    243         } else {
    244             // For an ongoing managed call, we use a message like:
    245             // "Answering will end your ongoing call".
    246             if (numOtherCalls > 1) {
    247                 // Multiple ongoing manage calls, so don't bother specifing whether it is a video
    248                 // call or audio call.
    249                 disconnectText = mContext.getString(R.string.answering_ends_other_managed_calls);
    250             } else if (isOngoingVideo) {
    251                 disconnectText = mContext.getString(
    252                         R.string.answering_ends_other_managed_video_call);
    253             } else {
    254                 disconnectText = mContext.getString(R.string.answering_ends_other_managed_call);
    255             }
    256         }
    257 
    258         final Notification.Builder builder = new Notification.Builder(mContext);
    259         builder.setOngoing(true);
    260         builder.setExtras(extras);
    261         builder.setPriority(Notification.PRIORITY_HIGH);
    262         builder.setCategory(Notification.CATEGORY_CALL);
    263         builder.setContentTitle(incomingCallText);
    264         builder.setContentText(disconnectText);
    265         builder.setSmallIcon(R.drawable.ic_phone);
    266         builder.setChannelId(NotificationChannelManager.CHANNEL_ID_INCOMING_CALLS);
    267         // Ensures this is a heads up notification.  A heads-up notification is typically only shown
    268         // if there is a fullscreen intent.  However since this notification doesn't have that we
    269         // will use this trick to get it to show as one anyways.
    270         builder.setVibrate(new long[0]);
    271         builder.setColor(mContext.getResources().getColor(R.color.theme_color));
    272         builder.addAction(
    273                 R.anim.on_going_call,
    274                 getActionText(R.string.answer_incoming_call, R.color.notification_action_answer),
    275                 PendingIntent.getBroadcast(mContext, 0, answerIntent,
    276                         PendingIntent.FLAG_CANCEL_CURRENT));
    277         builder.addAction(
    278                 R.drawable.ic_close_dk,
    279                 getActionText(R.string.decline_incoming_call, R.color.notification_action_decline),
    280                 PendingIntent.getBroadcast(mContext, 0, rejectIntent,
    281                         PendingIntent.FLAG_CANCEL_CURRENT));
    282         return builder;
    283     }
    284 
    285     private CharSequence getActionText(int stringRes, int colorRes) {
    286         CharSequence string = mContext.getText(stringRes);
    287         if (string == null) {
    288             return "";
    289         }
    290         Spannable spannable = new SpannableString(string);
    291         spannable.setSpan(
    292                     new ForegroundColorSpan(mContext.getColor(colorRes)), 0, spannable.length(), 0);
    293         return spannable;
    294     }
    295 }
    296