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