VoipCallMonitor updates; stopFGS when notifs are removed
VoipCallMonitor was failing to revoke foreground service delegation if
the user dismissed an ongoing call notification for an active call.
This change ensures FGS is revoked if an application does not have a
notification indicating an ongoing call is active.
Fixes: 286090019
Test: 2 new unit tests + manual testing
- verified an app can update a notif from incoming call to
an ongoing call and maintain FGS
Change-Id: Iabc60e9616d4cfaf78cad7d10c601d9593543ee0
diff --git a/src/com/android/server/telecom/voip/VoipCallMonitor.java b/src/com/android/server/telecom/voip/VoipCallMonitor.java
index 9254395..3779a6d 100644
--- a/src/com/android/server/telecom/voip/VoipCallMonitor.java
+++ b/src/com/android/server/telecom/voip/VoipCallMonitor.java
@@ -36,6 +36,7 @@
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;
@@ -46,6 +47,7 @@
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;
@@ -89,13 +91,21 @@
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) {
+ 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);
@@ -154,7 +164,6 @@
Set<Call> callList = mAccountHandleToCallMap.computeIfAbsent(phoneAccountHandle,
k -> new HashSet<>());
callList.add(call);
-
CompletableFuture.completedFuture(null).thenComposeAsync(
(x) -> {
startFGSDelegation(call.getCallingPackageIdentity().mCallingPackagePid,
@@ -223,11 +232,15 @@
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);
+ stopMonitorWorks(c); // remove the call from tacking in this class
}
}
+
mAccountHandleToCallMap.remove(handle);
if (mActivityManagerInternal != null) {
@@ -253,6 +266,8 @@
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;
@@ -261,6 +276,8 @@
}
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);
@@ -288,12 +305,7 @@
mActivityManagerInternal = ami;
}
- @VisibleForTesting
- public void setNotificationListenerService(NotificationListenerService listener) {
- mNotificationListener = listener;
- }
-
- private class NotificationInfo {
+ private static class NotificationInfo extends Object {
private String mPackageName;
private UserHandle mUserHandle;
@@ -305,8 +317,49 @@
boolean matchesCall(Call call) {
PhoneAccountHandle accountHandle = call.getTargetPhoneAccount();
return mPackageName != null && mPackageName.equals(
- accountHandle.getComponentName().getPackageName())
+ 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/testapps/transactionalVoipApp/res/layout/in_call_activity.xml b/testapps/transactionalVoipApp/res/layout/in_call_activity.xml
index bed2e1a..a92a99b 100644
--- a/testapps/transactionalVoipApp/res/layout/in_call_activity.xml
+++ b/testapps/transactionalVoipApp/res/layout/in_call_activity.xml
@@ -29,6 +29,13 @@
/>
<Button
+ android:id="@+id/updateCallStyleNotification"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/update_notification"
+ />
+
+ <Button
android:id="@+id/answer_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
diff --git a/testapps/transactionalVoipApp/res/values/strings.xml b/testapps/transactionalVoipApp/res/values/strings.xml
index c8486c1..8239a0e 100644
--- a/testapps/transactionalVoipApp/res/values/strings.xml
+++ b/testapps/transactionalVoipApp/res/values/strings.xml
@@ -39,5 +39,6 @@
<!-- extra functionality -->
<string name="start_stream">start streaming</string>
<string name="crash_app">throw exception</string>
+ <string name="update_notification"> update notification to ongoing call style</string>
</resources>
\ No newline at end of file
diff --git a/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/InCallActivity.java b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/InCallActivity.java
index 53f5e9c..707c325 100644
--- a/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/InCallActivity.java
+++ b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/InCallActivity.java
@@ -21,11 +21,9 @@
import static android.telecom.CallAttributes.DIRECTION_OUTGOING;
import android.app.Activity;
-import android.graphics.Color;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.MediaPlayer;
-import android.net.StringNetworkSpecifier;
import android.net.Uri;
import android.os.Bundle;
import android.os.OutcomeReceiver;
@@ -174,8 +172,15 @@
"Intentionally throwing RuntimeException from InCallActivity");
}
});
- }
+ findViewById(R.id.updateCallStyleNotification).setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Utils.updateCallStyleNotification_toOngoingCall(getApplicationContext());
+ }
+ });
+ }
@Override
protected void onStop() {
diff --git a/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/Utils.java b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/Utils.java
index 98de790..0de2b19 100644
--- a/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/Utils.java
+++ b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/Utils.java
@@ -17,6 +17,7 @@
package com.android.server.telecom.transactionalVoipApp;
import android.app.Notification;
+import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Person;
import android.content.ComponentName;
@@ -38,9 +39,11 @@
public class Utils {
public static final String TAG = "TransactionalAppUtils";
+ public static final String CALLER_NAME = "Sundar Pichai";
public static final String sEXTRAS_KEY = "ExtrasKey";
public static final String sCALL_DIRECTION_KEY = "CallDirectionKey";
public static final String CHANNEL_ID = "TelecomVoipAppChannelId";
+ public static final int CALL_NOTIFICATION_ID = 123456;
private static final int SAMPLING_RATE_HZ = 44100;
public static final PhoneAccountHandle PHONE_ACCOUNT_HANDLE = new PhoneAccountHandle(
@@ -70,7 +73,7 @@
.setContentTitle("Incoming call")
.setSmallIcon(R.drawable.ic_android_black_24dp)
.setStyle(Notification.CallStyle.forIncomingCall(
- new Person.Builder().setName("Tom Stu").setImportant(true).build(),
+ new Person.Builder().setName(CALLER_NAME).setImportant(true).build(),
pendingAnswer, pendingReject)
)
.setFullScreenIntent(pendingAnswer, true)
@@ -79,6 +82,29 @@
return callStyleNotification;
}
+
+ public static void updateCallStyleNotification_toOngoingCall(Context context) {
+ PendingIntent ongoingCall = PendingIntent.getActivity(context, 0,
+ new Intent(""), PendingIntent.FLAG_IMMUTABLE);
+
+ Notification callStyleNotification = new Notification.Builder(context,
+ CHANNEL_ID)
+ .setContentText("active call in the TransactionalTestApp")
+ .setContentTitle("Ongoing call")
+ .setSmallIcon(R.drawable.ic_android_black_24dp)
+ .setStyle(Notification.CallStyle.forOngoingCall(
+ new Person.Builder().setName(CALLER_NAME).setImportant(true).build(),
+ ongoingCall)
+ )
+ .setFullScreenIntent(ongoingCall, true)
+ .build();
+
+ NotificationManager notificationManager =
+ context.getSystemService(NotificationManager.class);
+
+ notificationManager.notify(CALL_NOTIFICATION_ID, callStyleNotification);
+ }
+
public static MediaPlayer createMediaPlayer(Context context) {
int audioToPlay = (Math.random() > 0.5f) ?
com.android.server.telecom.transactionalVoipApp.R.raw.sample_audio :
diff --git a/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/VoipAppMainActivity.java b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/VoipAppMainActivity.java
index ae7d9d0..7578b9d 100644
--- a/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/VoipAppMainActivity.java
+++ b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/VoipAppMainActivity.java
@@ -99,7 +99,7 @@
}
private void startInCallActivity(int direction) {
- mNotificationManager.notify(123456,
+ mNotificationManager.notify(Utils.CALL_NOTIFICATION_ID,
Utils.createCallStyleNotification(getApplicationContext()));
Bundle extras = new Bundle();
extras.putInt(Utils.sCALL_DIRECTION_KEY, direction);
diff --git a/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java b/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
index c9ea34f..c66b0f7 100644
--- a/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
+++ b/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
@@ -18,30 +18,30 @@
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.app.Notification;
+import android.app.PendingIntent;
+import android.app.Person;
import android.content.ComponentName;
+import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.UserHandle;
-import android.service.notification.NotificationListenerService;
+import android.service.notification.StatusBarNotification;
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.CallState;
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.voip.VoipCallMonitor;
@@ -55,16 +55,18 @@
@RunWith(JUnit4.class)
public class VoipCallMonitorTest extends TelecomTestCase {
private VoipCallMonitor mMonitor;
+ private static final String NAME = "John Smith";
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";
+ public static final String CHANNEL_ID = "TelecomVoipAppChannelId";
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;
+ @Mock private IBinder mServiceConnection;
private final PhoneAccountHandle mHandle1User1 = new PhoneAccountHandle(
new ComponentName(PKG_NAME_1, CLS_NAME), ID_1, USER_HANDLE_1);
@@ -77,12 +79,11 @@
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();
+ when(mActivityManagerInternal.startForegroundServiceDelegate(any(
+ ForegroundServiceDelegationOptions.class), any(ServiceConnection.class)))
+ .thenReturn(true);
}
@SmallTest
@@ -206,13 +207,98 @@
.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.
+ * Note: post the notification AFTER foreground service delegation is gained
+ */
+ @SmallTest
+ @Test
+ public void testStopFgsIfCallNotificationIsRemoved_PostedAfterFgsIsGained() {
+ // GIVEN
+ StatusBarNotification sbn = createStatusBarNotificationFromHandle(mHandle1User1);
+
+ // WHEN
+ // FGS is gained after the call is added to VoipCallMonitor
+ ServiceConnection c = addCallAndVerifyFgsIsGained(createTestCall("1", mHandle1User1));
+ // simulate an app posting a call style notification after FGS is gained
+ mMonitor.postNotification(sbn);
+
+ // 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);
+ }
+
+ /**
+ * 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
+ */
+ @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);
when(call.getTargetPhoneAccount()).thenReturn(handle);
when(call.isTransactionalCall()).thenReturn(true);
when(call.getExtras()).thenReturn(new Bundle());
when(call.getId()).thenReturn(id);
- when(call.getCallingPackageIdentity()).thenReturn( new Call.CallingPackageIdentity() );
+ when(call.getCallingPackageIdentity()).thenReturn(new Call.CallingPackageIdentity());
+ when(call.getState()).thenReturn(CallState.ACTIVE);
return call;
}
+
+ private Notification createCallStyleNotification() {
+ PendingIntent pendingOngoingIntent = PendingIntent.getActivity(mContext, 0,
+ new Intent(""), PendingIntent.FLAG_IMMUTABLE);
+
+ return new Notification.Builder(mContext,
+ CHANNEL_ID)
+ .setStyle(Notification.CallStyle.forOngoingCall(
+ new Person.Builder().setName(NAME).setImportant(true).build(),
+ pendingOngoingIntent)
+ )
+ .setFullScreenIntent(pendingOngoingIntent, true)
+ .build();
+ }
+
+ private StatusBarNotification createStatusBarNotificationFromHandle(PhoneAccountHandle handle) {
+ return new StatusBarNotification(
+ handle.getComponentName().getPackageName(), "", 0, "", 0, 0,
+ createCallStyleNotification(), handle.getUserHandle(), "", 0);
+ }
+
+ private ServiceConnection addCallAndVerifyFgsIsGained(Call call) {
+ ArgumentCaptor<ServiceConnection> captor = ArgumentCaptor.forClass(ServiceConnection.class);
+ // add the call to the VoipCallMonitor under test which will start FGS
+ mMonitor.onCallAdded(call);
+ // FGS should be granted within the timeout
+ verify(mActivityManagerInternal, timeout(TIMEOUT))
+ .startForegroundServiceDelegate(any(
+ ForegroundServiceDelegationOptions.class),
+ captor.capture());
+ // 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);
+ return serviceConnection;
+ }
}