diff options
5 files changed, 181 insertions, 31 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java index fad748021559..eb7854e63a85 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.dagger; +import android.app.IActivityManager; import android.app.NotificationManager; import android.content.Context; import android.os.Handler; @@ -68,6 +69,7 @@ import com.android.systemui.util.time.SystemClock; import com.android.wm.shell.bubbles.Bubbles; import java.util.Optional; +import java.util.concurrent.Executor; import dagger.Binds; import dagger.Lazy; @@ -239,10 +241,13 @@ public interface StatusBarDependenciesModule { CommonNotifCollection notifCollection, FeatureFlags featureFlags, SystemClock systemClock, - ActivityStarter activityStarter) { + ActivityStarter activityStarter, + @Main Executor mainExecutor, + IActivityManager iActivityManager) { OngoingCallController ongoingCallController = new OngoingCallController( - notifCollection, featureFlags, systemClock, activityStarter); + notifCollection, featureFlags, systemClock, activityStarter, mainExecutor, + iActivityManager); ongoingCallController.init(); return ongoingCallController; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java index 76657ad5ab07..9986a233e170 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java @@ -106,12 +106,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue private final OngoingCallListener mOngoingCallListener = new OngoingCallListener() { @Override - public void onOngoingCallStarted(boolean animate) { - disable(getContext().getDisplayId(), mDisabled1, mDisabled2, animate); - } - - @Override - public void onOngoingCallEnded(boolean animate) { + public void onOngoingCallStateChanged(boolean animate) { disable(getContext().getDisplayId(), mDisabled1, mDisabled2, animate); } }; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt index 51bb6434e61c..6d1df5b5fa19 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt @@ -16,6 +16,9 @@ package com.android.systemui.statusbar.phone.ongoingcall +import android.app.ActivityManager +import android.app.IActivityManager +import android.app.IUidObserver import android.app.Notification import android.app.Notification.CallStyle.CALL_TYPE_ONGOING import android.content.Intent @@ -25,6 +28,7 @@ import android.widget.Chronometer import com.android.systemui.R import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.FeatureFlags import com.android.systemui.statusbar.notification.collection.NotificationEntry @@ -32,6 +36,7 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.Co import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener import com.android.systemui.statusbar.policy.CallbackController import com.android.systemui.util.time.SystemClock +import java.util.concurrent.Executor import javax.inject.Inject /** @@ -42,12 +47,17 @@ class OngoingCallController @Inject constructor( private val notifCollection: CommonNotifCollection, private val featureFlags: FeatureFlags, private val systemClock: SystemClock, - private val activityStarter: ActivityStarter + private val activityStarter: ActivityStarter, + @Main private val mainExecutor: Executor, + private val iActivityManager: IActivityManager ) : CallbackController<OngoingCallListener> { /** Null if there's no ongoing call. */ private var ongoingCallInfo: OngoingCallInfo? = null + /** True if the application managing the call is visible to the user. */ + private var isCallAppVisible: Boolean = true private var chipView: ViewGroup? = null + private var uidObserver: IUidObserver.Stub? = null private val mListeners: MutableList<OngoingCallListener> = mutableListOf() @@ -67,8 +77,9 @@ class OngoingCallController @Inject constructor( override fun onEntryUpdated(entry: NotificationEntry) { if (isOngoingCallNotification(entry)) { ongoingCallInfo = OngoingCallInfo( - entry.sbn.notification.`when`, - entry.sbn.notification.contentIntent.intent) + entry.sbn.notification.`when`, + entry.sbn.notification.contentIntent.intent, + entry.sbn.uid) updateChip() } } @@ -76,7 +87,10 @@ class OngoingCallController @Inject constructor( override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { if (isOngoingCallNotification(entry)) { ongoingCallInfo = null - mListeners.forEach { l -> l.onOngoingCallEnded(animate = true) } + mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) } + if (uidObserver != null) { + iActivityManager.unregisterUidObserver(uidObserver) + } } } } @@ -100,9 +114,13 @@ class OngoingCallController @Inject constructor( } /** - * Returns true if there's an active ongoing call that can be displayed in a status bar chip. + * Returns true if there's an active ongoing call that should be displayed in a status bar chip. */ - fun hasOngoingCall(): Boolean = ongoingCallInfo != null + fun hasOngoingCall(): Boolean { + return ongoingCallInfo != null && + // When the user is in the phone app, don't show the chip. + !isCallAppVisible + } override fun addCallback(listener: OngoingCallListener) { synchronized(mListeners) { @@ -137,7 +155,9 @@ class OngoingCallController @Inject constructor( ActivityLaunchAnimator.Controller.fromView(it)) } - mListeners.forEach { l -> l.onOngoingCallStarted(animate = true) } + setUpUidObserver(currentOngoingCallInfo) + + mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) } } else { // If we failed to update the chip, don't store the ongoing call info. Then // [hasOngoingCall] will return false and we fall back to typical notification handling. @@ -150,9 +170,52 @@ class OngoingCallController @Inject constructor( } } + /** + * Sets up an [IUidObserver] to monitor the status of the application managing the ongoing call. + */ + private fun setUpUidObserver(currentOngoingCallInfo: OngoingCallInfo) { + isCallAppVisible = isProcessVisibleToUser( + iActivityManager.getUidProcessState(currentOngoingCallInfo.uid, null)) + + uidObserver = object : IUidObserver.Stub() { + override fun onUidStateChanged( + uid: Int, procState: Int, procStateSeq: Long, capability: Int) { + if (uid == currentOngoingCallInfo.uid) { + val oldIsCallAppVisible = isCallAppVisible + isCallAppVisible = isProcessVisibleToUser(procState) + if (oldIsCallAppVisible != isCallAppVisible) { + // Animations may be run as a result of the call's state change, so ensure + // the listener is notified on the main thread. + mainExecutor.execute { + mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) } + } + } + } + } + + override fun onUidGone(uid: Int, disabled: Boolean) {} + override fun onUidActive(uid: Int) {} + override fun onUidIdle(uid: Int, disabled: Boolean) {} + override fun onUidCachedChanged(uid: Int, cached: Boolean) {} + } + + iActivityManager.registerUidObserver( + uidObserver, + ActivityManager.UID_OBSERVER_PROCSTATE, + ActivityManager.PROCESS_STATE_UNKNOWN, + null + ) + } + + /** Returns true if the given [procState] represents a process that's visible to the user. */ + private fun isProcessVisibleToUser(procState: Int): Boolean { + return procState <= ActivityManager.PROCESS_STATE_TOP + } + private class OngoingCallInfo( val callStartTime: Long, - val intent: Intent + val intent: Intent, + val uid: Int ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallListener.kt index 7c583a1491e0..7a124308d597 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallListener.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallListener.kt @@ -16,11 +16,13 @@ package com.android.systemui.statusbar.phone.ongoingcall -/** A listener that's notified when an ongoing call is started or ended. */ +/** A listener that's notified when the state of an ongoing call has changed. */ interface OngoingCallListener { - /** Called when an ongoing call is started. */ - fun onOngoingCallStarted(animate: Boolean) - /** Called when an ongoing call is ended. */ - fun onOngoingCallEnded(animate: Boolean) + /** + * Called when the state of an ongoing call has changed in any way that may affect view + * visibility (including call starting, call stopping, application managing the call becoming + * visible or invisible). + */ + fun onOngoingCallStateChanged(animate: Boolean) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt index c244290fdacd..896e33073cc3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt @@ -16,6 +16,9 @@ package com.android.systemui.statusbar.phone.ongoingcall +import android.app.ActivityManager +import android.app.IActivityManager +import android.app.IUidObserver import android.app.Notification import android.app.PendingIntent import android.app.Person @@ -34,31 +37,46 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener +import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock +import com.android.systemui.util.mockito.any import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor -import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.* import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.Mockito.eq import org.mockito.Mockito.mock -import org.mockito.Mockito.verify import org.mockito.Mockito.never import org.mockito.Mockito.times -import org.mockito.Mockito.`when` +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +private const val CALL_UID = 900 + +// A process state that represents the process being visible to the user. +private const val PROC_STATE_VISIBLE = ActivityManager.PROCESS_STATE_TOP + +// A process state that represents the process being invisible to the user. +private const val PROC_STATE_INVISIBLE = ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE + @SmallTest @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper class OngoingCallControllerTest : SysuiTestCase() { + private val clock = FakeSystemClock() + private val mainExecutor = FakeExecutor(clock) + private lateinit var controller: OngoingCallController private lateinit var notifCollectionListener: NotifCollectionListener @Mock private lateinit var mockOngoingCallListener: OngoingCallListener @Mock private lateinit var mockActivityStarter: ActivityStarter + @Mock private lateinit var mockIActivityManager: IActivityManager private lateinit var chipView: LinearLayout @@ -76,7 +94,12 @@ class OngoingCallControllerTest : SysuiTestCase() { val notificationCollection = mock(CommonNotifCollection::class.java) controller = OngoingCallController( - notificationCollection, featureFlags, FakeSystemClock(), mockActivityStarter) + notificationCollection, + featureFlags, + clock, + mockActivityStarter, + mainExecutor, + mockIActivityManager) controller.init() controller.addCallback(mockOngoingCallListener) controller.setChipView(chipView) @@ -84,34 +107,37 @@ class OngoingCallControllerTest : SysuiTestCase() { val collectionListenerCaptor = ArgumentCaptor.forClass(NotifCollectionListener::class.java) verify(notificationCollection).addCollectionListener(collectionListenerCaptor.capture()) notifCollectionListener = collectionListenerCaptor.value!! + + `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) + .thenReturn(PROC_STATE_INVISIBLE) } @Test fun onEntryUpdated_isOngoingCallNotif_listenerNotifiedWithRightCallTime() { notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - verify(mockOngoingCallListener).onOngoingCallStarted(anyBoolean()) + verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) } @Test fun onEntryUpdated_notOngoingCallNotif_listenerNotNotified() { notifCollectionListener.onEntryUpdated(createNotCallNotifEntry()) - verify(mockOngoingCallListener, never()).onOngoingCallStarted(anyBoolean()) + verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean()) } @Test fun onEntryRemoved_ongoingCallNotif_listenerNotified() { notifCollectionListener.onEntryRemoved(createOngoingCallNotifEntry(), REASON_USER_STOPPED) - verify(mockOngoingCallListener).onOngoingCallEnded(anyBoolean()) + verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) } @Test fun onEntryRemoved_notOngoingCallNotif_listenerNotNotified() { notifCollectionListener.onEntryRemoved(createNotCallNotifEntry(), REASON_USER_STOPPED) - verify(mockOngoingCallListener, never()).onOngoingCallEnded(anyBoolean()) + verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean()) } @Test @@ -120,13 +146,26 @@ class OngoingCallControllerTest : SysuiTestCase() { } @Test - fun hasOngoingCall_ongoingCallNotifSentAndChipViewSet_returnsTrue() { + fun hasOngoingCall_ongoingCallNotifSentAndCallAppNotVisible_returnsTrue() { + `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) + .thenReturn(PROC_STATE_INVISIBLE) + notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) assertThat(controller.hasOngoingCall()).isTrue() } @Test + fun hasOngoingCall_ongoingCallNotifSentButCallAppVisible_returnsFalse() { + `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) + .thenReturn(PROC_STATE_VISIBLE) + + notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) + + assertThat(controller.hasOngoingCall()).isFalse() + } + + @Test fun hasOngoingCall_ongoingCallNotifSentButInvalidChipView_returnsFalse() { val invalidChipView = LinearLayout(context) controller.setChipView(invalidChipView) @@ -169,7 +208,52 @@ class OngoingCallControllerTest : SysuiTestCase() { // Verify the listener was notified once for the initial call and again when the new view // was set. - verify(mockOngoingCallListener, times(2)).onOngoingCallStarted(anyBoolean()) + verify(mockOngoingCallListener, times(2)) + .onOngoingCallStateChanged(anyBoolean()) + } + + @Test + fun callProcessChangesToVisible_listenerNotified() { + // Start the call while the process is invisible. + `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) + .thenReturn(PROC_STATE_INVISIBLE) + notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) + + val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java) + verify(mockIActivityManager).registerUidObserver( + captor.capture(), any(), any(), nullable(String::class.java)) + val uidObserver = captor.value + + // Update the process to visible. + uidObserver.onUidStateChanged(CALL_UID, PROC_STATE_VISIBLE, 0, 0) + mainExecutor.advanceClockToLast() + mainExecutor.runAllReady(); + + // Once for when the call was started, and another time when the process visibility changes. + verify(mockOngoingCallListener, times(2)) + .onOngoingCallStateChanged(anyBoolean()) + } + + @Test + fun callProcessChangesToInvisible_listenerNotified() { + // Start the call while the process is visible. + `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) + .thenReturn(PROC_STATE_VISIBLE) + notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) + + val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java) + verify(mockIActivityManager).registerUidObserver( + captor.capture(), any(), any(), nullable(String::class.java)) + val uidObserver = captor.value + + // Update the process to invisible. + uidObserver.onUidStateChanged(CALL_UID, PROC_STATE_INVISIBLE, 0, 0) + mainExecutor.advanceClockToLast() + mainExecutor.runAllReady(); + + // Once for when the call was started, and another time when the process visibility changes. + verify(mockOngoingCallListener, times(2)) + .onOngoingCallStateChanged(anyBoolean()) } private fun createOngoingCallNotifEntry(): NotificationEntry { @@ -179,6 +263,7 @@ class OngoingCallControllerTest : SysuiTestCase() { val contentIntent = mock(PendingIntent::class.java) `when`(contentIntent.intent).thenReturn(mock(Intent::class.java)) notificationEntryBuilder.modifyNotification(context).setContentIntent(contentIntent) + notificationEntryBuilder.setUid(CALL_UID) return notificationEntryBuilder.build() } |