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.car.dialer.notification;
18 
19 import android.app.Notification;
20 import android.app.NotificationChannel;
21 import android.app.NotificationManager;
22 import android.app.PendingIntent;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.graphics.drawable.Icon;
26 import android.telecom.Call;
27 import android.text.TextUtils;
28 
29 import androidx.annotation.StringRes;
30 
31 import com.android.car.dialer.R;
32 import com.android.car.dialer.log.L;
33 import com.android.car.telephony.common.CallDetail;
34 import com.android.car.telephony.common.TelecomUtils;
35 
36 import java.util.HashSet;
37 import java.util.Set;
38 import java.util.concurrent.CompletableFuture;
39 
40 /** Controller that manages the heads up notification for incoming calls. */
41 public final class InCallNotificationController {
42     private static final String TAG = "CD.InCallNotificationController";
43     private static final String CHANNEL_ID = "com.android.car.dialer.incoming";
44     // A random number that is used for notification id.
45     private static final int NOTIFICATION_ID = 20181105;
46 
47     private static InCallNotificationController sInCallNotificationController;
48 
49     private boolean mShowFullscreenIncallUi;
50 
51     /**
52      * Initialized a globally accessible {@link InCallNotificationController} which can be retrieved
53      * by {@link #get}. If this function is called a second time before calling {@link #tearDown()},
54      * an {@link IllegalStateException} will be thrown.
55      *
56      * @param applicationContext Application context.
57      */
init(Context applicationContext)58     public static void init(Context applicationContext) {
59         if (sInCallNotificationController == null) {
60             sInCallNotificationController = new InCallNotificationController(applicationContext);
61         } else {
62             throw new IllegalStateException("InCallNotificationController has been initialized.");
63         }
64     }
65 
66     /**
67      * Gets the global {@link InCallNotificationController} instance. Make sure
68      * {@link #init(Context)} is called before calling this method.
69      */
get()70     public static InCallNotificationController get() {
71         if (sInCallNotificationController == null) {
72             throw new IllegalStateException(
73                     "Call InCallNotificationController.init(Context) before calling this function");
74         }
75         return sInCallNotificationController;
76     }
77 
tearDown()78     public static void tearDown() {
79         sInCallNotificationController = null;
80     }
81 
82     private final Context mContext;
83     private final NotificationManager mNotificationManager;
84     private final Notification.Builder mNotificationBuilder;
85     private final Set<String> mActiveInCallNotifications;
86     private CompletableFuture<Void> mNotificationFuture;
87 
InCallNotificationController(Context context)88     private InCallNotificationController(Context context) {
89         mContext = context;
90 
91         mShowFullscreenIncallUi = mContext.getResources().getBoolean(
92                 R.bool.config_show_hun_fullscreen_incall_ui);
93         mNotificationManager =
94                 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
95 
96         CharSequence name = mContext.getString(R.string.in_call_notification_channel_name);
97         NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID, name,
98                 NotificationManager.IMPORTANCE_HIGH);
99         mNotificationManager.createNotificationChannel(notificationChannel);
100 
101         mNotificationBuilder = new Notification.Builder(mContext, CHANNEL_ID)
102                 .setSmallIcon(R.drawable.ic_phone)
103                 .setContentText(mContext.getString(R.string.notification_incoming_call))
104                 .setCategory(Notification.CATEGORY_CALL)
105                 .setOngoing(true)
106                 .setAutoCancel(false);
107 
108         mActiveInCallNotifications = new HashSet<>();
109     }
110 
111 
112     /** Show a new incoming call notification or update the existing incoming call notification. */
showInCallNotification(Call call)113     public void showInCallNotification(Call call) {
114         L.d(TAG, "showInCallNotification");
115 
116         if (mNotificationFuture != null) {
117             mNotificationFuture.cancel(true);
118         }
119 
120         CallDetail callDetail = CallDetail.fromTelecomCallDetail(call.getDetails());
121         String number = callDetail.getNumber();
122         String callId = call.getDetails().getTelecomCallId();
123         mActiveInCallNotifications.add(callId);
124 
125         if (mShowFullscreenIncallUi) {
126             mNotificationBuilder.setFullScreenIntent(
127                     getFullscreenIntent(call), /* highPriority= */true);
128         }
129         mNotificationBuilder
130                 .setLargeIcon((Icon) null)
131                 .setContentTitle(TelecomUtils.getBidiWrappedNumber(number))
132                 .setActions(
133                         getAction(call, R.string.answer_call,
134                                 NotificationService.ACTION_ANSWER_CALL),
135                         getAction(call, R.string.decline_call,
136                                 NotificationService.ACTION_DECLINE_CALL));
137         mNotificationManager.notify(
138                 callId,
139                 NOTIFICATION_ID,
140                 mNotificationBuilder.build());
141 
142         mNotificationFuture = NotificationUtils.getDisplayNameAndRoundedAvatar(mContext, number)
143                 .thenAcceptAsync((pair) -> {
144                     // Check that the notification hasn't already been dismissed
145                     if (mActiveInCallNotifications.contains(callId)) {
146                         mNotificationBuilder
147                                 .setLargeIcon(pair.second)
148                                 .setContentTitle(TelecomUtils.getBidiWrappedNumber(pair.first));
149 
150                         mNotificationManager.notify(
151                                 callId,
152                                 NOTIFICATION_ID,
153                                 mNotificationBuilder.build());
154                     }
155                 }, mContext.getMainExecutor());
156     }
157 
158     /** Cancel the incoming call notification for the given call. */
cancelInCallNotification(Call call)159     public void cancelInCallNotification(Call call) {
160         L.d(TAG, "cancelInCallNotification");
161         if (call.getDetails() != null) {
162             String callId = call.getDetails().getTelecomCallId();
163             cancelInCallNotification(callId);
164         }
165     }
166 
167     /**
168      * Cancel the incoming call notification for the given call id. Any action that dismisses the
169      * notification needs to call this explicitly.
170      */
cancelInCallNotification(String callId)171     void cancelInCallNotification(String callId) {
172         if (TextUtils.isEmpty(callId)) {
173             return;
174         }
175         mActiveInCallNotifications.remove(callId);
176         mNotificationManager.cancel(callId, NOTIFICATION_ID);
177     }
178 
getFullscreenIntent(Call call)179     private PendingIntent getFullscreenIntent(Call call) {
180         Intent intent = getIntent(NotificationService.ACTION_SHOW_FULLSCREEN_UI, call);
181         return PendingIntent.getService(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
182     }
183 
getAction(Call call, @StringRes int actionText, String intentAction)184     private Notification.Action getAction(Call call, @StringRes int actionText,
185             String intentAction) {
186         CharSequence text = mContext.getString(actionText);
187         PendingIntent intent = PendingIntent.getService(
188                 mContext,
189                 0,
190                 getIntent(intentAction, call),
191                 PendingIntent.FLAG_UPDATE_CURRENT);
192         return new Notification.Action.Builder(null, text, intent).build();
193     }
194 
getIntent(String action, Call call)195     private Intent getIntent(String action, Call call) {
196         Intent intent = new Intent(action, null, mContext, NotificationService.class);
197         intent.putExtra(NotificationService.EXTRA_CALL_ID, call.getDetails().getTelecomCallId());
198         return intent;
199     }
200 }
201