diff options
5 files changed, 161 insertions, 15 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CastTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CastTileTest.java index 765c5749cd4b..d880aa604849 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CastTileTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CastTileTest.java @@ -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; @@ -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; @@ -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 = @@ -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 */ @@ -540,7 +567,8 @@ public class CastTileTest extends SysuiTestCase { mConnectivityRepository, mJavaAdapter, mFeatureFlags, - mShadeDialogContextInteractor + mShadeDialogContextInteractor, + mCastDetailsViewModelFactory ); mCastTile.initialize(); @@ -584,7 +612,8 @@ public class CastTileTest extends SysuiTestCase { mConnectivityRepository, mJavaAdapter, mFeatureFlags, - mShadeDialogContextInteractor + mShadeDialogContextInteractor, + mCastDetailsViewModelFactory ); mCastTile.initialize(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt index d40ecc9565ae..4cd875594b43 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt @@ -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 @@ -131,6 +133,7 @@ private fun MapTileDetailsContent(tileDetailsViewModel: TileDetailsViewModel) { is BluetoothDetailsViewModel -> BluetoothDetailsContent(tileDetailsViewModel.detailsContentViewModel) is ModesDetailsViewModel -> ModesDetailsContent(tileDetailsViewModel) + is CastDetailsViewModel -> CastDetailsContent() } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java index c60e3da9d833..349c771048dc 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java @@ -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; @@ -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( @@ -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); @@ -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)) { @@ -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 { @@ -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); } @@ -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); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/CastDetailsContent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/CastDetailsContent.kt new file mode 100644 index 000000000000..e31108ea450b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/CastDetailsContent.kt @@ -0,0 +1,37 @@ +/* + * 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) + }, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/CastDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/CastDetailsViewModel.kt new file mode 100644 index 000000000000..efe04a25244f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/CastDetailsViewModel.kt @@ -0,0 +1,50 @@ +/* + * 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..." +} |