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

Commit 7110b90f authored by Andre Le's avatar Andre Le
Browse files

CastDetailsView: Show up cast chooser UI within the details view

Add the support to show up the chooser UI within the cast details view.
This includes showing R.layout.media_route_chooser_dialog within the
details view UI, as well as initializing a
MediaRouteChooserContentManager instance to control the backend
behavior.

Bug: 378514236
Flag: com.android.systemui.qs_tile_detailed_view
Test: CastDetailsViewModelTest, CastDetailsViewContentTest
Change-Id: I8011ae6c3ef3729aa814a936d35072a34a519c6a
parent bf57ec20
Loading
Loading
Loading
Loading
+23 −2
Original line number Original line Diff line number Diff line
@@ -31,12 +31,16 @@ 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.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
    }
    }


@@ -58,10 +62,27 @@ fun CastDetailsContent(castDetailsViewModel: CastDetailsViewModel) {
    }
    }
}
}


@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
@Composable
fun CastControllerView(contentManager: MediaRouteControllerContentManager) {
fun CastControllerView(contentManager: MediaRouteControllerContentManager) {
    AndroidView(
    AndroidView(
        modifier = Modifier.fillMaxWidth().testTag("CastControllerView"),
        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 =
+15 −1
Original line number Original line Diff line number Diff line
@@ -23,6 +23,7 @@ import android.provider.Settings
import androidx.compose.runtime.getValue
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
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
@@ -38,7 +39,10 @@ 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 var detailsViewTitle by mutableStateOf(DEFAULT_TITLE)
    private val detailsViewSubTitle = if (shouldShowChooserDialog()) DEFAULT_SUBTITLE else ""
    private val detailsViewSubTitle = if (shouldShowChooserDialog()) DEFAULT_SUBTITLE else ""
    var deviceIcon: Drawable? by mutableStateOf(null)
    var deviceIcon: Drawable? by mutableStateOf(null)
@@ -52,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)
    }
    }
@@ -81,9 +89,15 @@ constructor(
        // TODO(b/378514236): Finish implementing this function.
        // TODO(b/378514236): Finish implementing this function.
    }
    }


    override fun showProgressBarWhenEmpty(): Boolean {
        return false
    }

    companion object {
    companion object {
        // TODO(b/388321032): Replace this string with a string in a translatable xml file.
        // 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_TITLE = "Cast screen to device"
        const val DEFAULT_SUBTITLE = "Searching for devices..."
        const val DEFAULT_SUBTITLE = "Searching for devices..."
        const val CHOOSER_VIEW_TEST_TAG = "CastChooserView"
        const val CONTROLLER_VIEW_TEST_TAG = "CastControllerView"
    }
    }
}
}
+25 −1
Original line number Original line Diff line number Diff line
@@ -24,8 +24,10 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.filters.SmallTest
import com.android.internal.app.MediaRouteChooserContentManager
import com.android.internal.app.MediaRouteControllerContentManager
import com.android.internal.app.MediaRouteControllerContentManager
import com.android.systemui.SysuiTestCase
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.qs.tiles.impl.mediaroute.mediaRouteControllerContentManager
import com.android.systemui.testKosmos
import com.android.systemui.testKosmos
import org.junit.Rule
import org.junit.Rule
@@ -42,13 +44,34 @@ import org.mockito.kotlin.verify
class CastDetailsContentTest : SysuiTestCase() {
class CastDetailsContentTest : SysuiTestCase() {
    @get:Rule val composeRule = createComposeRule()
    @get:Rule val composeRule = createComposeRule()
    private val kosmos = testKosmos()
    private val kosmos = testKosmos()
    private val chooserContentManager: MediaRouteChooserContentManager =
        kosmos.mediaRouteChooserContentManager
    private val controllerContentManager: MediaRouteControllerContentManager =
    private val controllerContentManager: MediaRouteControllerContentManager =
        kosmos.mediaRouteControllerContentManager
        kosmos.mediaRouteControllerContentManager
    private val viewModel: CastDetailsViewModel =
    private val viewModel: CastDetailsViewModel =
        mock<CastDetailsViewModel> {
        mock<CastDetailsViewModel> {
            on { createChooserContentManager() } doReturn chooserContentManager
            on { createControllerContentManager() } doReturn controllerContentManager
            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
    @Test
    fun shouldShowChooserDialogFalse_showControllerUI() {
    fun shouldShowChooserDialogFalse_showControllerUI() {
        viewModel.stub { on { shouldShowChooserDialog() } doReturn false }
        viewModel.stub { on { shouldShowChooserDialog() } doReturn false }
@@ -56,9 +79,10 @@ class CastDetailsContentTest : SysuiTestCase() {
        composeRule.setContent { CastDetailsContent(viewModel) }
        composeRule.setContent { CastDetailsContent(viewModel) }
        composeRule.waitForIdle()
        composeRule.waitForIdle()


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


        verify(controllerContentManager).bindViews(any())
        verify(controllerContentManager).bindViews(any())
        verify(controllerContentManager).onAttachedToWindow()
        verify(controllerContentManager).onAttachedToWindow()
+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>() }