Merge "[Ongoing Call] Hide chip when app is in foreground." into sc-dev

This commit is contained in:
Caitlin Cassidy
2021-05-06 19:11:36 +00:00
committed by Android (Google) Code Review
5 changed files with 181 additions and 31 deletions

View File

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

View File

@@ -105,12 +105,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);
}
};

View File

@@ -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
)
}

View File

@@ -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)
}

View File

@@ -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,12 +146,25 @@ 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)
@@ -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()
}