1 /* 2 * Copyright (C) 2022 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.server.telecom.voip; 18 19 import static android.app.ForegroundServiceDelegationOptions.DELEGATION_SERVICE_PHONE_CALL; 20 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA; 21 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE; 22 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE; 23 import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL; 24 25 import android.app.ActivityManager; 26 import android.app.ActivityManagerInternal; 27 import android.app.ForegroundServiceDelegationOptions; 28 import android.app.Notification; 29 import android.content.ComponentName; 30 import android.content.Context; 31 import android.content.ServiceConnection; 32 import android.os.Handler; 33 import android.os.HandlerThread; 34 import android.os.IBinder; 35 import android.os.RemoteException; 36 import android.os.UserHandle; 37 import android.service.notification.NotificationListenerService; 38 import android.service.notification.StatusBarNotification; 39 import android.telecom.Log; 40 import android.telecom.PhoneAccountHandle; 41 42 import com.android.internal.annotations.VisibleForTesting; 43 import com.android.server.LocalServices; 44 import com.android.server.telecom.Call; 45 46 import com.android.server.telecom.CallsManagerListenerBase; 47 import com.android.server.telecom.LogUtils; 48 import com.android.server.telecom.LoggedHandlerExecutor; 49 import com.android.server.telecom.TelecomSystem; 50 51 import java.util.ArrayList; 52 import java.util.HashMap; 53 import java.util.HashSet; 54 import java.util.List; 55 import java.util.Map; 56 import java.util.Objects; 57 import java.util.Set; 58 import java.util.concurrent.CompletableFuture; 59 60 public class VoipCallMonitor extends CallsManagerListenerBase { 61 62 private final List<Call> mNotificationPendingCalls; 63 // Same notification may be passed as different object in onNotificationPosted and 64 // onNotificationRemoved. Use its string as key to cache ongoing notifications. 65 private final Map<NotificationInfo, Call> mNotificationInfoToCallMap; 66 private final Map<PhoneAccountHandle, Set<Call>> mAccountHandleToCallMap; 67 private ActivityManagerInternal mActivityManagerInternal; 68 private final Map<PhoneAccountHandle, ServiceConnection> mServices; 69 private NotificationListenerService mNotificationListener; 70 private final Object mLock = new Object(); 71 private final HandlerThread mHandlerThread; 72 private final Handler mHandler; 73 private final Context mContext; 74 private List<NotificationInfo> mCachedNotifications; 75 private TelecomSystem.SyncRoot mSyncRoot; 76 VoipCallMonitor(Context context, TelecomSystem.SyncRoot lock)77 public VoipCallMonitor(Context context, TelecomSystem.SyncRoot lock) { 78 mSyncRoot = lock; 79 mContext = context; 80 mHandlerThread = new HandlerThread(this.getClass().getSimpleName()); 81 mHandlerThread.start(); 82 mHandler = new Handler(mHandlerThread.getLooper()); 83 mNotificationPendingCalls = new ArrayList<>(); 84 mCachedNotifications = new ArrayList<>(); 85 mNotificationInfoToCallMap = new HashMap<>(); 86 mServices = new HashMap<>(); 87 mAccountHandleToCallMap = new HashMap<>(); 88 mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); 89 90 mNotificationListener = new NotificationListenerService() { 91 @Override 92 public void onNotificationPosted(StatusBarNotification sbn) { 93 synchronized (mLock) { 94 if (sbn.getNotification().isStyle(Notification.CallStyle.class)) { 95 NotificationInfo info = new NotificationInfo(sbn.getPackageName(), 96 sbn.getUser()); 97 boolean sbnMatched = false; 98 for (Call call : mNotificationPendingCalls) { 99 if (info.matchesCall(call)) { 100 Log.i(this, "onNotificationPosted: found a pending " 101 + "callId=[%s] for the call notification w/ " 102 + "id=[%s]", 103 call.getId(), sbn.getId()); 104 mNotificationPendingCalls.remove(call); 105 mNotificationInfoToCallMap.put(info, call); 106 sbnMatched = true; 107 break; 108 } 109 } 110 if (!sbnMatched && 111 !mCachedNotifications.contains(info) /* don't re-add if update */) { 112 Log.i(this, "onNotificationPosted: could not find a" 113 + "call for the call notification w/ id=[%s]", 114 sbn.getId()); 115 // notification may post before we started to monitor the call, cache 116 // this notification and try to match it later with new added call. 117 mCachedNotifications.add(info); 118 } 119 } 120 } 121 } 122 123 @Override 124 public void onNotificationRemoved(StatusBarNotification sbn) { 125 synchronized (mLock) { 126 NotificationInfo info = new NotificationInfo(sbn.getPackageName(), 127 sbn.getUser()); 128 mCachedNotifications.remove(info); 129 if (mNotificationInfoToCallMap.isEmpty()) { 130 return; 131 } 132 Call call = mNotificationInfoToCallMap.getOrDefault(info, null); 133 if (call != null) { 134 // TODO: fix potential bug for multiple calls of same voip app. 135 mNotificationInfoToCallMap.remove(info, call); 136 stopFGSDelegation(call); 137 } 138 } 139 } 140 }; 141 142 } 143 startMonitor()144 public void startMonitor() { 145 try { 146 mNotificationListener.registerAsSystemService(mContext, 147 new ComponentName(this.getClass().getPackageName(), 148 this.getClass().getCanonicalName()), ActivityManager.getCurrentUser()); 149 } catch (RemoteException e) { 150 Log.e(this, e, "Cannot register notification listener"); 151 } 152 } 153 stopMonitor()154 public void stopMonitor() { 155 try { 156 mNotificationListener.unregisterAsSystemService(); 157 } catch (RemoteException e) { 158 Log.e(this, e, "Cannot unregister notification listener"); 159 } 160 } 161 162 @Override onCallAdded(Call call)163 public void onCallAdded(Call call) { 164 if (!call.isTransactionalCall()) { 165 return; 166 } 167 168 synchronized (mLock) { 169 PhoneAccountHandle phoneAccountHandle = call.getTargetPhoneAccount(); 170 Set<Call> callList = mAccountHandleToCallMap.computeIfAbsent(phoneAccountHandle, 171 k -> new HashSet<>()); 172 callList.add(call); 173 CompletableFuture.completedFuture(null).thenComposeAsync( 174 (x) -> { 175 startFGSDelegation(call.getCallingPackageIdentity().mCallingPackagePid, 176 call.getCallingPackageIdentity().mCallingPackageUid, call); 177 return null; 178 }, new LoggedHandlerExecutor(mHandler, "VCM.oCA", mSyncRoot)); 179 } 180 } 181 182 @Override onCallRemoved(Call call)183 public void onCallRemoved(Call call) { 184 if (!call.isTransactionalCall()) { 185 return; 186 } 187 188 synchronized (mLock) { 189 stopMonitorWorks(call); 190 PhoneAccountHandle phoneAccountHandle = call.getTargetPhoneAccount(); 191 Set<Call> callList = mAccountHandleToCallMap.computeIfAbsent(phoneAccountHandle, 192 k -> new HashSet<>()); 193 callList.remove(call); 194 195 if (callList.isEmpty()) { 196 stopFGSDelegation(call); 197 } 198 } 199 } 200 startFGSDelegation(int pid, int uid, Call call)201 private void startFGSDelegation(int pid, int uid, Call call) { 202 Log.i(this, "startFGSDelegation for call %s", call.getId()); 203 if (mActivityManagerInternal != null) { 204 PhoneAccountHandle handle = call.getTargetPhoneAccount(); 205 ForegroundServiceDelegationOptions options = new ForegroundServiceDelegationOptions(pid, 206 uid, handle.getComponentName().getPackageName(), null /* clientAppThread */, 207 false /* isSticky */, String.valueOf(handle.hashCode()), 208 FOREGROUND_SERVICE_TYPE_PHONE_CALL | 209 FOREGROUND_SERVICE_TYPE_MICROPHONE | 210 FOREGROUND_SERVICE_TYPE_CAMERA | 211 FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE /* foregroundServiceTypes */, 212 DELEGATION_SERVICE_PHONE_CALL /* delegationService */); 213 ServiceConnection fgsConnection = new ServiceConnection() { 214 @Override 215 public void onServiceConnected(ComponentName name, IBinder service) { 216 mServices.put(handle, this); 217 startMonitorWorks(call); 218 } 219 220 @Override 221 public void onServiceDisconnected(ComponentName name) { 222 mServices.remove(handle); 223 } 224 }; 225 try { 226 if (mActivityManagerInternal 227 .startForegroundServiceDelegate(options, fgsConnection)) { 228 Log.addEvent(call, LogUtils.Events.GAINED_FGS_DELEGATION); 229 } else { 230 Log.addEvent(call, LogUtils.Events.GAIN_FGS_DELEGATION_FAILED); 231 } 232 } catch (Exception e) { 233 Log.i(this, "startForegroundServiceDelegate failed due to: " + e); 234 } 235 } 236 } 237 238 @VisibleForTesting stopFGSDelegation(Call call)239 public void stopFGSDelegation(Call call) { 240 synchronized (mLock) { 241 Log.i(this, "stopFGSDelegation of call %s", call); 242 PhoneAccountHandle handle = call.getTargetPhoneAccount(); 243 Set<Call> calls = mAccountHandleToCallMap.get(handle); 244 245 // Every call for the package that is losing foreground service delegation should be 246 // removed from tracking maps/contains in this class 247 if (calls != null) { 248 for (Call c : calls) { 249 stopMonitorWorks(c); // remove the call from tacking in this class 250 } 251 } 252 253 mAccountHandleToCallMap.remove(handle); 254 255 if (mActivityManagerInternal != null) { 256 ServiceConnection fgsConnection = mServices.get(handle); 257 if (fgsConnection != null) { 258 mActivityManagerInternal.stopForegroundServiceDelegate(fgsConnection); 259 Log.addEvent(call, LogUtils.Events.LOST_FGS_DELEGATION); 260 } 261 } 262 } 263 } 264 startMonitorWorks(Call call)265 private void startMonitorWorks(Call call) { 266 startMonitorNotification(call); 267 } 268 stopMonitorWorks(Call call)269 private void stopMonitorWorks(Call call) { 270 stopMonitorNotification(call); 271 } 272 startMonitorNotification(Call call)273 private void startMonitorNotification(Call call) { 274 synchronized (mLock) { 275 boolean sbnMatched = false; 276 for (NotificationInfo info : mCachedNotifications) { 277 if (info.matchesCall(call)) { 278 Log.i(this, "startMonitorNotification: found a cached call " 279 + "notification for call=[%s]", call); 280 mCachedNotifications.remove(info); 281 mNotificationInfoToCallMap.put(info, call); 282 sbnMatched = true; 283 break; 284 } 285 } 286 if (!sbnMatched) { 287 // Only continue to 288 Log.i(this, "startMonitorNotification: could not find a call" 289 + " notification for the call=[%s];", call); 290 mNotificationPendingCalls.add(call); 291 CompletableFuture<Void> future = new CompletableFuture<>(); 292 mHandler.postDelayed(() -> future.complete(null), 5000L); 293 future.thenComposeAsync( 294 (x) -> { 295 if (mNotificationPendingCalls.contains(call)) { 296 Log.i(this, "Notification for voip-call %s haven't " 297 + "posted in time, stop delegation.", call.getId()); 298 stopFGSDelegation(call); 299 mNotificationPendingCalls.remove(call); 300 return null; 301 } 302 return null; 303 }, new LoggedHandlerExecutor(mHandler, "VCM.sMN", mSyncRoot)); 304 } 305 } 306 } 307 stopMonitorNotification(Call call)308 private void stopMonitorNotification(Call call) { 309 mNotificationPendingCalls.remove(call); 310 } 311 312 @VisibleForTesting setActivityManagerInternal(ActivityManagerInternal ami)313 public void setActivityManagerInternal(ActivityManagerInternal ami) { 314 mActivityManagerInternal = ami; 315 } 316 317 private static class NotificationInfo extends Object { 318 private String mPackageName; 319 private UserHandle mUserHandle; 320 NotificationInfo(String packageName, UserHandle userHandle)321 NotificationInfo(String packageName, UserHandle userHandle) { 322 mPackageName = packageName; 323 mUserHandle = userHandle; 324 } 325 matchesCall(Call call)326 boolean matchesCall(Call call) { 327 PhoneAccountHandle accountHandle = call.getTargetPhoneAccount(); 328 return mPackageName != null && mPackageName.equals( 329 accountHandle.getComponentName().getPackageName()) 330 && mUserHandle != null && mUserHandle.equals(accountHandle.getUserHandle()); 331 } 332 333 @Override equals(Object obj)334 public boolean equals(Object obj) { 335 if (!(obj instanceof NotificationInfo)) { 336 return false; 337 } 338 NotificationInfo that = (NotificationInfo) obj; 339 return Objects.equals(this.mPackageName, that.mPackageName) 340 && Objects.equals(this.mUserHandle, that.mUserHandle); 341 } 342 343 @Override hashCode()344 public int hashCode() { 345 return Objects.hash(mPackageName, mUserHandle); 346 } 347 348 @Override toString()349 public String toString() { 350 StringBuilder sb = new StringBuilder(); 351 sb.append("{ NotificationInfo: [mPackageName: ") 352 .append(mPackageName) 353 .append("], [mUserHandle=") 354 .append(mUserHandle) 355 .append("] }"); 356 return sb.toString(); 357 } 358 } 359 360 @VisibleForTesting postNotification(StatusBarNotification statusBarNotification)361 public void postNotification(StatusBarNotification statusBarNotification) { 362 mNotificationListener.onNotificationPosted(statusBarNotification); 363 } 364 365 @VisibleForTesting removeNotification(StatusBarNotification statusBarNotification)366 public void removeNotification(StatusBarNotification statusBarNotification) { 367 mNotificationListener.onNotificationRemoved(statusBarNotification); 368 } 369 370 @VisibleForTesting getCallsForHandle(PhoneAccountHandle handle)371 public Set<Call> getCallsForHandle(PhoneAccountHandle handle){ 372 return mAccountHandleToCallMap.get(handle); 373 } 374 } 375