• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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