• 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 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