handle 2 calls from the same app case in VoipCallMonitor

It was pointed out that AM was throwing an exception when a second
voip call was added to telecom for the same app.  Upon investigation,
foreground service delegation should only be granted once for an app
instead of on a per call basis.

Changes:
- FGS is started ONLY if the application does not have it already
  instead of every time the call is added
- FGS is revoked if the notification is removed and the call is
  not disconnected after 5 seconds
- only support call-style notifications posted AFTER the call is
  added
- The logging around FGS events has changed.  Prev logged when
  start/stop requests are sent to AM. Now, fgs gain/revoked is
  logged when AM calls the service.

Flag: com.android.server.telecom.flags.voip_call_monitor_refactor
Bug: 381129034
Test: manual
Change-Id: Ic5f02575b1efc72d5e848907bd1006ca4cdf2edb
diff --git a/flags/Android.bp b/flags/Android.bp
index 54b1443..6f9caae 100644
--- a/flags/Android.bp
+++ b/flags/Android.bp
@@ -46,5 +46,6 @@
         "telecom_headless_system_user_mode.aconfig",
         "telecom_session_flags.aconfig",
         "telecom_metrics_flags.aconfig",
+        "telecom_voip_flags.aconfig",
     ],
 }
diff --git a/flags/telecom_voip_flags.aconfig b/flags/telecom_voip_flags.aconfig
new file mode 100644
index 0000000..67635e9
--- /dev/null
+++ b/flags/telecom_voip_flags.aconfig
@@ -0,0 +1,13 @@
+package: "com.android.server.telecom.flags"
+container: "system"
+
+# OWNER=tjstuart TARGET=25Q2
+flag {
+  name: "voip_call_monitor_refactor"
+  namespace: "telecom"
+  description: "VoipCallMonitor reworked to handle multi calling scenarios for the same app"
+  bug: "381129034"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
\ No newline at end of file
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 9e566e2..75d8d7d 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -1883,7 +1883,6 @@
         return mTargetPhoneAccountHandle;
     }
 
-    @VisibleForTesting
     public PhoneAccountHandle getTargetPhoneAccount() {
         return mTargetPhoneAccountHandle;
     }
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index f78de5e..f6a1fd8 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -153,6 +153,7 @@
 import com.android.server.telecom.ui.ToastFactory;
 import com.android.server.telecom.callsequencing.voip.VoipCallMonitor;
 import com.android.server.telecom.callsequencing.TransactionManager;
+import com.android.server.telecom.callsequencing.voip.VoipCallMonitorLegacy;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -488,6 +489,7 @@
     private final EmergencyCallHelper mEmergencyCallHelper;
     private final RoleManagerAdapter mRoleManagerAdapter;
     private final VoipCallMonitor mVoipCallMonitor;
+    private final VoipCallMonitorLegacy mVoipCallMonitorLegacy;
     private final CallEndpointController mCallEndpointController;
     private final CallAnomalyWatchdog mCallAnomalyWatchdog;
 
@@ -763,12 +765,18 @@
         mClockProxy = clockProxy;
         mToastFactory = toastFactory;
         mRoleManagerAdapter = roleManagerAdapter;
-        mVoipCallMonitor = new VoipCallMonitor(mContext, mLock);
         mTransactionManager = transactionManager;
         mBlockedNumbersAdapter = blockedNumbersAdapter;
         mCallStreamingController = new CallStreamingController(mContext, mLock);
         mCallStreamingNotification = callStreamingNotification;
         mFeatureFlags = featureFlags;
+        if (mFeatureFlags.voipCallMonitorRefactor()) {
+            mVoipCallMonitor = new VoipCallMonitor(mContext, mLock);
+            mVoipCallMonitorLegacy = null;
+        } else {
+            mVoipCallMonitor = null;
+            mVoipCallMonitorLegacy = new VoipCallMonitorLegacy(mContext, mLock);
+        }
         mTelephonyFeatureFlags = telephonyFlags;
         mMetricsController = metricsController;
         mBlockedNumbersManager = mFeatureFlags.telecomMainlineBlockedNumbersManager()
@@ -804,13 +812,18 @@
 
         // this needs to be after the mCallAudioManager
         mListeners.add(mPhoneStateBroadcaster);
-        mListeners.add(mVoipCallMonitor);
         mListeners.add(mCallStreamingNotification);
         if (featureFlags.enableCallAudioWatchdog()) {
             mListeners.add(mCallAudioWatchDog);
         }
 
-        mVoipCallMonitor.startMonitor();
+        if (mFeatureFlags.voipCallMonitorRefactor()) {
+            mVoipCallMonitor.registerNotificationListener();
+            mListeners.add(mVoipCallMonitor);
+        } else {
+            mVoipCallMonitorLegacy.startMonitor();
+            mListeners.add(mVoipCallMonitorLegacy);
+        }
 
         // There is no USER_SWITCHED broadcast for user 0, handle it here explicitly.
         final UserManager userManager = mContext.getSystemService(UserManager.class);
diff --git a/src/com/android/server/telecom/LogUtils.java b/src/com/android/server/telecom/LogUtils.java
index a3502bd..6222601 100644
--- a/src/com/android/server/telecom/LogUtils.java
+++ b/src/com/android/server/telecom/LogUtils.java
@@ -226,6 +226,8 @@
         public static final String FLASH_NOTIFICATION_START = "FLASH_NOTIFICATION_START";
         public static final String FLASH_NOTIFICATION_STOP = "FLASH_NOTIFICATION_STOP";
         public static final String GAINED_FGS_DELEGATION = "GAINED_FGS_DELEGATION";
+        public static final String ALREADY_HAS_FGS_DELEGATION = "ALREADY_HAS_FGS_DELEGATION";
+        public static final String MAINTAINING_FGS_DELEGATION = "MAINTAINING_FGS_DELEGATION";
         public static final String GAIN_FGS_DELEGATION_FAILED = "GAIN_FGS_DELEGATION_FAILED";
         public static final String LOST_FGS_DELEGATION = "LOST_FGS_DELEGATION";
         public static final String START_STREAMING = "START_STREAMING";
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index edfbb1b..31be26e 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -80,6 +80,8 @@
 import com.android.internal.telecom.ICallEventCallback;
 import com.android.internal.telecom.ITelecomService;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.telecom.callsequencing.voip.OutgoingCallTransactionSequencing;
+import com.android.server.telecom.callsequencing.voip.VoipCallMonitor;
 import com.android.server.telecom.components.UserCallIntentProcessorFactory;
 import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.metrics.ApiStats;
@@ -168,6 +170,23 @@
     private final ITelecomService.Stub mBinderImpl = new ITelecomService.Stub() {
 
         @Override
+        public boolean hasForegroundServiceDelegation(
+                PhoneAccountHandle handle,
+                String packageName) {
+            enforceCallingPackage(packageName, "hasForegroundServiceDelegation");
+            long token = Binder.clearCallingIdentity();
+            try {
+                VoipCallMonitor vcm = mCallsManager.getVoipCallMonitor();
+                if (vcm != null) {
+                    return vcm.hasForegroundServiceDelegation(handle);
+                }
+                return false;
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        @Override
         public void addCall(CallAttributes callAttributes, ICallEventCallback callEventCallback,
                 String callId, String callingPackage) {
             ApiStats.ApiEvent event = new ApiStats.ApiEvent(ApiStats.API_ADDCALL,
diff --git a/src/com/android/server/telecom/callsequencing/voip/VoipCallMonitor.java b/src/com/android/server/telecom/callsequencing/voip/VoipCallMonitor.java
index 1d1a1a6..dc770e0 100644
--- a/src/com/android/server/telecom/callsequencing/voip/VoipCallMonitor.java
+++ b/src/com/android/server/telecom/callsequencing/voip/VoipCallMonitor.java
@@ -32,8 +32,8 @@
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
+import android.os.Looper;
 import android.os.RemoteException;
-import android.os.UserHandle;
 import android.service.notification.NotificationListenerService;
 import android.service.notification.StatusBarNotification;
 import android.telecom.Log;
@@ -42,106 +42,144 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.LocalServices;
 import com.android.server.telecom.Call;
-
 import com.android.server.telecom.CallsManagerListenerBase;
 import com.android.server.telecom.LogUtils;
-import com.android.server.telecom.LoggedHandlerExecutor;
 import com.android.server.telecom.TelecomSystem;
 
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
-import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
 
 public class VoipCallMonitor extends CallsManagerListenerBase {
-
-    private final List<Call> mNotificationPendingCalls;
-    // Same notification may be passed as different object in onNotificationPosted and
-    // onNotificationRemoved. Use its string as key to cache ongoing notifications.
-    private final Map<NotificationInfo, Call> mNotificationInfoToCallMap;
-    private final Map<PhoneAccountHandle, Set<Call>> mAccountHandleToCallMap;
+    private static final long NOTIFICATION_NOT_POSTED_IN_TIME_TIMEOUT = 5000L;
+    private static final long NOTIFICATION_REMOVED_BUT_CALL_IS_STILL_ONGOING_TIMEOUT = 5000L;
+    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 ConcurrentHashMap<String, Call> mNotificationIdToCall;
+    private final ConcurrentHashMap<PhoneAccountHandle, Set<Call>> mAccountHandleToCallMap;
+    private final ConcurrentHashMap<PhoneAccountHandle, ServiceConnection> mServices;
     private ActivityManagerInternal mActivityManagerInternal;
-    private final Map<PhoneAccountHandle, ServiceConnection> mServices;
-    private NotificationListenerService mNotificationListener;
-    private final Object mLock = new Object();
-    private final HandlerThread mHandlerThread;
-    private final Handler mHandler;
+    private final NotificationListenerService mNotificationListener;
+    private final Handler mHandlerForClass;
     private final Context mContext;
-    private List<NotificationInfo> mCachedNotifications;
-    private TelecomSystem.SyncRoot mSyncRoot;
+    private final TelecomSystem.SyncRoot mSyncRoot;
 
     public VoipCallMonitor(Context context, TelecomSystem.SyncRoot lock) {
         mSyncRoot = lock;
         mContext = context;
-        mHandlerThread = new HandlerThread(this.getClass().getSimpleName());
-        mHandlerThread.start();
-        mHandler = new Handler(mHandlerThread.getLooper());
-        mNotificationPendingCalls = new ArrayList<>();
-        mCachedNotifications = new ArrayList<>();
-        mNotificationInfoToCallMap = new HashMap<>();
-        mServices = new HashMap<>();
-        mAccountHandleToCallMap = new HashMap<>();
+        mHandlerForClass = new Handler(Looper.getMainLooper());
+        mNewCallsMissingCallStyleNotification = new ArrayList<>();
+        mNotificationIdToCall = new ConcurrentHashMap<>();
+        mServices = new ConcurrentHashMap<>();
+        mAccountHandleToCallMap = new ConcurrentHashMap<>();
         mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
-
         mNotificationListener = new NotificationListenerService() {
             @Override
             public void onNotificationPosted(StatusBarNotification sbn) {
-                synchronized (mLock) {
-                    if (sbn.getNotification().isStyle(Notification.CallStyle.class)) {
-                        NotificationInfo info = new NotificationInfo(sbn.getPackageName(),
-                                sbn.getUser());
-                        boolean sbnMatched = false;
-                        for (Call call : mNotificationPendingCalls) {
-                            if (info.matchesCall(call)) {
-                                Log.i(this, "onNotificationPosted: found a pending "
-                                                + "callId=[%s] for the call notification w/ "
-                                                + "id=[%s]",
-                                        call.getId(), sbn.getId());
-                                mNotificationPendingCalls.remove(call);
-                                mNotificationInfoToCallMap.put(info, call);
-                                sbnMatched = true;
-                                break;
-                            }
+                if (isCallStyleNotification(sbn)) {
+                    Log.i(this, "onNotificationPosted: sbn=[%s]", sbn);
+                    boolean foundCallForNotification = false;
+                    // 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)) {
+                        if (isNotificationForCall(sbn, call)) {
+                            Log.i(this, "onNotificationPosted: found a pending "
+                                    + "call=[%s] for sbn.id=[%s]", call, sbn.getId());
+                            mNotificationIdToCall.put(
+                                    getNotificationIdToCallKey(sbn),
+                                    call);
+                            removeFromNotificationTracking(call);
+                            foundCallForNotification = true;
+                            break;
                         }
-                        if (!sbnMatched &&
-                                !mCachedNotifications.contains(info) /* don't re-add if update */) {
-                            Log.i(this, "onNotificationPosted: could not find a"
-                                            + "call for the call notification w/ id=[%s]",
-                                    sbn.getId());
-                            // notification may post before we started to monitor the call, cache
-                            // this notification and try to match it later with new added call.
-                            mCachedNotifications.add(info);
-                        }
+                    }
+                    // Case 2: Call-Style Notification was posted BEFORE the Call was added
+                    // --> 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"
+                                + " 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());
                     }
                 }
             }
 
             @Override
             public void onNotificationRemoved(StatusBarNotification sbn) {
-                synchronized (mLock) {
-                    NotificationInfo info = new NotificationInfo(sbn.getPackageName(),
-                            sbn.getUser());
-                    mCachedNotifications.remove(info);
-                    if (mNotificationInfoToCallMap.isEmpty()) {
-                        return;
+                if (!isCallStyleNotification(sbn)) {
+                    return;
+                }
+                Log.i(this, "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);
+                            }
+                        }, NOTIFICATION_REMOVED_BUT_CALL_IS_STILL_ONGOING_TIMEOUT);
                     }
-                    Call call = mNotificationInfoToCallMap.getOrDefault(info, null);
-                    if (call != null) {
-                        // TODO: fix potential bug for multiple calls of same voip app.
-                        mNotificationInfoToCallMap.remove(info, call);
-                        stopFGSDelegation(call);
+                    mNotificationIdToCall.remove(getNotificationIdToCallKey(sbn));
+                }
+            }
+
+            // TODO:: b/383403913 fix gap in matching notifications
+            private boolean isNotificationForCall(StatusBarNotification sbn, Call call) {
+                PhoneAccountHandle callHandle = getTargetPhoneAccount(call);
+                if (callHandle == null) {
+                    return false;
+                }
+                String callPackageName = VoipCallMonitor.this.getPackageName(call);
+                return Objects.equals(sbn.getUser(), callHandle.getUserHandle()) &&
+                        Objects.equals(sbn.getPackageName(), callPackageName);
+            }
+
+            private Call getCallFromStatusBarNotificationId(StatusBarNotification sbn) {
+                if (mNotificationIdToCall.size() == 0) {
+                    return null;
+                }
+                String targetKey = getNotificationIdToCallKey(sbn);
+                for (Map.Entry<String, Call> entry : mNotificationIdToCall.entrySet()) {
+                    if (targetKey.equals(entry.getKey())) {
+                        return entry.getValue();
                     }
                 }
+                return null;
+            }
+
+            private String getNotificationIdToCallKey(StatusBarNotification sbn) {
+                return sbn.getPackageName() + DElIMITER + sbn.getId();
+            }
+
+            private boolean isCallStyleNotification(StatusBarNotification sbn) {
+                return sbn.getNotification().isStyle(Notification.CallStyle.class);
+            }
+
+            private boolean isCallStillBeingTracked(Call call) {
+                PhoneAccountHandle handle = getTargetPhoneAccount(call);
+                if (call == null || handle == null) {
+                    return false;
+                }
+                return mAccountHandleToCallMap
+                        .computeIfAbsent(handle, k -> new HashSet<>())
+                        .contains(call);
             }
         };
 
     }
 
-    public void startMonitor() {
+    public void registerNotificationListener() {
         try {
             mNotificationListener.registerAsSystemService(mContext,
                     new ComponentName(this.getClass().getPackageName(),
@@ -151,7 +189,7 @@
         }
     }
 
-    public void stopMonitor() {
+    public void unregisterNotificationListener() {
         try {
             mNotificationListener.unregisterAsSystemService();
         } catch (RemoteException e) {
@@ -161,71 +199,70 @@
 
     @Override
     public void onCallAdded(Call call) {
-        if (!call.isTransactionalCall()) {
+        PhoneAccountHandle handle = getTargetPhoneAccount(call);
+        if (!isTransactional(call) || handle == null) {
             return;
         }
-
-        synchronized (mLock) {
-            PhoneAccountHandle phoneAccountHandle = call.getTargetPhoneAccount();
-            Set<Call> callList = mAccountHandleToCallMap.computeIfAbsent(phoneAccountHandle,
-                    k -> new HashSet<>());
-            callList.add(call);
-            CompletableFuture.completedFuture(null).thenComposeAsync(
-                    (x) -> {
-                        startFGSDelegation(call.getCallingPackageIdentity().mCallingPackagePid,
-                                call.getCallingPackageIdentity().mCallingPackageUid, call);
-                        return null;
-                    }, new LoggedHandlerExecutor(mHandler, "VCM.oCA", mSyncRoot));
-        }
+        int callingPid = getCallingPackagePid(call);
+        int callingUid = getCallingPackageUid(call);
+        mAccountHandleToCallMap
+                .computeIfAbsent(handle, k -> new HashSet<>())
+                .add(call);
+        maybeStartFGSDelegation(callingPid, callingUid, handle, call);
     }
 
     @Override
     public void onCallRemoved(Call call) {
-        if (!call.isTransactionalCall()) {
+        PhoneAccountHandle handle = getTargetPhoneAccount(call);
+        if (!isTransactional(call) || handle == null) {
             return;
         }
-
-        synchronized (mLock) {
-            stopMonitorWorks(call);
-            PhoneAccountHandle phoneAccountHandle = call.getTargetPhoneAccount();
-            Set<Call> callList = mAccountHandleToCallMap.computeIfAbsent(phoneAccountHandle,
-                    k -> new HashSet<>());
-            callList.remove(call);
-
-            if (callList.isEmpty()) {
-                stopFGSDelegation(call);
-            }
+        removeFromNotificationTracking(call);
+        Set<Call> ongoingCalls = mAccountHandleToCallMap
+                .computeIfAbsent(handle, k -> new HashSet<>());
+        ongoingCalls.remove(call);
+        Log.d(this, "onCallRemoved: callList.size=[%d]", ongoingCalls.size());
+        if (ongoingCalls.isEmpty()) {
+            stopFGSDelegation(call, handle);
+        } else {
+            Log.addEvent(call, LogUtils.Events.MAINTAINING_FGS_DELEGATION);
         }
     }
 
-    private void startFGSDelegation(int pid, int uid, Call call) {
-        Log.i(this, "startFGSDelegation for call %s", call.getId());
+    private void maybeStartFGSDelegation(int pid, int uid, PhoneAccountHandle handle, Call call) {
+        Log.i(this, "maybeStartFGSDelegation for call=[%s]", call);
         if (mActivityManagerInternal != null) {
-            PhoneAccountHandle handle = call.getTargetPhoneAccount();
+            if (mServices.containsKey(handle)) {
+                Log.addEvent(call, LogUtils.Events.ALREADY_HAS_FGS_DELEGATION);
+                startMonitoringNotification(call, handle);
+                return;
+            }
             ForegroundServiceDelegationOptions options = new ForegroundServiceDelegationOptions(pid,
                     uid, handle.getComponentName().getPackageName(), null /* clientAppThread */,
                     false /* isSticky */, String.valueOf(handle.hashCode()),
                     FOREGROUND_SERVICE_TYPE_PHONE_CALL |
-                    FOREGROUND_SERVICE_TYPE_MICROPHONE |
-                    FOREGROUND_SERVICE_TYPE_CAMERA |
-                    FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE /* foregroundServiceTypes */,
+                            FOREGROUND_SERVICE_TYPE_MICROPHONE |
+                            FOREGROUND_SERVICE_TYPE_CAMERA |
+                            FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE /* foregroundServiceTypes */,
                     DELEGATION_SERVICE_PHONE_CALL /* delegationService */);
             ServiceConnection fgsConnection = new ServiceConnection() {
                 @Override
                 public void onServiceConnected(ComponentName name, IBinder service) {
+                    Log.addEvent(call, LogUtils.Events.GAINED_FGS_DELEGATION);
                     mServices.put(handle, this);
-                    startMonitorWorks(call);
+                    startMonitoringNotification(call, handle);
                 }
 
                 @Override
                 public void onServiceDisconnected(ComponentName name) {
+                    Log.addEvent(call, LogUtils.Events.LOST_FGS_DELEGATION);
                     mServices.remove(handle);
                 }
             };
             try {
                 if (mActivityManagerInternal
                         .startForegroundServiceDelegate(options, fgsConnection)) {
-                    Log.addEvent(call, LogUtils.Events.GAINED_FGS_DELEGATION);
+                    Log.i(this, "maybeStartFGSDelegation: startForegroundServiceDelegate success");
                 } else {
                     Log.addEvent(call, LogUtils.Events.GAIN_FGS_DELEGATION_FAILED);
                 }
@@ -236,77 +273,136 @@
     }
 
     @VisibleForTesting
-    public void stopFGSDelegation(Call call) {
-        synchronized (mLock) {
-            Log.i(this, "stopFGSDelegation of call %s", call);
-            PhoneAccountHandle handle = call.getTargetPhoneAccount();
-            Set<Call> calls = mAccountHandleToCallMap.get(handle);
-
-            // Every call for the package that is losing foreground service delegation should be
-            // removed from tracking maps/contains in this class
-            if (calls != null) {
-                for (Call c : calls) {
-                    stopMonitorWorks(c); // remove the call from tacking in this class
-                }
+    public void stopFGSDelegation(Call call, PhoneAccountHandle handle) {
+        Log.i(this, "stopFGSDelegation of call=[%s]", call);
+        if (handle == null) {
+            return;
+        }
+        // In the event this class is waiting for any new calls to post a notification, cleanup
+        for (Call ongoingCall :  new ArrayList<>(mAccountHandleToCallMap.get(handle))) {
+            removeFromNotificationTracking(ongoingCall);
+        }
+        if (mActivityManagerInternal != null) {
+            ServiceConnection fgsConnection = mServices.get(handle);
+            if (fgsConnection != null) {
+                Log.i(this, "stopFGSDelegation: requesting stopForegroundServiceDelegate");
+                mActivityManagerInternal.stopForegroundServiceDelegate(fgsConnection);
             }
+        }
+        mAccountHandleToCallMap.remove(handle);
+    }
 
-            mAccountHandleToCallMap.remove(handle);
+    private void startMonitoringNotification(Call call, PhoneAccountHandle handle) {
+        String packageName = getPackageName(call);
+        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);
+        // If no notification is posted, stop foreground service delegation!
+        mHandlerForClass.postDelayed(() -> {
+            if (isStillMissingNotification(call)) {
+                Log.i(this, "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"
+                        + " notification for call.id[%s] at timeout", callId);
+            }
+        }, NOTIFICATION_NOT_POSTED_IN_TIME_TIMEOUT);
+    }
 
-            if (mActivityManagerInternal != null) {
-                ServiceConnection fgsConnection = mServices.get(handle);
-                if (fgsConnection != null) {
-                    mActivityManagerInternal.stopForegroundServiceDelegate(fgsConnection);
-                    Log.addEvent(call, LogUtils.Events.LOST_FGS_DELEGATION);
-                }
+    /**
+     * 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) {
+                return null;
+            } else {
+                return call.getTargetPhoneAccount();
             }
         }
     }
 
-    private void startMonitorWorks(Call call) {
-        startMonitorNotification(call);
-    }
-
-    private void stopMonitorWorks(Call call) {
-        stopMonitorNotification(call);
-    }
-
-    private void startMonitorNotification(Call call) {
-        synchronized (mLock) {
-            boolean sbnMatched = false;
-            for (NotificationInfo info : mCachedNotifications) {
-                if (info.matchesCall(call)) {
-                    Log.i(this, "startMonitorNotification: found a cached call "
-                            + "notification for call=[%s]", call);
-                    mCachedNotifications.remove(info);
-                    mNotificationInfoToCallMap.put(info, call);
-                    sbnMatched = true;
-                    break;
-                }
-            }
-            if (!sbnMatched) {
-                // Only continue to
-                Log.i(this, "startMonitorNotification: could not find a call"
-                        + " notification for the call=[%s];", call);
-                mNotificationPendingCalls.add(call);
-                CompletableFuture<Void> future = new CompletableFuture<>();
-                mHandler.postDelayed(() -> future.complete(null), 5000L);
-                future.thenComposeAsync(
-                        (x) -> {
-                            if (mNotificationPendingCalls.contains(call)) {
-                                Log.i(this, "Notification for voip-call %s haven't "
-                                        + "posted in time, stop delegation.", call.getId());
-                                stopFGSDelegation(call);
-                                mNotificationPendingCalls.remove(call);
-                                return null;
-                            }
-                            return null;
-                        }, new LoggedHandlerExecutor(mHandler, "VCM.sMN", mSyncRoot));
+    private int getCallingPackageUid(Call call) {
+        synchronized (mSyncRoot) {
+            if (call == null) {
+                return -1;
+            } else {
+                return call.getCallingPackageIdentity().mCallingPackageUid;
             }
         }
     }
 
-    private void stopMonitorNotification(Call call) {
-        mNotificationPendingCalls.remove(call);
+    private int getCallingPackagePid(Call call) {
+        synchronized (mSyncRoot) {
+            if (call == null) {
+                return -1;
+            } else {
+                return call.getCallingPackageIdentity().mCallingPackagePid;
+            }
+        }
+    }
+
+    private String getCallId(Call call) {
+        synchronized (mSyncRoot) {
+            if (call == null) {
+                return "";
+            } else {
+                return call.getId();
+            }
+        }
+    }
+
+    private boolean isCallDisconnected(Call call) {
+        synchronized (mSyncRoot) {
+            if (call == null) {
+                return true;
+            } else {
+                return call.isDisconnected();
+            }
+        }
+    }
+
+    private boolean isTransactional(Call call) {
+        synchronized (mSyncRoot) {
+            if (call == null) {
+                return false;
+            } else {
+                return call.isTransactionalCall();
+            }
+        }
+    }
+
+    private String getPackageName(Call call) {
+        String pn = "";
+        try {
+            pn = getTargetPhoneAccount(call).getComponentName().getPackageName();
+        } catch (Exception e) {
+            // fall through
+        }
+        return pn;
     }
 
     @VisibleForTesting
@@ -314,49 +410,6 @@
         mActivityManagerInternal = ami;
     }
 
-    private static class NotificationInfo extends Object {
-        private String mPackageName;
-        private UserHandle mUserHandle;
-
-        NotificationInfo(String packageName, UserHandle userHandle) {
-            mPackageName = packageName;
-            mUserHandle = userHandle;
-        }
-
-        boolean matchesCall(Call call) {
-            PhoneAccountHandle accountHandle = call.getTargetPhoneAccount();
-            return mPackageName != null && mPackageName.equals(
-                    accountHandle.getComponentName().getPackageName())
-                    && mUserHandle != null && mUserHandle.equals(accountHandle.getUserHandle());
-        }
-
-        @Override
-        public boolean equals(Object obj) {
-            if (!(obj instanceof NotificationInfo)) {
-                return false;
-            }
-            NotificationInfo that = (NotificationInfo) obj;
-            return Objects.equals(this.mPackageName, that.mPackageName)
-                    && Objects.equals(this.mUserHandle, that.mUserHandle);
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(mPackageName, mUserHandle);
-        }
-
-        @Override
-        public String toString() {
-            StringBuilder sb = new StringBuilder();
-            sb.append("{ NotificationInfo: [mPackageName: ")
-                    .append(mPackageName)
-                    .append("], [mUserHandle=")
-                    .append(mUserHandle)
-                    .append("]  }");
-            return sb.toString();
-        }
-    }
-
     @VisibleForTesting
     public void postNotification(StatusBarNotification statusBarNotification) {
         mNotificationListener.onNotificationPosted(statusBarNotification);
@@ -367,8 +420,9 @@
         mNotificationListener.onNotificationRemoved(statusBarNotification);
     }
 
-    @VisibleForTesting
-    public Set<Call> getCallsForHandle(PhoneAccountHandle handle){
-        return mAccountHandleToCallMap.get(handle);
+    public boolean hasForegroundServiceDelegation(PhoneAccountHandle handle) {
+        boolean hasFgs = mServices.containsKey(handle);
+        Log.i(this, "hasForegroundServiceDelegation: handle=[%s], hasFgs=[%b]", handle, hasFgs);
+        return hasFgs;
     }
 }
diff --git a/src/com/android/server/telecom/callsequencing/voip/VoipCallMonitorLegacy.java b/src/com/android/server/telecom/callsequencing/voip/VoipCallMonitorLegacy.java
new file mode 100644
index 0000000..78f5d52
--- /dev/null
+++ b/src/com/android/server/telecom/callsequencing/voip/VoipCallMonitorLegacy.java
@@ -0,0 +1,373 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.callsequencing.voip;
+
+import static android.app.ForegroundServiceDelegationOptions.DELEGATION_SERVICE_PHONE_CALL;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL;
+
+import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
+import android.app.ForegroundServiceDelegationOptions;
+import android.app.Notification;
+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.RemoteException;
+import android.os.UserHandle;
+import android.service.notification.NotificationListenerService;
+import android.service.notification.StatusBarNotification;
+import android.telecom.Log;
+import android.telecom.PhoneAccountHandle;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.LocalServices;
+import com.android.server.telecom.Call;
+
+import com.android.server.telecom.CallsManagerListenerBase;
+import com.android.server.telecom.LogUtils;
+import com.android.server.telecom.LoggedHandlerExecutor;
+import com.android.server.telecom.TelecomSystem;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+
+public class VoipCallMonitorLegacy extends CallsManagerListenerBase {
+
+    private final List<Call> mNotificationPendingCalls;
+    // Same notification may be passed as different object in onNotificationPosted and
+    // onNotificationRemoved. Use its string as key to cache ongoing notifications.
+    private final Map<NotificationInfo, Call> mNotificationInfoToCallMap;
+    private final Map<PhoneAccountHandle, Set<Call>> mAccountHandleToCallMap;
+    private ActivityManagerInternal mActivityManagerInternal;
+    private final Map<PhoneAccountHandle, ServiceConnection> mServices;
+    private NotificationListenerService mNotificationListener;
+    private final Object mLock = new Object();
+    private final HandlerThread mHandlerThread;
+    private final Handler mHandler;
+    private final Context mContext;
+    private List<NotificationInfo> mCachedNotifications;
+    private TelecomSystem.SyncRoot mSyncRoot;
+
+    public VoipCallMonitorLegacy(Context context, TelecomSystem.SyncRoot lock) {
+        mSyncRoot = lock;
+        mContext = context;
+        mHandlerThread = new HandlerThread(this.getClass().getSimpleName());
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+        mNotificationPendingCalls = new ArrayList<>();
+        mCachedNotifications = new ArrayList<>();
+        mNotificationInfoToCallMap = new HashMap<>();
+        mServices = new HashMap<>();
+        mAccountHandleToCallMap = new HashMap<>();
+        mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
+
+        mNotificationListener = new NotificationListenerService() {
+            @Override
+            public void onNotificationPosted(StatusBarNotification sbn) {
+                synchronized (mLock) {
+                    if (sbn.getNotification().isStyle(Notification.CallStyle.class)) {
+                        NotificationInfo info = new NotificationInfo(sbn.getPackageName(),
+                                sbn.getUser());
+                        boolean sbnMatched = false;
+                        for (Call call : mNotificationPendingCalls) {
+                            if (info.matchesCall(call)) {
+                                Log.i(this, "onNotificationPosted: found a pending "
+                                                + "callId=[%s] for the call notification w/ "
+                                                + "id=[%s]",
+                                        call.getId(), sbn.getId());
+                                mNotificationPendingCalls.remove(call);
+                                mNotificationInfoToCallMap.put(info, call);
+                                sbnMatched = true;
+                                break;
+                            }
+                        }
+                        if (!sbnMatched &&
+                                !mCachedNotifications.contains(info) /* don't re-add if update */) {
+                            Log.i(this, "onNotificationPosted: could not find a"
+                                            + "call for the call notification w/ id=[%s]",
+                                    sbn.getId());
+                            // notification may post before we started to monitor the call, cache
+                            // this notification and try to match it later with new added call.
+                            mCachedNotifications.add(info);
+                        }
+                    }
+                }
+            }
+
+            @Override
+            public void onNotificationRemoved(StatusBarNotification sbn) {
+                synchronized (mLock) {
+                    NotificationInfo info = new NotificationInfo(sbn.getPackageName(),
+                            sbn.getUser());
+                    mCachedNotifications.remove(info);
+                    if (mNotificationInfoToCallMap.isEmpty()) {
+                        return;
+                    }
+                    Call call = mNotificationInfoToCallMap.getOrDefault(info, null);
+                    if (call != null) {
+                        mNotificationInfoToCallMap.remove(info, call);
+                        CompletableFuture<Void> future = new CompletableFuture<>();
+                        mHandler.postDelayed(() -> future.complete(null), 5000L);
+                        stopFGSDelegation(call);
+                    }
+                }
+            }
+        };
+
+    }
+
+    public void startMonitor() {
+        try {
+            mNotificationListener.registerAsSystemService(mContext,
+                    new ComponentName(this.getClass().getPackageName(),
+                            this.getClass().getCanonicalName()), ActivityManager.getCurrentUser());
+        } catch (RemoteException e) {
+            Log.e(this, e, "Cannot register notification listener");
+        }
+    }
+
+    public void stopMonitor() {
+        try {
+            mNotificationListener.unregisterAsSystemService();
+        } catch (RemoteException e) {
+            Log.e(this, e, "Cannot unregister notification listener");
+        }
+    }
+
+    @Override
+    public void onCallAdded(Call call) {
+        if (!call.isTransactionalCall()) {
+            return;
+        }
+
+        synchronized (mLock) {
+            PhoneAccountHandle phoneAccountHandle = call.getTargetPhoneAccount();
+            Set<Call> callList = mAccountHandleToCallMap.computeIfAbsent(phoneAccountHandle,
+                    k -> new HashSet<>());
+            callList.add(call);
+            CompletableFuture.completedFuture(null).thenComposeAsync(
+                    (x) -> {
+                        startFGSDelegation(call.getCallingPackageIdentity().mCallingPackagePid,
+                                call.getCallingPackageIdentity().mCallingPackageUid, call);
+                        return null;
+                    }, new LoggedHandlerExecutor(mHandler, "VCM.oCA", mSyncRoot));
+        }
+    }
+
+    @Override
+    public void onCallRemoved(Call call) {
+        if (!call.isTransactionalCall()) {
+            return;
+        }
+        synchronized (mLock) {
+            stopMonitorWorks(call);
+            PhoneAccountHandle phoneAccountHandle = call.getTargetPhoneAccount();
+            Set<Call> callList = mAccountHandleToCallMap.computeIfAbsent(phoneAccountHandle,
+                    k -> new HashSet<>());
+            callList.remove(call);
+            if (callList.isEmpty()) {
+                stopFGSDelegation(call);
+            }
+        }
+    }
+
+    private void startFGSDelegation(int pid, int uid, Call call) {
+        Log.i(this, "startFGSDelegation for call %s", call.getId());
+        if (mActivityManagerInternal != null) {
+            PhoneAccountHandle handle = call.getTargetPhoneAccount();
+            ForegroundServiceDelegationOptions options = new ForegroundServiceDelegationOptions(pid,
+                    uid, handle.getComponentName().getPackageName(), null /* clientAppThread */,
+                    false /* isSticky */, String.valueOf(handle.hashCode()),
+                    FOREGROUND_SERVICE_TYPE_PHONE_CALL |
+                    FOREGROUND_SERVICE_TYPE_MICROPHONE |
+                    FOREGROUND_SERVICE_TYPE_CAMERA |
+                    FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE /* foregroundServiceTypes */,
+                    DELEGATION_SERVICE_PHONE_CALL /* delegationService */);
+            ServiceConnection fgsConnection = new ServiceConnection() {
+                @Override
+                public void onServiceConnected(ComponentName name, IBinder service) {
+                    mServices.put(handle, this);
+                    startMonitorWorks(call);
+                }
+
+                @Override
+                public void onServiceDisconnected(ComponentName name) {
+                    mServices.remove(handle);
+                }
+            };
+            try {
+                if (mActivityManagerInternal
+                        .startForegroundServiceDelegate(options, fgsConnection)) {
+                    Log.addEvent(call, LogUtils.Events.GAINED_FGS_DELEGATION);
+                } else {
+                    Log.addEvent(call, LogUtils.Events.GAIN_FGS_DELEGATION_FAILED);
+                }
+            } catch (Exception e) {
+                Log.i(this, "startForegroundServiceDelegate failed due to: " + e);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    public void stopFGSDelegation(Call call) {
+        synchronized (mLock) {
+            Log.i(this, "stopFGSDelegation of call %s", call);
+            PhoneAccountHandle handle = call.getTargetPhoneAccount();
+            Set<Call> calls = mAccountHandleToCallMap.get(handle);
+
+            // Every call for the package that is losing foreground service delegation should be
+            // removed from tracking maps/contains in this class
+            if (calls != null) {
+                for (Call c : calls) {
+                    stopMonitorWorks(c); // remove the call from tacking in this class
+                }
+            }
+
+            mAccountHandleToCallMap.remove(handle);
+
+            if (mActivityManagerInternal != null) {
+                ServiceConnection fgsConnection = mServices.get(handle);
+                if (fgsConnection != null) {
+                    mActivityManagerInternal.stopForegroundServiceDelegate(fgsConnection);
+                    Log.addEvent(call, LogUtils.Events.LOST_FGS_DELEGATION);
+                }
+            }
+        }
+    }
+
+    private void startMonitorWorks(Call call) {
+        startMonitorNotification(call);
+    }
+
+    private void stopMonitorWorks(Call call) {
+        stopMonitorNotification(call);
+    }
+
+    private void startMonitorNotification(Call call) {
+        synchronized (mLock) {
+            boolean sbnMatched = false;
+            for (NotificationInfo info : mCachedNotifications) {
+                if (info.matchesCall(call)) {
+                    Log.i(this, "startMonitorNotification: found a cached call "
+                            + "notification for call=[%s]", call);
+                    mCachedNotifications.remove(info);
+                    mNotificationInfoToCallMap.put(info, call);
+                    sbnMatched = true;
+                    break;
+                }
+            }
+            if (!sbnMatched) {
+                // Only continue to
+                Log.i(this, "startMonitorNotification: could not find a call"
+                        + " notification for the call=[%s];", call);
+                mNotificationPendingCalls.add(call);
+                CompletableFuture<Void> future = new CompletableFuture<>();
+                mHandler.postDelayed(() -> future.complete(null), 5000L);
+                future.thenComposeAsync(
+                        (x) -> {
+                            if (mNotificationPendingCalls.contains(call)) {
+                                Log.i(this, "Notification for voip-call %s haven't "
+                                        + "posted in time, stop delegation.", call.getId());
+                                stopFGSDelegation(call);
+                                mNotificationPendingCalls.remove(call);
+                                return null;
+                            }
+                            return null;
+                        }, new LoggedHandlerExecutor(mHandler, "VCM.sMN", mSyncRoot));
+            }
+        }
+    }
+
+    private void stopMonitorNotification(Call call) {
+        mNotificationPendingCalls.remove(call);
+    }
+
+    @VisibleForTesting
+    public void setActivityManagerInternal(ActivityManagerInternal ami) {
+        mActivityManagerInternal = ami;
+    }
+
+    private static class NotificationInfo extends Object {
+        private String mPackageName;
+        private UserHandle mUserHandle;
+
+        NotificationInfo(String packageName, UserHandle userHandle) {
+            mPackageName = packageName;
+            mUserHandle = userHandle;
+        }
+
+        boolean matchesCall(Call call) {
+            PhoneAccountHandle accountHandle = call.getTargetPhoneAccount();
+            return mPackageName != null && mPackageName.equals(
+                    accountHandle.getComponentName().getPackageName())
+                    && mUserHandle != null && mUserHandle.equals(accountHandle.getUserHandle());
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (!(obj instanceof NotificationInfo)) {
+                return false;
+            }
+            NotificationInfo that = (NotificationInfo) obj;
+            return Objects.equals(this.mPackageName, that.mPackageName)
+                    && Objects.equals(this.mUserHandle, that.mUserHandle);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mPackageName, mUserHandle);
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append("{ NotificationInfo: [mPackageName: ")
+                    .append(mPackageName)
+                    .append("], [mUserHandle=")
+                    .append(mUserHandle)
+                    .append("]  }");
+            return sb.toString();
+        }
+    }
+
+    @VisibleForTesting
+    public void postNotification(StatusBarNotification statusBarNotification) {
+        mNotificationListener.onNotificationPosted(statusBarNotification);
+    }
+
+    @VisibleForTesting
+    public void removeNotification(StatusBarNotification statusBarNotification) {
+        mNotificationListener.onNotificationRemoved(statusBarNotification);
+    }
+
+    @VisibleForTesting
+    public Set<Call> getCallsForHandle(PhoneAccountHandle handle){
+        return mAccountHandleToCallMap.get(handle);
+    }
+}
diff --git a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
index 1e65011..447e5ab 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
@@ -99,6 +99,7 @@
 import com.android.server.telecom.WiredHeadsetManager;
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 import com.android.server.telecom.callfiltering.BlockedNumbersAdapter;
+import com.android.server.telecom.callsequencing.voip.VoipCallMonitor;
 import com.android.server.telecom.components.UserCallIntentProcessor;
 import com.android.server.telecom.flags.FeatureFlags;
 import com.android.server.telecom.ui.IncomingCallNotifier;
@@ -418,7 +419,11 @@
                 handlerThread.quitSafely();
             }
             handlerThreads.clear();
-            mTelecomSystem.getCallsManager().getVoipCallMonitor().stopMonitor();
+
+            VoipCallMonitor vcm = mTelecomSystem.getCallsManager().getVoipCallMonitor();
+            if (vcm != null) {
+                vcm.unregisterNotificationListener();
+            }
         }
         waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
         waitForHandlerAction(mHandlerThread.getThreadHandler(), TEST_TIMEOUT);
diff --git a/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java b/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
index bf68f8c..e6d1bc9 100644
--- a/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
+++ b/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
@@ -25,7 +25,6 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -51,6 +50,7 @@
 import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.callsequencing.voip.VoipCallMonitor;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -68,7 +68,7 @@
     private static final String ID_1 = "id1";
     public static final String CHANNEL_ID = "TelecomVoipAppChannelId";
     private static final UserHandle USER_HANDLE_1 = new UserHandle(1);
-    private static final long TIMEOUT = 5000L;
+    private static final long TIMEOUT = 6000L;
 
     @Mock private TelecomSystem.SyncRoot mLock;
     @Mock private ActivityManagerInternal mActivityManagerInternal;
@@ -86,12 +86,19 @@
         mMonitor = new VoipCallMonitor(mContext, mLock);
         mActivityManagerInternal = mock(ActivityManagerInternal.class);
         mMonitor.setActivityManagerInternal(mActivityManagerInternal);
-        mMonitor.startMonitor();
+        mMonitor.registerNotificationListener();
         when(mActivityManagerInternal.startForegroundServiceDelegate(any(
                 ForegroundServiceDelegationOptions.class), any(ServiceConnection.class)))
                 .thenReturn(true);
     }
 
+    @Override
+    @After
+    public void tearDown() throws Exception {
+        mMonitor.unregisterNotificationListener();
+        super.tearDown();
+    }
+
     /**
      * This test ensures VoipCallMonitor is passing the correct foregroundServiceTypes when starting
      * foreground service delegation on behalf of a client.
@@ -106,12 +113,12 @@
         mMonitor.onCallAdded(call);
 
         verify(mActivityManagerInternal, timeout(TIMEOUT)).startForegroundServiceDelegate(
-                 optionsCaptor.capture(), any(ServiceConnection.class));
+                optionsCaptor.capture(), any(ServiceConnection.class));
 
-        assertEquals( FOREGROUND_SERVICE_TYPE_PHONE_CALL |
-                FOREGROUND_SERVICE_TYPE_MICROPHONE |
-                FOREGROUND_SERVICE_TYPE_CAMERA |
-                FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE,
+        assertEquals(FOREGROUND_SERVICE_TYPE_PHONE_CALL |
+                        FOREGROUND_SERVICE_TYPE_MICROPHONE |
+                        FOREGROUND_SERVICE_TYPE_CAMERA |
+                        FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE,
                 optionsCaptor.getValue().mForegroundServiceTypes);
 
         mMonitor.onCallRemoved(call);
@@ -120,51 +127,49 @@
     @SmallTest
     @Test
     public void testStartMonitorForOneCall() {
+        // GIVEN - a single call and notification for a voip app
         Call call = createTestCall("testCall", mHandle1User1);
-        IBinder service = mock(IBinder.class);
+        StatusBarNotification sbn = createStatusBarNotificationFromHandle(mHandle1User1);
 
-        ArgumentCaptor<ServiceConnection> captor = ArgumentCaptor.forClass(ServiceConnection.class);
-        mMonitor.onCallAdded(call);
-        verify(mActivityManagerInternal, timeout(TIMEOUT)).startForegroundServiceDelegate(any(
-                ForegroundServiceDelegationOptions.class), captor.capture());
-        ServiceConnection conn = captor.getValue();
-        conn.onServiceConnected(mHandle1User1.getComponentName(), service);
+        // WHEN - the Voip call is added and a notification is posted, verify FGS is gained
+        addCallAndVerifyFgsIsGained(call);
+        mMonitor.postNotification(sbn);
 
+        // THEN - when the Voip call is removed, verify that FGS is revoked for the app
         mMonitor.onCallRemoved(call);
-        verify(mActivityManagerInternal, timeout(TIMEOUT)).stopForegroundServiceDelegate(eq(conn));
+        mMonitor.removeNotification(sbn);
+        verify(mActivityManagerInternal, timeout(TIMEOUT))
+                .stopForegroundServiceDelegate(any(ServiceConnection.class));
     }
 
+    /**
+     * Verify FGS is not lost if another call is ongoing for a Voip app
+     */
     @SmallTest
     @Test
-    public void testMonitorForTwoCallsOnSameHandle() {
+    public void testStopDelegation_SameApp() {
+        // GIVEN - 2 consecutive calls for a single Voip app
         Call call1 = createTestCall("testCall1", mHandle1User1);
+        StatusBarNotification sbn1 = createStatusBarNotificationFromHandle(mHandle1User1);
         Call call2 = createTestCall("testCall2", mHandle1User1);
-        IBinder service = mock(IBinder.class);
+        StatusBarNotification sbn2 = createStatusBarNotificationFromHandle(mHandle1User1);
 
-        ArgumentCaptor<ServiceConnection> captor1 =
-                ArgumentCaptor.forClass(ServiceConnection.class);
-        mMonitor.onCallAdded(call1);
-        verify(mActivityManagerInternal, timeout(TIMEOUT).times(1))
-                .startForegroundServiceDelegate(any(ForegroundServiceDelegationOptions.class),
-                        captor1.capture());
-        ServiceConnection conn1 = captor1.getValue();
-        conn1.onServiceConnected(mHandle1User1.getComponentName(), service);
-
-        ArgumentCaptor<ServiceConnection> captor2 =
-                ArgumentCaptor.forClass(ServiceConnection.class);
+        // WHEN - the second call is added and the first is disconnected
+        mMonitor.postNotification(sbn1);
+        addCallAndVerifyFgsIsGained(call1);
+        mMonitor.postNotification(sbn2);
         mMonitor.onCallAdded(call2);
-        verify(mActivityManagerInternal, timeout(TIMEOUT).times(2))
-                .startForegroundServiceDelegate(any(ForegroundServiceDelegationOptions.class),
-                        captor2.capture());
-        ServiceConnection conn2 = captor2.getValue();
-        conn2.onServiceConnected(mHandle1User1.getComponentName(), service);
-
         mMonitor.onCallRemoved(call1);
-        verify(mActivityManagerInternal, never()).stopForegroundServiceDelegate(
-                any(ServiceConnection.class));
+
+        // 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.removeNotification(sbn1);
+        // once all calls are removed, verify FGS is stopped
         mMonitor.onCallRemoved(call2);
+        mMonitor.removeNotification(sbn2);
         verify(mActivityManagerInternal, timeout(TIMEOUT).times(1))
-                .stopForegroundServiceDelegate(eq(conn2));
+                .stopForegroundServiceDelegate(any(ServiceConnection.class));
     }
 
     @SmallTest
@@ -204,40 +209,6 @@
         verify(mActivityManagerInternal).stopForegroundServiceDelegate(eq(conn1));
     }
 
-    @SmallTest
-    @Test
-    public void testStopDelegation() {
-        Call call1 = createTestCall("testCall1", mHandle1User1);
-        Call call2 = createTestCall("testCall2", mHandle1User1);
-        IBinder service = mock(IBinder.class);
-
-        ArgumentCaptor<ServiceConnection> captor1 =
-                ArgumentCaptor.forClass(ServiceConnection.class);
-        mMonitor.onCallAdded(call1);
-        verify(mActivityManagerInternal, timeout(TIMEOUT).times(1))
-                .startForegroundServiceDelegate(any(ForegroundServiceDelegationOptions.class),
-                        captor1.capture());
-        ServiceConnection conn1 = captor1.getValue();
-        conn1.onServiceConnected(mHandle1User1.getComponentName(), service);
-
-        ArgumentCaptor<ServiceConnection> captor2 =
-                ArgumentCaptor.forClass(ServiceConnection.class);
-        mMonitor.onCallAdded(call2);
-        verify(mActivityManagerInternal, timeout(TIMEOUT).times(2))
-                .startForegroundServiceDelegate(any(ForegroundServiceDelegationOptions.class),
-                        captor2.capture());
-        ServiceConnection conn2 = captor2.getValue();
-        conn2.onServiceConnected(mHandle1User1.getComponentName(), service);
-
-        mMonitor.stopFGSDelegation(call1);
-        verify(mActivityManagerInternal, timeout(TIMEOUT).times(1))
-                .stopForegroundServiceDelegate(eq(conn2));
-        conn2.onServiceDisconnected(mHandle1User1.getComponentName());
-        mMonitor.onCallRemoved(call1);
-        verify(mActivityManagerInternal, timeout(TIMEOUT).times(1))
-                .stopForegroundServiceDelegate(any(ServiceConnection.class));
-    }
-
     /**
      * Ensure an app loses foreground service delegation if the user dismisses the call style
      * notification or the app removes the notification.
@@ -263,28 +234,8 @@
     }
 
     /**
-     * Ensure an app loses foreground service delegation if the user dismisses the call style
-     * notification or the app removes the notification.
-     * Note: post the notification BEFORE foreground service delegation is gained
+     * Helpers for testing
      */
-    @SmallTest
-    @Test
-    public void testStopFgsIfCallNotificationIsRemoved_PostedBeforeFgsIsGained() {
-        // GIVEN
-        StatusBarNotification sbn = createStatusBarNotificationFromHandle(mHandle1User1);
-
-        // WHEN
-        //  an app posts a call style notification before FGS is gained
-        mMonitor.postNotification(sbn);
-        // FGS is gained after the call is added to VoipCallMonitor
-        ServiceConnection c = addCallAndVerifyFgsIsGained(createTestCall("1", mHandle1User1));
-
-        // THEN
-        // 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);
-    }
 
     private Call createTestCall(String id, PhoneAccountHandle handle) {
         Call call = mock(Call.class);
@@ -329,7 +280,9 @@
         // onServiceConnected must be called in order for VoipCallMonitor to start monitoring for
         // a notification before the timeout expires
         ServiceConnection serviceConnection = captor.getValue();
-        serviceConnection.onServiceConnected(mHandle1User1.getComponentName(), mServiceConnection);
+        serviceConnection.onServiceConnected(
+                call.getTargetPhoneAccount().getComponentName(),
+                mServiceConnection);
         return serviceConnection;
     }
 }