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;
+ }
+}