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

Commit 72536c07 authored by Andre Le's avatar Andre Le Committed by Android (Google) Code Review
Browse files

Merge changes I8011ae6c,Id3d61623 into main

* changes:
  CastDetailsView: Show up cast chooser UI within the details view
  CastDetailsView: Add more UI support for cast controller details view
parents 2c7d3369 7110b90f
Loading
Loading
Loading
Loading
+70 −12
Original line number Original line Diff line number Diff line
@@ -16,7 +16,7 @@


package com.android.systemui.qs.tiles.dialog
package com.android.systemui.qs.tiles.dialog


import android.content.Context
import android.graphics.drawable.Drawable
import android.media.MediaRouter
import android.media.MediaRouter
import android.provider.Settings
import android.provider.Settings
import android.testing.TestableLooper.RunWithLooper
import android.testing.TestableLooper.RunWithLooper
@@ -27,8 +27,10 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.qs.tiles.base.domain.actions.FakeQSTileIntentUserInputHandler
import com.android.systemui.qs.tiles.base.domain.actions.FakeQSTileIntentUserInputHandler
import com.android.systemui.qs.tiles.base.domain.actions.intentInputs
import com.android.systemui.qs.tiles.base.domain.actions.intentInputs
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.mock
import org.mockito.kotlin.stub
import org.mockito.kotlin.stub
@@ -38,13 +40,22 @@ import org.mockito.kotlin.stub
@RunWith(AndroidJUnit4::class)
@RunWith(AndroidJUnit4::class)
class CastDetailsViewModelTest : SysuiTestCase() {
class CastDetailsViewModelTest : SysuiTestCase() {
    var inputHandler: FakeQSTileIntentUserInputHandler = FakeQSTileIntentUserInputHandler()
    var inputHandler: FakeQSTileIntentUserInputHandler = FakeQSTileIntentUserInputHandler()
    private var context: Context = mock()
    private var mediaRouter: MediaRouter = mock()
    private var selectedRoute: MediaRouter.RouteInfo = mock()
    private var selectedRoute: MediaRouter.RouteInfo = mock()
    private var mediaRouter: MediaRouter =
        mock<MediaRouter> { on { selectedRoute } doReturn selectedRoute }

    @Before
    fun SetUp() {
        // We need to set up a fake system service here since shouldShowChooserDialog access's
        // context system service, and we want to use the mocked selectedRoute to test this
        // function's behavior.
        context.addMockSystemService(MediaRouter::class.java, mediaRouter)
    }


    @Test
    @Test
    fun testClickOnSettingsButton() {
    fun testClickOnSettingsButton() {
        var viewModel = CastDetailsViewModel(inputHandler, context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY)
        val viewModel =
            CastDetailsViewModel(inputHandler, context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY)


        viewModel.clickOnSettingsButton()
        viewModel.clickOnSettingsButton()


@@ -56,14 +67,7 @@ class CastDetailsViewModelTest : SysuiTestCase() {


    @Test
    @Test
    fun testShouldShowChooserDialog() {
    fun testShouldShowChooserDialog() {
        context.stub {
        val viewModel =
            on { getSystemService(MediaRouter::class.java) } doReturn mediaRouter
        }
        mediaRouter.stub {
            on { selectedRoute } doReturn selectedRoute
        }

        var viewModel =
            CastDetailsViewModel(inputHandler, context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY)
            CastDetailsViewModel(inputHandler, context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY)


        assertThat(viewModel.shouldShowChooserDialog())
        assertThat(viewModel.shouldShowChooserDialog())
@@ -74,4 +78,58 @@ class CastDetailsViewModelTest : SysuiTestCase() {
                )
                )
            )
            )
    }
    }

    @Test
    fun shouldShowChooserDialogFalse_subTitleEmpty() {
        selectedRoute.stub {
            on { isDefault } doReturn false
            on { matchesTypes(anyInt()) } doReturn true
        }
        val viewModel =
            CastDetailsViewModel(inputHandler, context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY)

        assertThat(viewModel.shouldShowChooserDialog()).isEqualTo(false)
        assertThat(viewModel.subTitle).isEqualTo("")
    }

    @Test
    fun shouldShowChooserDialogTrue_useDefaultSubTitle() {
        selectedRoute.stub { on { isDefault } doReturn true }
        val viewModel =
            CastDetailsViewModel(inputHandler, context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY)

        assertThat(viewModel.shouldShowChooserDialog()).isEqualTo(true)
        assertThat(viewModel.subTitle).isEqualTo("Searching for devices...")
    }

    @Test
    fun shouldShowChooserDialogTrue_useDefaultTitle() {
        selectedRoute.stub { on { isDefault } doReturn true }
        val viewModel =
            CastDetailsViewModel(inputHandler, context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY)

        assertThat(viewModel.shouldShowChooserDialog()).isEqualTo(true)
        assertThat(viewModel.title).isEqualTo("Cast screen to device")
    }

    @Test
    fun setMediaRouteDeviceTitle() {
        val viewModel =
            CastDetailsViewModel(inputHandler, context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY)

        viewModel.setMediaRouteDeviceTitle("test")

        assertThat(viewModel.title).isEqualTo("test")
    }

    @Test
    fun setMediaRouteDeviceIcon() {
        val viewModel =
            CastDetailsViewModel(inputHandler, context, MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY)
        val testIcon = mock<Drawable>()

        viewModel.setMediaRouteDeviceIcon(testIcon)

        assertThat(viewModel.deviceIcon).isEqualTo(testIcon)
    }
}
}
+58 −3
Original line number Original line Diff line number Diff line
@@ -17,19 +17,30 @@
package com.android.systemui.qs.tiles.dialog
package com.android.systemui.qs.tiles.dialog


import android.view.LayoutInflater
import android.view.LayoutInflater
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.viewinterop.AndroidView
import com.android.compose.ui.graphics.painter.rememberDrawablePainter
import com.android.internal.R
import com.android.internal.R
import com.android.internal.app.MediaRouteChooserContentManager
import com.android.internal.app.MediaRouteControllerContentManager
import com.android.internal.app.MediaRouteControllerContentManager


@Composable
@Composable
fun CastDetailsContent(castDetailsViewModel: CastDetailsViewModel) {
fun CastDetailsContent(castDetailsViewModel: CastDetailsViewModel) {
    if (castDetailsViewModel.shouldShowChooserDialog()) {
    if (castDetailsViewModel.shouldShowChooserDialog()) {
        // TODO(b/378514236): Show the chooser UI here.
        val contentManager: MediaRouteChooserContentManager = remember {
            castDetailsViewModel.createChooserContentManager()
        }
        CastChooserView(contentManager)
        return
        return
    }
    }


@@ -37,8 +48,41 @@ fun CastDetailsContent(castDetailsViewModel: CastDetailsViewModel) {
        castDetailsViewModel.createControllerContentManager()
        castDetailsViewModel.createControllerContentManager()
    }
    }


    Column(
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Image(
            painter = rememberDrawablePainter(castDetailsViewModel.deviceIcon),
            // TODO(b/388321032): Replace this string with a string in a translatable xml file.
            contentDescription = "device icon",
        )
        CastControllerView(contentManager)
        CastControllerDisconnectButton(contentManager)
    }
}

@Composable
fun CastChooserView(contentManager: MediaRouteChooserContentManager) {
    AndroidView(
        modifier = Modifier.fillMaxWidth().testTag(CastDetailsViewModel.CHOOSER_VIEW_TEST_TAG),
        factory = { context ->
            // Inflate with the existing dialog xml layout
            val view =
                LayoutInflater.from(context).inflate(R.layout.media_route_chooser_dialog, null)
            contentManager.bindViews(view)
            contentManager.onAttachedToWindow()

            view
        },
        onRelease = { contentManager.onDetachedFromWindow() },
    )
}

@Composable
fun CastControllerView(contentManager: MediaRouteControllerContentManager) {
    AndroidView(
    AndroidView(
        modifier = Modifier.fillMaxWidth().fillMaxHeight(),
        modifier = Modifier.fillMaxWidth().testTag(CastDetailsViewModel.CONTROLLER_VIEW_TEST_TAG),
        factory = { context ->
        factory = { context ->
            // Inflate with the existing dialog xml layout
            // Inflate with the existing dialog xml layout
            val view =
            val view =
@@ -51,3 +95,14 @@ fun CastDetailsContent(castDetailsViewModel: CastDetailsViewModel) {
        onRelease = { contentManager.onDetachedFromWindow() },
        onRelease = { contentManager.onDetachedFromWindow() },
    )
    )
}
}

@Composable
fun CastControllerDisconnectButton(contentManager: MediaRouteControllerContentManager) {
    Button(
        onClick = { contentManager.onDisconnectButtonClick() },
        modifier = Modifier.fillMaxWidth(),
    ) {
        // TODO(b/388321032): Replace this string with a string in a translatable xml file.
        Text(text = "Disconnect")
    }
}
+32 −7
Original line number Original line Diff line number Diff line
@@ -20,6 +20,10 @@ import android.content.Context
import android.content.Intent
import android.content.Intent
import android.graphics.drawable.Drawable
import android.graphics.drawable.Drawable
import android.provider.Settings
import android.provider.Settings
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.android.internal.app.MediaRouteChooserContentManager
import com.android.internal.app.MediaRouteControllerContentManager
import com.android.internal.app.MediaRouteControllerContentManager
import com.android.internal.app.MediaRouteDialogPresenter
import com.android.internal.app.MediaRouteDialogPresenter
import com.android.systemui.plugins.qs.TileDetailsViewModel
import com.android.systemui.plugins.qs.TileDetailsViewModel
@@ -35,7 +39,14 @@ constructor(
    private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
    private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
    @Assisted private val context: Context,
    @Assisted private val context: Context,
    @Assisted private val routeTypes: Int,
    @Assisted private val routeTypes: Int,
) : MediaRouteControllerContentManager.Delegate, TileDetailsViewModel {
) :
    MediaRouteChooserContentManager.Delegate,
    MediaRouteControllerContentManager.Delegate,
    TileDetailsViewModel {
    private var detailsViewTitle by mutableStateOf(DEFAULT_TITLE)
    private val detailsViewSubTitle = if (shouldShowChooserDialog()) DEFAULT_SUBTITLE else ""
    var deviceIcon: Drawable? by mutableStateOf(null)

    @AssistedFactory
    @AssistedFactory
    fun interface Factory {
    fun interface Factory {
        fun create(context: Context, routeTypes: Int): CastDetailsViewModel
        fun create(context: Context, routeTypes: Int): CastDetailsViewModel
@@ -45,6 +56,10 @@ constructor(
        return MediaRouteDialogPresenter.shouldShowChooserDialog(context, routeTypes)
        return MediaRouteDialogPresenter.shouldShowChooserDialog(context, routeTypes)
    }
    }


    fun createChooserContentManager(): MediaRouteChooserContentManager {
        return MediaRouteChooserContentManager(context, this)
    }

    fun createControllerContentManager(): MediaRouteControllerContentManager {
    fun createControllerContentManager(): MediaRouteControllerContentManager {
        return MediaRouteControllerContentManager(context, this)
        return MediaRouteControllerContentManager(context, this)
    }
    }
@@ -56,23 +71,33 @@ constructor(
        )
        )
    }
    }


    // TODO(b/388321032): Replace this string with a string in a translatable xml file,
    override val title: String
    override val title: String
        get() = "Cast screen to device"
        get() = detailsViewTitle


    // TODO(b/388321032): Replace this string with a string in a translatable xml file,
    override val subTitle: String
    override val subTitle: String
        get() = "Searching for devices..."
        get() = detailsViewSubTitle


    override fun setMediaRouteDeviceTitle(title: CharSequence?) {
    override fun setMediaRouteDeviceTitle(title: CharSequence?) {
        // TODO(b/378514236): Finish implementing this function.
        detailsViewTitle = title.toString()
    }
    }


    override fun setMediaRouteDeviceIcon(icon: Drawable?) {
    override fun setMediaRouteDeviceIcon(icon: Drawable?) {
        // TODO(b/378514236): Finish implementing this function.
        deviceIcon = icon
    }
    }


    override fun dismissView() {
    override fun dismissView() {
        // TODO(b/378514236): Finish implementing this function.
        // TODO(b/378514236): Finish implementing this function.
    }
    }

    override fun showProgressBarWhenEmpty(): Boolean {
        return false
    }

    companion object {
        // TODO(b/388321032): Replace this string with a string in a translatable xml file.
        const val DEFAULT_TITLE = "Cast screen to device"
        const val DEFAULT_SUBTITLE = "Searching for devices..."
        const val CHOOSER_VIEW_TEST_TAG = "CastChooserView"
        const val CONTROLLER_VIEW_TEST_TAG = "CastControllerView"
    }
}
}
+103 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2025 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.qs.tiles.dialog

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.app.MediaRouteChooserContentManager
import com.android.internal.app.MediaRouteControllerContentManager
import com.android.systemui.SysuiTestCase
import com.android.systemui.qs.tiles.impl.mediaroute.mediaRouteChooserContentManager
import com.android.systemui.qs.tiles.impl.mediaroute.mediaRouteControllerContentManager
import com.android.systemui.testKosmos
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.stub
import org.mockito.kotlin.verify

@SmallTest
@RunWith(AndroidJUnit4::class)
class CastDetailsContentTest : SysuiTestCase() {
    @get:Rule val composeRule = createComposeRule()
    private val kosmos = testKosmos()
    private val chooserContentManager: MediaRouteChooserContentManager =
        kosmos.mediaRouteChooserContentManager
    private val controllerContentManager: MediaRouteControllerContentManager =
        kosmos.mediaRouteControllerContentManager
    private val viewModel: CastDetailsViewModel =
        mock<CastDetailsViewModel> {
            on { createChooserContentManager() } doReturn chooserContentManager
            on { createControllerContentManager() } doReturn controllerContentManager
        }

    @Test
    fun shouldShowChooserDialogTrue_showChooserUI() {
        viewModel.stub { on { shouldShowChooserDialog() } doReturn true }

        composeRule.setContent { CastDetailsContent(viewModel) }
        composeRule.waitForIdle()

        composeRule.onNodeWithTag(CastDetailsViewModel.CHOOSER_VIEW_TEST_TAG).assertExists()
        composeRule
            .onNodeWithTag(CastDetailsViewModel.CONTROLLER_VIEW_TEST_TAG)
            .assertDoesNotExist()
        composeRule.onNodeWithContentDescription("device icon").assertDoesNotExist()
        composeRule.onNodeWithText("Disconnect").assertDoesNotExist()

        verify(chooserContentManager).bindViews(any())
        verify(chooserContentManager).onAttachedToWindow()
    }

    @Test
    fun shouldShowChooserDialogFalse_showControllerUI() {
        viewModel.stub { on { shouldShowChooserDialog() } doReturn false }

        composeRule.setContent { CastDetailsContent(viewModel) }
        composeRule.waitForIdle()

        composeRule.onNodeWithTag(CastDetailsViewModel.CONTROLLER_VIEW_TEST_TAG).assertExists()
        composeRule.onNodeWithContentDescription("device icon").assertExists()
        composeRule.onNodeWithText("Disconnect").assertExists()
        composeRule.onNodeWithTag(CastDetailsViewModel.CHOOSER_VIEW_TEST_TAG).assertDoesNotExist()

        verify(controllerContentManager).bindViews(any())
        verify(controllerContentManager).onAttachedToWindow()
    }

    @Test
    fun clickOnDisconnectButton_shouldCallDisconnect() {
        viewModel.stub { on { shouldShowChooserDialog() } doReturn false }

        composeRule.setContent { CastControllerDisconnectButton(controllerContentManager) }
        composeRule.waitForIdle()

        composeRule.onNodeWithText("Disconnect").assertIsDisplayed().performClick()
        composeRule.waitForIdle()

        verify(controllerContentManager).onDisconnectButtonClick()
    }
}
+24 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2025 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.qs.tiles.impl.mediaroute

import com.android.internal.app.MediaRouteChooserContentManager
import com.android.systemui.kosmos.Kosmos
import org.mockito.kotlin.mock

val Kosmos.mediaRouteChooserContentManager: MediaRouteChooserContentManager by
    Kosmos.Fixture { mock<MediaRouteChooserContentManager>() }
Loading