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

Commit 544550eb authored by Michael Mikhail's avatar Michael Mikhail
Browse files

Add media view-model classes

Flag: ACONFIG media_controls_refactor DISABLED
Bug: 328207006
Test: atest MediaControlPanelTest
Test: atest SystemUiRoboTests:MediaControlInteractorTest

Change-Id: I0e0ef9cc0cebfa77cb7dc5fb00f24fd879b17b6d
parent b2818816
Loading
Loading
Loading
Loading
+127 −1
Original line number Diff line number Diff line
@@ -16,10 +16,17 @@

package com.android.systemui.media.controls.domain.interactor

import android.app.PendingIntent
import android.os.Bundle
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.logging.InstanceId
import com.android.systemui.SysuiTestCase
import com.android.systemui.activityIntentHelper
import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.animation.Expandable
import com.android.systemui.bluetooth.mockBroadcastDialogController
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.media.controls.domain.pipeline.MediaDataFilterImpl
@@ -28,13 +35,22 @@ import com.android.systemui.media.controls.domain.pipeline.interactor.mediaContr
import com.android.systemui.media.controls.domain.pipeline.mediaDataFilter
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.util.mediaInstanceId
import com.android.systemui.media.mediaOutputDialogManager
import com.android.systemui.mockActivityIntentHelper
import com.android.systemui.plugins.activityStarter
import com.android.systemui.statusbar.notificationLockscreenUserManager
import com.android.systemui.statusbar.policy.keyguardStateController
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.never
import org.mockito.Mockito.verify

@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -44,10 +60,16 @@ class MediaControlInteractorTest : SysuiTestCase() {
    private val testScope = kosmos.testScope

    private val mediaDataFilter: MediaDataFilterImpl = kosmos.mediaDataFilter
    private val activityStarter = kosmos.activityStarter
    private val keyguardStateController = kosmos.keyguardStateController
    private val instanceId: InstanceId = kosmos.mediaInstanceId
    private val notificationLockscreenUserManager = kosmos.notificationLockscreenUserManager

    private val underTest: MediaControlInteractor = kosmos.mediaControlInteractor
    private val underTest: MediaControlInteractor =
        with(kosmos) {
            activityIntentHelper = mockActivityIntentHelper
            kosmos.mediaControlInteractor
        }

    @Test
    fun onMediaDataUpdated() =
@@ -84,9 +106,113 @@ class MediaControlInteractorTest : SysuiTestCase() {
            assertThat(controlModel?.artistName).isEqualTo(SESSION_ARTIST_2)
        }

    @Test
    fun startSettings() {
        underTest.startSettings()

        verify(activityStarter).startActivity(any(), eq(true))
    }

    @Test
    fun startClickIntent_showOverLockscreen() {
        whenever(keyguardStateController.isShowing).thenReturn(true)
        whenever(kosmos.activityIntentHelper.wouldPendingShowOverLockscreen(any(), any()))
            .thenReturn(true)

        val clickIntent = mock<PendingIntent> { whenever(isActivity).thenReturn(true) }
        val expandable = mock<Expandable>()

        underTest.startClickIntent(expandable, clickIntent)

        verify(clickIntent).send(any<Bundle>())
    }

    @Test
    fun startClickIntent_hideOverLockscreen() {
        whenever(keyguardStateController.isShowing).thenReturn(false)

        val clickIntent = mock<PendingIntent> { whenever(isActivity).thenReturn(true) }
        val expandable = mock<Expandable>()
        val activityController = mock<ActivityTransitionAnimator.Controller>()
        whenever(expandable.activityTransitionController(any())).thenReturn(activityController)

        underTest.startClickIntent(expandable, clickIntent)

        verify(activityStarter)
            .postStartActivityDismissingKeyguard(eq(clickIntent), eq(activityController))
    }

    @Test
    fun startDeviceIntent_showOverLockscreen() {
        whenever(keyguardStateController.isShowing).thenReturn(true)
        whenever(kosmos.activityIntentHelper.wouldPendingShowOverLockscreen(any(), any()))
            .thenReturn(true)

        val deviceIntent = mock<PendingIntent> { whenever(isActivity).thenReturn(true) }

        underTest.startDeviceIntent(deviceIntent)

        verify(deviceIntent).send(any<Bundle>())
    }

    @Test
    fun startDeviceIntent_intentNotActivity() {
        whenever(keyguardStateController.isShowing).thenReturn(true)
        whenever(kosmos.activityIntentHelper.wouldPendingShowOverLockscreen(any(), any()))
            .thenReturn(true)

        val deviceIntent = mock<PendingIntent> { whenever(isActivity).thenReturn(false) }

        underTest.startDeviceIntent(deviceIntent)

        verify(deviceIntent, never()).send(any<Bundle>())
    }

    @Test
    fun startDeviceIntent_hideOverLockscreen() {
        whenever(keyguardStateController.isShowing).thenReturn(false)

        val deviceIntent = mock<PendingIntent> { whenever(isActivity).thenReturn(true) }

        underTest.startDeviceIntent(deviceIntent)

        verify(activityStarter).postStartActivityDismissingKeyguard(eq(deviceIntent))
    }

    @Test
    fun startMediaOutputDialog() {
        val expandable = mock<Expandable>()
        val dialogTransitionController = mock<DialogTransitionAnimator.Controller>()
        whenever(expandable.dialogTransitionController(any()))
            .thenReturn(dialogTransitionController)

        underTest.startMediaOutputDialog(expandable, PACKAGE_NAME)

        verify(kosmos.mediaOutputDialogManager)
            .createAndShowWithController(eq(PACKAGE_NAME), eq(true), eq(dialogTransitionController))
    }

    @Test
    fun startBroadcastDialog() {
        val expandable = mock<Expandable>()
        val dialogTransitionController = mock<DialogTransitionAnimator.Controller>()
        whenever(expandable.dialogTransitionController()).thenReturn(dialogTransitionController)

        underTest.startBroadcastDialog(expandable, APP_NAME, PACKAGE_NAME)

        verify(kosmos.mockBroadcastDialogController)
            .createBroadcastDialogWithController(
                eq(APP_NAME),
                eq(PACKAGE_NAME),
                eq(dialogTransitionController)
            )
    }

    companion object {
        private const val USER_ID = 0
        private const val KEY = "key"
        private const val PACKAGE_NAME = "com.example.app"
        private const val APP_NAME = "app"
        private const val SESSION_ARTIST = "artist"
        private const val SESSION_ARTIST_2 = "artist2"
    }
+18 −0
Original line number Diff line number Diff line
@@ -56,4 +56,22 @@ public class BroadcastDialogController {
            broadcastDialog.show();
        }
    }

    /** Creates a [BroadcastDialog] for the user to switch broadcast or change the output device
     *
     * @param currentBroadcastAppName Indicates the APP name currently broadcasting
     * @param outputPkgName Indicates the output media package name to be switched
     * @param controller Indicates the dialog controller of the source view.
     */
    public void createBroadcastDialogWithController(
            String currentBroadcastAppName, String outputPkgName,
            DialogTransitionAnimator.Controller controller) {
        SystemUIDialog broadcastDialog = mBroadcastDialogFactory.create(
                currentBroadcastAppName, outputPkgName).createDialog();
        if (controller != null) {
            mDialogTransitionAnimator.show(broadcastDialog, controller);
        } else {
            broadcastDialog.show();
        }
    }
}
+13 −2
Original line number Diff line number Diff line
@@ -590,7 +590,7 @@ class MediaDataProcessor(
    }

    /** Dismiss a media entry. Returns false if the key was not found. */
    fun dismissMediaData(key: String, delay: Long): Boolean {
    fun dismissMediaData(key: String, delayMs: Long): Boolean {
        val existed = mediaDataRepository.mediaEntries.value[key] != null
        backgroundExecutor.execute {
            mediaDataRepository.mediaEntries.value[key]?.let { mediaData ->
@@ -602,10 +602,21 @@ class MediaDataProcessor(
                }
            }
        }
        foregroundExecutor.executeDelayed({ removeEntry(key) }, delay)
        foregroundExecutor.executeDelayed({ removeEntry(key) }, delayMs)
        return existed
    }

    /** Dismiss a media entry. Returns false if the corresponding key was not found. */
    fun dismissMediaData(instanceId: InstanceId, delayMs: Long): Boolean {
        val mediaEntries = mediaDataRepository.mediaEntries.value
        val filteredEntries = mediaEntries.filter { (_, data) -> data.instanceId == instanceId }
        return if (filteredEntries.isNotEmpty()) {
            dismissMediaData(filteredEntries.keys.first(), delayMs)
        } else {
            false
        }
    }

    /**
     * Called whenever the recommendation has been expired or removed by the user. This will remove
     * the recommendation card entirely from the carousel.
+112 −3
Original line number Diff line number Diff line
@@ -16,20 +16,43 @@

package com.android.systemui.media.controls.domain.pipeline.interactor

import android.app.ActivityOptions
import android.app.BroadcastOptions
import android.app.PendingIntent
import android.content.Intent
import android.media.session.MediaSession
import android.provider.Settings
import android.util.Log
import com.android.internal.jank.Cuj
import com.android.internal.logging.InstanceId
import com.android.systemui.ActivityIntentHelper
import com.android.systemui.animation.DialogCuj
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.animation.Expandable
import com.android.systemui.bluetooth.BroadcastDialogController
import com.android.systemui.media.controls.data.repository.MediaFilterRepository
import com.android.systemui.media.controls.domain.pipeline.MediaDataProcessor
import com.android.systemui.media.controls.shared.model.MediaControlModel
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.dialog.MediaOutputDialogManager
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.statusbar.policy.KeyguardStateController
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map

/** Encapsulates business logic for single media control. */
class MediaControlInteractor(
    instanceId: InstanceId,
    private val instanceId: InstanceId,
    repository: MediaFilterRepository,
    private val mediaDataProcessor: MediaDataProcessor,
    private val keyguardStateController: KeyguardStateController,
    private val activityStarter: ActivityStarter,
    private val activityIntentHelper: ActivityIntentHelper,
    private val lockscreenUserManager: NotificationLockscreenUserManager,
    private val mediaOutputDialogManager: MediaOutputDialogManager,
    private val broadcastDialogController: BroadcastDialogController,
) {

    val mediaControl: Flow<MediaControlModel?> =
@@ -37,8 +60,19 @@ class MediaControlInteractor(
            .map { entries -> entries[instanceId]?.let { toMediaControlModel(it) } }
            .distinctUntilChanged()

    fun removeMediaControl(key: String, delayMs: Long): Boolean {
        return mediaDataProcessor.dismissMediaData(key, delayMs)
    fun removeMediaControl(
        token: MediaSession.Token?,
        instanceId: InstanceId,
        delayMs: Long
    ): Boolean {
        val dismissed = mediaDataProcessor.dismissMediaData(instanceId, delayMs)
        if (!dismissed) {
            Log.w(
                TAG,
                "Manager failed to dismiss media of instanceId=$instanceId, Token uid=${token?.uid}"
            )
        }
        return dismissed
    }

    private fun toMediaControlModel(data: MediaData): MediaControlModel {
@@ -53,14 +87,89 @@ class MediaControlInteractor(
                appName = app,
                songName = song,
                artistName = artist,
                showExplicit = isExplicit,
                artwork = artwork,
                deviceData = device,
                semanticActionButtons = semanticActions,
                notificationActionButtons = actions,
                actionsToShowInCollapsed = actionsToShowInCompact,
                isDismissible = isClearable,
                isResume = resumption,
                resumeProgress = resumeProgress,
            )
        }
    }

    fun startSettings() {
        activityStarter.startActivity(SETTINGS_INTENT, /* dismissShade= */ true)
    }

    fun startClickIntent(expandable: Expandable, clickIntent: PendingIntent) {
        if (!launchOverLockscreen(clickIntent)) {
            activityStarter.postStartActivityDismissingKeyguard(
                clickIntent,
                expandable.activityTransitionController(Cuj.CUJ_SHADE_APP_LAUNCH_FROM_MEDIA_PLAYER)
            )
        }
    }

    fun startDeviceIntent(deviceIntent: PendingIntent) {
        if (deviceIntent.isActivity) {
            if (!launchOverLockscreen(deviceIntent)) {
                activityStarter.postStartActivityDismissingKeyguard(deviceIntent)
            }
        } else {
            Log.w(TAG, "Device pending intent of instanceId=$instanceId is not an activity.")
        }
    }

    private fun launchOverLockscreen(pendingIntent: PendingIntent): Boolean {
        val showOverLockscreen =
            keyguardStateController.isShowing &&
                activityIntentHelper.wouldPendingShowOverLockscreen(
                    pendingIntent,
                    lockscreenUserManager.currentUserId
                )
        if (showOverLockscreen) {
            try {
                val options = BroadcastOptions.makeBasic()
                options.isInteractive = true
                options.pendingIntentBackgroundActivityStartMode =
                    ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
                pendingIntent.send(options.toBundle())
            } catch (e: PendingIntent.CanceledException) {
                Log.e(TAG, "pending intent of $instanceId was canceled")
            }
            return true
        }
        return false
    }

    fun startMediaOutputDialog(expandable: Expandable, packageName: String) {
        mediaOutputDialogManager.createAndShowWithController(
            packageName,
            true,
            expandable.dialogController()
        )
    }

    fun startBroadcastDialog(expandable: Expandable, broadcastApp: String, packageName: String) {
        broadcastDialogController.createBroadcastDialogWithController(
            broadcastApp,
            packageName,
            expandable.dialogTransitionController()
        )
    }

    private fun Expandable.dialogController(): DialogTransitionAnimator.Controller? {
        return dialogTransitionController(
            cuj =
                DialogCuj(Cuj.CUJ_SHADE_DIALOG_OPEN, MediaOutputDialogManager.INTERACTION_JANK_TAG)
        )
    }

    companion object {
        private const val TAG = "MediaControlInteractor"
        private val SETTINGS_INTENT = Intent(Settings.ACTION_MEDIA_CONTROLS_SETTINGS)
    }
}
+2 −0
Original line number Diff line number Diff line
@@ -33,6 +33,7 @@ data class MediaControlModel(
    val appName: String?,
    val songName: CharSequence?,
    val artistName: CharSequence?,
    val showExplicit: Boolean,
    val artwork: Icon?,
    val deviceData: MediaDeviceData?,
    /** [MediaButton] contains [MediaAction] objects which represent specific buttons in the UI */
@@ -43,6 +44,7 @@ data class MediaControlModel(
     * [Notification.MediaStyle.setShowActionsInCompactView].
     */
    val actionsToShowInCollapsed: List<Int>,
    val isDismissible: Boolean,
    /** Whether player is in resumption state. */
    val isResume: Boolean,
    /** Track seek bar progress (0 - 1) when [isResume] is true. */
Loading