Loading packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CastTileTest.java +31 −2 Original line number Diff line number Diff line Loading @@ -50,6 +50,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.QsEventLogger; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tiles.dialog.CastDetailsViewModel; import com.android.systemui.shade.domain.interactor.FakeShadeDialogContextInteractor; import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor; import com.android.systemui.statusbar.connectivity.IconState; Loading @@ -63,6 +64,7 @@ import com.android.systemui.statusbar.policy.HotspotController; import com.android.systemui.statusbar.policy.KeyguardStateController; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; Loading Loading @@ -104,6 +106,8 @@ public class CastTileTest extends SysuiTestCase { private DialogTransitionAnimator mDialogTransitionAnimator; @Mock private QsEventLogger mUiEventLogger; @Mock private CastDetailsViewModel.Factory mCastDetailsViewModelFactory; private final TileJavaAdapter mJavaAdapter = new TileJavaAdapter(); private final FakeConnectivityRepository mConnectivityRepository = Loading Loading @@ -517,6 +521,29 @@ public class CastTileTest extends SysuiTestCase { assertTrue(mCastTile.getState().forceExpandIcon); } @Test public void testDetailsViewUnavailableState_returnsNull() { createAndStartTileNewImpl(); mTestableLooper.processAllMessages(); assertEquals(Tile.STATE_UNAVAILABLE, mCastTile.getState().state); mCastTile.getDetailsViewModel(Assert::assertNull); } @Test public void testDetailsViewAvailableState_returnsNotNull() { createAndStartTileNewImpl(); CastDevice device = createConnectedCastDevice(); List<CastDevice> devices = new ArrayList<>(); devices.add(device); when(mController.getCastDevices()).thenReturn(devices); mConnectivityRepository.setWifiConnected(true); mTestableLooper.processAllMessages(); assertEquals(Tile.STATE_ACTIVE, mCastTile.getState().state); mCastTile.getDetailsViewModel(Assert::assertNotNull); } /** * For simplicity, let this method still set the field even though that's kind of gross */ Loading @@ -540,7 +567,8 @@ public class CastTileTest extends SysuiTestCase { mConnectivityRepository, mJavaAdapter, mFeatureFlags, mShadeDialogContextInteractor mShadeDialogContextInteractor, mCastDetailsViewModelFactory ); mCastTile.initialize(); Loading Loading @@ -584,7 +612,8 @@ public class CastTileTest extends SysuiTestCase { mConnectivityRepository, mJavaAdapter, mFeatureFlags, mShadeDialogContextInteractor mShadeDialogContextInteractor, mCastDetailsViewModelFactory ); mCastTile.initialize(); Loading packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt +3 −0 Original line number Diff line number Diff line Loading @@ -43,6 +43,8 @@ import com.android.systemui.bluetooth.qsdialog.BluetoothDetailsViewModel import com.android.systemui.plugins.qs.TileDetailsViewModel import com.android.systemui.qs.flags.QsDetailedView import com.android.systemui.qs.panels.ui.viewmodel.DetailsViewModel import com.android.systemui.qs.tiles.dialog.CastDetailsContent import com.android.systemui.qs.tiles.dialog.CastDetailsViewModel import com.android.systemui.qs.tiles.dialog.InternetDetailsContent import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel import com.android.systemui.qs.tiles.dialog.ModesDetailsContent Loading Loading @@ -131,6 +133,7 @@ private fun MapTileDetailsContent(tileDetailsViewModel: TileDetailsViewModel) { is BluetoothDetailsViewModel -> BluetoothDetailsContent(tileDetailsViewModel.detailsContentViewModel) is ModesDetailsViewModel -> ModesDetailsContent(tileDetailsViewModel) is CastDetailsViewModel -> CastDetailsContent() } } Loading packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java +40 −13 Original line number Diff line number Diff line Loading @@ -48,11 +48,13 @@ import com.android.systemui.flags.FeatureFlags; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.qs.QSTile.BooleanState; import com.android.systemui.plugins.qs.TileDetailsViewModel; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.QsEventLogger; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.qs.tiles.dialog.CastDetailsViewModel; import com.android.systemui.res.R; import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor; import com.android.systemui.statusbar.connectivity.NetworkController; Loading Loading @@ -93,6 +95,7 @@ public class CastTile extends QSTileImpl<BooleanState> { private final ShadeDialogContextInteractor mShadeDialogContextInteractor; private boolean mCastTransportAllowed; private boolean mHotspotConnected; private final CastDetailsViewModel.Factory mCastDetailsViewModelFactory; @Inject public CastTile( Loading @@ -113,7 +116,8 @@ public class CastTile extends QSTileImpl<BooleanState> { ConnectivityRepository connectivityRepository, TileJavaAdapter javaAdapter, FeatureFlags featureFlags, ShadeDialogContextInteractor shadeDialogContextInteractor ShadeDialogContextInteractor shadeDialogContextInteractor, CastDetailsViewModel.Factory castDetailsViewModelFactory ) { super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, statusBarStateController, activityStarter, qsLogger); Loading @@ -124,6 +128,7 @@ public class CastTile extends QSTileImpl<BooleanState> { mJavaAdapter = javaAdapter; mFeatureFlags = featureFlags; mShadeDialogContextInteractor = shadeDialogContextInteractor; mCastDetailsViewModelFactory = castDetailsViewModelFactory; mController.observe(this, mCallback); mKeyguard.observe(this, mCallback); if (!mFeatureFlags.isEnabled(SIGNAL_CALLBACK_DEPRECATION)) { Loading Loading @@ -172,12 +177,7 @@ public class CastTile extends QSTileImpl<BooleanState> { @Override protected void handleClick(@Nullable Expandable expandable) { if (getState().state == Tile.STATE_UNAVAILABLE) { return; } List<CastDevice> activeDevices = getActiveDevices(); if (willPopDialog()) { handleClick(() -> { if (!mKeyguard.isShowing()) { showDialog(expandable); } else { Loading @@ -187,16 +187,43 @@ public class CastTile extends QSTileImpl<BooleanState> { showDialog(null /* view */); }); } }); } @Override public boolean getDetailsViewModel(Consumer<TileDetailsViewModel> callback) { CastDetailsViewModel viewModel = mCastDetailsViewModelFactory.create(); handleClick(() -> { if (!mKeyguard.isShowing()) { callback.accept(viewModel); } else { mActivityStarter.dismissKeyguardThenExecute(() -> { callback.accept(viewModel); return false; }, null /* cancelAction */, true/* afterKeyguardGone */); } }); return true; } private void handleClick(Runnable showPromptCallback) { if (getState().state == Tile.STATE_UNAVAILABLE) { return; } List<CastDevice> activeDevices = getActiveDevices(); if (willShowPrompt()) { showPromptCallback.run(); } else { mController.stopCasting(activeDevices.get(0), StopReason.STOP_QS_TILE); } } // We want to pop up the media route selection dialog if we either have no active devices // (neither routes nor projection), or if we have an active route. In other cases, we assume // that a projection is active. This is messy, but this tile never correctly handled the // case where multiple devices were active :-/. private boolean willPopDialog() { // We want to pop up the media route selection dialog (or show the cast details view) if we // either have no active devices (neither routes nor projection), or if we have an active // route. In other cases, we assume that a projection is active. This is messy, but this tile // never correctly handled the case where multiple devices were active :-/. private boolean willShowPrompt() { List<CastDevice> activeDevices = getActiveDevices(); return activeDevices.isEmpty() || (activeDevices.get(0).getTag() instanceof RouteInfo); } Loading Loading @@ -303,7 +330,7 @@ public class CastTile extends QSTileImpl<BooleanState> { state.secondaryLabel = ""; } state.expandedAccessibilityClassName = Button.class.getName(); state.forceExpandIcon = willPopDialog(); state.forceExpandIcon = willShowPrompt(); } else { state.state = Tile.STATE_UNAVAILABLE; String noWifi = mContext.getString(R.string.quick_settings_cast_no_network); Loading packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/CastDetailsContent.kt 0 → 100644 +37 −0 Original line number 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 android.view.LayoutInflater import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.AndroidView import com.android.internal.R @Composable fun CastDetailsContent() { // TODO(b/378514236): Finish implementing this function. AndroidView( modifier = Modifier.fillMaxWidth().fillMaxHeight(), factory = { context -> // Inflate with the existing dialog xml layout LayoutInflater.from(context).inflate(R.layout.media_route_controller_dialog, null) }, ) } packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/CastDetailsViewModel.kt 0 → 100644 +50 −0 Original line number 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 android.content.Intent import android.provider.Settings import com.android.systemui.plugins.qs.TileDetailsViewModel import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject /** The view model used for the screen record details view in the Quick Settings */ class CastDetailsViewModel @AssistedInject constructor(private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler) : TileDetailsViewModel { @AssistedFactory fun interface Factory { fun create(): CastDetailsViewModel } override fun clickOnSettingsButton() { qsTileIntentUserActionHandler.handle( /* expandable= */ null, Intent(Settings.ACTION_CAST_SETTINGS), ) } // TODO(b/388321032): Replace this string with a string in a translatable xml file, override val title: String get() = "Cast screen to device" // TODO(b/388321032): Replace this string with a string in a translatable xml file, override val subTitle: String get() = "Searching for devices..." } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CastTileTest.java +31 −2 Original line number Diff line number Diff line Loading @@ -50,6 +50,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.QsEventLogger; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tiles.dialog.CastDetailsViewModel; import com.android.systemui.shade.domain.interactor.FakeShadeDialogContextInteractor; import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor; import com.android.systemui.statusbar.connectivity.IconState; Loading @@ -63,6 +64,7 @@ import com.android.systemui.statusbar.policy.HotspotController; import com.android.systemui.statusbar.policy.KeyguardStateController; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; Loading Loading @@ -104,6 +106,8 @@ public class CastTileTest extends SysuiTestCase { private DialogTransitionAnimator mDialogTransitionAnimator; @Mock private QsEventLogger mUiEventLogger; @Mock private CastDetailsViewModel.Factory mCastDetailsViewModelFactory; private final TileJavaAdapter mJavaAdapter = new TileJavaAdapter(); private final FakeConnectivityRepository mConnectivityRepository = Loading Loading @@ -517,6 +521,29 @@ public class CastTileTest extends SysuiTestCase { assertTrue(mCastTile.getState().forceExpandIcon); } @Test public void testDetailsViewUnavailableState_returnsNull() { createAndStartTileNewImpl(); mTestableLooper.processAllMessages(); assertEquals(Tile.STATE_UNAVAILABLE, mCastTile.getState().state); mCastTile.getDetailsViewModel(Assert::assertNull); } @Test public void testDetailsViewAvailableState_returnsNotNull() { createAndStartTileNewImpl(); CastDevice device = createConnectedCastDevice(); List<CastDevice> devices = new ArrayList<>(); devices.add(device); when(mController.getCastDevices()).thenReturn(devices); mConnectivityRepository.setWifiConnected(true); mTestableLooper.processAllMessages(); assertEquals(Tile.STATE_ACTIVE, mCastTile.getState().state); mCastTile.getDetailsViewModel(Assert::assertNotNull); } /** * For simplicity, let this method still set the field even though that's kind of gross */ Loading @@ -540,7 +567,8 @@ public class CastTileTest extends SysuiTestCase { mConnectivityRepository, mJavaAdapter, mFeatureFlags, mShadeDialogContextInteractor mShadeDialogContextInteractor, mCastDetailsViewModelFactory ); mCastTile.initialize(); Loading Loading @@ -584,7 +612,8 @@ public class CastTileTest extends SysuiTestCase { mConnectivityRepository, mJavaAdapter, mFeatureFlags, mShadeDialogContextInteractor mShadeDialogContextInteractor, mCastDetailsViewModelFactory ); mCastTile.initialize(); Loading
packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt +3 −0 Original line number Diff line number Diff line Loading @@ -43,6 +43,8 @@ import com.android.systemui.bluetooth.qsdialog.BluetoothDetailsViewModel import com.android.systemui.plugins.qs.TileDetailsViewModel import com.android.systemui.qs.flags.QsDetailedView import com.android.systemui.qs.panels.ui.viewmodel.DetailsViewModel import com.android.systemui.qs.tiles.dialog.CastDetailsContent import com.android.systemui.qs.tiles.dialog.CastDetailsViewModel import com.android.systemui.qs.tiles.dialog.InternetDetailsContent import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel import com.android.systemui.qs.tiles.dialog.ModesDetailsContent Loading Loading @@ -131,6 +133,7 @@ private fun MapTileDetailsContent(tileDetailsViewModel: TileDetailsViewModel) { is BluetoothDetailsViewModel -> BluetoothDetailsContent(tileDetailsViewModel.detailsContentViewModel) is ModesDetailsViewModel -> ModesDetailsContent(tileDetailsViewModel) is CastDetailsViewModel -> CastDetailsContent() } } Loading
packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java +40 −13 Original line number Diff line number Diff line Loading @@ -48,11 +48,13 @@ import com.android.systemui.flags.FeatureFlags; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.qs.QSTile.BooleanState; import com.android.systemui.plugins.qs.TileDetailsViewModel; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.QsEventLogger; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.qs.tiles.dialog.CastDetailsViewModel; import com.android.systemui.res.R; import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor; import com.android.systemui.statusbar.connectivity.NetworkController; Loading Loading @@ -93,6 +95,7 @@ public class CastTile extends QSTileImpl<BooleanState> { private final ShadeDialogContextInteractor mShadeDialogContextInteractor; private boolean mCastTransportAllowed; private boolean mHotspotConnected; private final CastDetailsViewModel.Factory mCastDetailsViewModelFactory; @Inject public CastTile( Loading @@ -113,7 +116,8 @@ public class CastTile extends QSTileImpl<BooleanState> { ConnectivityRepository connectivityRepository, TileJavaAdapter javaAdapter, FeatureFlags featureFlags, ShadeDialogContextInteractor shadeDialogContextInteractor ShadeDialogContextInteractor shadeDialogContextInteractor, CastDetailsViewModel.Factory castDetailsViewModelFactory ) { super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, statusBarStateController, activityStarter, qsLogger); Loading @@ -124,6 +128,7 @@ public class CastTile extends QSTileImpl<BooleanState> { mJavaAdapter = javaAdapter; mFeatureFlags = featureFlags; mShadeDialogContextInteractor = shadeDialogContextInteractor; mCastDetailsViewModelFactory = castDetailsViewModelFactory; mController.observe(this, mCallback); mKeyguard.observe(this, mCallback); if (!mFeatureFlags.isEnabled(SIGNAL_CALLBACK_DEPRECATION)) { Loading Loading @@ -172,12 +177,7 @@ public class CastTile extends QSTileImpl<BooleanState> { @Override protected void handleClick(@Nullable Expandable expandable) { if (getState().state == Tile.STATE_UNAVAILABLE) { return; } List<CastDevice> activeDevices = getActiveDevices(); if (willPopDialog()) { handleClick(() -> { if (!mKeyguard.isShowing()) { showDialog(expandable); } else { Loading @@ -187,16 +187,43 @@ public class CastTile extends QSTileImpl<BooleanState> { showDialog(null /* view */); }); } }); } @Override public boolean getDetailsViewModel(Consumer<TileDetailsViewModel> callback) { CastDetailsViewModel viewModel = mCastDetailsViewModelFactory.create(); handleClick(() -> { if (!mKeyguard.isShowing()) { callback.accept(viewModel); } else { mActivityStarter.dismissKeyguardThenExecute(() -> { callback.accept(viewModel); return false; }, null /* cancelAction */, true/* afterKeyguardGone */); } }); return true; } private void handleClick(Runnable showPromptCallback) { if (getState().state == Tile.STATE_UNAVAILABLE) { return; } List<CastDevice> activeDevices = getActiveDevices(); if (willShowPrompt()) { showPromptCallback.run(); } else { mController.stopCasting(activeDevices.get(0), StopReason.STOP_QS_TILE); } } // We want to pop up the media route selection dialog if we either have no active devices // (neither routes nor projection), or if we have an active route. In other cases, we assume // that a projection is active. This is messy, but this tile never correctly handled the // case where multiple devices were active :-/. private boolean willPopDialog() { // We want to pop up the media route selection dialog (or show the cast details view) if we // either have no active devices (neither routes nor projection), or if we have an active // route. In other cases, we assume that a projection is active. This is messy, but this tile // never correctly handled the case where multiple devices were active :-/. private boolean willShowPrompt() { List<CastDevice> activeDevices = getActiveDevices(); return activeDevices.isEmpty() || (activeDevices.get(0).getTag() instanceof RouteInfo); } Loading Loading @@ -303,7 +330,7 @@ public class CastTile extends QSTileImpl<BooleanState> { state.secondaryLabel = ""; } state.expandedAccessibilityClassName = Button.class.getName(); state.forceExpandIcon = willPopDialog(); state.forceExpandIcon = willShowPrompt(); } else { state.state = Tile.STATE_UNAVAILABLE; String noWifi = mContext.getString(R.string.quick_settings_cast_no_network); Loading
packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/CastDetailsContent.kt 0 → 100644 +37 −0 Original line number 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 android.view.LayoutInflater import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.AndroidView import com.android.internal.R @Composable fun CastDetailsContent() { // TODO(b/378514236): Finish implementing this function. AndroidView( modifier = Modifier.fillMaxWidth().fillMaxHeight(), factory = { context -> // Inflate with the existing dialog xml layout LayoutInflater.from(context).inflate(R.layout.media_route_controller_dialog, null) }, ) }
packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/CastDetailsViewModel.kt 0 → 100644 +50 −0 Original line number 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 android.content.Intent import android.provider.Settings import com.android.systemui.plugins.qs.TileDetailsViewModel import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject /** The view model used for the screen record details view in the Quick Settings */ class CastDetailsViewModel @AssistedInject constructor(private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler) : TileDetailsViewModel { @AssistedFactory fun interface Factory { fun create(): CastDetailsViewModel } override fun clickOnSettingsButton() { qsTileIntentUserActionHandler.handle( /* expandable= */ null, Intent(Settings.ACTION_CAST_SETTINGS), ) } // TODO(b/388321032): Replace this string with a string in a translatable xml file, override val title: String get() = "Cast screen to device" // TODO(b/388321032): Replace this string with a string in a translatable xml file, override val subTitle: String get() = "Searching for devices..." }