Shortcut Helper - Implement lifecycle of new version
- Repository holds the lifecycle state. It listens to system events
(show, hide, toggle requests) and ui events (user leaving) to map the
lifecycle state
- ShortcutHelperActivityStarter starts the activity when state becomes
active
- ShortcutHelperActivity finishes itself when the state becomes inactive
Bug: 335387428
Test: Unit tests in this CL
Test: Manually
Flag: com.android.systemui.keyboard_shortcut_helper_rewrite
Change-Id: Icf4d24440f5bc0937bfee48c859c0d40db979883
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 59e2b91..b9e70ef 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -1110,7 +1110,7 @@
android:resource="@xml/home_controls_dream_metadata" />
</service>
- <activity android:name="com.android.systemui.keyboard.shortcut.ShortcutHelperActivity"
+ <activity android:name="com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity"
android:exported="false"
android:theme="@style/ShortcutHelperTheme"
android:excludeFromRecents="true"
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt
index c6fb4f9..fc9406b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt
@@ -19,12 +19,13 @@
import com.android.systemui.keyboard.data.repository.KeyboardRepository
import com.android.systemui.keyboard.data.repository.KeyboardRepositoryImpl
+import com.android.systemui.keyboard.shortcut.ShortcutHelperModule
import com.android.systemui.keyboard.stickykeys.data.repository.StickyKeysRepository
import com.android.systemui.keyboard.stickykeys.data.repository.StickyKeysRepositoryImpl
import dagger.Binds
import dagger.Module
-@Module
+@Module(includes = [ShortcutHelperModule::class])
abstract class KeyboardModule {
@Binds
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt
new file mode 100644
index 0000000..5635f80
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.keyboard.shortcut
+
+import android.app.Activity
+import com.android.systemui.CoreStartable
+import com.android.systemui.Flags.keyboardShortcutHelperRewrite
+import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperRepository
+import com.android.systemui.keyboard.shortcut.ui.ShortcutHelperActivityStarter
+import com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity
+import dagger.Binds
+import dagger.Lazy
+import dagger.Module
+import dagger.Provides
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+
+@Module
+interface ShortcutHelperModule {
+
+ @Binds
+ @IntoMap
+ @ClassKey(ShortcutHelperActivity::class)
+ fun activity(impl: ShortcutHelperActivity): Activity
+
+ companion object {
+ @Provides
+ @IntoMap
+ @ClassKey(ShortcutHelperActivityStarter::class)
+ fun starter(implLazy: Lazy<ShortcutHelperActivityStarter>): CoreStartable {
+ return if (keyboardShortcutHelperRewrite()) {
+ implLazy.get()
+ } else {
+ // No-op implementation when the flag is disabled.
+ NoOpStartable
+ }
+ }
+
+ @Provides
+ @IntoMap
+ @ClassKey(ShortcutHelperRepository::class)
+ fun repo(implLazy: Lazy<ShortcutHelperRepository>): CoreStartable {
+ return if (keyboardShortcutHelperRewrite()) {
+ implLazy.get()
+ } else {
+ // No-op implementation when the flag is disabled.
+ NoOpStartable
+ }
+ }
+ }
+}
+
+private object NoOpStartable : CoreStartable {
+ override fun start() {}
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperRepository.kt
new file mode 100644
index 0000000..9450af4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperRepository.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.keyboard.shortcut.data.repository
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import com.android.systemui.CoreStartable
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Active
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Inactive
+import com.android.systemui.statusbar.CommandQueue
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+
+@SysUISingleton
+class ShortcutHelperRepository
+@Inject
+constructor(
+ private val commandQueue: CommandQueue,
+ private val broadcastDispatcher: BroadcastDispatcher,
+) : CoreStartable {
+
+ val state = MutableStateFlow<ShortcutHelperState>(Inactive)
+
+ override fun start() {
+ registerBroadcastReceiver(
+ action = Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS,
+ onReceive = { state.value = Active() }
+ )
+ registerBroadcastReceiver(
+ action = Intent.ACTION_DISMISS_KEYBOARD_SHORTCUTS,
+ onReceive = { state.value = Inactive }
+ )
+ commandQueue.addCallback(
+ object : CommandQueue.Callbacks {
+ override fun dismissKeyboardShortcutsMenu() {
+ state.value = Inactive
+ }
+
+ override fun toggleKeyboardShortcutsMenu(deviceId: Int) {
+ state.value =
+ if (state.value is Inactive) {
+ Active(deviceId)
+ } else {
+ Inactive
+ }
+ }
+ }
+ )
+ }
+
+ fun hide() {
+ state.value = Inactive
+ }
+
+ private fun registerBroadcastReceiver(action: String, onReceive: () -> Unit) {
+ broadcastDispatcher.registerReceiver(
+ receiver =
+ object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ onReceive()
+ }
+ },
+ filter = IntentFilter(action),
+ flags = Context.RECEIVER_EXPORTED or Context.RECEIVER_VISIBLE_TO_INSTANT_APPS
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperInteractor.kt
new file mode 100644
index 0000000..d3f7e24
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperInteractor.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.keyboard.shortcut.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperRepository
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+@SysUISingleton
+class ShortcutHelperInteractor
+@Inject
+constructor(private val repository: ShortcutHelperRepository) {
+
+ val state: Flow<ShortcutHelperState> = repository.state
+
+ fun onUserLeave() {
+ repository.hide()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutHelperState.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutHelperState.kt
new file mode 100644
index 0000000..d22d6c8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutHelperState.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.keyboard.shortcut.shared.model
+
+sealed interface ShortcutHelperState {
+ data object Inactive : ShortcutHelperState
+
+ data class Active(val deviceId: Int? = null) : ShortcutHelperState
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarter.kt
new file mode 100644
index 0000000..fbf52e7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarter.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.keyboard.shortcut.ui
+
+import android.content.Context
+import android.content.Intent
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity
+import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class ShortcutHelperActivityStarter(
+ private val context: Context,
+ @Application private val applicationScope: CoroutineScope,
+ private val viewModel: ShortcutHelperViewModel,
+ private val startActivity: (Intent) -> Unit,
+) : CoreStartable {
+
+ @Inject
+ constructor(
+ context: Context,
+ @Application applicationScope: CoroutineScope,
+ viewModel: ShortcutHelperViewModel,
+ ) : this(
+ context,
+ applicationScope,
+ viewModel,
+ startActivity = { intent -> context.startActivity(intent) }
+ )
+
+ override fun start() {
+ applicationScope.launch {
+ viewModel.shouldShow.collect { shouldShow ->
+ if (shouldShow) {
+ startShortcutHelperActivity()
+ }
+ }
+ }
+ }
+
+ private fun startShortcutHelperActivity() {
+ startActivity(
+ Intent(context, ShortcutHelperActivity::class.java)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperActivity.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
similarity index 86%
rename from packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperActivity.kt
rename to packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
index 692fbb0..934f9ee 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.keyboard.shortcut
+package com.android.systemui.keyboard.shortcut.ui.view
import android.graphics.Insets
import android.os.Bundle
@@ -24,16 +24,25 @@
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.core.view.updatePadding
+import androidx.lifecycle.flowWithLifecycle
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
import com.android.systemui.res.R
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN
+import javax.inject.Inject
+import kotlinx.coroutines.launch
/**
* Activity that hosts the new version of the keyboard shortcut helper. It will be used both for
* small and large screen devices.
*/
-class ShortcutHelperActivity : ComponentActivity() {
+class ShortcutHelperActivity
+@Inject
+constructor(
+ private val viewModel: ShortcutHelperViewModel,
+) : ComponentActivity() {
private val bottomSheetContainer
get() = requireViewById<View>(R.id.shortcut_helper_sheet_container)
@@ -53,6 +62,24 @@
setUpPredictiveBack()
setUpSheetDismissListener()
setUpDismissOnTouchOutside()
+ observeFinishRequired()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ if (isFinishing) {
+ viewModel.onUserLeave()
+ }
+ }
+
+ private fun observeFinishRequired() {
+ lifecycleScope.launch {
+ viewModel.shouldShow.flowWithLifecycle(lifecycle).collect { shouldShow ->
+ if (!shouldShow) {
+ finish()
+ }
+ }
+ }
}
private fun setupEdgeToEdge() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt
new file mode 100644
index 0000000..7e48c65
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.keyboard.shortcut.ui.viewmodel
+
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperInteractor
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+class ShortcutHelperViewModel
+@Inject
+constructor(
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+ private val interactor: ShortcutHelperInteractor
+) {
+
+ val shouldShow =
+ interactor.state
+ .map { it is ShortcutHelperState.Active }
+ .distinctUntilChanged()
+ .flowOn(backgroundDispatcher)
+
+ fun onUserLeave() {
+ interactor.onUserLeave()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt
new file mode 100644
index 0000000..05a2ca2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt
@@ -0,0 +1,132 @@
+/*
+ * 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.keyboard.shortcut.ui
+
+import android.content.Intent
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyboard.shortcut.fakeShortcutHelperStartActivity
+import com.android.systemui.keyboard.shortcut.shortcutHelperActivityStarter
+import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
+import com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class ShortcutHelperActivityStarterTest : SysuiTestCase() {
+
+ private val kosmos =
+ Kosmos().also {
+ it.testCase = this
+ it.testDispatcher = UnconfinedTestDispatcher()
+ }
+
+ private val testScope = kosmos.testScope
+ private val testHelper = kosmos.shortcutHelperTestHelper
+ private val fakeStartActivity = kosmos.fakeShortcutHelperStartActivity
+ private val starter = kosmos.shortcutHelperActivityStarter
+
+ @Test
+ fun start_doesNotStartByDefault() =
+ testScope.runTest {
+ starter.start()
+
+ assertThat(fakeStartActivity.startIntents).isEmpty()
+ }
+
+ @Test
+ fun start_onToggle_startsActivity() =
+ testScope.runTest {
+ starter.start()
+
+ testHelper.toggle(deviceId = 456)
+
+ verifyShortcutHelperActivityStarted()
+ }
+
+ @Test
+ fun start_onToggle_multipleTimesStartsActivityOnlyWhenNotStarted() =
+ testScope.runTest {
+ starter.start()
+
+ testHelper.toggle(deviceId = 456)
+ testHelper.toggle(deviceId = 456)
+ testHelper.toggle(deviceId = 456)
+ testHelper.toggle(deviceId = 456)
+
+ verifyShortcutHelperActivityStarted(numTimes = 2)
+ }
+
+ @Test
+ fun start_onRequestShowShortcuts_startsActivity() =
+ testScope.runTest {
+ starter.start()
+
+ testHelper.showFromActivity()
+
+ verifyShortcutHelperActivityStarted()
+ }
+
+ @Test
+ fun start_onRequestShowShortcuts_multipleTimes_startsActivityOnlyOnce() =
+ testScope.runTest {
+ starter.start()
+
+ testHelper.showFromActivity()
+ testHelper.showFromActivity()
+ testHelper.showFromActivity()
+
+ verifyShortcutHelperActivityStarted(numTimes = 1)
+ }
+
+ @Test
+ fun start_onRequestShowShortcuts_multipleTimes_startsActivityOnlyWhenNotStarted() =
+ testScope.runTest {
+ starter.start()
+
+ testHelper.hideFromActivity()
+ testHelper.hideForSystem()
+ testHelper.toggle(deviceId = 987)
+ testHelper.showFromActivity()
+ testHelper.hideFromActivity()
+ testHelper.hideForSystem()
+ testHelper.toggle(deviceId = 456)
+ testHelper.showFromActivity()
+
+ verifyShortcutHelperActivityStarted(numTimes = 2)
+ }
+
+ private fun verifyShortcutHelperActivityStarted(numTimes: Int = 1) {
+ assertThat(fakeStartActivity.startIntents).hasSize(numTimes)
+ fakeStartActivity.startIntents.forEach { intent ->
+ assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
+ assertThat(intent.filterEquals(Intent(context, ShortcutHelperActivity::class.java)))
+ .isTrue()
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt
new file mode 100644
index 0000000..8653308
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt
@@ -0,0 +1,130 @@
+/*
+ * 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.keyboard.shortcut.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
+import com.android.systemui.keyboard.shortcut.shortcutHelperViewModel
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class ShortcutHelperViewModelTest : SysuiTestCase() {
+
+ private val kosmos =
+ Kosmos().also {
+ it.testCase = this
+ it.testDispatcher = UnconfinedTestDispatcher()
+ }
+
+ private val testScope = kosmos.testScope
+ private val testHelper = kosmos.shortcutHelperTestHelper
+
+ private val viewModel = kosmos.shortcutHelperViewModel
+
+ @Test
+ fun shouldShow_falseByDefault() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(viewModel.shouldShow)
+
+ assertThat(shouldShow).isFalse()
+ }
+
+ @Test
+ fun shouldShow_trueAfterShowRequested() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(viewModel.shouldShow)
+
+ testHelper.showFromActivity()
+
+ assertThat(shouldShow).isTrue()
+ }
+
+ @Test
+ fun shouldShow_trueAfterToggleRequested() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(viewModel.shouldShow)
+
+ testHelper.toggle(deviceId = 123)
+
+ assertThat(shouldShow).isTrue()
+ }
+
+ @Test
+ fun shouldShow_falseAfterToggleTwice() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(viewModel.shouldShow)
+
+ testHelper.toggle(deviceId = 123)
+ testHelper.toggle(deviceId = 123)
+
+ assertThat(shouldShow).isFalse()
+ }
+
+ @Test
+ fun shouldShow_falseAfterViewDestroyed() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(viewModel.shouldShow)
+
+ testHelper.toggle(deviceId = 567)
+ viewModel.onUserLeave()
+
+ assertThat(shouldShow).isFalse()
+ }
+
+ @Test
+ fun shouldShow_doesNotEmitDuplicateValues() =
+ testScope.runTest {
+ val shouldShowValues by collectValues(viewModel.shouldShow)
+
+ testHelper.hideForSystem()
+ testHelper.toggle(deviceId = 987)
+ testHelper.showFromActivity()
+ viewModel.onUserLeave()
+ testHelper.hideFromActivity()
+ testHelper.hideForSystem()
+ testHelper.toggle(deviceId = 456)
+ testHelper.showFromActivity()
+
+ assertThat(shouldShowValues).containsExactly(false, true, false, true).inOrder()
+ }
+
+ @Test
+ fun shouldShow_emitsLatestValueToNewSubscribers() =
+ testScope.runTest {
+ val shouldShow by collectLastValue(viewModel.shouldShow)
+
+ testHelper.showFromActivity()
+
+ val shouldShowNew by collectLastValue(viewModel.shouldShow)
+ assertThat(shouldShowNew).isEqualTo(shouldShow)
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/FakeShortcutHelperStartActivity.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/FakeShortcutHelperStartActivity.kt
new file mode 100644
index 0000000..3190171
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/FakeShortcutHelperStartActivity.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.keyboard.shortcut
+
+import android.content.Intent
+
+class FakeShortcutHelperStartActivity : (Intent) -> Unit {
+
+ val startIntents = mutableListOf<Intent>()
+
+ override fun invoke(intent: Intent) {
+ startIntents += intent
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
new file mode 100644
index 0000000..38f2a56
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.keyboard.shortcut
+
+import android.content.applicationContext
+import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperRepository
+import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperTestHelper
+import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperInteractor
+import com.android.systemui.keyboard.shortcut.ui.ShortcutHelperActivityStarter
+import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
+import com.android.systemui.keyguard.data.repository.fakeCommandQueue
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+
+val Kosmos.shortcutHelperRepository by
+ Kosmos.Fixture { ShortcutHelperRepository(fakeCommandQueue, broadcastDispatcher) }
+
+val Kosmos.shortcutHelperTestHelper by
+ Kosmos.Fixture {
+ ShortcutHelperTestHelper(
+ shortcutHelperRepository,
+ applicationContext,
+ broadcastDispatcher,
+ fakeCommandQueue
+ )
+ }
+
+val Kosmos.shortcutHelperInteractor by
+ Kosmos.Fixture { ShortcutHelperInteractor(shortcutHelperRepository) }
+
+val Kosmos.shortcutHelperViewModel by
+ Kosmos.Fixture { ShortcutHelperViewModel(testDispatcher, shortcutHelperInteractor) }
+
+val Kosmos.fakeShortcutHelperStartActivity by Kosmos.Fixture { FakeShortcutHelperStartActivity() }
+
+val Kosmos.shortcutHelperActivityStarter by
+ Kosmos.Fixture {
+ ShortcutHelperActivityStarter(
+ applicationContext,
+ applicationCoroutineScope,
+ shortcutHelperViewModel,
+ fakeShortcutHelperStartActivity,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt
new file mode 100644
index 0000000..772ce41
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperTestHelper.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.keyboard.shortcut.data.repository
+
+import android.content.Context
+import android.content.Intent
+import com.android.systemui.broadcast.FakeBroadcastDispatcher
+import com.android.systemui.keyguard.data.repository.FakeCommandQueue
+
+class ShortcutHelperTestHelper(
+ repo: ShortcutHelperRepository,
+ private val context: Context,
+ private val fakeBroadcastDispatcher: FakeBroadcastDispatcher,
+ private val fakeCommandQueue: FakeCommandQueue,
+) {
+
+ init {
+ repo.start()
+ }
+
+ fun hideFromActivity() {
+ fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+ context,
+ Intent(Intent.ACTION_DISMISS_KEYBOARD_SHORTCUTS)
+ )
+ }
+
+ fun showFromActivity() {
+ fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+ context,
+ Intent(Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS)
+ )
+ }
+
+ fun toggle(deviceId: Int) {
+ fakeCommandQueue.doForEachCallback { it.toggleKeyboardShortcutsMenu(deviceId) }
+ }
+
+ fun hideForSystem() {
+ fakeCommandQueue.doForEachCallback { it.dismissKeyboardShortcutsMenu() }
+ }
+}