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