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 import javax.inject.Inject;
41 import javax.inject.Singleton;
42 
43 import dagger.hilt.android.qualifiers.ApplicationContext;
44 
45 /** Controller that manages the heads up notification for incoming calls. */
46 @Singleton
47 public final class InCallNotificationController {
48     private static final String TAG = "CD.InCallNotificationController";
49     private static final String CHANNEL_ID = "com.android.car.dialer.incoming";
50     // A random number that is used for notification id.
51     private static final int NOTIFICATION_ID = 20181105;
52 
53     private boolean mShowFullscreenIncallUi;
54 
55     private final Context mContext;
56     private final NotificationManager mNotificationManager;
57     private final Notification.Builder mNotificationBuilder;
58     private final Set<String> mActiveInCallNotifications;
59     private CompletableFuture<Void> mNotificationFuture;
60 
61     @Inject
InCallNotificationController(@pplicationContext Context context)62     public InCallNotificationController(@ApplicationContext Context context) {
63         mContext = context;
64 
65         mShowFullscreenIncallUi = mContext.getResources().getBoolean(
66                 R.bool.config_show_hun_fullscreen_incall_ui);
67         mNotificationManager =
68                 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
69 
70         CharSequence name = mContext.getString(R.string.in_call_notification_channel_name);
71         NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID, name,
72                 NotificationManager.IMPORTANCE_HIGH);
73         mNotificationManager.createNotificationChannel(notificationChannel);
74 
75         mNotificationBuilder = new Notification.Builder(mContext, CHANNEL_ID)
76                 .setSmallIcon(R.drawable.ic_phone)
77                 .setColor(mContext.getColor(R.color.notification_app_icon_color))
78                 .setCategory(Notification.CATEGORY_CALL)
79                 .setOngoing(true)
80                 .setAutoCancel(false);
81 
82         mActiveInCallNotifications = new HashSet<>();
83     }
84 
85 
86     /** Show a new incoming call notification or update the existing incoming call notification. */
showInCallNotification(Call call)87     public void showInCallNotification(Call call) {
88         L.d(TAG, "showInCallNotification");
89 
90         if (mNotificationFuture != null) {
91             mNotificationFuture.cancel(true);
92         }
93 
94         CallDetail callDetail = CallDetail.fromTelecomCallDetail(call.getDetails());
95         String callNumber = callDetail.getNumber();
96         mActiveInCallNotifications.add(callNumber);
97 
98         if (mShowFullscreenIncallUi) {
99             mNotificationBuilder.setFullScreenIntent(
100                     getFullscreenIntent(call), /* highPriority= */true);
101         }
102         mNotificationBuilder
103                 .setLargeIcon((Icon) null)
104                 .setContentTitle(TelecomUtils.getBidiWrappedNumber(callNumber))
105                 .setContentText(mContext.getString(R.string.notification_incoming_call))
106                 .setActions(
107                         getAction(call, R.string.answer_call,
108                                 NotificationService.ACTION_ANSWER_CALL),
109                         getAction(call, R.string.decline_call,
110                                 NotificationService.ACTION_DECLINE_CALL));
111         mNotificationManager.notify(
112                 callNumber,
113                 NOTIFICATION_ID,
114                 mNotificationBuilder.build());
115 
116         mNotificationFuture = NotificationUtils.getDisplayNameAndRoundedAvatar(mContext, callNumber)
117                 .thenAcceptAsync((pair) -> {
118                     // Check that the notification hasn't already been dismissed
119                     if (mActiveInCallNotifications.contains(callNumber)) {
120                         mNotificationBuilder
121                                 .setLargeIcon(pair.second)
122                                 .setContentTitle(TelecomUtils.getBidiWrappedNumber(pair.first));
123 
124                         mNotificationManager.notify(
125                                 callNumber,
126                                 NOTIFICATION_ID,
127                                 mNotificationBuilder.build());
128                     }
129                 }, mContext.getMainExecutor());
130     }
131 
132     /** Cancel the incoming call notification for the given call. */
cancelInCallNotification(Call call)133     public void cancelInCallNotification(Call call) {
134         L.d(TAG, "cancelInCallNotification");
135         if (call.getDetails() != null) {
136             String callNumber = CallDetail.fromTelecomCallDetail(call.getDetails()).getNumber();
137             cancelInCallNotification(callNumber);
138         }
139     }
140 
141     /**
142      * Cancel the incoming call notification for the given call id. Any action that dismisses the
143      * notification needs to call this explicitly.
144      */
cancelInCallNotification(String callId)145     void cancelInCallNotification(String callId) {
146         if (TextUtils.isEmpty(callId)) {
147             return;
148         }
149         mActiveInCallNotifications.remove(callId);
150         mNotificationManager.cancel(callId, NOTIFICATION_ID);
151     }
152 
getFullscreenIntent(Call call)153     private PendingIntent getFullscreenIntent(Call call) {
154         Intent intent = getIntent(NotificationService.ACTION_SHOW_FULLSCREEN_UI, call);
155         return PendingIntent.getService(mContext, 0, intent,
156                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
157     }
158 
getAction(Call call, @StringRes int actionText, String intentAction)159     private Notification.Action getAction(Call call, @StringRes int actionText,
160             String intentAction) {
161         CharSequence text = mContext.getString(actionText);
162         PendingIntent intent = PendingIntent.getService(
163                 mContext,
164                 0,
165                 getIntent(intentAction, call),
166                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
167         return new Notification.Action.Builder(null, text, intent).build();
168     }
169 
getIntent(String action, Call call)170     private Intent getIntent(String action, Call call) {
171         Intent intent = new Intent(action, null, mContext, NotificationService.class);
172         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
173         intent.putExtra(NotificationService.EXTRA_PHONE_NUMBER,
174                 CallDetail.fromTelecomCallDetail(call.getDetails()).getNumber());
175         return intent;
176     }
177 }
178