Merge "Moving CrashRecoveryModuleTests to presubmits" into main
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index d4d9d00..fdb4523 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -372,6 +372,7 @@
// ==> activity view
// ==> manage button
bringChildToFront(mManageButton);
+ setManageClickListener();
applyThemeAttrs();
@@ -502,6 +503,7 @@
R.layout.bubble_manage_button, this /* parent */, false /* attach */);
addView(mManageButton);
mManageButton.setVisibility(visibility);
+ setManageClickListener();
post(() -> {
int touchAreaHeight =
getResources().getDimensionPixelSize(
@@ -646,9 +648,8 @@
}
}
- // TODO: Could listener be passed when we pass StackView / can we avoid setting this like this
- void setManageClickListener(OnClickListener manageClickListener) {
- mManageButton.setOnClickListener(manageClickListener);
+ private void setManageClickListener() {
+ mManageButton.setOnClickListener(v -> mStackView.onManageBubbleClicked());
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index f93f19d..8f8b77b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -1374,7 +1374,6 @@
// The menu itself should respect locale direction so the icons are on the correct side.
mManageMenu.setLayoutDirection(LAYOUT_DIRECTION_LOCALE);
addView(mManageMenu);
- updateManageButtonListener();
}
/**
@@ -3375,14 +3374,6 @@
mExpandedViewContainer.setAlpha(0f);
mExpandedViewContainer.addView(bev);
- postDelayed(() -> {
- // Set the Manage button click handler from postDelayed. This appears to resolve
- // a race condition with adding the BubbleExpandedView view to the expanded view
- // container. Due to the race condition the click handler sometimes is not set up
- // correctly and is never called.
- updateManageButtonListener();
- }, 0);
-
if (!mIsExpansionAnimating) {
mIsBubbleSwitchAnimating = true;
mSurfaceSynchronizer.syncSurfaceAndRun(() -> {
@@ -3392,13 +3383,8 @@
}
}
- private void updateManageButtonListener() {
- BubbleExpandedView bev = getExpandedView();
- if (mIsExpanded && bev != null) {
- bev.setManageClickListener((view) -> {
- showManageMenu(true /* show */);
- });
- }
+ void onManageBubbleClicked() {
+ showManageMenu(true /* show */);
}
/**
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt
index d5c9102..a67e7c6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt
@@ -21,64 +21,68 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.DialogTransitionAnimator
-import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
+import com.android.systemui.animation.Expandable
import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject
+import com.android.systemui.qs.tiles.base.actions.qsTileIntentUserInputHandler
import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel
-import com.android.systemui.statusbar.phone.SystemUIDialog
-import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate
-import com.google.common.truth.Truth
-import kotlin.coroutines.EmptyCoroutineContext
+import com.android.systemui.statusbar.policy.ui.dialog.mockModesDialogDelegate
+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.Mock
-import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
-import org.mockito.kotlin.whenever
@SmallTest
@RunWith(AndroidJUnit4::class)
@EnableFlags(android.app.Flags.FLAG_MODES_UI)
class ModesTileUserActionInteractorTest : SysuiTestCase() {
- private val inputHandler = FakeQSTileIntentUserInputHandler()
+ private val kosmos = testKosmos()
+ private val inputHandler = kosmos.qsTileIntentUserInputHandler
+ private val mockDialogDelegate = kosmos.mockModesDialogDelegate
- @Mock private lateinit var dialogTransitionAnimator: DialogTransitionAnimator
- @Mock private lateinit var dialogDelegate: ModesDialogDelegate
- @Mock private lateinit var mockDialog: SystemUIDialog
+ private val underTest =
+ ModesTileUserActionInteractor(
+ inputHandler,
+ mockDialogDelegate,
+ )
- private lateinit var underTest: ModesTileUserActionInteractor
+ @Test
+ fun handleClick_active() = runTest {
+ val expandable = mock<Expandable>()
+ underTest.handleInput(
+ QSTileInputTestKtx.click(data = ModesTileModel(true), expandable = expandable))
- @Before
- fun setup() {
- MockitoAnnotations.initMocks(this)
-
- whenever(dialogDelegate.createDialog()).thenReturn(mockDialog)
-
- underTest =
- ModesTileUserActionInteractor(
- EmptyCoroutineContext,
- inputHandler,
- dialogTransitionAnimator,
- dialogDelegate,
- )
+ verify(mockDialogDelegate).showDialog(eq(expandable))
}
@Test
- fun handleClick() = runTest {
- underTest.handleInput(QSTileInputTestKtx.click(ModesTileModel(false)))
+ fun handleClick_inactive() = runTest {
+ val expandable = mock<Expandable>()
+ underTest.handleInput(
+ QSTileInputTestKtx.click(data = ModesTileModel(false), expandable = expandable))
- verify(mockDialog).show()
+ verify(mockDialogDelegate).showDialog(eq(expandable))
}
@Test
- fun handleLongClick() = runTest {
+ fun handleLongClick_active() = runTest {
+ underTest.handleInput(QSTileInputTestKtx.longClick(ModesTileModel(true)))
+
+ QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
+ assertThat(it.intent.action).isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS)
+ }
+ }
+
+ @Test
+ fun handleLongClick_inactive() = runTest {
underTest.handleInput(QSTileInputTestKtx.longClick(ModesTileModel(false)))
QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
- Truth.assertThat(it.intent.action).isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS)
+ assertThat(it.intent.action).isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS)
}
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt
new file mode 100644
index 0000000..bf0a39b
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.statusbar.policy.ui.dialog
+
+import android.app.Dialog
+import android.content.Intent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.animation.mockActivityTransitionAnimatorController
+import com.android.systemui.animation.mockDialogTransitionAnimator
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.mainCoroutineContext
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.activityStarter
+import com.android.systemui.runOnMainThreadAndWaitForIdleSync
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.statusbar.phone.systemUIDialogFactory
+import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.modesDialogViewModel
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ModesDialogDelegateTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val activityStarter = kosmos.activityStarter
+ private val mockDialogTransitionAnimator = kosmos.mockDialogTransitionAnimator
+ private val mockAnimationController = kosmos.mockActivityTransitionAnimatorController
+ private lateinit var underTest: ModesDialogDelegate
+
+ @Before
+ fun setup() {
+ whenever(
+ mockDialogTransitionAnimator.createActivityTransitionController(
+ any<SystemUIDialog>(),
+ eq(null)
+ )
+ )
+ .thenReturn(mockAnimationController)
+
+ underTest =
+ ModesDialogDelegate(
+ kosmos.systemUIDialogFactory,
+ mockDialogTransitionAnimator,
+ activityStarter,
+ { kosmos.modesDialogViewModel },
+ kosmos.mainCoroutineContext,
+ )
+ }
+
+ @Test
+ fun launchFromDialog_whenDialogNotOpen() {
+ val intent: Intent = mock()
+
+ runOnMainThreadAndWaitForIdleSync { underTest.launchFromDialog(intent) }
+
+ verify(activityStarter)
+ .startActivity(eq(intent), eq(true), eq<ActivityTransitionAnimator.Controller?>(null))
+ }
+
+ @Test
+ fun launchFromDialog_whenDialogOpen() =
+ testScope.runTest {
+ val intent: Intent = mock()
+ lateinit var dialog: Dialog
+
+ runOnMainThreadAndWaitForIdleSync {
+ kosmos.applicationCoroutineScope.launch { dialog = underTest.showDialog() }
+ runCurrent()
+ underTest.launchFromDialog(intent)
+ }
+
+ verify(mockDialogTransitionAnimator)
+ .createActivityTransitionController(any<Dialog>(), eq(null))
+ verify(activityStarter).startActivity(eq(intent), eq(true), eq(mockAnimationController))
+
+ runOnMainThreadAndWaitForIdleSync { dialog.dismiss() }
+ }
+
+ @Test
+ fun dismiss_clearsDialogReference() {
+ val dialog = runOnMainThreadAndWaitForIdleSync { underTest.createDialog() }
+
+ assertThat(underTest.currentDialog).isEqualTo(dialog)
+
+ runOnMainThreadAndWaitForIdleSync {
+ dialog.show()
+ dialog.dismiss()
+ }
+
+ assertThat(underTest.currentDialog).isNull()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt
index fdfc7f1..349b62e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt
@@ -18,6 +18,8 @@
package com.android.systemui.statusbar.policy.ui.dialog.viewmodel
+import android.content.Intent
+import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.notification.modes.TestModeBuilder
@@ -27,6 +29,7 @@
import com.android.systemui.kosmos.testScope
import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository
import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
+import com.android.systemui.statusbar.policy.ui.dialog.mockModesDialogDelegate
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -34,16 +37,21 @@
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mockito.clearInvocations
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.verify
@SmallTest
@RunWith(AndroidJUnit4::class)
class ModesDialogViewModelTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- val repository = kosmos.fakeZenModeRepository
- val interactor = kosmos.zenModeInteractor
+ private val repository = kosmos.fakeZenModeRepository
+ private val interactor = kosmos.zenModeInteractor
+ private val mockDialogDelegate = kosmos.mockModesDialogDelegate
- val underTest = ModesDialogViewModel(context, interactor, kosmos.testDispatcher)
+ private val underTest =
+ ModesDialogViewModel(context, interactor, kosmos.testDispatcher, mockDialogDelegate)
@Test
fun tiles_filtersOutDisabledModes() =
@@ -64,7 +72,8 @@
.setEnabled(false)
.setManualInvocationAllowed(true)
.build(),
- ))
+ )
+ )
runCurrent()
assertThat(tiles?.size).isEqualTo(2)
@@ -108,7 +117,8 @@
.setActive(false)
.setManualInvocationAllowed(false)
.build(),
- ))
+ )
+ )
runCurrent()
assertThat(tiles?.size).isEqualTo(3)
@@ -161,4 +171,56 @@
assertThat(tiles?.first()?.enabled).isFalse()
}
+
+ @Test
+ fun onLongClick_launchesIntent() =
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+ val intentCaptor = argumentCaptor<Intent>()
+
+ val modeId = "id"
+ repository.addModes(
+ listOf(
+ TestModeBuilder()
+ .setId(modeId)
+ .setId("A")
+ .setActive(true)
+ .setManualInvocationAllowed(true)
+ .build(),
+ TestModeBuilder()
+ .setId(modeId)
+ .setId("B")
+ .setActive(false)
+ .setManualInvocationAllowed(true)
+ .build(),
+ )
+ )
+ runCurrent()
+
+ assertThat(tiles?.size).isEqualTo(2)
+
+ // Trigger onLongClick for A
+ tiles?.first()?.onLongClick?.let { it() }
+ runCurrent()
+
+ // Check that it launched the correct intent
+ verify(mockDialogDelegate).launchFromDialog(intentCaptor.capture())
+ var intent = intentCaptor.lastValue
+ assertThat(intent.action).isEqualTo(Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS)
+ assertThat(intent.extras?.getString(Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID))
+ .isEqualTo("A")
+
+ clearInvocations(mockDialogDelegate)
+
+ // Trigger onLongClick for B
+ tiles?.last()?.onLongClick?.let { it() }
+ runCurrent()
+
+ // Check that it launched the correct intent
+ verify(mockDialogDelegate).launchFromDialog(intentCaptor.capture())
+ intent = intentCaptor.lastValue
+ assertThat(intent.action).isEqualTo(Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS)
+ assertThat(intent.extras?.getString(Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID))
+ .isEqualTo("B")
+ }
}
diff --git a/packages/SystemUI/res/color/disconnected_network_primary_color.xml b/packages/SystemUI/res/color/disconnected_network_primary_color.xml
new file mode 100644
index 0000000..536bf78
--- /dev/null
+++ b/packages/SystemUI/res/color/disconnected_network_primary_color.xml
@@ -0,0 +1,20 @@
+<!--
+ ~ 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.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+ <item android:color="?androidprv:attr/materialColorPrimaryContainer" />
+</selector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml
index 22d156d..0029180 100644
--- a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml
+++ b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml
@@ -170,8 +170,7 @@
android:layout_height="28dp"
android:layout_marginStart="7dp"
android:layout_marginEnd="16dp"
- android:layout_gravity="center_vertical"
- android:background="?android:attr/textColorSecondary"/>
+ android:layout_gravity="center_vertical"/>
<FrameLayout
android:layout_width="@dimen/settingslib_switch_track_width"
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 36912ac..c428705d 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -1386,9 +1386,13 @@
<item name="android:textColor">?androidprv:attr/materialColorOnSurfaceVariant</item>
</style>
- <style name="TextAppearance.InternetDialog.Active"/>
+ <style name="TextAppearance.InternetDialog.Active">
+ <item name="android:textColor">?androidprv:attr/materialColorOnPrimaryContainer</item>
+ </style>
- <style name="TextAppearance.InternetDialog.Secondary.Active"/>
+ <style name="TextAppearance.InternetDialog.Secondary.Active">
+ <item name="android:textColor">?androidprv:attr/materialColorOnPrimaryContainer</item>
+ </style>
<style name="FgsManagerDialogTitle">
<item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
@@ -1424,17 +1428,32 @@
<item name="android:orientation">horizontal</item>
<item name="android:focusable">true</item>
<item name="android:clickable">true</item>
+ <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
+ </style>
+
+ <style name="BluetoothTileDialog.Device.Active">
+ <item name="android:textColor">?androidprv:attr/materialColorOnPrimaryContainer</item>
</style>
<style name="BluetoothTileDialog.DeviceName">
<item name="android:textSize">14sp</item>
<item name="android:textAppearance">@style/TextAppearance.Dialog.Title</item>
+ <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item>
</style>
<style name="BluetoothTileDialog.DeviceSummary">
<item name="android:ellipsize">end</item>
<item name="android:maxLines">2</item>
<item name="android:textAppearance">@style/TextAppearance.Dialog.Body.Message</item>
+ <item name="android:textColor">?androidprv:attr/materialColorOnSurfaceVariant</item>
+ </style>
+
+ <style name="BluetoothTileDialog.DeviceName.Active">
+ <item name="android:textColor">?androidprv:attr/materialColorOnPrimaryContainer</item>
+ </style>
+
+ <style name="BluetoothTileDialog.DeviceSummary.Active">
+ <item name="android:textColor">?androidprv:attr/materialColorOnPrimaryContainer</item>
</style>
<style name="BroadcastDialog">
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
index 911145b..f5b9a05 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
@@ -37,6 +37,7 @@
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
+import com.android.internal.R as InternalR
import com.android.internal.logging.UiEventLogger
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.res.R
@@ -367,6 +368,7 @@
private val nameView = view.requireViewById<TextView>(R.id.bluetooth_device_name)
private val summaryView = view.requireViewById<TextView>(R.id.bluetooth_device_summary)
private val iconView = view.requireViewById<ImageView>(R.id.bluetooth_device_icon)
+ private val iconGear = view.requireViewById<ImageView>(R.id.gear_icon_image)
private val gearView = view.requireViewById<View>(R.id.gear_icon)
internal fun bind(
@@ -380,6 +382,36 @@
mutableDeviceItemClick.tryEmit(item)
uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_CLICKED)
}
+
+ // updating icon colors
+ val tintColor =
+ com.android.settingslib.Utils.getColorAttr(
+ context,
+ if (item.isActive) InternalR.attr.materialColorOnPrimaryContainer
+ else InternalR.attr.materialColorOnSurface
+ )
+ .defaultColor
+
+ // update icons
+ iconView.apply {
+ item.iconWithDescription?.let {
+ setImageDrawable(it.first.apply { mutate()?.setTint(tintColor) })
+ contentDescription = it.second
+ }
+ }
+
+ iconGear.apply { drawable?.let { it.mutate()?.setTint(tintColor) } }
+
+ // update text styles
+ nameView.setTextAppearance(
+ if (item.isActive) R.style.BluetoothTileDialog_DeviceName_Active
+ else R.style.BluetoothTileDialog_DeviceName
+ )
+ summaryView.setTextAppearance(
+ if (item.isActive) R.style.BluetoothTileDialog_DeviceSummary_Active
+ else R.style.BluetoothTileDialog_DeviceSummary
+ )
+
accessibilityDelegate =
object : AccessibilityDelegate() {
override fun onInitializeAccessibilityNodeInfo(
@@ -398,12 +430,7 @@
}
nameView.text = item.deviceName
summaryView.text = item.connectionSummary
- iconView.apply {
- item.iconWithDescription?.let {
- setImageDrawable(it.first)
- contentDescription = it.second
- }
- }
+
gearView.setOnClickListener {
deviceItemOnClickCallback.onDeviceItemGearClicked(item, it)
}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt
index 0ea98d1..a78130f 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt
@@ -52,4 +52,5 @@
val background: Int? = null,
var isEnabled: Boolean = true,
var actionAccessibilityLabel: String = "",
+ var isActive: Boolean = false
)
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
index 51b2280..d7893db 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
@@ -55,7 +55,8 @@
type: DeviceItemType,
connectionSummary: String,
background: Int,
- actionAccessibilityLabel: String
+ actionAccessibilityLabel: String,
+ isActive: Boolean
): DeviceItem {
return DeviceItem(
type = type,
@@ -68,7 +69,8 @@
},
background = background,
isEnabled = !cachedDevice.isBusy,
- actionAccessibilityLabel = actionAccessibilityLabel
+ actionAccessibilityLabel = actionAccessibilityLabel,
+ isActive = isActive
)
}
}
@@ -91,7 +93,8 @@
DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE,
cachedDevice.connectionSummary ?: "",
backgroundOn,
- context.getString(actionAccessibilityLabelDisconnect)
+ context.getString(actionAccessibilityLabelDisconnect),
+ isActive = true
)
}
}
@@ -116,7 +119,8 @@
cachedDevice.connectionSummary.takeUnless { it.isNullOrEmpty() }
?: context.getString(audioSharing),
if (cachedDevice.isBusy) backgroundOffBusy else backgroundOn,
- ""
+ "",
+ isActive = !cachedDevice.isBusy
)
}
}
@@ -150,7 +154,8 @@
cachedDevice.connectionSummary.takeUnless { it.isNullOrEmpty() }
?: context.getString(connected),
if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff,
- context.getString(actionAccessibilityLabelActivate)
+ context.getString(actionAccessibilityLabelActivate),
+ isActive = false
)
}
}
@@ -188,7 +193,8 @@
cachedDevice.connectionSummary.takeUnless { it.isNullOrEmpty() }
?: context.getString(connected),
if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff,
- context.getString(actionAccessibilityLabelDisconnect)
+ context.getString(actionAccessibilityLabelDisconnect),
+ isActive = false
)
}
}
@@ -216,7 +222,8 @@
cachedDevice.connectionSummary.takeUnless { it.isNullOrEmpty() }
?: context.getString(saved),
if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff,
- context.getString(actionAccessibilityLabelActivate)
+ context.getString(actionAccessibilityLabelActivate),
+ isActive = false
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
index bd0e729..04c6fa9 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
@@ -301,8 +301,8 @@
mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_SHOWN_EXPANDED);
setExpandedView(this::animateIn);
}
- mView.announceForAccessibility(
- getAccessibilityAnnouncement(mClipboardModel.getType()));
+ mWindow.withWindowAttached(() -> mView.announceForAccessibility(
+ getAccessibilityAnnouncement(mClipboardModel.getType())));
} else if (!mIsMinimized) {
setExpandedView(() -> {
});
@@ -320,8 +320,8 @@
setExpandedView();
}
animateIn();
- mView.announceForAccessibility(
- getAccessibilityAnnouncement(mClipboardModel.getType()));
+ mWindow.withWindowAttached(() -> mView.announceForAccessibility(
+ getAccessibilityAnnouncement(mClipboardModel.getType())));
} else if (!mIsMinimized) {
setExpandedView();
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java
index edc49cac2..f018336 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java
@@ -489,6 +489,10 @@
mMobileDataToggle.setVisibility(mCanConfigMobileData ? View.VISIBLE : View.INVISIBLE);
mMobileToggleDivider.setVisibility(
mCanConfigMobileData ? View.VISIBLE : View.INVISIBLE);
+ int primaryColor = isNetworkConnected
+ ? R.color.connected_network_primary_color
+ : R.color.disconnected_network_primary_color;
+ mMobileToggleDivider.setBackgroundColor(dialog.getContext().getColor(primaryColor));
// Display the info for the non-DDS if it's actively being used
int autoSwitchNonDdsSubId = mInternetDialogController.getActiveAutoSwitchNonDdsSubId();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt
index 4c6563d..083bf05 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt
@@ -16,14 +16,10 @@
package com.android.systemui.qs.tiles.impl.modes.domain.interactor
-//noinspection CleanArchitectureDependencyViolation: dialog needs to be opened on click
import android.content.Intent
import android.provider.Settings
-import com.android.internal.jank.InteractionJankMonitor
-import com.android.systemui.animation.DialogCuj
-import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.animation.Expandable
-import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
import com.android.systemui.qs.tiles.base.interactor.QSTileInput
import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
@@ -31,15 +27,13 @@
import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate
import javax.inject.Inject
-import kotlin.coroutines.CoroutineContext
-import kotlinx.coroutines.withContext
+@SysUISingleton
class ModesTileUserActionInteractor
@Inject
constructor(
- @Main private val coroutineContext: CoroutineContext,
- private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
- private val dialogTransitionAnimator: DialogTransitionAnimator,
+ private val qsTileIntentUserInputHandler: QSTileIntentUserInputHandler,
+ // TODO(b/353896370): The domain layer should not have to depend on the UI layer.
private val dialogDelegate: ModesDialogDelegate,
) : QSTileUserActionInteractor<ModesTileModel> {
val longClickIntent = Intent(Settings.ACTION_ZEN_MODE_SETTINGS)
@@ -51,29 +45,14 @@
handleClick(action.expandable)
}
is QSTileUserAction.LongClick -> {
- qsTileIntentUserActionHandler.handle(action.expandable, longClickIntent)
+ qsTileIntentUserInputHandler.handle(action.expandable, longClickIntent)
}
}
}
}
suspend fun handleClick(expandable: Expandable?) {
- // Show a dialog with the list of modes to configure. Dialogs shown by the
- // DialogTransitionAnimator must be created and shown on the main thread, so we post it to
- // the UI handler.
- withContext(coroutineContext) {
- val dialog = dialogDelegate.createDialog()
-
- expandable
- ?.dialogTransitionController(
- DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG)
- )
- ?.let { controller -> dialogTransitionAnimator.show(dialog, controller) }
- ?: dialog.show()
- }
- }
-
- companion object {
- private const val INTERACTION_JANK_TAG = "configure_priority_modes"
+ // Show a dialog with the list of modes to configure.
+ dialogDelegate.showDialog(expandable)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
index 2b094d6..8aa989f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
@@ -18,67 +18,155 @@
import android.content.Intent
import android.provider.Settings
+import android.util.Log
import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
import com.android.compose.PlatformButton
import com.android.compose.PlatformOutlinedButton
+import com.android.internal.annotations.VisibleForTesting
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.systemui.animation.DialogCuj
import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.Expandable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dialog.ui.composable.AlertDialogContent
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.ComponentSystemUIDialog
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.statusbar.phone.SystemUIDialogFactory
import com.android.systemui.statusbar.phone.create
import com.android.systemui.statusbar.policy.ui.dialog.composable.ModeTileGrid
import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel
+import com.android.systemui.util.Assert
import javax.inject.Inject
+import javax.inject.Provider
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.withContext
+@SysUISingleton
class ModesDialogDelegate
@Inject
constructor(
private val sysuiDialogFactory: SystemUIDialogFactory,
private val dialogTransitionAnimator: DialogTransitionAnimator,
private val activityStarter: ActivityStarter,
- private val viewModel: ModesDialogViewModel,
+ // Using a provider to avoid a circular dependency.
+ private val viewModel: Provider<ModesDialogViewModel>,
+ @Main private val mainCoroutineContext: CoroutineContext,
) : SystemUIDialog.Delegate {
+ // NOTE: This should only be accessed/written from the main thread.
+ @VisibleForTesting var currentDialog: ComponentSystemUIDialog? = null
+
override fun createDialog(): SystemUIDialog {
- return sysuiDialogFactory.create { dialog ->
- AlertDialogContent(
- title = { Text(stringResource(R.string.zen_modes_dialog_title)) },
- content = { ModeTileGrid(viewModel) },
- neutralButton = {
- PlatformOutlinedButton(
- onClick = {
- val animationController =
- dialogTransitionAnimator.createActivityTransitionController(
- dialog.getButton(SystemUIDialog.BUTTON_NEUTRAL)
- )
- if (animationController == null) {
- // The controller will take care of dismissing for us after the
- // animation, but let's make sure we dismiss the dialog if we don't
- // animate it.
- dialog.dismiss()
- }
- activityStarter.startActivity(
- ZEN_MODE_SETTINGS_INTENT,
- true /* dismissShade */,
- animationController
- )
- }
- ) {
- Text(stringResource(R.string.zen_modes_dialog_settings))
+ Assert.isMainThread()
+ if (currentDialog != null) {
+ Log.w(TAG, "Dialog is already open, dismissing it and creating a new one.")
+ currentDialog?.dismiss()
+ }
+
+ currentDialog = sysuiDialogFactory.create() { ModesDialogContent(it) }
+ currentDialog
+ ?.lifecycle
+ ?.addObserver(
+ object : DefaultLifecycleObserver {
+ override fun onStop(owner: LifecycleOwner) {
+ Assert.isMainThread()
+ currentDialog = null
}
- },
- positiveButton = {
- PlatformButton(onClick = { dialog.dismiss() }) {
- Text(stringResource(R.string.zen_modes_dialog_done))
- }
- },
+ }
+ )
+
+ return currentDialog!!
+ }
+
+ @Composable
+ private fun ModesDialogContent(dialog: SystemUIDialog) {
+ AlertDialogContent(
+ title = { Text(stringResource(R.string.zen_modes_dialog_title)) },
+ content = { ModeTileGrid(viewModel.get()) },
+ neutralButton = {
+ PlatformOutlinedButton(onClick = { openSettings(dialog) }) {
+ Text(stringResource(R.string.zen_modes_dialog_settings))
+ }
+ },
+ positiveButton = {
+ PlatformButton(onClick = { dialog.dismiss() }) {
+ Text(stringResource(R.string.zen_modes_dialog_done))
+ }
+ },
+ )
+ }
+
+ private fun openSettings(dialog: SystemUIDialog) {
+ val animationController =
+ dialogTransitionAnimator.createActivityTransitionController(dialog)
+ if (animationController == null) {
+ // The controller will take care of dismissing for us after
+ // the animation, but let's make sure we dismiss the dialog
+ // if we don't animate it.
+ dialog.dismiss()
+ }
+ activityStarter.startActivity(
+ ZEN_MODE_SETTINGS_INTENT,
+ true /* dismissShade */,
+ animationController
+ )
+ }
+
+ suspend fun showDialog(expandable: Expandable? = null): SystemUIDialog {
+ // Dialogs shown by the DialogTransitionAnimator must be created and shown on the main
+ // thread, so we post it to the UI handler.
+ withContext(mainCoroutineContext) {
+ // Create the dialog if necessary
+ if (currentDialog == null) {
+ createDialog()
+ }
+
+ expandable
+ ?.dialogTransitionController(
+ DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG)
+ )
+ ?.let { controller -> dialogTransitionAnimator.show(currentDialog!!, controller) }
+ ?: currentDialog!!.show()
+ }
+
+ return currentDialog!!
+ }
+
+ /**
+ * Launches the [intent] by animating from the dialog. If the dialog is not showing, just
+ * launches it normally without animating.
+ */
+ fun launchFromDialog(intent: Intent) {
+ Assert.isMainThread()
+ if (currentDialog == null) {
+ Log.w(
+ TAG,
+ "Cannot launch from dialog, the dialog is not present. " +
+ "Will launch activity without animating."
)
}
+
+ val animationController =
+ currentDialog?.let { dialogTransitionAnimator.createActivityTransitionController(it) }
+ if (animationController == null) {
+ currentDialog?.dismiss()
+ }
+ activityStarter.startActivity(
+ intent,
+ true, /* dismissShade */
+ animationController,
+ )
}
companion object {
+ private const val TAG = "ModesDialogDelegate"
private val ZEN_MODE_SETTINGS_INTENT = Intent(Settings.ACTION_ZEN_MODE_SETTINGS)
+ private const val INTERACTION_JANK_TAG = "configure_priority_modes"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt
index e84c8b6..7e64090 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt
@@ -17,11 +17,15 @@
package com.android.systemui.statusbar.policy.ui.dialog.viewmodel
import android.content.Context
+import android.content.Intent
+import android.provider.Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS
+import android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID
import com.android.settingslib.notification.modes.ZenMode
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
+import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
@@ -39,6 +43,7 @@
val context: Context,
zenModeInteractor: ZenModeInteractor,
@Background val bgDispatcher: CoroutineDispatcher,
+ private val dialogDelegate: ModesDialogDelegate,
) {
// Modes that should be displayed in the dialog
// TODO(b/346519570): Include modes that have not been set up yet.
@@ -71,7 +76,10 @@
}
},
onLongClick = {
- // TODO(b/346519570): Open settings page for mode.
+ val intent: Intent =
+ Intent(ACTION_AUTOMATIC_ZEN_RULE_SETTINGS)
+ .putExtra(EXTRA_AUTOMATIC_ZEN_RULE_ID, mode.id)
+ dialogDelegate.launchFromDialog(intent)
}
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt
index 27b6ea6..74d9692 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt
@@ -27,7 +27,6 @@
import com.android.internal.logging.MetricsLogger
import com.android.settingslib.notification.data.repository.FakeZenModeRepository
import com.android.systemui.SysuiTestCase
-import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -47,7 +46,6 @@
import com.android.systemui.util.settings.FakeSettings
import com.android.systemui.util.settings.SecureSettings
import com.google.common.truth.Truth.assertThat
-import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
@@ -82,8 +80,6 @@
@Mock private lateinit var qsTileConfigProvider: QSTileConfigProvider
- @Mock private lateinit var dialogTransitionAnimator: DialogTransitionAnimator
-
@Mock private lateinit var dialogDelegate: ModesDialogDelegate
private val inputHandler = FakeQSTileIntentUserInputHandler()
@@ -131,9 +127,7 @@
userActionInteractor =
ModesTileUserActionInteractor(
- EmptyCoroutineContext,
inputHandler,
- dialogTransitionAnimator,
dialogDelegate,
)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt
index b23767e..5ac41ec 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt
@@ -18,6 +18,10 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testCase
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.mockActivityTransitionAnimatorController by
+ Kosmos.Fixture { mock<ActivityTransitionAnimator.Controller>() }
val Kosmos.activityTransitionAnimator by
Kosmos.Fixture {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorKosmos.kt
new file mode 100644
index 0000000..2ecfb45
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorKosmos.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.qs.tiles.impl.modes.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.tiles.base.actions.qsTileIntentUserInputHandler
+import com.android.systemui.statusbar.policy.ui.dialog.modesDialogDelegate
+import javax.inject.Provider
+
+val Kosmos.modesTileUserActionInteractor: ModesTileUserActionInteractor by
+ Kosmos.Fixture {
+ ModesTileUserActionInteractor(
+ qsTileIntentUserInputHandler,
+ Provider { modesDialogDelegate }.get(),
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateKosmos.kt
new file mode 100644
index 0000000..99bb479
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateKosmos.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.statusbar.policy.ui.dialog
+
+import com.android.systemui.animation.dialogTransitionAnimator
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.mainCoroutineContext
+import com.android.systemui.plugins.activityStarter
+import com.android.systemui.statusbar.phone.systemUIDialogFactory
+import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.modesDialogViewModel
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.mockModesDialogDelegate by Kosmos.Fixture { mock<ModesDialogDelegate>() }
+
+var Kosmos.modesDialogDelegate: ModesDialogDelegate by
+ Kosmos.Fixture {
+ ModesDialogDelegate(
+ systemUIDialogFactory,
+ dialogTransitionAnimator,
+ activityStarter,
+ { modesDialogViewModel },
+ mainCoroutineContext,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelKosmos.kt
new file mode 100644
index 0000000..00020f8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelKosmos.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.statusbar.policy.ui.dialog.viewmodel
+
+import android.content.mockedContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
+import com.android.systemui.statusbar.policy.ui.dialog.modesDialogDelegate
+import javax.inject.Provider
+
+val Kosmos.modesDialogViewModel: ModesDialogViewModel by
+ Kosmos.Fixture {
+ ModesDialogViewModel(
+ mockedContext,
+ zenModeInteractor,
+ testDispatcher,
+ Provider { modesDialogDelegate }.get(),
+ )
+ }