From eaba7a2a86ac71b24da148e2e42532a1602c6f5b Mon Sep 17 00:00:00 2001 From: Michael Mikhail Date: Tue, 10 Jan 2023 20:57:45 +0000 Subject: [PATCH] Hide resume players on entering the lockdown mode Don't add the resume player when onNotificationRemoved due to entering the lockdown mode. The media keeps playing and the resume player should not be visible. Hides media carousel on entering lockdown mode. Tracks when the keyguard is gone to make sure media carousel is visible. Bug: 253335072 Test: atest MediaDataManagerTest. Test: atest MediaCarouselControllerTest. Change-Id: I5d4ed30a24f43db984d73c58bbc551a0ec89509c --- .../KeyguardTransitionInteractor.kt | 4 ++ .../controls/pipeline/MediaDataManager.kt | 8 ++- .../controls/ui/MediaCarouselController.kt | 48 ++++++++++++++++ .../controls/pipeline/MediaDataManagerTest.kt | 17 ++++++ .../ui/MediaCarouselControllerTest.kt | 56 +++++++++++++++++++ 5 files changed, 132 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index 53c80f65e44fc..84bcdf9f645f9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -42,6 +42,10 @@ class KeyguardTransitionInteractor constructor( repository: KeyguardTransitionRepository, ) { + /** (any)->GONE transition information */ + val anyStateToGoneTransition: Flow = + repository.transitions.filter { step -> step.to == KeyguardState.GONE } + /** (any)->AOD transition information */ val anyStateToAodTransition: Flow = repository.transitions.filter { step -> step.to == KeyguardState.AOD } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt index db777772d5591..520edef7d1095 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt @@ -50,6 +50,7 @@ import android.text.TextUtils import android.util.Log import androidx.media.utils.MediaConstants import com.android.internal.logging.InstanceId +import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.Dumpable import com.android.systemui.R import com.android.systemui.broadcast.BroadcastDispatcher @@ -177,6 +178,7 @@ class MediaDataManager( private val mediaFlags: MediaFlags, private val logger: MediaUiEventLogger, private val smartspaceManager: SmartspaceManager, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, ) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener { companion object { @@ -241,6 +243,7 @@ class MediaDataManager( mediaFlags: MediaFlags, logger: MediaUiEventLogger, smartspaceManager: SmartspaceManager, + keyguardUpdateMonitor: KeyguardUpdateMonitor, ) : this( context, backgroundExecutor, @@ -264,6 +267,7 @@ class MediaDataManager( mediaFlags, logger, smartspaceManager, + keyguardUpdateMonitor, ) private val appChangeReceiver = @@ -1336,7 +1340,9 @@ class MediaDataManager( Assert.isMainThread() val removed = mediaEntries.remove(key) ?: return - if (useMediaResumption && removed.resumeAction != null && removed.isLocalSession()) { + if (keyguardUpdateMonitor.isUserInLockdown(removed.userId)) { + logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) + } else if (useMediaResumption && removed.resumeAction != null && removed.isLocalSession()) { convertToResumePlayer(removed) } else if (mediaFlags.isRetainingPlayersEnabled()) { handlePossibleRemoval(removed, notificationRemoved = true) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt index fac1d5eae7949..b72923a5d22cd 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt @@ -30,13 +30,20 @@ import android.view.ViewGroup import android.view.animation.PathInterpolator import android.widget.LinearLayout import androidx.annotation.VisibleForTesting +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle import com.android.internal.logging.InstanceId +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.Dumpable import com.android.systemui.R import com.android.systemui.classifier.FalsingCollector import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.models.player.MediaViewHolder import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder @@ -63,6 +70,10 @@ import java.io.PrintWriter import java.util.TreeMap import javax.inject.Inject import javax.inject.Provider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.launch private const val TAG = "MediaCarouselController" private val settingsIntent = Intent().setAction(ACTION_MEDIA_CONTROLS_SETTINGS) @@ -91,6 +102,8 @@ constructor( private val logger: MediaUiEventLogger, private val debugLogger: MediaCarouselControllerLogger, private val mediaFlags: MediaFlags, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, ) : Dumpable { /** The current width of the carousel */ private var currentCarouselWidth: Int = 0 @@ -213,6 +226,17 @@ constructor( } } + private val keyguardUpdateMonitorCallback = + object : KeyguardUpdateMonitorCallback() { + override fun onStrongAuthStateChanged(userId: Int) { + if (keyguardUpdateMonitor.isUserInLockdown(userId)) { + hideMediaCarousel() + } else if (keyguardUpdateMonitor.isUserUnlocked(userId)) { + showMediaCarousel() + } + } + } + /** * Update MediaCarouselScrollHandler.visibleToUser to reflect media card container visibility. * It will be called when the container is out of view. @@ -487,6 +511,13 @@ constructor( } } ) + keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) + mediaCarousel.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + // A backup to show media carousel (if available) once the keyguard is gone. + listenForAnyStateToGoneKeyguardTransition(this) + } + } } private fun inflateSettingsButton() { @@ -516,6 +547,23 @@ constructor( return mediaCarousel } + private fun hideMediaCarousel() { + mediaCarousel.visibility = View.GONE + } + + private fun showMediaCarousel() { + mediaCarousel.visibility = View.VISIBLE + } + + @VisibleForTesting + internal fun listenForAnyStateToGoneKeyguardTransition(scope: CoroutineScope): Job { + return scope.launch { + keyguardTransitionInteractor.anyStateToGoneTransition + .filter { it.transitionState == TransitionState.FINISHED } + .collect { showMediaCarousel() } + } + } + private fun reorderAllPlayers( previousVisiblePlayerKey: MediaPlayerData.MediaSortKey?, key: String? = null diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt index 5c7d2421cd837..6f1b42bf1d193 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt @@ -40,6 +40,7 @@ import android.testing.TestableLooper.RunWithLooper import androidx.media.utils.MediaConstants import androidx.test.filters.SmallTest import com.android.internal.logging.InstanceId +import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.InstanceIdSequenceFake import com.android.systemui.R import com.android.systemui.SysuiTestCase @@ -126,6 +127,7 @@ class MediaDataManagerTest : SysuiTestCase() { @Mock lateinit var pendingIntent: PendingIntent @Mock lateinit var activityStarter: ActivityStarter @Mock lateinit var smartspaceManager: SmartspaceManager + @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor lateinit var smartspaceMediaDataProvider: SmartspaceMediaDataProvider @Mock lateinit var mediaSmartspaceTarget: SmartspaceTarget @Mock private lateinit var mediaRecommendationItem: SmartspaceAction @@ -187,6 +189,7 @@ class MediaDataManagerTest : SysuiTestCase() { mediaFlags = mediaFlags, logger = logger, smartspaceManager = smartspaceManager, + keyguardUpdateMonitor = keyguardUpdateMonitor ) verify(tunerService) .addTunable(capture(tunableCaptor), eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION)) @@ -242,6 +245,7 @@ class MediaDataManagerTest : SysuiTestCase() { whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(false) whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false) whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId()) + whenever(keyguardUpdateMonitor.isUserInLockdown(any())).thenReturn(false) } @After @@ -682,6 +686,19 @@ class MediaDataManagerTest : SysuiTestCase() { verify(listener).onMediaDataRemoved(eq("0:$PACKAGE_NAME")) } + fun testOnNotificationRemoved_lockDownMode() { + whenever(keyguardUpdateMonitor.isUserInLockdown(any())).thenReturn(true) + + addNotificationAndLoad() + val data = mediaDataCaptor.value + mediaDataManager.onNotificationRemoved(KEY) + + verify(listener, never()).onMediaDataRemoved(eq(KEY)) + verify(logger, never()) + .logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + } + @Test fun testAddResumptionControls() { // WHEN resumption controls are added diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt index e201b6b67f916..997198e116c08 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt @@ -21,12 +21,20 @@ import android.content.res.Configuration import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.util.MathUtils.abs +import android.view.View import androidx.test.filters.SmallTest import com.android.internal.logging.InstanceId +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.media.controls.MediaTestUtils import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData @@ -49,6 +57,9 @@ import javax.inject.Provider import junit.framework.Assert.assertEquals import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Ignore import org.junit.Test @@ -90,11 +101,15 @@ class MediaCarouselControllerTest : SysuiTestCase() { @Mock lateinit var mediaCarousel: MediaScrollView @Mock lateinit var pageIndicator: PageIndicator @Mock lateinit var mediaFlags: MediaFlags + @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + @Mock lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor + private lateinit var transitionRepository: FakeKeyguardTransitionRepository @Captor lateinit var listener: ArgumentCaptor @Captor lateinit var configListener: ArgumentCaptor @Captor lateinit var newConfig: ArgumentCaptor @Captor lateinit var visualStabilityCallback: ArgumentCaptor + @Captor lateinit var keyguardCallback: ArgumentCaptor private val clock = FakeSystemClock() private lateinit var mediaCarouselController: MediaCarouselController @@ -102,6 +117,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { @Before fun setup() { MockitoAnnotations.initMocks(this) + transitionRepository = FakeKeyguardTransitionRepository() mediaCarouselController = MediaCarouselController( context, @@ -119,11 +135,14 @@ class MediaCarouselControllerTest : SysuiTestCase() { logger, debugLogger, mediaFlags, + keyguardUpdateMonitor, + KeyguardTransitionInteractor(repository = transitionRepository), ) verify(configurationController).addCallback(capture(configListener)) verify(mediaDataManager).addListener(capture(listener)) verify(visualStabilityProvider) .addPersistentReorderingAllowedListener(capture(visualStabilityCallback)) + verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCallback)) whenever(mediaControlPanelFactory.get()).thenReturn(panel) whenever(panel.mediaViewController).thenReturn(mediaViewController) whenever(mediaDataManager.smartspaceMediaData).thenReturn(smartspaceMediaData) @@ -740,4 +759,41 @@ class MediaCarouselControllerTest : SysuiTestCase() { assertTrue(MediaPlayerData.visiblePlayerKeys().elementAt(0).isSsMediaRec) assertFalse(MediaPlayerData.visiblePlayerKeys().elementAt(0).data.active) } + + @Test + fun testOnLockDownMode_hideMediaCarousel() { + whenever(keyguardUpdateMonitor.isUserInLockdown(context.userId)).thenReturn(true) + mediaCarouselController.mediaCarousel = mediaCarousel + + keyguardCallback.value.onStrongAuthStateChanged(context.userId) + + verify(mediaCarousel).visibility = View.GONE + } + + @Test + fun testLockDownModeOff_showMediaCarousel() { + whenever(keyguardUpdateMonitor.isUserInLockdown(context.userId)).thenReturn(false) + whenever(keyguardUpdateMonitor.isUserUnlocked(context.userId)).thenReturn(true) + mediaCarouselController.mediaCarousel = mediaCarousel + + keyguardCallback.value.onStrongAuthStateChanged(context.userId) + + verify(mediaCarousel).visibility = View.VISIBLE + } + + @ExperimentalCoroutinesApi + @Test + fun testKeyguardGone_showMediaCarousel() = + runTest(UnconfinedTestDispatcher()) { + mediaCarouselController.mediaCarousel = mediaCarousel + + val job = mediaCarouselController.listenForAnyStateToGoneKeyguardTransition(this) + transitionRepository.sendTransitionStep( + TransitionStep(to = KeyguardState.GONE, transitionState = TransitionState.FINISHED) + ) + + verify(mediaCarousel).visibility = View.VISIBLE + + job.cancel() + } }