Add monitor related code to make sure that voip app invoke platform
APIs in expected way.

Bug: 261877110
Test: atest VoipCallMonitorTest.java
Change-Id: Idfdf6f29b88b6491a140b25017244a7ced18a8a9
diff --git a/Android.bp b/Android.bp
index 1b422aa..c5141ca 100644
--- a/Android.bp
+++ b/Android.bp
@@ -28,6 +28,9 @@
     static_libs: [
         "androidx.annotation_annotation",
     ],
+    libs: [
+        "services",
+    ],
     resource_dirs: ["res"],
     proto: {
         type: "nano",
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 648b4a9..d42dcff 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -64,6 +64,7 @@
     <uses-permission android:name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME"/>
     <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
     <uses-permission android:name="com.android.phone.permission.ACCESS_LAST_KNOWN_CELL_ID"/>
+    <uses-permission android:name="android.permission.STATUS_BAR_SERVICE" />
 
     <permission android:name="android.permission.BROADCAST_CALLLOG_INFO"
          android:label="Broadcast the call type/duration information"
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 3d62076..9ba08ea 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -555,6 +555,7 @@
     private boolean mIsSelfManaged = false;
 
     private boolean mIsTransactionalCall = false;
+    private int mOwnerPid = -1;
 
     /**
      * Indicates whether this call is streaming.
@@ -1830,6 +1831,14 @@
         setConnectionProperties(getConnectionProperties());
     }
 
+    public void setOwnerPid(int pid) {
+        mOwnerPid = pid;
+    }
+
+    public int getOwnerPid() {
+        return mOwnerPid;
+    }
+
     public void setTransactionServiceWrapper(TransactionalServiceWrapper service) {
         mTransactionalService = service;
     }
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index f680084..4f1766d 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -135,6 +135,7 @@
 import com.android.server.telecom.ui.DisconnectedCallNotifier;
 import com.android.server.telecom.ui.IncomingCallNotifier;
 import com.android.server.telecom.ui.ToastFactory;
+import com.android.server.telecom.voip.VoipCallMonitor;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -427,6 +428,7 @@
     private final Handler mHandler = new Handler(Looper.getMainLooper());
     private final EmergencyCallHelper mEmergencyCallHelper;
     private final RoleManagerAdapter mRoleManagerAdapter;
+    private final VoipCallMonitor mVoipCallMonitor;
     private final CallEndpointController mCallEndpointController;
     private final CallAnomalyWatchdog mCallAnomalyWatchdog;
     private final CallStreamingController mCallStreamingController;
@@ -639,6 +641,7 @@
         mClockProxy = clockProxy;
         mToastFactory = toastFactory;
         mRoleManagerAdapter = roleManagerAdapter;
+        mVoipCallMonitor = new VoipCallMonitor(mContext, mLock);
         mCallStreamingController = new CallStreamingController(mContext);
 
         mListeners.add(mInCallWakeLockController);
@@ -659,6 +662,9 @@
 
         // this needs to be after the mCallAudioManager
         mListeners.add(mPhoneStateBroadcaster);
+        mListeners.add(mVoipCallMonitor);
+
+        mVoipCallMonitor.startMonitor();
 
         // There is no USER_SWITCHED broadcast for user 0, handle it here explicitly.
         final UserManager userManager = UserManager.get(mContext);
@@ -1390,6 +1396,8 @@
         // set properties for transactional call
         if (extras.containsKey(TelecomManager.TRANSACTION_CALL_ID_KEY)) {
             call.setIsTransactionalCall(true);
+            call.setOwnerPid(extras.getInt(CallAttributes.CALLER_PID, -1));
+            extras.remove(CallAttributes.CALLER_PID);
             call.setConnectionCapabilities(
                     extras.getInt(CallAttributes.CALL_CAPABILITIES_KEY,
                             CallAttributes.SUPPORTS_SET_INACTIVE), true);
@@ -1698,6 +1706,8 @@
 
             if (extras.containsKey(TelecomManager.TRANSACTION_CALL_ID_KEY)) {
                 call.setIsTransactionalCall(true);
+                call.setOwnerPid(extras.getInt(CallAttributes.CALLER_PID, -1));
+                extras.remove(CallAttributes.CALLER_PID);
                 call.setConnectionCapabilities(
                         extras.getInt(CallAttributes.CALL_CAPABILITIES_KEY,
                                 CallAttributes.SUPPORTS_SET_INACTIVE), true);
@@ -6180,6 +6190,10 @@
         return mRinger;
     }
 
+    @VisibleForTesting
+    public VoipCallMonitor getVoipCallMonitor() {
+        return mVoipCallMonitor;
+    }
 
     /**
      * This method should only be used for testing.
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index 6826290..6357f66 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -193,6 +193,10 @@
                 enforcePhoneAccountIsRegisteredEnabled(handle, handle.getUserHandle());
                 enforceCallingPackage(callingPackage, "addCall");
 
+                // add extras about info used for FGS delegation
+                Bundle extras = new Bundle();
+                extras.putInt(CallAttributes.CALLER_PID, Binder.getCallingPid());
+
                 VoipCallTransaction transaction = null;
                 // create transaction based on the call direction
                 switch (callAttributes.getDirection()) {
@@ -202,7 +206,7 @@
                         break;
                     case DIRECTION_INCOMING:
                         transaction = new IncomingCallTransaction(callId, callAttributes,
-                                mCallsManager);
+                                mCallsManager, extras);
                         break;
                     default:
                         throw new IllegalArgumentException(String.format("Invalid Call Direction. "
diff --git a/src/com/android/server/telecom/voip/IncomingCallTransaction.java b/src/com/android/server/telecom/voip/IncomingCallTransaction.java
index 7bb9736..37fecb7 100644
--- a/src/com/android/server/telecom/voip/IncomingCallTransaction.java
+++ b/src/com/android/server/telecom/voip/IncomingCallTransaction.java
@@ -20,7 +20,6 @@
 
 import android.os.Bundle;
 import android.telecom.CallAttributes;
-import android.telecom.CallControl;
 import android.telecom.CallException;
 import android.telecom.TelecomManager;
 import android.util.Log;
@@ -37,14 +36,21 @@
     private final String mCallId;
     private final CallAttributes mCallAttributes;
     private final CallsManager mCallsManager;
+    private final Bundle mExtras;
 
     public IncomingCallTransaction(String callId, CallAttributes callAttributes,
-            CallsManager callsManager) {
+            CallsManager callsManager, Bundle extras) {
+        mExtras = extras;
         mCallId = callId;
         mCallAttributes = callAttributes;
         mCallsManager = callsManager;
     }
 
+    public IncomingCallTransaction(String callId, CallAttributes callAttributes,
+            CallsManager callsManager) {
+        this(callId, callAttributes, callsManager, new Bundle());
+    }
+
     @Override
     public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
         Log.d(TAG, "processTransaction");
@@ -69,11 +75,10 @@
         }
     }
 
-    private Bundle generateExtras(CallAttributes callAttributes){
-        Bundle extras = new Bundle();
-        extras.putString(TelecomManager.TRANSACTION_CALL_ID_KEY, mCallId);
-        extras.putInt(CALL_CAPABILITIES_KEY, callAttributes.getCallCapabilities());
-        extras.putInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE, callAttributes.getCallType());
-        return extras;
+    private Bundle generateExtras(CallAttributes callAttributes) {
+        mExtras.putString(TelecomManager.TRANSACTION_CALL_ID_KEY, mCallId);
+        mExtras.putInt(CALL_CAPABILITIES_KEY, callAttributes.getCallCapabilities());
+        mExtras.putInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE, callAttributes.getCallType());
+        return mExtras;
     }
 }
diff --git a/src/com/android/server/telecom/voip/OutgoingCallTransaction.java b/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
index 169fc48..3ecda6d 100644
--- a/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
+++ b/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
@@ -43,14 +43,21 @@
     private final String mCallingPackage;
     private final CallAttributes mCallAttributes;
     private final CallsManager mCallsManager;
+    private final Bundle mExtras;
+
+    public OutgoingCallTransaction(String callId, Context context, CallAttributes callAttributes,
+            CallsManager callsManager, Bundle extras) {
+        mCallId = callId;
+        mContext = context;
+        mCallAttributes = callAttributes;
+        mCallsManager = callsManager;
+        mExtras = extras;
+        mCallingPackage = mContext.getOpPackageName();
+    }
 
     public OutgoingCallTransaction(String callId, Context context, CallAttributes callAttributes,
             CallsManager callsManager) {
-        mCallId = callId;
-        mContext = context;
-        mCallingPackage = mContext.getOpPackageName();
-        mCallAttributes = callAttributes;
-        mCallsManager = callsManager;
+        this(callId, context, callAttributes, callsManager, new Bundle());
     }
 
     @Override
@@ -112,13 +119,12 @@
         }
     }
 
-    private Bundle generateExtras(CallAttributes callAttributes){
-        Bundle extras = new Bundle();
-        extras.setDefusable(true);
-        extras.putString(TelecomManager.TRANSACTION_CALL_ID_KEY, mCallId);
-        extras.putInt(CALL_CAPABILITIES_KEY, callAttributes.getCallCapabilities());
-        extras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+    private Bundle generateExtras(CallAttributes callAttributes) {
+        mExtras.setDefusable(true);
+        mExtras.putString(TelecomManager.TRANSACTION_CALL_ID_KEY, mCallId);
+        mExtras.putInt(CALL_CAPABILITIES_KEY, callAttributes.getCallCapabilities());
+        mExtras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
                 callAttributes.getCallType());
-        return extras;
+        return mExtras;
     }
 }
diff --git a/src/com/android/server/telecom/voip/VoipCallMonitor.java b/src/com/android/server/telecom/voip/VoipCallMonitor.java
new file mode 100644
index 0000000..04af98f
--- /dev/null
+++ b/src/com/android/server/telecom/voip/VoipCallMonitor.java
@@ -0,0 +1,251 @@
+/*
+ * 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.voip;
+
+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.TelecomSystem;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class VoipCallMonitor extends CallsManagerListenerBase {
+
+    private final List<Call> mPendingCalls;
+    private final Map<StatusBarNotification, Call> mNotifications;
+    private final Map<PhoneAccountHandle, Set<Call>> mPhoneAccountHandleListMap;
+    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;
+
+    public VoipCallMonitor(Context context, TelecomSystem.SyncRoot lock) {
+        mContext = context;
+        mHandlerThread = new HandlerThread(this.getClass().getSimpleName());
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+        mPendingCalls = new ArrayList<>();
+        mNotifications = new HashMap<>();
+        mServices = new HashMap<>();
+        mPhoneAccountHandleListMap = new HashMap<>();
+        mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
+
+        mNotificationListener = new NotificationListenerService() {
+            @Override
+            public void onNotificationPosted(StatusBarNotification sbn) {
+                synchronized (mLock) {
+                    if (mPendingCalls.isEmpty()) {
+                        return;
+                    }
+                    if (sbn.getNotification().isStyle(Notification.CallStyle.class)) {
+                        String packageName = sbn.getPackageName();
+                        UserHandle userHandle = sbn.getUser();
+
+                        for (Call call : mPendingCalls) {
+                            if (packageName != null &&
+                                    packageName.equals(call.getTargetPhoneAccount()
+                                            .getComponentName().getPackageName())
+                                    && userHandle != null
+                                    && userHandle.equals(call.getInitiatingUser())) {
+                                mPendingCalls.remove(call);
+                                mNotifications.put(sbn, call);
+                                break;
+                            }
+                        }
+                    }
+                }
+            }
+
+            @Override
+            public void onNotificationRemoved(StatusBarNotification sbn) {
+                synchronized (mLock) {
+                    if (mNotifications.isEmpty()) {
+                        return;
+                    }
+                    Call call = mNotifications.getOrDefault(sbn, null);
+                    if (call != null) {
+                        mNotifications.remove(sbn, call);
+                        stopFGSDelegation(call.getTargetPhoneAccount());
+                    }
+                }
+            }
+        };
+
+    }
+
+    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 = mPhoneAccountHandleListMap.computeIfAbsent(phoneAccountHandle,
+                    k -> new HashSet<>());
+            callList.add(call);
+
+            mHandler.post(() -> startFGSDelegation(call.getOwnerPid(),
+                    phoneAccountHandle.getUserHandle().getIdentifier(), call));
+        }
+    }
+
+    @Override
+    public void onCallRemoved(Call call) {
+        if (!call.isTransactionalCall()) {
+            return;
+        }
+
+        synchronized (mLock) {
+            stopMonitorWorks(call);
+            PhoneAccountHandle phoneAccountHandle = call.getTargetPhoneAccount();
+            Set<Call> callList = mPhoneAccountHandleListMap.computeIfAbsent(phoneAccountHandle,
+                    k -> new HashSet<>());
+            callList.remove(call);
+
+            if (callList.isEmpty()) {
+                stopFGSDelegation(phoneAccountHandle);
+            }
+        }
+    }
+
+    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()),
+                    0 /* foregroundServiceType */,
+                    ForegroundServiceDelegationOptions.DELEGATION_SERVICE_PHONE_CALL);
+            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 {
+                mActivityManagerInternal.startForegroundServiceDelegate(options, fgsConnection);
+            } catch (Exception e) {
+                Log.i(this, "startForegroundServiceDelegate failed due to: " + e);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    public void stopFGSDelegation(PhoneAccountHandle handle) {
+        synchronized (mLock) {
+            Log.i(this, "stopFGSDelegation of handle %s", handle);
+            Set<Call> calls = mPhoneAccountHandleListMap.get(handle);
+            for (Call call : calls) {
+                stopMonitorWorks(call);
+            }
+            mPhoneAccountHandleListMap.remove(handle);
+
+            if (mActivityManagerInternal != null) {
+                ServiceConnection fgsConnection = mServices.get(handle);
+                if (fgsConnection != null) {
+                    mActivityManagerInternal.stopForegroundServiceDelegate(fgsConnection);
+                }
+            }
+        }
+    }
+
+    private void startMonitorWorks(Call call) {
+        startMonitorNotification(call);
+    }
+
+    private void stopMonitorWorks(Call call) {
+        stopMonitorNotification(call);
+    }
+
+    private void startMonitorNotification(Call call) {
+        mPendingCalls.add(call);
+        mHandler.postDelayed(() -> {
+            synchronized (mLock) {
+                if (mPendingCalls.contains(call)) {
+                    stopFGSDelegation(call.getTargetPhoneAccount());
+                    mPendingCalls.remove(call);
+                }
+            }
+        }, 5000L);
+    }
+
+    private void stopMonitorNotification(Call call) {
+        mPendingCalls.remove(call);
+    }
+
+    @VisibleForTesting
+    public void setActivityManagerInternal(ActivityManagerInternal ami) {
+        mActivityManagerInternal = ami;
+    }
+
+    @VisibleForTesting
+    public void setNotificationListenerService(NotificationListenerService listener) {
+        mNotificationListener = listener;
+    }
+}
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index c84db3b..4ca6030 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -44,6 +44,9 @@
     <!-- Used to access PlatformCompat APIs -->
     <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG" />
     <uses-permission android:name="android.permission.LOG_COMPAT_CHANGE" />
+    
+    <!-- Used to register NotificationListenerService -->
+    <uses-permission android:name="android.permission.STATUS_BAR_SERVICE" />
 
     <application android:label="@string/app_name"
                  android:debuggable="true">
diff --git a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
index e3a1471..eacc47e 100644
--- a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
@@ -57,7 +57,9 @@
 import android.media.AudioManager;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.HandlerThread;
 import android.os.IInterface;
+import android.os.Looper;
 import android.os.PersistableBundle;
 import android.os.Process;
 import android.os.UserHandle;
@@ -110,6 +112,7 @@
  * property points to an application context implementing all the nontrivial functionality.
  */
 public class ComponentContextFixture implements TestFixture<Context> {
+    private HandlerThread mHandlerThread;
 
     public class FakeApplicationContext extends MockContext {
         @Override
@@ -305,6 +308,15 @@
         }
 
         @Override
+        public Looper getMainLooper() {
+            if (mHandlerThread == null) {
+                mHandlerThread = new HandlerThread(this.getClass().getSimpleName());
+                mHandlerThread.start();
+            }
+            return mHandlerThread.getLooper();
+        }
+
+        @Override
         public ContentResolver getContentResolver() {
             return new ContentResolver(mApplicationContextSpy) {
                 @Override
diff --git a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
index 7e7235d..9b0d736 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomSystemTest.java
@@ -66,7 +66,6 @@
 import android.telecom.VideoProfile;
 import android.telephony.TelephonyManager;
 import android.telephony.TelephonyRegistryManager;
-import android.text.TextUtils;
 
 import com.android.internal.telecom.IInCallAdapter;
 import com.android.server.telecom.AsyncRingtonePlayer;
@@ -391,6 +390,7 @@
                 handlerThread.quitSafely();
             }
             handlerThreads.clear();
+            mTelecomSystem.getCallsManager().getVoipCallMonitor().stopMonitor();
         }
         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
new file mode 100644
index 0000000..7b6bd3e
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
@@ -0,0 +1,217 @@
+/*
+ * 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.tests;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManagerInternal;
+import android.app.ForegroundServiceDelegationOptions;
+import android.content.ComponentName;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.UserHandle;
+import android.service.notification.NotificationListenerService;
+import android.telecom.PhoneAccountHandle;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.voip.VoipCallMonitor;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+
+@RunWith(JUnit4.class)
+public class VoipCallMonitorTest extends TelecomTestCase {
+    private VoipCallMonitor mMonitor;
+    private static final String PKG_NAME_1 = "telecom.voip.test1";
+    private static final String PKG_NAME_2 = "telecom.voip.test2";
+    private static final String CLS_NAME = "VoipActivity";
+    private static final String ID_1 = "id1";
+    private static final UserHandle USER_HANDLE_1 = new UserHandle(1);
+    private static final long TIMEOUT = 5000L;
+
+    @Mock private TelecomSystem.SyncRoot mLock;
+    @Mock private ActivityManagerInternal mActivityManagerInternal;
+    @Mock private NotificationListenerService mListenerService;
+
+    private final PhoneAccountHandle mHandle1User1 = new PhoneAccountHandle(
+            new ComponentName(PKG_NAME_1, CLS_NAME), ID_1, USER_HANDLE_1);
+    private final PhoneAccountHandle mHandle2User1 = new PhoneAccountHandle(
+            new ComponentName(PKG_NAME_2, CLS_NAME), ID_1, USER_HANDLE_1);
+
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        mMonitor = new VoipCallMonitor(mContext, mLock);
+        mActivityManagerInternal = mock(ActivityManagerInternal.class);
+        mListenerService = mock(NotificationListenerService.class);
+        mMonitor.setActivityManagerInternal(mActivityManagerInternal);
+        mMonitor.setNotificationListenerService(mListenerService);
+        doNothing().when(mListenerService).registerAsSystemService(eq(mContext),
+                any(ComponentName.class), anyInt());
+        mMonitor.startMonitor();
+    }
+
+    @SmallTest
+    @Test
+    public void testStartMonitorForOneCall() {
+        Call call = createTestCall("testCall", mHandle1User1);
+        IBinder service = mock(IBinder.class);
+
+        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);
+
+        mMonitor.onCallRemoved(call);
+        verify(mActivityManagerInternal, timeout(TIMEOUT)).stopForegroundServiceDelegate(eq(conn));
+    }
+
+    @SmallTest
+    @Test
+    public void testMonitorForTwoCallsOnSameHandle() {
+        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.onCallRemoved(call1);
+        verify(mActivityManagerInternal, never()).stopForegroundServiceDelegate(
+                any(ServiceConnection.class));
+        mMonitor.onCallRemoved(call2);
+        verify(mActivityManagerInternal, timeout(TIMEOUT).times(1))
+                .stopForegroundServiceDelegate(eq(conn2));
+    }
+
+    @SmallTest
+    @Test
+    public void testMonitorForTwoCallsOnDifferentHandle() {
+        Call call1 = createTestCall("testCall1", mHandle1User1);
+        Call call2 = createTestCall("testCall2", mHandle2User1);
+        IBinder service = mock(IBinder.class);
+
+        ArgumentCaptor<ServiceConnection> connCaptor1 = ArgumentCaptor.forClass(
+                ServiceConnection.class);
+        ArgumentCaptor<ForegroundServiceDelegationOptions> optionsCaptor1 =
+                ArgumentCaptor.forClass(ForegroundServiceDelegationOptions.class);
+        mMonitor.onCallAdded(call1);
+        verify(mActivityManagerInternal, timeout(TIMEOUT).times(1))
+                .startForegroundServiceDelegate(optionsCaptor1.capture(), connCaptor1.capture());
+        ForegroundServiceDelegationOptions options1 = optionsCaptor1.getValue();
+        ServiceConnection conn1 = connCaptor1.getValue();
+        conn1.onServiceConnected(mHandle1User1.getComponentName(), service);
+        assertEquals(PKG_NAME_1, options1.getComponentName().getPackageName());
+
+        ArgumentCaptor<ServiceConnection> connCaptor2 = ArgumentCaptor.forClass(
+                ServiceConnection.class);
+        ArgumentCaptor<ForegroundServiceDelegationOptions> optionsCaptor2 =
+                ArgumentCaptor.forClass(ForegroundServiceDelegationOptions.class);
+        mMonitor.onCallAdded(call2);
+        verify(mActivityManagerInternal, timeout(TIMEOUT).times(2))
+                .startForegroundServiceDelegate(optionsCaptor2.capture(), connCaptor2.capture());
+        ForegroundServiceDelegationOptions options2 = optionsCaptor2.getValue();
+        ServiceConnection conn2 = connCaptor2.getValue();
+        conn2.onServiceConnected(mHandle2User1.getComponentName(), service);
+        assertEquals(PKG_NAME_2, options2.getComponentName().getPackageName());
+
+        mMonitor.onCallRemoved(call2);
+        verify(mActivityManagerInternal).stopForegroundServiceDelegate(eq(conn2));
+        mMonitor.onCallRemoved(call1);
+        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(mHandle1User1);
+        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));
+    }
+
+    private Call createTestCall(String id, PhoneAccountHandle handle) {
+        Call call = mock(Call.class);
+        when(call.getTargetPhoneAccount()).thenReturn(handle);
+        when(call.isTransactionalCall()).thenReturn(true);
+        when(call.getExtras()).thenReturn(new Bundle());
+        when(call.getId()).thenReturn(id);
+        return call;
+    }
+}