Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6afee499 authored by Caitlin Cassidy's avatar Caitlin Cassidy Committed by Android (Google) Code Review
Browse files

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

parents 11cbeca7 5f139c7a
Loading
Loading
Loading
Loading
+7 −2
Original line number Original line Diff line number Diff line
@@ -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;
@@ -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;
@@ -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;
    }
    }
+1 −6
Original line number Original line Diff line number Diff line
@@ -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);
        }
        }
    };
    };
+71 −8
Original line number Original line Diff line number Diff line
@@ -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
@@ -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
@@ -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


/**
/**
@@ -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()


@@ -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()
            }
            }
        }
        }
@@ -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)
                }
            }
            }
        }
        }
    }
    }
@@ -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) {
@@ -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.
@@ -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
    )
    )
}
}


+7 −5
Original line number Original line Diff line number Diff line
@@ -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)
}
}
+95 −10
Original line number Original line Diff line number Diff line
@@ -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
@@ -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


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