VoipCallMonitor updates; stopFGS when notifs are removed am: 1feb0f280c
Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/services/Telecomm/+/23619482
Change-Id: I502afd2fc2c4459bbcb62c1ad76a04a1b41dc461
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
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;
+ }
}