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

Commit de20b87b authored by Michael Mikhail's avatar Michael Mikhail
Browse files

[Flexiglass] remove unnecessary view-models in media.controls package

Flag: com.android.systemui.scene_container
Bug: 436384980
Test: build.
Change-Id: Ib9bdbecb7849b81c975e0a3fce29e6a3919cb27c
parent 16509cc3
Loading
Loading
Loading
Loading
+0 −168
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.platform.test.annotations.DisableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.logging.InstanceId
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.DisableSceneContainer
import com.android.systemui.kosmos.testScope
import com.android.systemui.media.controls.domain.pipeline.MediaDataFilterImpl
import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor
import com.android.systemui.media.controls.domain.pipeline.mediaDataFilter
import com.android.systemui.media.controls.shared.mediaLogger
import com.android.systemui.media.controls.shared.mockMediaLogger
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.statusbar.notificationLockscreenUserManager
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
import org.mockito.Mockito
import org.mockito.kotlin.eq
import org.mockito.kotlin.never
import org.mockito.kotlin.reset
import org.mockito.kotlin.verify

@SmallTest
@RunWith(AndroidJUnit4::class)
@DisableFlags(Flags.FLAG_MEDIA_CONTROLS_IN_COMPOSE)
@DisableSceneContainer
class MediaCarouselViewModelTest : SysuiTestCase() {

    private val kosmos = testKosmos().apply { mediaLogger = mockMediaLogger }
    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 underTest: MediaCarouselViewModel = kosmos.mediaCarouselViewModel

    @Before
    fun setUp() {
        kosmos.mediaCarouselInteractor.start()

        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)

        context.setMockPackageManager(packageManager)
    }

    @Test
    fun loadMediaControls_mediaItemsAreUpdated() =
        testScope.runTest {
            val sortedMedia by collectLastValue(underTest.mediaItems)
            val instanceId1 = InstanceId.fakeInstanceId(123)
            val instanceId2 = InstanceId.fakeInstanceId(456)

            loadMediaControl(KEY, instanceId1, isPlaying = true)
            loadMediaControl(KEY_2, instanceId2, isPlaying = true)
            loadMediaControl(KEY, instanceId1, isPlaying = false)

            var mediaControl2 = sortedMedia?.get(0) as MediaControlViewModel
            var mediaControl1 = sortedMedia?.get(1) as MediaControlViewModel
            assertThat(mediaControl2.instanceId).isEqualTo(instanceId2)
            assertThat(mediaControl1.instanceId).isEqualTo(instanceId1)

            loadMediaControl(KEY, instanceId1, isPlaying = true)
            loadMediaControl(KEY_2, instanceId2, isPlaying = false)

            mediaControl2 = sortedMedia?.get(0) as MediaControlViewModel
            mediaControl1 = sortedMedia?.get(1) as MediaControlViewModel
            assertThat(mediaControl2.instanceId).isEqualTo(instanceId2)
            assertThat(mediaControl1.instanceId).isEqualTo(instanceId1)

            underTest.onReorderingAllowed()

            mediaControl1 = sortedMedia?.get(0) as MediaControlViewModel
            mediaControl2 = sortedMedia?.get(1) as MediaControlViewModel
            assertThat(mediaControl1.instanceId).isEqualTo(instanceId1)
            assertThat(mediaControl2.instanceId).isEqualTo(instanceId2)
        }

    @Test
    fun addMediaControlThenRemove_mediaEventsAreLogged() =
        testScope.runTest {
            val sortedMedia by collectLastValue(underTest.mediaItems)
            val instanceId = InstanceId.fakeInstanceId(123)

            loadMediaControl(KEY, instanceId)

            val mediaControl = sortedMedia?.get(0) as MediaControlViewModel
            assertThat(mediaControl.instanceId).isEqualTo(instanceId)

            // when media control is added to carousel
            mediaControl.onAdded(mediaControl)

            verify(kosmos.mediaLogger).logMediaCardAdded(eq(instanceId))

            reset(kosmos.mediaLogger)

            // when media control is updated.
            mediaControl.onUpdated(mediaControl)

            verify(kosmos.mediaLogger, never()).logMediaCardAdded(eq(instanceId))

            mediaDataFilter.onMediaDataRemoved(KEY, true)
            assertThat(sortedMedia).isEmpty()

            // when media control is removed from carousel
            mediaControl.onRemoved(true)

            verify(kosmos.mediaLogger).logMediaCardRemoved(eq(instanceId))
        }

    private fun loadMediaControl(key: String, instanceId: InstanceId, isPlaying: Boolean = true) {
        whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true)
        whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true)
        val mediaData =
            MediaData(
                userId = USER_ID,
                packageName = PACKAGE_NAME,
                notificationKey = key,
                instanceId = instanceId,
                isPlaying = isPlaying,
            )

        mediaDataFilter.onMediaDataLoaded(key, key, mediaData)
    }

    companion object {
        private const val USER_ID = 0
        private const val KEY = "key"
        private const val KEY_2 = "key2"
        private const val PACKAGE_NAME = "com.example.app"
    }
}
+0 −346
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.content.packageManager
import android.content.pm.ApplicationInfo
import android.media.MediaMetadata
import android.media.session.MediaSession
import android.media.session.PlaybackState
import android.platform.test.annotations.EnableFlags
import androidx.constraintlayout.widget.ConstraintSet
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.media.LocalMediaManager.MediaDeviceState
import com.android.systemui.Flags.FLAG_ENABLE_SUGGESTED_DEVICE_UI
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.mediaDataFilter
import com.android.systemui.media.controls.shared.model.MediaButton
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.MediaDeviceData
import com.android.systemui.media.controls.shared.model.SuggestedMediaDeviceData
import com.android.systemui.media.controls.shared.model.SuggestionData
import com.android.systemui.media.controls.util.mediaInstanceId
import com.android.systemui.res.R
import com.android.systemui.statusbar.notificationLockscreenUserManager
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito
import org.mockito.kotlin.eq
import org.mockito.kotlin.whenever

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

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

    private val mediaDataFilter = 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 = kosmos.mediaInstanceId
    private val underTest: MediaControlViewModel = kosmos.mediaControlViewModel

    @Before
    fun setUp() {
        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)
        context.setMockPackageManager(packageManager)
    }

    @Test
    fun addMediaControl_mediaControlViewModelIsLoaded() =
        testScope.runTest {
            val playerModel by collectLastValue(underTest.player)
            val mediaData = initMediaData(ARTIST, TITLE)

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

    @Test
    fun emitDuplicateMediaControls_mediaControlIsNotBound() =
        testScope.runTest {
            val playerModel by collectLastValue(underTest.player)
            val mediaData = initMediaData(ARTIST, TITLE)

            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)

            assertThat(playerModel).isNotNull()
            assertThat(playerModel?.titleName).isEqualTo(TITLE)
            assertThat(playerModel?.artistName).isEqualTo(ARTIST)
            assertThat(underTest.setPlayer(playerModel!!)).isTrue()

            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)

            assertThat(playerModel).isNotNull()
            assertThat(playerModel?.titleName).isEqualTo(TITLE)
            assertThat(playerModel?.artistName).isEqualTo(ARTIST)
            assertThat(underTest.setPlayer(playerModel!!)).isFalse()
        }

    @Test
    fun emitDifferentMediaControls_mediaControlIsBound() =
        testScope.runTest {
            val playerModel by collectLastValue(underTest.player)
            var mediaData = initMediaData(ARTIST, TITLE)

            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)

            assertThat(playerModel).isNotNull()
            assertThat(playerModel?.titleName).isEqualTo(TITLE)
            assertThat(playerModel?.artistName).isEqualTo(ARTIST)
            assertThat(underTest.setPlayer(playerModel!!)).isTrue()

            mediaData = initMediaData(ARTIST_2, TITLE_2)

            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)

            assertThat(playerModel).isNotNull()
            assertThat(playerModel?.titleName).isEqualTo(TITLE_2)
            assertThat(playerModel?.artistName).isEqualTo(ARTIST_2)
            assertThat(underTest.setPlayer(playerModel!!)).isTrue()
        }

    @Test
    fun reservedButtons_showScrubbingTimes() =
        testScope.runTest {
            val playerModel by collectLastValue(underTest.player)
            val mediaData =
                initMediaData(ARTIST, TITLE)
                    .copy(semanticActions = MediaButton(reserveNext = true, reservePrev = true))

            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)

            assertThat(playerModel?.actionButtons).isNotNull()
            assertThat(playerModel!!.useSemanticActions).isTrue()
            assertThat(playerModel!!.canShowTime).isTrue()

            val buttons = playerModel!!.actionButtons

            val prevButton = buttons.find { it.buttonId == R.id.actionPrev }!!
            assertThat(prevButton.notVisibleValue).isEqualTo(ConstraintSet.GONE)
            assertThat(prevButton.isVisibleWhenScrubbing).isEqualTo(false)

            val nextButton = buttons.find { it.buttonId == R.id.actionNext }!!
            assertThat(nextButton.notVisibleValue).isEqualTo(ConstraintSet.GONE)
            assertThat(nextButton.isVisibleWhenScrubbing).isEqualTo(false)
        }

    @EnableFlags(FLAG_ENABLE_SUGGESTED_DEVICE_UI)
    @Test
    fun onMediaDataLoadedWithNoSuggestionData() =
        testScope.runTest {
            val playerModel by collectLastValue(underTest.player)
            val mediaData = initMediaData(artist = ARTIST, title = TITLE, suggestionData = null)

            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)

            val suggestionModel = playerModel!!.deviceSuggestion
            assertThat(suggestionModel.isValidSuggestion).isFalse()
            assertThat(suggestionModel.buttonText).isNull()
            assertThat(suggestionModel.onClicked).isNull()
            assertThat(suggestionModel.isConnecting).isFalse()
            assertThat(suggestionModel.icon).isNull()
        }

    @EnableFlags(FLAG_ENABLE_SUGGESTED_DEVICE_UI)
    @Test
    fun onMediaDataLoadedWithConnectingSuggestionData() =
        testScope.runTest {
            val playerModel by collectLastValue(underTest.player)
            val mediaData =
                initMediaData(
                    artist = ARTIST,
                    title = TITLE,
                    suggestionData =
                        createSuggestionData(DEVICE_NAME, MediaDeviceState.STATE_CONNECTING),
                )

            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)

            val suggestionModel = playerModel!!.deviceSuggestion
            assertThat(suggestionModel.isValidSuggestion).isTrue()
            assertThat(suggestionModel.buttonText)
                .isEqualTo(
                    context.getString(R.string.media_suggestion_disconnected_text, DEVICE_NAME)
                )
            assertThat(suggestionModel.onClicked).isNull()
            assertThat(suggestionModel.isConnecting).isTrue()
            assertThat(suggestionModel.icon).isNotNull()
        }

    @EnableFlags(FLAG_ENABLE_SUGGESTED_DEVICE_UI)
    @Test
    fun onMediaDataLoadedWithDisconnectedSuggestionData() =
        testScope.runTest {
            val playerModel by collectLastValue(underTest.player)
            val mediaData =
                initMediaData(
                    artist = ARTIST,
                    title = TITLE,
                    suggestionData =
                        createSuggestionData(DEVICE_NAME, MediaDeviceState.STATE_DISCONNECTED),
                )

            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)

            val suggestionModel = playerModel!!.deviceSuggestion
            assertThat(suggestionModel.isValidSuggestion).isTrue()
            assertThat(suggestionModel.buttonText)
                .isEqualTo(
                    context.getString(R.string.media_suggestion_disconnected_text, DEVICE_NAME)
                )
            assertThat(suggestionModel.onClicked).isNotNull()
            assertThat(suggestionModel.isConnecting).isFalse()
            assertThat(suggestionModel.icon).isNotNull()
        }

    @EnableFlags(FLAG_ENABLE_SUGGESTED_DEVICE_UI)
    @Test
    fun onMediaDataLoadedWithErrorSuggestionData() =
        testScope.runTest {
            val playerModel by collectLastValue(underTest.player)
            val mediaData =
                initMediaData(
                    artist = ARTIST,
                    title = TITLE,
                    suggestionData =
                        createSuggestionData(DEVICE_NAME, MediaDeviceState.STATE_CONNECTING_FAILED),
                )

            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)

            val suggestionModel = playerModel!!.deviceSuggestion
            assertThat(suggestionModel.isValidSuggestion).isTrue()
            assertThat(suggestionModel.buttonText)
                .isEqualTo(context.getString(R.string.media_suggestion_failure_text))
            assertThat(suggestionModel.onClicked).isNotNull()
            assertThat(suggestionModel.isConnecting).isFalse()
            assertThat(suggestionModel.icon).isNotNull()
        }

    @EnableFlags(FLAG_ENABLE_SUGGESTED_DEVICE_UI)
    @Test
    fun onMediaDataLoadedWithConnectedSuggestionData() =
        testScope.runTest {
            val playerModel by collectLastValue(underTest.player)
            val mediaData =
                initMediaData(
                    artist = ARTIST,
                    title = TITLE,
                    suggestionData =
                        createSuggestionData(DEVICE_NAME, MediaDeviceState.STATE_CONNECTED),
                )

            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)

            val suggestionModel = playerModel!!.deviceSuggestion
            assertThat(suggestionModel.isValidSuggestion).isFalse()
            assertThat(suggestionModel.buttonText).isNull()
            assertThat(suggestionModel.onClicked).isNull()
            assertThat(suggestionModel.isConnecting).isFalse()
            assertThat(suggestionModel.icon).isNull()
        }

    private fun initMediaData(
        artist: String,
        title: String,
        suggestionData: SuggestionData? = null,
    ): MediaData {
        val device = MediaDeviceData(true, null, DEVICE_NAME, null)

        // 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,
            suggestionData = suggestionData,
            instanceId = instanceId,
        )
    }

    private fun createSuggestionData(deviceName: String, state: Int) =
        SuggestionData(
            suggestedMediaDeviceData =
                SuggestedMediaDeviceData(
                    name = deviceName,
                    icon = drawable!!,
                    connectionState = state,
                    connect = {},
                ),
            onSuggestionSpaceVisible = Runnable {},
        )

    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 ARTIST_2 = "ARTIST_2"
        private const val TITLE_2 = "TITLE_2"
        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"
    }
}
+2 −2
Original line number Diff line number Diff line
@@ -20,8 +20,6 @@ import static android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS;

import static com.android.systemui.Flags.communalHub;
import static com.android.systemui.media.controls.domain.pipeline.MediaActionsKt.getNotificationActions;
import static com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.MEDIA_PLAYER_SCRIM_END_ALPHA;
import static com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.MEDIA_PLAYER_SCRIM_START_ALPHA;

import android.animation.Animator;
import android.animation.AnimatorInflater;
@@ -177,6 +175,8 @@ public class MediaControlPanel {
    // Time in millis for playing turbulence noise that is played after a touch ripple.
    @VisibleForTesting
    static final long TURBULENCE_NOISE_PLAY_DURATION = 7500L;
    public static final float MEDIA_PLAYER_SCRIM_START_ALPHA = 0.65f;
    public static final float MEDIA_PLAYER_SCRIM_END_ALPHA = 0.75f;

    private final SeekBarViewModel mSeekBarViewModel;
    private final CommunalSceneInteractor mCommunalSceneInteractor;
+0 −28
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.graphics.drawable.Drawable

/** Models UI state for media guts menu */
data class GutsViewModel(
    val gutsText: CharSequence,
    val isDismissEnabled: Boolean = true,
    val onDismissClicked: () -> Unit,
    val cancelTextBackground: Drawable?,
    val onSettingsClicked: () -> Unit,
)
+0 −47

File deleted.

Preview size limit exceeded, changes collapsed.

Loading