Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9971ad5e authored by Thomas Stuart's avatar Thomas Stuart
Browse files

VoipCallMonitor updates

In order of importance
- Do not revoke FGS the call-style notification is removed
  for an ongoing call
- Change mNewCallsMissingCallStyleNotification list to queue
- Only clear the mNewCallsMissingCallStyleNotification if a
  notification is posted for the new Or FGS is stopped for the app
- use the same TAG for the NotificationListener and VoipCallMonitor

Flag: com.android.server.telecom.flags.voip_call_monitor_refactor
Bug: 389898878
Test: new unit test -
      atest com.android.server.telecom.tests.VoipCallMonitorTest#
      test2CallsInQuickSuccession
Change-Id: Iecc8ecfd6376772ba42651d24198b81cf80a6f31
parent fae32401
Loading
Loading
Loading
Loading
+4 −1
Original line number Diff line number Diff line
@@ -771,7 +771,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;
+58 −60
Original line number Diff line number Diff line
@@ -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;
@@ -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;
@@ -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<>();
@@ -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;
                        }
                    }
@@ -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);
                    }
                }
            }
@@ -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);
                    }
@@ -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");
        }
    }

@@ -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");
        }
    }

@@ -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 {
@@ -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);
@@ -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);
            }
        }
@@ -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);
@@ -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) {
@@ -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;
    }

@@ -434,4 +427,9 @@ public class VoipCallMonitor extends CallsManagerListenerBase {
    public ConcurrentHashMap<PhoneAccountHandle, Set<Call>> getAccountToCallsMapping() {
        return mAccountHandleToCallMap;
    }

    @VisibleForTesting
    public  ConcurrentLinkedQueue<Call> getNewCallsMissingCallStyleNotificationQueue(){
        return mNewCallsMissingCallStyleNotification;
    }
}
+107 −15
Original line number Diff line number Diff line
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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();
@@ -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));
    }

@@ -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));
    }

@@ -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
@@ -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));
    }

    /**
@@ -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);
    }

@@ -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();
    }
}