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.bluetooth.BluetoothDevice;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.provider.CallLog;
27 import android.text.TextUtils;
28 
29 import androidx.annotation.NonNull;
30 import androidx.annotation.Nullable;
31 import androidx.annotation.StringRes;
32 import androidx.lifecycle.LiveData;
33 import androidx.lifecycle.Observer;
34 
35 import com.android.car.apps.common.util.LiveDataFunctions;
36 import com.android.car.dialer.R;
37 import com.android.car.dialer.livedata.UnreadMissedCallLiveData;
38 import com.android.car.dialer.log.L;
39 import com.android.car.dialer.ui.TelecomActivity;
40 import com.android.car.telephony.common.PhoneCallLog;
41 import com.android.car.telephony.common.TelecomUtils;
42 
43 import java.util.ArrayList;
44 import java.util.Collections;
45 import java.util.HashMap;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.concurrent.CompletableFuture;
49 
50 import javax.inject.Inject;
51 import javax.inject.Named;
52 import javax.inject.Singleton;
53 
54 import dagger.hilt.android.qualifiers.ApplicationContext;
55 
56 /** Controller that manages the missed call notifications. */
57 @Singleton
58 public final class MissedCallNotificationController {
59     private static final String TAG = "CD.MissedCallNotification";
60     private static final String CHANNEL_ID = "com.android.car.dialer.missedcall";
61     // A random number that is used for notification id.
62     private static final int NOTIFICATION_ID = 20190520;
63 
64     /** Tear down the global missed call notification controller. */
tearDown()65     public void tearDown() {
66         mUnreadMissedCallLiveData.removeObserver(mUnreadMissedCallObserver);
67     }
68 
69     private final Context mContext;
70     private final NotificationManager mNotificationManager;
71     private final LiveData<List<PhoneCallLog>> mUnreadMissedCallLiveData;
72     private final Observer<List<PhoneCallLog>> mUnreadMissedCallObserver;
73     private final List<PhoneCallLog> mCurrentPhoneCallLogList;
74     private final Map<String, CompletableFuture<Void>> mUpdateFutures = new HashMap<>();
75 
76     @Inject
MissedCallNotificationController( @pplicationContext Context context, @Named(R) LiveData<BluetoothDevice> currentHfpDeviceLiveData)77     MissedCallNotificationController(
78             @ApplicationContext Context context,
79             @Named("Hfp") LiveData<BluetoothDevice> currentHfpDeviceLiveData) {
80         mContext = context;
81         mNotificationManager =
82                 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
83         CharSequence name = mContext.getString(R.string.missed_call_notification_channel_name);
84         NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID, name,
85                 NotificationManager.IMPORTANCE_DEFAULT);
86         mNotificationManager.createNotificationChannel(notificationChannel);
87 
88         mCurrentPhoneCallLogList = new ArrayList<>();
89         mUnreadMissedCallLiveData = LiveDataFunctions.switchMapNonNull(
90                 currentHfpDeviceLiveData,
91                 device-> UnreadMissedCallLiveData.newInstance(context, device.getAddress()));
92         mUnreadMissedCallObserver = this::updateNotifications;
93         mUnreadMissedCallLiveData.observeForever(mUnreadMissedCallObserver);
94     }
95 
96     /**
97      * The phone call log list might be null when switching users if permission gets denied and
98      * throws exception.
99      */
updateNotifications(@ullable List<PhoneCallLog> phoneCallLogs)100     private void updateNotifications(@Nullable List<PhoneCallLog> phoneCallLogs) {
101         List<PhoneCallLog> updatedPhoneCallLogs =
102                 phoneCallLogs == null ? Collections.emptyList() : phoneCallLogs;
103         for (PhoneCallLog phoneCallLog : updatedPhoneCallLogs) {
104             showMissedCallNotification(phoneCallLog);
105             mCurrentPhoneCallLogList.remove(phoneCallLog);
106         }
107 
108         for (PhoneCallLog phoneCallLog : mCurrentPhoneCallLogList) {
109             cancelMissedCallNotification(phoneCallLog);
110         }
111         mCurrentPhoneCallLogList.clear();
112         mCurrentPhoneCallLogList.addAll(updatedPhoneCallLogs);
113     }
114 
showMissedCallNotification(PhoneCallLog callLog)115     private void showMissedCallNotification(PhoneCallLog callLog) {
116         L.d(TAG, "show missed call notification %s", callLog);
117         String phoneNumber = callLog.getPhoneNumberString();
118         String tag = getTag(callLog);
119         cancelLoadingRunnable(tag);
120         CompletableFuture<Void> updateFuture = NotificationUtils.getDisplayNameAndRoundedAvatar(
121                 mContext, phoneNumber)
122                 .thenAcceptAsync((pair) -> {
123                     int callLogSize = callLog.getAllCallRecords().size();
124                     Notification.Builder builder = new Notification.Builder(mContext, CHANNEL_ID)
125                             .setSmallIcon(R.drawable.ic_phone)
126                             .setColor(mContext.getColor(R.color.notification_app_icon_color))
127                             .setLargeIcon(pair.second)
128                             .setContentTitle(mContext.getResources().getQuantityString(
129                                     R.plurals.notification_missed_call, callLogSize, callLogSize))
130                             .setContentText(TelecomUtils.getBidiWrappedNumber(pair.first))
131                             .setContentIntent(getContentPendingIntent())
132                             .setDeleteIntent(getDeleteIntent(callLog))
133                             .setOnlyAlertOnce(true)
134                             .setShowWhen(true)
135                             .setWhen(callLog.getLastCallEndTimestamp())
136                             .setAutoCancel(false);
137 
138                     if (!TextUtils.isEmpty(phoneNumber)) {
139                         builder.addAction(getAction(phoneNumber, tag, R.string.call_back,
140                                 NotificationService.ACTION_CALL_BACK_MISSED));
141                         // TODO: add action button to send message
142                     }
143 
144                     mNotificationManager.notify(
145                             tag,
146                             NOTIFICATION_ID,
147                             builder.build());
148                 }, mContext.getMainExecutor());
149         mUpdateFutures.put(tag, updateFuture);
150     }
151 
cancelMissedCallNotification(PhoneCallLog phoneCallLog)152     private void cancelMissedCallNotification(PhoneCallLog phoneCallLog) {
153         L.d(TAG, "cancel missed call notification %s", phoneCallLog);
154         String tag = getTag(phoneCallLog);
155         cancelMissedCallNotification(tag);
156     }
157 
158     /**
159      * Explicitly cancels the notification that in some circumstances the database update operation
160      * has a delay to notify the cursor to reload.
161      */
cancelMissedCallNotification(String tag)162     void cancelMissedCallNotification(String tag) {
163         if (TextUtils.isEmpty(tag)) {
164             L.w(TAG, "Invalid notification tag, ignore canceling request.");
165             return;
166         }
167         cancelLoadingRunnable(tag);
168         mNotificationManager.cancel(tag, NOTIFICATION_ID);
169     }
170 
cancelLoadingRunnable(String tag)171     private void cancelLoadingRunnable(String tag) {
172         CompletableFuture<Void> completableFuture = mUpdateFutures.get(tag);
173         if (completableFuture != null) {
174             completableFuture.cancel(true);
175         }
176         mUpdateFutures.remove(tag);
177     }
178 
getContentPendingIntent()179     private PendingIntent getContentPendingIntent() {
180         Intent intent = new Intent(mContext, TelecomActivity.class);
181         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
182         intent.setAction(Intent.ACTION_VIEW);
183         intent.setType(CallLog.Calls.CONTENT_TYPE);
184         PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent,
185                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
186         return pendingIntent;
187     }
188 
getDeleteIntent(PhoneCallLog phoneCallLog)189     private PendingIntent getDeleteIntent(PhoneCallLog phoneCallLog) {
190         Intent intent = new Intent(NotificationService.ACTION_READ_MISSED, null, mContext,
191                 NotificationService.class);
192         String phoneNumberString = phoneCallLog.getPhoneNumberString();
193         if (TextUtils.isEmpty(phoneNumberString)) {
194             // For unknown call, pass the call log id to mark as read
195             intent.putExtra(NotificationService.EXTRA_CALL_LOG_ID, phoneCallLog.getPhoneLogId());
196         } else {
197             intent.putExtra(NotificationService.EXTRA_PHONE_NUMBER, phoneNumberString);
198         }
199         intent.putExtra(NotificationService.EXTRA_NOTIFICATION_TAG, getTag(phoneCallLog));
200         PendingIntent pendingIntent = PendingIntent.getService(
201                 mContext,
202                 // Unique id for PendingIntents with different extras
203                 /* requestCode= */(int) System.currentTimeMillis(),
204                 intent,
205                 PendingIntent.FLAG_IMMUTABLE);
206         return pendingIntent;
207     }
208 
getAction(String phoneNumberString, String tag, @StringRes int actionText, String intentAction)209     private Notification.Action getAction(String phoneNumberString, String tag,
210             @StringRes int actionText, String intentAction) {
211         CharSequence text = mContext.getString(actionText);
212         PendingIntent intent = getIntent(intentAction, phoneNumberString, tag);
213         return new Notification.Action.Builder(null, text, intent).build();
214     }
215 
getIntent(String action, String phoneNumberString, String tag)216     private PendingIntent getIntent(String action, String phoneNumberString, String tag) {
217         Intent intent = new Intent(action, null, mContext, NotificationService.class);
218         intent.putExtra(NotificationService.EXTRA_PHONE_NUMBER, phoneNumberString);
219         intent.putExtra(NotificationService.EXTRA_NOTIFICATION_TAG, tag);
220         return PendingIntent.getService(
221                 mContext,
222                 // Unique id for PendingIntents with different extras
223                 /* requestCode= */(int) System.currentTimeMillis(),
224                 intent,
225                 PendingIntent.FLAG_IMMUTABLE);
226     }
227 
getTag(@onNull PhoneCallLog phoneCallLog)228     private String getTag(@NonNull PhoneCallLog phoneCallLog) {
229         return String.valueOf(phoneCallLog.hashCode());
230     }
231 }
232