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