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 Original line Diff line number Diff line
@@ -77,33 +77,31 @@ class MediaControlInteractorTest : SysuiTestCase() {
            whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true)
            whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true)
            whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true)
            whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true)
            val controlModel by collectLastValue(underTest.mediaControl)
            val controlModel by collectLastValue(underTest.mediaControl)
            var mediaData =
            var mediaData = MediaData(userId = USER_ID, instanceId = instanceId, artist = ARTIST)
                MediaData(userId = USER_ID, instanceId = instanceId, artist = SESSION_ARTIST)


            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)
            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)


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


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


            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)
            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)


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


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


            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)
            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)


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


    @Test
    @Test
@@ -213,7 +211,7 @@ class MediaControlInteractorTest : SysuiTestCase() {
        private const val KEY = "key"
        private const val KEY = "key"
        private const val PACKAGE_NAME = "com.example.app"
        private const val PACKAGE_NAME = "com.example.app"
        private const val APP_NAME = "app"
        private const val APP_NAME = "app"
        private const val SESSION_ARTIST = "artist"
        private const val ARTIST = "artist"
        private const val SESSION_ARTIST_2 = "artist2"
        private const val ARTIST_2 = "artist2"
    }
    }
}
}
+135 −0
Original line number Original line 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 Original line 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.ActivityOptions
import android.app.BroadcastOptions
import android.app.BroadcastOptions
import android.app.PendingIntent
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.Intent
import android.media.session.MediaController
import android.media.session.MediaSession
import android.media.session.MediaSession
import android.media.session.PlaybackState
import android.provider.Settings
import android.provider.Settings
import android.util.Log
import android.util.Log
import com.android.internal.jank.Cuj
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.DialogTransitionAnimator
import com.android.systemui.animation.Expandable
import com.android.systemui.animation.Expandable
import com.android.systemui.bluetooth.BroadcastDialogController
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.data.repository.MediaFilterRepository
import com.android.systemui.media.controls.domain.pipeline.MediaDataProcessor
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.MediaControlModel
@@ -38,12 +42,14 @@ import com.android.systemui.media.dialog.MediaOutputDialogManager
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.kotlin.pairwiseBy
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.map


/** Encapsulates business logic for single media control. */
/** Encapsulates business logic for single media control. */
class MediaControlInteractor(
class MediaControlInteractor(
    @Application applicationContext: Context,
    private val instanceId: InstanceId,
    private val instanceId: InstanceId,
    repository: MediaFilterRepository,
    repository: MediaFilterRepository,
    private val mediaDataProcessor: MediaDataProcessor,
    private val mediaDataProcessor: MediaDataProcessor,
@@ -60,6 +66,19 @@ class MediaControlInteractor(
            .map { entries -> entries[instanceId]?.let { toMediaControlModel(it) } }
            .map { entries -> entries[instanceId]?.let { toMediaControlModel(it) } }
            .distinctUntilChanged()
            .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(
    fun removeMediaControl(
        token: MediaSession.Token?,
        token: MediaSession.Token?,
        instanceId: InstanceId,
        instanceId: InstanceId,
+19 −0
Original line number Original line Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.systemui.media.controls.ui.util


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