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

Commit 69ef80b3 authored by Michael Mikhail's avatar Michael Mikhail
Browse files

Add media control view model.

Flag: ACONFIG media_control_refactor DISABLED
Bug: 328207006
Test: atest SystemUiRoboTests:MediaControlViewModelTest
Change-Id: Ic3c73941f0458cf2613bd70d87b7a59b076000c9
parent 544550eb
Loading
Loading
Loading
Loading
+8 −10
Original line number Diff line number Diff line
@@ -77,33 +77,31 @@ class MediaControlInteractorTest : SysuiTestCase() {
            whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true)
            whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true)
            val controlModel by collectLastValue(underTest.mediaControl)
            var mediaData =
                MediaData(userId = USER_ID, instanceId = instanceId, artist = SESSION_ARTIST)
            var mediaData = MediaData(userId = USER_ID, instanceId = instanceId, artist = ARTIST)

            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)

            assertThat(controlModel?.instanceId).isEqualTo(instanceId)
            assertThat(controlModel?.artistName).isEqualTo(SESSION_ARTIST)
            assertThat(controlModel?.artistName).isEqualTo(ARTIST)

            mediaData =
                MediaData(userId = USER_ID, instanceId = instanceId, artist = SESSION_ARTIST_2)
            mediaData = MediaData(userId = USER_ID, instanceId = instanceId, artist = ARTIST_2)

            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)

            assertThat(controlModel?.instanceId).isEqualTo(instanceId)
            assertThat(controlModel?.artistName).isEqualTo(SESSION_ARTIST_2)
            assertThat(controlModel?.artistName).isEqualTo(ARTIST_2)

            mediaData =
                MediaData(
                    userId = USER_ID,
                    instanceId = InstanceId.fakeInstanceId(2),
                    artist = SESSION_ARTIST
                    artist = ARTIST
                )

            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)

            assertThat(controlModel?.instanceId).isNotEqualTo(mediaData.instanceId)
            assertThat(controlModel?.artistName).isEqualTo(SESSION_ARTIST_2)
            assertThat(controlModel?.artistName).isEqualTo(ARTIST_2)
        }

    @Test
@@ -213,7 +211,7 @@ class MediaControlInteractorTest : SysuiTestCase() {
        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"
        private const val ARTIST = "artist"
        private const val ARTIST_2 = "artist2"
    }
}
+135 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.media.controls.ui.viewmodel

import android.R
import android.content.packageManager
import android.content.pm.ApplicationInfo
import android.media.MediaMetadata
import android.media.session.MediaSession
import android.media.session.PlaybackState
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.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.media.controls.domain.pipeline.MediaDataFilterImpl
import com.android.systemui.media.controls.domain.pipeline.mediaDataFilter
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.MediaDeviceData
import com.android.systemui.media.controls.util.mediaInstanceId
import com.android.systemui.statusbar.notificationLockscreenUserManager
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.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.ArgumentMatchers
import org.mockito.Mockito

@SmallTest
@RunWith(AndroidJUnit4::class)
class MediaControlViewModelTest : SysuiTestCase() {

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope

    private val mediaDataFilter: MediaDataFilterImpl = kosmos.mediaDataFilter
    private val notificationLockscreenUserManager = kosmos.notificationLockscreenUserManager
    private val packageManager = kosmos.packageManager
    private val drawable = context.getDrawable(R.drawable.ic_media_play)
    private val instanceId: InstanceId = kosmos.mediaInstanceId

    private val underTest: MediaControlViewModel = kosmos.mediaControlViewModel

    @Test
    fun addMediaControl_mediaControlViewModelIsLoaded() =
        testScope.runTest {
            whenever(packageManager.getApplicationIcon(Mockito.anyString())).thenReturn(drawable)
            whenever(packageManager.getApplicationIcon(any(ApplicationInfo::class.java)))
                .thenReturn(drawable)
            whenever(packageManager.getApplicationInfo(eq(PACKAGE_NAME), ArgumentMatchers.anyInt()))
                .thenReturn(ApplicationInfo())
            whenever(packageManager.getApplicationLabel(any())).thenReturn(PACKAGE_NAME)
            whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true)
            whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true)
            val playerModel by collectLastValue(underTest.player)

            context.setMockPackageManager(packageManager)

            val mediaData = initMediaData()

            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)

            assertThat(playerModel).isNotNull()
            assertThat(playerModel?.titleName).isEqualTo(TITLE)
            assertThat(playerModel?.artistName).isEqualTo(ARTIST)
            assertThat(playerModel?.gutsMenu).isNotNull()
            assertThat(playerModel?.outputSwitcher).isNotNull()
            assertThat(playerModel?.actionButtons).isNotNull()
            assertThat(playerModel?.playTurbulenceNoise).isFalse()
        }

    private fun initMediaData(): MediaData {
        val device = MediaDeviceData(true, null, DEVICE_NAME, null, showBroadcastButton = true)

        // Create media session
        val metadataBuilder =
            MediaMetadata.Builder().apply {
                putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
                putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
            }
        val playbackBuilder =
            PlaybackState.Builder().apply {
                setState(PlaybackState.STATE_PAUSED, 6000L, 1f)
                setActions(PlaybackState.ACTION_PLAY)
            }
        val session =
            MediaSession(context, SESSION_KEY).apply {
                setMetadata(metadataBuilder.build())
                setPlaybackState(playbackBuilder.build())
            }
        session.isActive = true

        return MediaData(
            userId = USER_ID,
            artist = ARTIST,
            song = TITLE,
            packageName = PACKAGE,
            token = session.sessionToken,
            device = device,
            instanceId = instanceId
        )
    }

    companion object {
        private const val USER_ID = 0
        private const val KEY = "key"
        private const val PACKAGE_NAME = "com.example.app"
        private const val PACKAGE = "PKG"
        private const val ARTIST = "ARTIST"
        private const val TITLE = "TITLE"
        private const val DEVICE_NAME = "DEVICE_NAME"
        private const val SESSION_KEY = "SESSION_KEY"
        private const val SESSION_ARTIST = "SESSION_ARTIST"
        private const val SESSION_TITLE = "SESSION_TITLE"
    }
}
+19 −0
Original line number Diff line number Diff line
@@ -19,8 +19,11 @@ package com.android.systemui.media.controls.domain.pipeline.interactor
import android.app.ActivityOptions
import android.app.BroadcastOptions
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.media.session.MediaController
import android.media.session.MediaSession
import android.media.session.PlaybackState
import android.provider.Settings
import android.util.Log
import com.android.internal.jank.Cuj
@@ -30,6 +33,7 @@ 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.dagger.qualifiers.Application
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
@@ -38,12 +42,14 @@ 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 com.android.systemui.util.kotlin.pairwiseBy
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map

/** Encapsulates business logic for single media control. */
class MediaControlInteractor(
    @Application applicationContext: Context,
    private val instanceId: InstanceId,
    repository: MediaFilterRepository,
    private val mediaDataProcessor: MediaDataProcessor,
@@ -60,6 +66,19 @@ class MediaControlInteractor(
            .map { entries -> entries[instanceId]?.let { toMediaControlModel(it) } }
            .distinctUntilChanged()

    val isStartedPlaying: Flow<Boolean> =
        mediaControl
            .map { mediaControl ->
                mediaControl?.token?.let { token ->
                    MediaController(applicationContext, token).playbackState?.let {
                        it.state == PlaybackState.STATE_PLAYING
                    }
                }
                    ?: false
            }
            .pairwiseBy(initialValue = false) { wasPlaying, isPlaying -> !wasPlaying && isPlaying }
            .distinctUntilChanged()

    fun removeMediaControl(
        token: MediaSession.Token?,
        instanceId: InstanceId,
+19 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.systemui.media.controls.ui.util

import android.app.WallpaperColors
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
@@ -27,6 +28,7 @@ import android.util.Log
import com.android.systemui.media.controls.ui.animation.backgroundEndFromScheme
import com.android.systemui.media.controls.ui.animation.backgroundStartFromScheme
import com.android.systemui.monet.ColorScheme
import com.android.systemui.monet.Style
import com.android.systemui.util.getColorWithAlpha
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
@@ -94,4 +96,21 @@ object MediaArtworkHelper {
            )
        return LayerDrawable(arrayOf(albumArt, gradient))
    }

    /** Returns [ColorScheme] of media app given its [packageName]. */
    fun getColorScheme(
        applicationContext: Context,
        packageName: String,
        tag: String,
        style: Style = Style.TONAL_SPOT
    ): ColorScheme? {
        return try {
            // Set up media source app's logo.
            val icon = applicationContext.packageManager.getApplicationIcon(packageName)
            ColorScheme(WallpaperColors.fromDrawable(icon), darkTheme = true, style)
        } catch (e: PackageManager.NameNotFoundException) {
            Log.w(tag, "Fail to get media app info", e)
            null
        }
    }
}
+403 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading