Loading src/com/android/server/telecom/CallsManager.java +4 −1 Original line number Diff line number Diff line Loading @@ -775,7 +775,10 @@ public class CallsManager extends Call.ListenerBase mCallStreamingNotification = callStreamingNotification; mFeatureFlags = featureFlags; if (mFeatureFlags.voipCallMonitorRefactor()) { mVoipCallMonitor = new VoipCallMonitor(mContext, mLock); mVoipCallMonitor = new VoipCallMonitor( mContext, new Handler(Looper.getMainLooper()), mLock); mVoipCallMonitorLegacy = null; } else { mVoipCallMonitor = null; Loading src/com/android/server/telecom/callsequencing/voip/VoipCallMonitor.java +58 −60 Original line number Diff line number Diff line Loading @@ -30,9 +30,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.ServiceConnection; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; Loading @@ -53,14 +51,16 @@ import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; public class VoipCallMonitor extends CallsManagerListenerBase { private static final long NOTIFICATION_NOT_POSTED_IN_TIME_TIMEOUT = 5000L; private static final long NOTIFICATION_REMOVED_BUT_CALL_IS_STILL_ONGOING_TIMEOUT = 5000L; public static final long NOTIFICATION_NOT_POSTED_IN_TIME_TIMEOUT = 5000L; public static final long NOTIFICATION_REMOVED_BUT_CALL_IS_STILL_ONGOING_TIMEOUT = 5000L; private static final String TAG = VoipCallMonitor.class.getSimpleName(); private static final String DElIMITER = "#"; // This list caches calls that are added to the VoipCallMonitor and need an accompanying // Call-Style Notification! private final List<Call> mNewCallsMissingCallStyleNotification; private final ConcurrentLinkedQueue<Call> mNewCallsMissingCallStyleNotification; private final ConcurrentHashMap<String, Call> mNotificationIdToCall; private final ConcurrentHashMap<PhoneAccountHandle, Set<Call>> mAccountHandleToCallMap; private final ConcurrentHashMap<PhoneAccountHandle, ServiceConnection> mServices; Loading @@ -70,11 +70,11 @@ public class VoipCallMonitor extends CallsManagerListenerBase { private final Context mContext; private final TelecomSystem.SyncRoot mSyncRoot; public VoipCallMonitor(Context context, TelecomSystem.SyncRoot lock) { public VoipCallMonitor(Context context, Handler handler, TelecomSystem.SyncRoot lock) { mSyncRoot = lock; mContext = context; mHandlerForClass = new Handler(Looper.getMainLooper()); mNewCallsMissingCallStyleNotification = new ArrayList<>(); mHandlerForClass = handler; mNewCallsMissingCallStyleNotification = new ConcurrentLinkedQueue<>(); mNotificationIdToCall = new ConcurrentHashMap<>(); mServices = new ConcurrentHashMap<>(); mAccountHandleToCallMap = new ConcurrentHashMap<>(); Loading @@ -83,21 +83,18 @@ public class VoipCallMonitor extends CallsManagerListenerBase { @Override public void onNotificationPosted(StatusBarNotification sbn) { if (isCallStyleNotification(sbn)) { Log.i(this, "onNotificationPosted: sbn=[%s]", sbn); boolean foundCallForNotification = false; Log.i(TAG, "onNotificationPosted: sbn=[%s]", sbn); // Case 1: Call added to this class (via onCallAdded) BEFORE Call-Style // Notification is posted by the app (only supported scenario) // --> remove the newly added call from // mNewCallsMissingCallStyleNotification so FGS is not revoked. for (Call call : new ArrayList<>(mNewCallsMissingCallStyleNotification)) { Call newCallNoLongerAwaitingNotification = null; for (Call call : mNewCallsMissingCallStyleNotification) { if (isNotificationForCall(sbn, call)) { Log.i(this, "onNotificationPosted: found a pending " Log.i(TAG, "onNotificationPosted: found a pending " + "call=[%s] for sbn.id=[%s]", call, sbn.getId()); mNotificationIdToCall.put( getNotificationIdToCallKey(sbn), call); removeFromNotificationTracking(call); foundCallForNotification = true; newCallNoLongerAwaitingNotification = call; break; } } Loading @@ -105,11 +102,19 @@ public class VoipCallMonitor extends CallsManagerListenerBase { // --> Currently do not support this // Case 3: Call-Style Notification was updated (ex. incoming -> ongoing) // --> do nothing if (!foundCallForNotification) { Log.i(this, "onNotificationPosted: could not find a call for the" if (newCallNoLongerAwaitingNotification == null) { Log.i(TAG, "onNotificationPosted: could not find a call for the" + " sbn.id=[%s]. This could mean the notification posted" + " BEFORE the call is added (error) or it's an update from" + " incoming to ongoing (ok).", sbn.getId()); } else { // --> remove the newly added call from // mNewCallsMissingCallStyleNotification so FGS is not revoked when the // timeout is hit in VoipCallMonitor#startMonitoringNotification(...). The // timeout ensures the voip app posts a call-style notification within // 5 seconds! mNewCallsMissingCallStyleNotification .remove(newCallNoLongerAwaitingNotification); } } } Loading @@ -119,14 +124,17 @@ public class VoipCallMonitor extends CallsManagerListenerBase { if (!isCallStyleNotification(sbn)) { return; } Log.i(this, "onNotificationRemoved: Call-Style notification=[%s] removed", sbn); Log.i(TAG, "onNotificationRemoved: Call-Style notification=[%s] removed", sbn); Call call = getCallFromStatusBarNotificationId(sbn); if (call != null) { PhoneAccountHandle handle = getTargetPhoneAccount(call); if (!isCallDisconnected(call)) { mHandlerForClass.postDelayed(() -> { if (isCallStillBeingTracked(call)) { stopFGSDelegation(call, handle); Log.w(TAG, "onNotificationRemoved: notification has been removed for" + " more than 5 seconds but call still ongoing " + "c=[%s]", call); // TODO:: stopFGSDelegation(call, handle) when b/383403913 is fixed } }, NOTIFICATION_REMOVED_BUT_CALL_IS_STILL_ONGOING_TIMEOUT); } Loading Loading @@ -185,7 +193,7 @@ public class VoipCallMonitor extends CallsManagerListenerBase { new ComponentName(this.getClass().getPackageName(), this.getClass().getCanonicalName()), ActivityManager.getCurrentUser()); } catch (RemoteException e) { Log.e(this, e, "Cannot register notification listener"); Log.e(TAG, e, "Cannot register notification listener"); } } Loading @@ -193,7 +201,7 @@ public class VoipCallMonitor extends CallsManagerListenerBase { try { mNotificationListener.unregisterAsSystemService(); } catch (RemoteException e) { Log.e(this, e, "Cannot unregister notification listener"); Log.e(TAG, e, "Cannot unregister notification listener"); } } Loading @@ -217,11 +225,10 @@ public class VoipCallMonitor extends CallsManagerListenerBase { if (!isTransactional(call) || handle == null) { return; } removeFromNotificationTracking(call); Set<Call> ongoingCalls = mAccountHandleToCallMap .computeIfAbsent(handle, k -> new HashSet<>()); ongoingCalls.remove(call); Log.d(this, "onCallRemoved: callList.size=[%d]", ongoingCalls.size()); Log.d(TAG, "onCallRemoved: callList.size=[%d]", ongoingCalls.size()); if (ongoingCalls.isEmpty()) { stopFGSDelegation(call, handle); } else { Loading @@ -230,7 +237,7 @@ public class VoipCallMonitor extends CallsManagerListenerBase { } private void maybeStartFGSDelegation(int pid, int uid, PhoneAccountHandle handle, Call call) { Log.i(this, "maybeStartFGSDelegation for call=[%s]", call); Log.i(TAG, "maybeStartFGSDelegation for call=[%s]", call); if (mActivityManagerInternal != null) { if (mServices.containsKey(handle)) { Log.addEvent(call, LogUtils.Events.ALREADY_HAS_FGS_DELEGATION); Loading Loading @@ -262,34 +269,38 @@ public class VoipCallMonitor extends CallsManagerListenerBase { try { if (mActivityManagerInternal .startForegroundServiceDelegate(options, fgsConnection)) { Log.i(this, "maybeStartFGSDelegation: startForegroundServiceDelegate success"); Log.i(TAG, "maybeStartFGSDelegation: startForegroundServiceDelegate success"); } else { Log.addEvent(call, LogUtils.Events.GAIN_FGS_DELEGATION_FAILED); } } catch (Exception e) { Log.i(this, "startForegroundServiceDelegate failed due to: " + e); Log.i(TAG, "startForegroundServiceDelegate failed due to: " + e); } } } @VisibleForTesting public void stopFGSDelegation(Call call, PhoneAccountHandle handle) { Log.i(this, "stopFGSDelegation of call=[%s]", call); Log.i(TAG, "stopFGSDelegation of call=[%s]", call); if (handle == null) { return; } // In the event this class is waiting for any new calls to post a notification, remove // the call from the notification tracking container! Set<Call> ongoingCalls = mAccountHandleToCallMap.get(handle); if (ongoingCalls != null) { for (Call c : new ArrayList<>(ongoingCalls)) { removeFromNotificationTracking(c); // In the event this class is waiting for any new calls to post a notification, cleanup List<Call> toRemove = new ArrayList<>(); for (Call callAwaitingNotification : mNewCallsMissingCallStyleNotification) { if (handle.equals(callAwaitingNotification.getTargetPhoneAccount())) { Log.d(TAG, "stopFGSDelegation: removing call from notification tracking c=[%s]", callAwaitingNotification); toRemove.add(callAwaitingNotification); } } mNewCallsMissingCallStyleNotification.removeAll(toRemove); if (mActivityManagerInternal != null) { ServiceConnection fgsConnection = mServices.get(handle); if (fgsConnection != null) { Log.i(this, "stopFGSDelegation: requesting stopForegroundServiceDelegate"); Log.i(TAG, "stopFGSDelegation: requesting stopForegroundServiceDelegate"); mActivityManagerInternal.stopForegroundServiceDelegate(fgsConnection); } } Loading @@ -301,17 +312,17 @@ public class VoipCallMonitor extends CallsManagerListenerBase { String callId = getCallId(call); // Wait 5 seconds for a CallStyle notification to be posted for the call. // If the Call-Style Notification is not posted, FGS delegation needs to be revoked! Log.i(this, "startMonitoringNotification: starting timeout for call.id=[%s]", callId); addToNotificationTracking(call); Log.i(TAG, "startMonitoringNotification: starting timeout for call.id=[%s]", callId); mNewCallsMissingCallStyleNotification.add(call); // If no notification is posted, stop foreground service delegation! mHandlerForClass.postDelayed(() -> { if (isStillMissingNotification(call)) { Log.i(this, "startMonitoringNotification: A Call-Style-Notification" if (mNewCallsMissingCallStyleNotification.contains(call)) { Log.i(TAG, "startMonitoringNotification: A Call-Style-Notification" + " for voip-call=[%s] hasn't posted in time," + " stopping delegation for app=[%s].", call, packageName); stopFGSDelegation(call, handle); } else { Log.i(this, "startMonitoringNotification: found a call-style" Log.i(TAG, "startMonitoringNotification: found a call-style" + " notification for call.id[%s] at timeout", callId); } }, NOTIFICATION_NOT_POSTED_IN_TIME_TIMEOUT); Loading @@ -321,24 +332,6 @@ public class VoipCallMonitor extends CallsManagerListenerBase { * Helpers */ private void addToNotificationTracking(Call call) { synchronized (mNewCallsMissingCallStyleNotification) { mNewCallsMissingCallStyleNotification.add(call); } } private boolean isStillMissingNotification(Call call) { synchronized (mNewCallsMissingCallStyleNotification) { return mNewCallsMissingCallStyleNotification.contains(call); } } private void removeFromNotificationTracking(Call call) { synchronized (mNewCallsMissingCallStyleNotification) { mNewCallsMissingCallStyleNotification.remove(call); } } private PhoneAccountHandle getTargetPhoneAccount(Call call) { synchronized (mSyncRoot) { if (call == null) { Loading Loading @@ -426,7 +419,7 @@ public class VoipCallMonitor extends CallsManagerListenerBase { public boolean hasForegroundServiceDelegation(PhoneAccountHandle handle) { boolean hasFgs = mServices.containsKey(handle); Log.i(this, "hasForegroundServiceDelegation: handle=[%s], hasFgs=[%b]", handle, hasFgs); Log.i(TAG, "hasForegroundServiceDelegation: handle=[%s], hasFgs=[%b]", handle, hasFgs); return hasFgs; } Loading @@ -434,4 +427,9 @@ public class VoipCallMonitor extends CallsManagerListenerBase { public ConcurrentHashMap<PhoneAccountHandle, Set<Call>> getAccountToCallsMapping() { return mAccountHandleToCallMap; } @VisibleForTesting public ConcurrentLinkedQueue<Call> getNewCallsMissingCallStyleNotificationQueue(){ return mNewCallsMissingCallStyleNotification; } } tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java +107 −15 Original line number Diff line number Diff line Loading @@ -22,9 +22,13 @@ import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; Loading @@ -40,6 +44,7 @@ import android.content.ComponentName; import android.content.Intent; import android.content.ServiceConnection; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.UserHandle; import android.service.notification.StatusBarNotification; Loading @@ -54,6 +59,7 @@ import com.android.server.telecom.callsequencing.voip.VoipCallMonitor; import org.junit.After; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; Loading @@ -75,6 +81,7 @@ public class VoipCallMonitorTest extends TelecomTestCase { private static final UserHandle USER_HANDLE_1 = new UserHandle(1); private static final long TIMEOUT = 6000L; @Mock private Handler mHandler; @Mock private TelecomSystem.SyncRoot mLock; @Mock private ActivityManagerInternal mActivityManagerInternal; @Mock private IBinder mServiceConnection; Loading @@ -88,7 +95,8 @@ public class VoipCallMonitorTest extends TelecomTestCase { @Before public void setUp() throws Exception { super.setUp(); mMonitor = new VoipCallMonitor(mContext, mLock); mHandler = mock(Handler.class); mMonitor = new VoipCallMonitor(mContext, mHandler, mLock); mActivityManagerInternal = mock(ActivityManagerInternal.class); mMonitor.setActivityManagerInternal(mActivityManagerInternal); mMonitor.registerNotificationListener(); Loading Loading @@ -158,16 +166,18 @@ public class VoipCallMonitorTest extends TelecomTestCase { public void testStartMonitorForOneCall() { // GIVEN - a single call and notification for a voip app Call call = createTestCall("testCall", mHandle1User1); StatusBarNotification sbn = createStatusBarNotificationFromHandle(mHandle1User1); StatusBarNotification sbn = createStatusBarNotificationFromHandle(mHandle1User1, 1); // WHEN - the Voip call is added and a notification is posted, verify FGS is gained addCallAndVerifyFgsIsGained(call); mMonitor.postNotification(sbn); assertNotificationTimeoutTriggered(); assertFalse(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call)); // THEN - when the Voip call is removed, verify that FGS is revoked for the app mMonitor.onCallRemoved(call); mMonitor.removeNotification(sbn); verify(mActivityManagerInternal, timeout(TIMEOUT)) verify(mActivityManagerInternal, times(1)) .stopForegroundServiceDelegate(any(ServiceConnection.class)); } Loading @@ -179,25 +189,34 @@ public class VoipCallMonitorTest extends TelecomTestCase { public void testStopDelegation_SameApp() { // GIVEN - 2 consecutive calls for a single Voip app Call call1 = createTestCall("testCall1", mHandle1User1); StatusBarNotification sbn1 = createStatusBarNotificationFromHandle(mHandle1User1); StatusBarNotification sbn1 = createStatusBarNotificationFromHandle(mHandle1User1, 1); Call call2 = createTestCall("testCall2", mHandle1User1); StatusBarNotification sbn2 = createStatusBarNotificationFromHandle(mHandle1User1); StatusBarNotification sbn2 = createStatusBarNotificationFromHandle(mHandle1User1, 2); // WHEN - the second call is added and the first is disconnected mMonitor.postNotification(sbn1); // -- add the first all and post the corresponding notification addCallAndVerifyFgsIsGained(call1); mMonitor.postNotification(sbn2); assertTrue(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call1)); mMonitor.postNotification(sbn1); assertNotificationTimeoutTriggered(); assertFalse(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call1)); // -- add the second call and post the corresponding notification mMonitor.onCallAdded(call2); mMonitor.onCallRemoved(call1); assertTrue(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call2)); mMonitor.postNotification(sbn2); assertNotificationTimeoutTriggered(); assertFalse(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call2)); // THEN - assert FGS is maintained for the process since there is still an ongoing call verify(mActivityManagerInternal, timeout(TIMEOUT).times(0)) .stopForegroundServiceDelegate(any(ServiceConnection.class)); mMonitor.onCallRemoved(call1); mMonitor.removeNotification(sbn1); assertNotificationTimeoutTriggered(); verify(mActivityManagerInternal, times(0)) .stopForegroundServiceDelegate(any(ServiceConnection.class)); // once all calls are removed, verify FGS is stopped mMonitor.onCallRemoved(call2); mMonitor.removeNotification(sbn2); verify(mActivityManagerInternal, timeout(TIMEOUT).times(1)) verify(mActivityManagerInternal, times(1)) .stopForegroundServiceDelegate(any(ServiceConnection.class)); } Loading Loading @@ -245,9 +264,10 @@ public class VoipCallMonitorTest extends TelecomTestCase { */ @SmallTest @Test @Ignore("b/383403913") // when b/383403913 is fixed, remove the @Ignore public void testStopFgsIfCallNotificationIsRemoved_PostedAfterFgsIsGained() { // GIVEN StatusBarNotification sbn = createStatusBarNotificationFromHandle(mHandle1User1); StatusBarNotification sbn = createStatusBarNotificationFromHandle(mHandle1User1, 1); // WHEN // FGS is gained after the call is added to VoipCallMonitor Loading @@ -259,7 +279,65 @@ public class VoipCallMonitorTest extends TelecomTestCase { // shortly after posting the notification, simulate the user dismissing it mMonitor.removeNotification(sbn); // FGS should be removed once the notification is removed verify(mActivityManagerInternal, timeout(TIMEOUT)).stopForegroundServiceDelegate(c); assertNotificationTimeoutTriggered(); verify(mActivityManagerInternal, times(1)).stopForegroundServiceDelegate(c); } /** * Tests the behavior of foreground service (FGS) delegation for a VoIP app during a scenario * with two consecutive calls. In this scenario, the first call is disconnected shortly after * being created but the second call continues. The apps foreground service should be * maintained. * * GIVEN: Two calls (call1 and call2) are created for the same VoIP app. * WHEN: * - call1 is added, starting the FGS. * - call2 is added immediately after. * - call1 is removed. * - call1 notification is finally posted (late) * - call1 notification is removed shortly after since the call was disconnected * THEN: * - Verifies that the FGS is NOT stopped while call2 is still active. * - Verifies that the FGS IS stopped after call2 is removed and its notification is gone. */ @SmallTest @Test public void test2CallsInQuickSuccession() { // GIVEN - 2 consecutive calls for a single Voip app Call call1 = createTestCall("testCall1", mHandle1User1); StatusBarNotification sbn1 = createStatusBarNotificationFromHandle(mHandle1User1, 1); Call call2 = createTestCall("testCall2", mHandle1User1); StatusBarNotification sbn2 = createStatusBarNotificationFromHandle(mHandle1User1, 2); // WHEN - add the calls to the VoipCallMonitor class addCallAndVerifyFgsIsGained(call1); mMonitor.onCallAdded(call2); assertTrue(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call1)); assertTrue(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call2)); // -- mock the app disconnecting the first mMonitor.onCallRemoved(call1); // Shortly after, simulate the notification updates coming in to the class // -- post and remove the first call-style notification mMonitor.postNotification(sbn1); assertFalse(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call1)); mMonitor.removeNotification(sbn1); assertNotificationTimeoutTriggered(); // -- keep the second notification up since the call will continue mMonitor.postNotification(sbn2); assertFalse(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call2)); // THEN - assert FGS is maintained for the process since there is still an ongoing call assertNotificationTimeoutTriggered(); verify(mActivityManagerInternal, times(0)) .stopForegroundServiceDelegate(any(ServiceConnection.class)); // once all calls are removed, verify FGS is stopped mMonitor.onCallRemoved(call2); mMonitor.removeNotification(sbn2); verify(mActivityManagerInternal, timeout(TIMEOUT).times(1)) .stopForegroundServiceDelegate(any(ServiceConnection.class)); } /** Loading Loading @@ -291,9 +369,10 @@ public class VoipCallMonitorTest extends TelecomTestCase { .build(); } private StatusBarNotification createStatusBarNotificationFromHandle(PhoneAccountHandle handle) { private StatusBarNotification createStatusBarNotificationFromHandle( PhoneAccountHandle handle, int id) { return new StatusBarNotification( handle.getComponentName().getPackageName(), "", 0, "", 0, 0, handle.getComponentName().getPackageName(), "", id, "", 0, 0, createCallStyleNotification(), handle.getUserHandle(), "", 0); } Loading @@ -314,4 +393,17 @@ public class VoipCallMonitorTest extends TelecomTestCase { mServiceConnection); return serviceConnection; } /** * Verifies that a delayed runnable is posted to the handler to handle the notification timeout. * This also executes the captured runnable to simulate the timeout occurring. */ private void assertNotificationTimeoutTriggered() { ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class); verify(mHandler, atLeastOnce()).postDelayed( runnableCaptor.capture(), eq(VoipCallMonitor.NOTIFICATION_NOT_POSTED_IN_TIME_TIMEOUT)); Runnable capturedRunnable = runnableCaptor.getValue(); capturedRunnable.run(); } } Loading
src/com/android/server/telecom/CallsManager.java +4 −1 Original line number Diff line number Diff line Loading @@ -775,7 +775,10 @@ public class CallsManager extends Call.ListenerBase mCallStreamingNotification = callStreamingNotification; mFeatureFlags = featureFlags; if (mFeatureFlags.voipCallMonitorRefactor()) { mVoipCallMonitor = new VoipCallMonitor(mContext, mLock); mVoipCallMonitor = new VoipCallMonitor( mContext, new Handler(Looper.getMainLooper()), mLock); mVoipCallMonitorLegacy = null; } else { mVoipCallMonitor = null; Loading
src/com/android/server/telecom/callsequencing/voip/VoipCallMonitor.java +58 −60 Original line number Diff line number Diff line Loading @@ -30,9 +30,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.ServiceConnection; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; Loading @@ -53,14 +51,16 @@ import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; public class VoipCallMonitor extends CallsManagerListenerBase { private static final long NOTIFICATION_NOT_POSTED_IN_TIME_TIMEOUT = 5000L; private static final long NOTIFICATION_REMOVED_BUT_CALL_IS_STILL_ONGOING_TIMEOUT = 5000L; public static final long NOTIFICATION_NOT_POSTED_IN_TIME_TIMEOUT = 5000L; public static final long NOTIFICATION_REMOVED_BUT_CALL_IS_STILL_ONGOING_TIMEOUT = 5000L; private static final String TAG = VoipCallMonitor.class.getSimpleName(); private static final String DElIMITER = "#"; // This list caches calls that are added to the VoipCallMonitor and need an accompanying // Call-Style Notification! private final List<Call> mNewCallsMissingCallStyleNotification; private final ConcurrentLinkedQueue<Call> mNewCallsMissingCallStyleNotification; private final ConcurrentHashMap<String, Call> mNotificationIdToCall; private final ConcurrentHashMap<PhoneAccountHandle, Set<Call>> mAccountHandleToCallMap; private final ConcurrentHashMap<PhoneAccountHandle, ServiceConnection> mServices; Loading @@ -70,11 +70,11 @@ public class VoipCallMonitor extends CallsManagerListenerBase { private final Context mContext; private final TelecomSystem.SyncRoot mSyncRoot; public VoipCallMonitor(Context context, TelecomSystem.SyncRoot lock) { public VoipCallMonitor(Context context, Handler handler, TelecomSystem.SyncRoot lock) { mSyncRoot = lock; mContext = context; mHandlerForClass = new Handler(Looper.getMainLooper()); mNewCallsMissingCallStyleNotification = new ArrayList<>(); mHandlerForClass = handler; mNewCallsMissingCallStyleNotification = new ConcurrentLinkedQueue<>(); mNotificationIdToCall = new ConcurrentHashMap<>(); mServices = new ConcurrentHashMap<>(); mAccountHandleToCallMap = new ConcurrentHashMap<>(); Loading @@ -83,21 +83,18 @@ public class VoipCallMonitor extends CallsManagerListenerBase { @Override public void onNotificationPosted(StatusBarNotification sbn) { if (isCallStyleNotification(sbn)) { Log.i(this, "onNotificationPosted: sbn=[%s]", sbn); boolean foundCallForNotification = false; Log.i(TAG, "onNotificationPosted: sbn=[%s]", sbn); // Case 1: Call added to this class (via onCallAdded) BEFORE Call-Style // Notification is posted by the app (only supported scenario) // --> remove the newly added call from // mNewCallsMissingCallStyleNotification so FGS is not revoked. for (Call call : new ArrayList<>(mNewCallsMissingCallStyleNotification)) { Call newCallNoLongerAwaitingNotification = null; for (Call call : mNewCallsMissingCallStyleNotification) { if (isNotificationForCall(sbn, call)) { Log.i(this, "onNotificationPosted: found a pending " Log.i(TAG, "onNotificationPosted: found a pending " + "call=[%s] for sbn.id=[%s]", call, sbn.getId()); mNotificationIdToCall.put( getNotificationIdToCallKey(sbn), call); removeFromNotificationTracking(call); foundCallForNotification = true; newCallNoLongerAwaitingNotification = call; break; } } Loading @@ -105,11 +102,19 @@ public class VoipCallMonitor extends CallsManagerListenerBase { // --> Currently do not support this // Case 3: Call-Style Notification was updated (ex. incoming -> ongoing) // --> do nothing if (!foundCallForNotification) { Log.i(this, "onNotificationPosted: could not find a call for the" if (newCallNoLongerAwaitingNotification == null) { Log.i(TAG, "onNotificationPosted: could not find a call for the" + " sbn.id=[%s]. This could mean the notification posted" + " BEFORE the call is added (error) or it's an update from" + " incoming to ongoing (ok).", sbn.getId()); } else { // --> remove the newly added call from // mNewCallsMissingCallStyleNotification so FGS is not revoked when the // timeout is hit in VoipCallMonitor#startMonitoringNotification(...). The // timeout ensures the voip app posts a call-style notification within // 5 seconds! mNewCallsMissingCallStyleNotification .remove(newCallNoLongerAwaitingNotification); } } } Loading @@ -119,14 +124,17 @@ public class VoipCallMonitor extends CallsManagerListenerBase { if (!isCallStyleNotification(sbn)) { return; } Log.i(this, "onNotificationRemoved: Call-Style notification=[%s] removed", sbn); Log.i(TAG, "onNotificationRemoved: Call-Style notification=[%s] removed", sbn); Call call = getCallFromStatusBarNotificationId(sbn); if (call != null) { PhoneAccountHandle handle = getTargetPhoneAccount(call); if (!isCallDisconnected(call)) { mHandlerForClass.postDelayed(() -> { if (isCallStillBeingTracked(call)) { stopFGSDelegation(call, handle); Log.w(TAG, "onNotificationRemoved: notification has been removed for" + " more than 5 seconds but call still ongoing " + "c=[%s]", call); // TODO:: stopFGSDelegation(call, handle) when b/383403913 is fixed } }, NOTIFICATION_REMOVED_BUT_CALL_IS_STILL_ONGOING_TIMEOUT); } Loading Loading @@ -185,7 +193,7 @@ public class VoipCallMonitor extends CallsManagerListenerBase { new ComponentName(this.getClass().getPackageName(), this.getClass().getCanonicalName()), ActivityManager.getCurrentUser()); } catch (RemoteException e) { Log.e(this, e, "Cannot register notification listener"); Log.e(TAG, e, "Cannot register notification listener"); } } Loading @@ -193,7 +201,7 @@ public class VoipCallMonitor extends CallsManagerListenerBase { try { mNotificationListener.unregisterAsSystemService(); } catch (RemoteException e) { Log.e(this, e, "Cannot unregister notification listener"); Log.e(TAG, e, "Cannot unregister notification listener"); } } Loading @@ -217,11 +225,10 @@ public class VoipCallMonitor extends CallsManagerListenerBase { if (!isTransactional(call) || handle == null) { return; } removeFromNotificationTracking(call); Set<Call> ongoingCalls = mAccountHandleToCallMap .computeIfAbsent(handle, k -> new HashSet<>()); ongoingCalls.remove(call); Log.d(this, "onCallRemoved: callList.size=[%d]", ongoingCalls.size()); Log.d(TAG, "onCallRemoved: callList.size=[%d]", ongoingCalls.size()); if (ongoingCalls.isEmpty()) { stopFGSDelegation(call, handle); } else { Loading @@ -230,7 +237,7 @@ public class VoipCallMonitor extends CallsManagerListenerBase { } private void maybeStartFGSDelegation(int pid, int uid, PhoneAccountHandle handle, Call call) { Log.i(this, "maybeStartFGSDelegation for call=[%s]", call); Log.i(TAG, "maybeStartFGSDelegation for call=[%s]", call); if (mActivityManagerInternal != null) { if (mServices.containsKey(handle)) { Log.addEvent(call, LogUtils.Events.ALREADY_HAS_FGS_DELEGATION); Loading Loading @@ -262,34 +269,38 @@ public class VoipCallMonitor extends CallsManagerListenerBase { try { if (mActivityManagerInternal .startForegroundServiceDelegate(options, fgsConnection)) { Log.i(this, "maybeStartFGSDelegation: startForegroundServiceDelegate success"); Log.i(TAG, "maybeStartFGSDelegation: startForegroundServiceDelegate success"); } else { Log.addEvent(call, LogUtils.Events.GAIN_FGS_DELEGATION_FAILED); } } catch (Exception e) { Log.i(this, "startForegroundServiceDelegate failed due to: " + e); Log.i(TAG, "startForegroundServiceDelegate failed due to: " + e); } } } @VisibleForTesting public void stopFGSDelegation(Call call, PhoneAccountHandle handle) { Log.i(this, "stopFGSDelegation of call=[%s]", call); Log.i(TAG, "stopFGSDelegation of call=[%s]", call); if (handle == null) { return; } // In the event this class is waiting for any new calls to post a notification, remove // the call from the notification tracking container! Set<Call> ongoingCalls = mAccountHandleToCallMap.get(handle); if (ongoingCalls != null) { for (Call c : new ArrayList<>(ongoingCalls)) { removeFromNotificationTracking(c); // In the event this class is waiting for any new calls to post a notification, cleanup List<Call> toRemove = new ArrayList<>(); for (Call callAwaitingNotification : mNewCallsMissingCallStyleNotification) { if (handle.equals(callAwaitingNotification.getTargetPhoneAccount())) { Log.d(TAG, "stopFGSDelegation: removing call from notification tracking c=[%s]", callAwaitingNotification); toRemove.add(callAwaitingNotification); } } mNewCallsMissingCallStyleNotification.removeAll(toRemove); if (mActivityManagerInternal != null) { ServiceConnection fgsConnection = mServices.get(handle); if (fgsConnection != null) { Log.i(this, "stopFGSDelegation: requesting stopForegroundServiceDelegate"); Log.i(TAG, "stopFGSDelegation: requesting stopForegroundServiceDelegate"); mActivityManagerInternal.stopForegroundServiceDelegate(fgsConnection); } } Loading @@ -301,17 +312,17 @@ public class VoipCallMonitor extends CallsManagerListenerBase { String callId = getCallId(call); // Wait 5 seconds for a CallStyle notification to be posted for the call. // If the Call-Style Notification is not posted, FGS delegation needs to be revoked! Log.i(this, "startMonitoringNotification: starting timeout for call.id=[%s]", callId); addToNotificationTracking(call); Log.i(TAG, "startMonitoringNotification: starting timeout for call.id=[%s]", callId); mNewCallsMissingCallStyleNotification.add(call); // If no notification is posted, stop foreground service delegation! mHandlerForClass.postDelayed(() -> { if (isStillMissingNotification(call)) { Log.i(this, "startMonitoringNotification: A Call-Style-Notification" if (mNewCallsMissingCallStyleNotification.contains(call)) { Log.i(TAG, "startMonitoringNotification: A Call-Style-Notification" + " for voip-call=[%s] hasn't posted in time," + " stopping delegation for app=[%s].", call, packageName); stopFGSDelegation(call, handle); } else { Log.i(this, "startMonitoringNotification: found a call-style" Log.i(TAG, "startMonitoringNotification: found a call-style" + " notification for call.id[%s] at timeout", callId); } }, NOTIFICATION_NOT_POSTED_IN_TIME_TIMEOUT); Loading @@ -321,24 +332,6 @@ public class VoipCallMonitor extends CallsManagerListenerBase { * Helpers */ private void addToNotificationTracking(Call call) { synchronized (mNewCallsMissingCallStyleNotification) { mNewCallsMissingCallStyleNotification.add(call); } } private boolean isStillMissingNotification(Call call) { synchronized (mNewCallsMissingCallStyleNotification) { return mNewCallsMissingCallStyleNotification.contains(call); } } private void removeFromNotificationTracking(Call call) { synchronized (mNewCallsMissingCallStyleNotification) { mNewCallsMissingCallStyleNotification.remove(call); } } private PhoneAccountHandle getTargetPhoneAccount(Call call) { synchronized (mSyncRoot) { if (call == null) { Loading Loading @@ -426,7 +419,7 @@ public class VoipCallMonitor extends CallsManagerListenerBase { public boolean hasForegroundServiceDelegation(PhoneAccountHandle handle) { boolean hasFgs = mServices.containsKey(handle); Log.i(this, "hasForegroundServiceDelegation: handle=[%s], hasFgs=[%b]", handle, hasFgs); Log.i(TAG, "hasForegroundServiceDelegation: handle=[%s], hasFgs=[%b]", handle, hasFgs); return hasFgs; } Loading @@ -434,4 +427,9 @@ public class VoipCallMonitor extends CallsManagerListenerBase { public ConcurrentHashMap<PhoneAccountHandle, Set<Call>> getAccountToCallsMapping() { return mAccountHandleToCallMap; } @VisibleForTesting public ConcurrentLinkedQueue<Call> getNewCallsMissingCallStyleNotificationQueue(){ return mNewCallsMissingCallStyleNotification; } }
tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java +107 −15 Original line number Diff line number Diff line Loading @@ -22,9 +22,13 @@ import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; Loading @@ -40,6 +44,7 @@ import android.content.ComponentName; import android.content.Intent; import android.content.ServiceConnection; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.UserHandle; import android.service.notification.StatusBarNotification; Loading @@ -54,6 +59,7 @@ import com.android.server.telecom.callsequencing.voip.VoipCallMonitor; import org.junit.After; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; Loading @@ -75,6 +81,7 @@ public class VoipCallMonitorTest extends TelecomTestCase { private static final UserHandle USER_HANDLE_1 = new UserHandle(1); private static final long TIMEOUT = 6000L; @Mock private Handler mHandler; @Mock private TelecomSystem.SyncRoot mLock; @Mock private ActivityManagerInternal mActivityManagerInternal; @Mock private IBinder mServiceConnection; Loading @@ -88,7 +95,8 @@ public class VoipCallMonitorTest extends TelecomTestCase { @Before public void setUp() throws Exception { super.setUp(); mMonitor = new VoipCallMonitor(mContext, mLock); mHandler = mock(Handler.class); mMonitor = new VoipCallMonitor(mContext, mHandler, mLock); mActivityManagerInternal = mock(ActivityManagerInternal.class); mMonitor.setActivityManagerInternal(mActivityManagerInternal); mMonitor.registerNotificationListener(); Loading Loading @@ -158,16 +166,18 @@ public class VoipCallMonitorTest extends TelecomTestCase { public void testStartMonitorForOneCall() { // GIVEN - a single call and notification for a voip app Call call = createTestCall("testCall", mHandle1User1); StatusBarNotification sbn = createStatusBarNotificationFromHandle(mHandle1User1); StatusBarNotification sbn = createStatusBarNotificationFromHandle(mHandle1User1, 1); // WHEN - the Voip call is added and a notification is posted, verify FGS is gained addCallAndVerifyFgsIsGained(call); mMonitor.postNotification(sbn); assertNotificationTimeoutTriggered(); assertFalse(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call)); // THEN - when the Voip call is removed, verify that FGS is revoked for the app mMonitor.onCallRemoved(call); mMonitor.removeNotification(sbn); verify(mActivityManagerInternal, timeout(TIMEOUT)) verify(mActivityManagerInternal, times(1)) .stopForegroundServiceDelegate(any(ServiceConnection.class)); } Loading @@ -179,25 +189,34 @@ public class VoipCallMonitorTest extends TelecomTestCase { public void testStopDelegation_SameApp() { // GIVEN - 2 consecutive calls for a single Voip app Call call1 = createTestCall("testCall1", mHandle1User1); StatusBarNotification sbn1 = createStatusBarNotificationFromHandle(mHandle1User1); StatusBarNotification sbn1 = createStatusBarNotificationFromHandle(mHandle1User1, 1); Call call2 = createTestCall("testCall2", mHandle1User1); StatusBarNotification sbn2 = createStatusBarNotificationFromHandle(mHandle1User1); StatusBarNotification sbn2 = createStatusBarNotificationFromHandle(mHandle1User1, 2); // WHEN - the second call is added and the first is disconnected mMonitor.postNotification(sbn1); // -- add the first all and post the corresponding notification addCallAndVerifyFgsIsGained(call1); mMonitor.postNotification(sbn2); assertTrue(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call1)); mMonitor.postNotification(sbn1); assertNotificationTimeoutTriggered(); assertFalse(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call1)); // -- add the second call and post the corresponding notification mMonitor.onCallAdded(call2); mMonitor.onCallRemoved(call1); assertTrue(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call2)); mMonitor.postNotification(sbn2); assertNotificationTimeoutTriggered(); assertFalse(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call2)); // THEN - assert FGS is maintained for the process since there is still an ongoing call verify(mActivityManagerInternal, timeout(TIMEOUT).times(0)) .stopForegroundServiceDelegate(any(ServiceConnection.class)); mMonitor.onCallRemoved(call1); mMonitor.removeNotification(sbn1); assertNotificationTimeoutTriggered(); verify(mActivityManagerInternal, times(0)) .stopForegroundServiceDelegate(any(ServiceConnection.class)); // once all calls are removed, verify FGS is stopped mMonitor.onCallRemoved(call2); mMonitor.removeNotification(sbn2); verify(mActivityManagerInternal, timeout(TIMEOUT).times(1)) verify(mActivityManagerInternal, times(1)) .stopForegroundServiceDelegate(any(ServiceConnection.class)); } Loading Loading @@ -245,9 +264,10 @@ public class VoipCallMonitorTest extends TelecomTestCase { */ @SmallTest @Test @Ignore("b/383403913") // when b/383403913 is fixed, remove the @Ignore public void testStopFgsIfCallNotificationIsRemoved_PostedAfterFgsIsGained() { // GIVEN StatusBarNotification sbn = createStatusBarNotificationFromHandle(mHandle1User1); StatusBarNotification sbn = createStatusBarNotificationFromHandle(mHandle1User1, 1); // WHEN // FGS is gained after the call is added to VoipCallMonitor Loading @@ -259,7 +279,65 @@ public class VoipCallMonitorTest extends TelecomTestCase { // shortly after posting the notification, simulate the user dismissing it mMonitor.removeNotification(sbn); // FGS should be removed once the notification is removed verify(mActivityManagerInternal, timeout(TIMEOUT)).stopForegroundServiceDelegate(c); assertNotificationTimeoutTriggered(); verify(mActivityManagerInternal, times(1)).stopForegroundServiceDelegate(c); } /** * Tests the behavior of foreground service (FGS) delegation for a VoIP app during a scenario * with two consecutive calls. In this scenario, the first call is disconnected shortly after * being created but the second call continues. The apps foreground service should be * maintained. * * GIVEN: Two calls (call1 and call2) are created for the same VoIP app. * WHEN: * - call1 is added, starting the FGS. * - call2 is added immediately after. * - call1 is removed. * - call1 notification is finally posted (late) * - call1 notification is removed shortly after since the call was disconnected * THEN: * - Verifies that the FGS is NOT stopped while call2 is still active. * - Verifies that the FGS IS stopped after call2 is removed and its notification is gone. */ @SmallTest @Test public void test2CallsInQuickSuccession() { // GIVEN - 2 consecutive calls for a single Voip app Call call1 = createTestCall("testCall1", mHandle1User1); StatusBarNotification sbn1 = createStatusBarNotificationFromHandle(mHandle1User1, 1); Call call2 = createTestCall("testCall2", mHandle1User1); StatusBarNotification sbn2 = createStatusBarNotificationFromHandle(mHandle1User1, 2); // WHEN - add the calls to the VoipCallMonitor class addCallAndVerifyFgsIsGained(call1); mMonitor.onCallAdded(call2); assertTrue(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call1)); assertTrue(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call2)); // -- mock the app disconnecting the first mMonitor.onCallRemoved(call1); // Shortly after, simulate the notification updates coming in to the class // -- post and remove the first call-style notification mMonitor.postNotification(sbn1); assertFalse(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call1)); mMonitor.removeNotification(sbn1); assertNotificationTimeoutTriggered(); // -- keep the second notification up since the call will continue mMonitor.postNotification(sbn2); assertFalse(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call2)); // THEN - assert FGS is maintained for the process since there is still an ongoing call assertNotificationTimeoutTriggered(); verify(mActivityManagerInternal, times(0)) .stopForegroundServiceDelegate(any(ServiceConnection.class)); // once all calls are removed, verify FGS is stopped mMonitor.onCallRemoved(call2); mMonitor.removeNotification(sbn2); verify(mActivityManagerInternal, timeout(TIMEOUT).times(1)) .stopForegroundServiceDelegate(any(ServiceConnection.class)); } /** Loading Loading @@ -291,9 +369,10 @@ public class VoipCallMonitorTest extends TelecomTestCase { .build(); } private StatusBarNotification createStatusBarNotificationFromHandle(PhoneAccountHandle handle) { private StatusBarNotification createStatusBarNotificationFromHandle( PhoneAccountHandle handle, int id) { return new StatusBarNotification( handle.getComponentName().getPackageName(), "", 0, "", 0, 0, handle.getComponentName().getPackageName(), "", id, "", 0, 0, createCallStyleNotification(), handle.getUserHandle(), "", 0); } Loading @@ -314,4 +393,17 @@ public class VoipCallMonitorTest extends TelecomTestCase { mServiceConnection); return serviceConnection; } /** * Verifies that a delayed runnable is posted to the handler to handle the notification timeout. * This also executes the captured runnable to simulate the timeout occurring. */ private void assertNotificationTimeoutTriggered() { ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class); verify(mHandler, atLeastOnce()).postDelayed( runnableCaptor.capture(), eq(VoipCallMonitor.NOTIFICATION_NOT_POSTED_IN_TIME_TIMEOUT)); Runnable capturedRunnable = runnableCaptor.getValue(); capturedRunnable.run(); } }