Loading packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java +7 −2 Original line number Original line Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.dagger; package com.android.systemui.statusbar.dagger; import android.app.IActivityManager; import android.app.NotificationManager; import android.app.NotificationManager; import android.content.Context; import android.content.Context; import android.os.Handler; import android.os.Handler; Loading Loading @@ -68,6 +69,7 @@ import com.android.systemui.util.time.SystemClock; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.bubbles.Bubbles; import java.util.Optional; import java.util.Optional; import java.util.concurrent.Executor; import dagger.Binds; import dagger.Binds; import dagger.Lazy; import dagger.Lazy; Loading Loading @@ -239,10 +241,13 @@ public interface StatusBarDependenciesModule { CommonNotifCollection notifCollection, CommonNotifCollection notifCollection, FeatureFlags featureFlags, FeatureFlags featureFlags, SystemClock systemClock, SystemClock systemClock, ActivityStarter activityStarter) { ActivityStarter activityStarter, @Main Executor mainExecutor, IActivityManager iActivityManager) { OngoingCallController ongoingCallController = OngoingCallController ongoingCallController = new OngoingCallController( new OngoingCallController( notifCollection, featureFlags, systemClock, activityStarter); notifCollection, featureFlags, systemClock, activityStarter, mainExecutor, iActivityManager); ongoingCallController.init(); ongoingCallController.init(); return ongoingCallController; return ongoingCallController; } } Loading packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java +1 −6 Original line number Original line Diff line number Diff line Loading @@ -105,12 +105,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue private final OngoingCallListener mOngoingCallListener = new OngoingCallListener() { private final OngoingCallListener mOngoingCallListener = new OngoingCallListener() { @Override @Override public void onOngoingCallStarted(boolean animate) { public void onOngoingCallStateChanged(boolean animate) { disable(getContext().getDisplayId(), mDisabled1, mDisabled2, animate); } @Override public void onOngoingCallEnded(boolean animate) { disable(getContext().getDisplayId(), mDisabled1, mDisabled2, animate); disable(getContext().getDisplayId(), mDisabled1, mDisabled2, animate); } } }; }; Loading packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt +71 −8 Original line number Original line Diff line number Diff line Loading @@ -16,6 +16,9 @@ package com.android.systemui.statusbar.phone.ongoingcall 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 import android.app.Notification.CallStyle.CALL_TYPE_ONGOING import android.app.Notification.CallStyle.CALL_TYPE_ONGOING import android.content.Intent import android.content.Intent Loading @@ -25,6 +28,7 @@ import android.widget.Chronometer import com.android.systemui.R import com.android.systemui.R import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.FeatureFlags import com.android.systemui.statusbar.FeatureFlags import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntry Loading @@ -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.notification.collection.notifcollection.NotifCollectionListener import com.android.systemui.statusbar.policy.CallbackController import com.android.systemui.statusbar.policy.CallbackController import com.android.systemui.util.time.SystemClock import com.android.systemui.util.time.SystemClock import java.util.concurrent.Executor import javax.inject.Inject import javax.inject.Inject /** /** Loading @@ -42,12 +47,17 @@ class OngoingCallController @Inject constructor( private val notifCollection: CommonNotifCollection, private val notifCollection: CommonNotifCollection, private val featureFlags: FeatureFlags, private val featureFlags: FeatureFlags, private val systemClock: SystemClock, private val systemClock: SystemClock, private val activityStarter: ActivityStarter private val activityStarter: ActivityStarter, @Main private val mainExecutor: Executor, private val iActivityManager: IActivityManager ) : CallbackController<OngoingCallListener> { ) : CallbackController<OngoingCallListener> { /** Null if there's no ongoing call. */ /** Null if there's no ongoing call. */ private var ongoingCallInfo: OngoingCallInfo? = null 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 chipView: ViewGroup? = null private var uidObserver: IUidObserver.Stub? = null private val mListeners: MutableList<OngoingCallListener> = mutableListOf() private val mListeners: MutableList<OngoingCallListener> = mutableListOf() Loading @@ -68,7 +78,8 @@ class OngoingCallController @Inject constructor( if (isOngoingCallNotification(entry)) { if (isOngoingCallNotification(entry)) { ongoingCallInfo = OngoingCallInfo( ongoingCallInfo = OngoingCallInfo( entry.sbn.notification.`when`, entry.sbn.notification.`when`, entry.sbn.notification.contentIntent.intent) entry.sbn.notification.contentIntent.intent, entry.sbn.uid) updateChip() updateChip() } } } } Loading @@ -76,7 +87,10 @@ class OngoingCallController @Inject constructor( override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { if (isOngoingCallNotification(entry)) { if (isOngoingCallNotification(entry)) { ongoingCallInfo = null ongoingCallInfo = null mListeners.forEach { l -> l.onOngoingCallEnded(animate = true) } mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) } if (uidObserver != null) { iActivityManager.unregisterUidObserver(uidObserver) } } } } } } } Loading @@ -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) { override fun addCallback(listener: OngoingCallListener) { synchronized(mListeners) { synchronized(mListeners) { Loading Loading @@ -137,7 +155,9 @@ class OngoingCallController @Inject constructor( ActivityLaunchAnimator.Controller.fromView(it)) ActivityLaunchAnimator.Controller.fromView(it)) } } mListeners.forEach { l -> l.onOngoingCallStarted(animate = true) } setUpUidObserver(currentOngoingCallInfo) mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) } } else { } else { // If we failed to update the chip, don't store the ongoing call info. Then // 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. // [hasOngoingCall] will return false and we fall back to typical notification handling. Loading @@ -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( private class OngoingCallInfo( val callStartTime: Long, val callStartTime: Long, val intent: Intent val intent: Intent, val uid: Int ) ) } } Loading packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallListener.kt +7 −5 Original line number Original line Diff line number Diff line Loading @@ -16,11 +16,13 @@ package com.android.systemui.statusbar.phone.ongoingcall 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 { 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) } } packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt +95 −10 Original line number Original line Diff line number Diff line Loading @@ -16,6 +16,9 @@ package com.android.systemui.statusbar.phone.ongoingcall 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 import android.app.PendingIntent import android.app.PendingIntent import android.app.Person import android.app.Person Loading @@ -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.NotificationEntryBuilder import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener 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.time.FakeSystemClock import com.android.systemui.util.mockito.any import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Before import org.junit.Test import org.junit.Test import org.junit.runner.RunWith import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.* import org.mockito.Mock import org.mockito.Mock import org.mockito.Mockito.`when` import org.mockito.Mockito.eq import org.mockito.Mockito.mock import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.Mockito.never import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.times import org.mockito.Mockito.`when` import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations 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 @SmallTest @RunWith(AndroidTestingRunner::class) @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper @TestableLooper.RunWithLooper class OngoingCallControllerTest : SysuiTestCase() { class OngoingCallControllerTest : SysuiTestCase() { private val clock = FakeSystemClock() private val mainExecutor = FakeExecutor(clock) private lateinit var controller: OngoingCallController private lateinit var controller: OngoingCallController private lateinit var notifCollectionListener: NotifCollectionListener private lateinit var notifCollectionListener: NotifCollectionListener @Mock private lateinit var mockOngoingCallListener: OngoingCallListener @Mock private lateinit var mockOngoingCallListener: OngoingCallListener @Mock private lateinit var mockActivityStarter: ActivityStarter @Mock private lateinit var mockActivityStarter: ActivityStarter @Mock private lateinit var mockIActivityManager: IActivityManager private lateinit var chipView: LinearLayout private lateinit var chipView: LinearLayout Loading @@ -76,7 +94,12 @@ class OngoingCallControllerTest : SysuiTestCase() { val notificationCollection = mock(CommonNotifCollection::class.java) val notificationCollection = mock(CommonNotifCollection::class.java) controller = OngoingCallController( controller = OngoingCallController( notificationCollection, featureFlags, FakeSystemClock(), mockActivityStarter) notificationCollection, featureFlags, clock, mockActivityStarter, mainExecutor, mockIActivityManager) controller.init() controller.init() controller.addCallback(mockOngoingCallListener) controller.addCallback(mockOngoingCallListener) controller.setChipView(chipView) controller.setChipView(chipView) Loading @@ -84,34 +107,37 @@ class OngoingCallControllerTest : SysuiTestCase() { val collectionListenerCaptor = ArgumentCaptor.forClass(NotifCollectionListener::class.java) val collectionListenerCaptor = ArgumentCaptor.forClass(NotifCollectionListener::class.java) verify(notificationCollection).addCollectionListener(collectionListenerCaptor.capture()) verify(notificationCollection).addCollectionListener(collectionListenerCaptor.capture()) notifCollectionListener = collectionListenerCaptor.value!! notifCollectionListener = collectionListenerCaptor.value!! `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) .thenReturn(PROC_STATE_INVISIBLE) } } @Test @Test fun onEntryUpdated_isOngoingCallNotif_listenerNotifiedWithRightCallTime() { fun onEntryUpdated_isOngoingCallNotif_listenerNotifiedWithRightCallTime() { notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) verify(mockOngoingCallListener).onOngoingCallStarted(anyBoolean()) verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) } } @Test @Test fun onEntryUpdated_notOngoingCallNotif_listenerNotNotified() { fun onEntryUpdated_notOngoingCallNotif_listenerNotNotified() { notifCollectionListener.onEntryUpdated(createNotCallNotifEntry()) notifCollectionListener.onEntryUpdated(createNotCallNotifEntry()) verify(mockOngoingCallListener, never()).onOngoingCallStarted(anyBoolean()) verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean()) } } @Test @Test fun onEntryRemoved_ongoingCallNotif_listenerNotified() { fun onEntryRemoved_ongoingCallNotif_listenerNotified() { notifCollectionListener.onEntryRemoved(createOngoingCallNotifEntry(), REASON_USER_STOPPED) notifCollectionListener.onEntryRemoved(createOngoingCallNotifEntry(), REASON_USER_STOPPED) verify(mockOngoingCallListener).onOngoingCallEnded(anyBoolean()) verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) } } @Test @Test fun onEntryRemoved_notOngoingCallNotif_listenerNotNotified() { fun onEntryRemoved_notOngoingCallNotif_listenerNotNotified() { notifCollectionListener.onEntryRemoved(createNotCallNotifEntry(), REASON_USER_STOPPED) notifCollectionListener.onEntryRemoved(createNotCallNotifEntry(), REASON_USER_STOPPED) verify(mockOngoingCallListener, never()).onOngoingCallEnded(anyBoolean()) verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean()) } } @Test @Test Loading @@ -120,12 +146,25 @@ class OngoingCallControllerTest : SysuiTestCase() { } } @Test @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()) notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) assertThat(controller.hasOngoingCall()).isTrue() 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 @Test fun hasOngoingCall_ongoingCallNotifSentButInvalidChipView_returnsFalse() { fun hasOngoingCall_ongoingCallNotifSentButInvalidChipView_returnsFalse() { val invalidChipView = LinearLayout(context) val invalidChipView = LinearLayout(context) Loading Loading @@ -169,7 +208,52 @@ class OngoingCallControllerTest : SysuiTestCase() { // Verify the listener was notified once for the initial call and again when the new view // Verify the listener was notified once for the initial call and again when the new view // was set. // 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 { private fun createOngoingCallNotifEntry(): NotificationEntry { Loading @@ -179,6 +263,7 @@ class OngoingCallControllerTest : SysuiTestCase() { val contentIntent = mock(PendingIntent::class.java) val contentIntent = mock(PendingIntent::class.java) `when`(contentIntent.intent).thenReturn(mock(Intent::class.java)) `when`(contentIntent.intent).thenReturn(mock(Intent::class.java)) notificationEntryBuilder.modifyNotification(context).setContentIntent(contentIntent) notificationEntryBuilder.modifyNotification(context).setContentIntent(contentIntent) notificationEntryBuilder.setUid(CALL_UID) return notificationEntryBuilder.build() return notificationEntryBuilder.build() } } Loading Loading
packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java +7 −2 Original line number Original line Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.dagger; package com.android.systemui.statusbar.dagger; import android.app.IActivityManager; import android.app.NotificationManager; import android.app.NotificationManager; import android.content.Context; import android.content.Context; import android.os.Handler; import android.os.Handler; Loading Loading @@ -68,6 +69,7 @@ import com.android.systemui.util.time.SystemClock; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.bubbles.Bubbles; import java.util.Optional; import java.util.Optional; import java.util.concurrent.Executor; import dagger.Binds; import dagger.Binds; import dagger.Lazy; import dagger.Lazy; Loading Loading @@ -239,10 +241,13 @@ public interface StatusBarDependenciesModule { CommonNotifCollection notifCollection, CommonNotifCollection notifCollection, FeatureFlags featureFlags, FeatureFlags featureFlags, SystemClock systemClock, SystemClock systemClock, ActivityStarter activityStarter) { ActivityStarter activityStarter, @Main Executor mainExecutor, IActivityManager iActivityManager) { OngoingCallController ongoingCallController = OngoingCallController ongoingCallController = new OngoingCallController( new OngoingCallController( notifCollection, featureFlags, systemClock, activityStarter); notifCollection, featureFlags, systemClock, activityStarter, mainExecutor, iActivityManager); ongoingCallController.init(); ongoingCallController.init(); return ongoingCallController; return ongoingCallController; } } Loading
packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java +1 −6 Original line number Original line Diff line number Diff line Loading @@ -105,12 +105,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue private final OngoingCallListener mOngoingCallListener = new OngoingCallListener() { private final OngoingCallListener mOngoingCallListener = new OngoingCallListener() { @Override @Override public void onOngoingCallStarted(boolean animate) { public void onOngoingCallStateChanged(boolean animate) { disable(getContext().getDisplayId(), mDisabled1, mDisabled2, animate); } @Override public void onOngoingCallEnded(boolean animate) { disable(getContext().getDisplayId(), mDisabled1, mDisabled2, animate); disable(getContext().getDisplayId(), mDisabled1, mDisabled2, animate); } } }; }; Loading
packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt +71 −8 Original line number Original line Diff line number Diff line Loading @@ -16,6 +16,9 @@ package com.android.systemui.statusbar.phone.ongoingcall 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 import android.app.Notification.CallStyle.CALL_TYPE_ONGOING import android.app.Notification.CallStyle.CALL_TYPE_ONGOING import android.content.Intent import android.content.Intent Loading @@ -25,6 +28,7 @@ import android.widget.Chronometer import com.android.systemui.R import com.android.systemui.R import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.FeatureFlags import com.android.systemui.statusbar.FeatureFlags import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntry Loading @@ -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.notification.collection.notifcollection.NotifCollectionListener import com.android.systemui.statusbar.policy.CallbackController import com.android.systemui.statusbar.policy.CallbackController import com.android.systemui.util.time.SystemClock import com.android.systemui.util.time.SystemClock import java.util.concurrent.Executor import javax.inject.Inject import javax.inject.Inject /** /** Loading @@ -42,12 +47,17 @@ class OngoingCallController @Inject constructor( private val notifCollection: CommonNotifCollection, private val notifCollection: CommonNotifCollection, private val featureFlags: FeatureFlags, private val featureFlags: FeatureFlags, private val systemClock: SystemClock, private val systemClock: SystemClock, private val activityStarter: ActivityStarter private val activityStarter: ActivityStarter, @Main private val mainExecutor: Executor, private val iActivityManager: IActivityManager ) : CallbackController<OngoingCallListener> { ) : CallbackController<OngoingCallListener> { /** Null if there's no ongoing call. */ /** Null if there's no ongoing call. */ private var ongoingCallInfo: OngoingCallInfo? = null 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 chipView: ViewGroup? = null private var uidObserver: IUidObserver.Stub? = null private val mListeners: MutableList<OngoingCallListener> = mutableListOf() private val mListeners: MutableList<OngoingCallListener> = mutableListOf() Loading @@ -68,7 +78,8 @@ class OngoingCallController @Inject constructor( if (isOngoingCallNotification(entry)) { if (isOngoingCallNotification(entry)) { ongoingCallInfo = OngoingCallInfo( ongoingCallInfo = OngoingCallInfo( entry.sbn.notification.`when`, entry.sbn.notification.`when`, entry.sbn.notification.contentIntent.intent) entry.sbn.notification.contentIntent.intent, entry.sbn.uid) updateChip() updateChip() } } } } Loading @@ -76,7 +87,10 @@ class OngoingCallController @Inject constructor( override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { if (isOngoingCallNotification(entry)) { if (isOngoingCallNotification(entry)) { ongoingCallInfo = null ongoingCallInfo = null mListeners.forEach { l -> l.onOngoingCallEnded(animate = true) } mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) } if (uidObserver != null) { iActivityManager.unregisterUidObserver(uidObserver) } } } } } } } Loading @@ -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) { override fun addCallback(listener: OngoingCallListener) { synchronized(mListeners) { synchronized(mListeners) { Loading Loading @@ -137,7 +155,9 @@ class OngoingCallController @Inject constructor( ActivityLaunchAnimator.Controller.fromView(it)) ActivityLaunchAnimator.Controller.fromView(it)) } } mListeners.forEach { l -> l.onOngoingCallStarted(animate = true) } setUpUidObserver(currentOngoingCallInfo) mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) } } else { } else { // If we failed to update the chip, don't store the ongoing call info. Then // 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. // [hasOngoingCall] will return false and we fall back to typical notification handling. Loading @@ -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( private class OngoingCallInfo( val callStartTime: Long, val callStartTime: Long, val intent: Intent val intent: Intent, val uid: Int ) ) } } Loading
packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallListener.kt +7 −5 Original line number Original line Diff line number Diff line Loading @@ -16,11 +16,13 @@ package com.android.systemui.statusbar.phone.ongoingcall 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 { 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) } }
packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt +95 −10 Original line number Original line Diff line number Diff line Loading @@ -16,6 +16,9 @@ package com.android.systemui.statusbar.phone.ongoingcall 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 import android.app.PendingIntent import android.app.PendingIntent import android.app.Person import android.app.Person Loading @@ -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.NotificationEntryBuilder import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener 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.time.FakeSystemClock import com.android.systemui.util.mockito.any import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Before import org.junit.Test import org.junit.Test import org.junit.runner.RunWith import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.* import org.mockito.Mock import org.mockito.Mock import org.mockito.Mockito.`when` import org.mockito.Mockito.eq import org.mockito.Mockito.mock import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.Mockito.never import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.times import org.mockito.Mockito.`when` import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations 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 @SmallTest @RunWith(AndroidTestingRunner::class) @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper @TestableLooper.RunWithLooper class OngoingCallControllerTest : SysuiTestCase() { class OngoingCallControllerTest : SysuiTestCase() { private val clock = FakeSystemClock() private val mainExecutor = FakeExecutor(clock) private lateinit var controller: OngoingCallController private lateinit var controller: OngoingCallController private lateinit var notifCollectionListener: NotifCollectionListener private lateinit var notifCollectionListener: NotifCollectionListener @Mock private lateinit var mockOngoingCallListener: OngoingCallListener @Mock private lateinit var mockOngoingCallListener: OngoingCallListener @Mock private lateinit var mockActivityStarter: ActivityStarter @Mock private lateinit var mockActivityStarter: ActivityStarter @Mock private lateinit var mockIActivityManager: IActivityManager private lateinit var chipView: LinearLayout private lateinit var chipView: LinearLayout Loading @@ -76,7 +94,12 @@ class OngoingCallControllerTest : SysuiTestCase() { val notificationCollection = mock(CommonNotifCollection::class.java) val notificationCollection = mock(CommonNotifCollection::class.java) controller = OngoingCallController( controller = OngoingCallController( notificationCollection, featureFlags, FakeSystemClock(), mockActivityStarter) notificationCollection, featureFlags, clock, mockActivityStarter, mainExecutor, mockIActivityManager) controller.init() controller.init() controller.addCallback(mockOngoingCallListener) controller.addCallback(mockOngoingCallListener) controller.setChipView(chipView) controller.setChipView(chipView) Loading @@ -84,34 +107,37 @@ class OngoingCallControllerTest : SysuiTestCase() { val collectionListenerCaptor = ArgumentCaptor.forClass(NotifCollectionListener::class.java) val collectionListenerCaptor = ArgumentCaptor.forClass(NotifCollectionListener::class.java) verify(notificationCollection).addCollectionListener(collectionListenerCaptor.capture()) verify(notificationCollection).addCollectionListener(collectionListenerCaptor.capture()) notifCollectionListener = collectionListenerCaptor.value!! notifCollectionListener = collectionListenerCaptor.value!! `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) .thenReturn(PROC_STATE_INVISIBLE) } } @Test @Test fun onEntryUpdated_isOngoingCallNotif_listenerNotifiedWithRightCallTime() { fun onEntryUpdated_isOngoingCallNotif_listenerNotifiedWithRightCallTime() { notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) verify(mockOngoingCallListener).onOngoingCallStarted(anyBoolean()) verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) } } @Test @Test fun onEntryUpdated_notOngoingCallNotif_listenerNotNotified() { fun onEntryUpdated_notOngoingCallNotif_listenerNotNotified() { notifCollectionListener.onEntryUpdated(createNotCallNotifEntry()) notifCollectionListener.onEntryUpdated(createNotCallNotifEntry()) verify(mockOngoingCallListener, never()).onOngoingCallStarted(anyBoolean()) verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean()) } } @Test @Test fun onEntryRemoved_ongoingCallNotif_listenerNotified() { fun onEntryRemoved_ongoingCallNotif_listenerNotified() { notifCollectionListener.onEntryRemoved(createOngoingCallNotifEntry(), REASON_USER_STOPPED) notifCollectionListener.onEntryRemoved(createOngoingCallNotifEntry(), REASON_USER_STOPPED) verify(mockOngoingCallListener).onOngoingCallEnded(anyBoolean()) verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) } } @Test @Test fun onEntryRemoved_notOngoingCallNotif_listenerNotNotified() { fun onEntryRemoved_notOngoingCallNotif_listenerNotNotified() { notifCollectionListener.onEntryRemoved(createNotCallNotifEntry(), REASON_USER_STOPPED) notifCollectionListener.onEntryRemoved(createNotCallNotifEntry(), REASON_USER_STOPPED) verify(mockOngoingCallListener, never()).onOngoingCallEnded(anyBoolean()) verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean()) } } @Test @Test Loading @@ -120,12 +146,25 @@ class OngoingCallControllerTest : SysuiTestCase() { } } @Test @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()) notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) assertThat(controller.hasOngoingCall()).isTrue() 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 @Test fun hasOngoingCall_ongoingCallNotifSentButInvalidChipView_returnsFalse() { fun hasOngoingCall_ongoingCallNotifSentButInvalidChipView_returnsFalse() { val invalidChipView = LinearLayout(context) val invalidChipView = LinearLayout(context) Loading Loading @@ -169,7 +208,52 @@ class OngoingCallControllerTest : SysuiTestCase() { // Verify the listener was notified once for the initial call and again when the new view // Verify the listener was notified once for the initial call and again when the new view // was set. // 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 { private fun createOngoingCallNotifEntry(): NotificationEntry { Loading @@ -179,6 +263,7 @@ class OngoingCallControllerTest : SysuiTestCase() { val contentIntent = mock(PendingIntent::class.java) val contentIntent = mock(PendingIntent::class.java) `when`(contentIntent.intent).thenReturn(mock(Intent::class.java)) `when`(contentIntent.intent).thenReturn(mock(Intent::class.java)) notificationEntryBuilder.modifyNotification(context).setContentIntent(contentIntent) notificationEntryBuilder.modifyNotification(context).setContentIntent(contentIntent) notificationEntryBuilder.setUid(CALL_UID) return notificationEntryBuilder.build() return notificationEntryBuilder.build() } } Loading