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() + } }