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