Merge "Auto Add/Remove a11y tiles when the ACCESSIBILITY_QS_TILES updates" into main
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt
new file mode 100644
index 0000000..9287edf
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.accessibility.data.repository
+
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AccessibilityQsShortcutsRepositoryImplTest : SysuiTestCase() {
+ private val testDispatcher = StandardTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+ private val secureSettings = FakeSettings()
+
+ private val userA11yQsShortcutsRepositoryFactory =
+ object : UserA11yQsShortcutsRepository.Factory {
+ override fun create(userId: Int): UserA11yQsShortcutsRepository {
+ return UserA11yQsShortcutsRepository(
+ userId,
+ secureSettings,
+ testScope.backgroundScope,
+ testDispatcher,
+ )
+ }
+ }
+
+ private val underTest =
+ AccessibilityQsShortcutsRepositoryImpl(userA11yQsShortcutsRepositoryFactory)
+
+ @Test
+ fun a11yQsShortcutTargetsForCorrectUsers() =
+ testScope.runTest {
+ val user0 = 0
+ val targetsForUser0 = setOf("a", "b", "c")
+ val user1 = 1
+ val targetsForUser1 = setOf("A")
+ val targetsFromUser0 by collectLastValue(underTest.a11yQsShortcutTargets(user0))
+ val targetsFromUser1 by collectLastValue(underTest.a11yQsShortcutTargets(user1))
+
+ storeA11yQsShortcutTargetsForUser(targetsForUser0, user0)
+ storeA11yQsShortcutTargetsForUser(targetsForUser1, user1)
+
+ assertThat(targetsFromUser0).isEqualTo(targetsForUser0)
+ assertThat(targetsFromUser1).isEqualTo(targetsForUser1)
+ }
+
+ private fun storeA11yQsShortcutTargetsForUser(a11yQsTargets: Set<String>, forUser: Int) {
+ secureSettings.putStringForUser(
+ SETTING_NAME,
+ a11yQsTargets.joinToString(separator = ":"),
+ forUser
+ )
+ }
+
+ companion object {
+ private const val SETTING_NAME = Settings.Secure.ACCESSIBILITY_QS_TARGETS
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt
new file mode 100644
index 0000000..ce22e28
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.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.accessibility.data.repository
+
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class UserA11yQsShortcutsRepositoryTest : SysuiTestCase() {
+ private val secureSettings = FakeSettings()
+ private val testDispatcher = StandardTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ private val underTest =
+ UserA11yQsShortcutsRepository(
+ USER_ID,
+ secureSettings,
+ testScope.backgroundScope,
+ testDispatcher
+ )
+
+ @Test
+ fun targetsMatchesSetting() =
+ testScope.runTest {
+ val observedTargets by collectLastValue(underTest.targets)
+ val a11yQsTargets = setOf("a", "b", "c")
+ secureSettings.putStringForUser(
+ SETTING_NAME,
+ a11yQsTargets.joinToString(SEPARATOR),
+ USER_ID
+ )
+
+ assertThat(observedTargets).isEqualTo(a11yQsTargets)
+ }
+
+ companion object {
+ private const val USER_ID = 0
+ private const val SEPARATOR = ":"
+ private const val SETTING_NAME = Settings.Secure.ACCESSIBILITY_QS_TARGETS
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableListTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableListTest.kt
new file mode 100644
index 0000000..311122d
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableListTest.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2023 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.pipeline.domain.autoaddable
+
+import android.content.ComponentName
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.view.accessibility.Flags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.accessibility.AccessibilityShortcutController
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.ColorCorrectionTile
+import com.android.systemui.qs.tiles.ColorInversionTile
+import com.android.systemui.qs.tiles.OneHandedModeTile
+import com.android.systemui.qs.tiles.ReduceBrightColorsTile
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class A11yShortcutAutoAddableListTest : SysuiTestCase() {
+
+ private val factory =
+ object : A11yShortcutAutoAddable.Factory {
+ override fun create(
+ spec: TileSpec,
+ componentName: ComponentName
+ ): A11yShortcutAutoAddable {
+ return A11yShortcutAutoAddable(mock(), mock(), spec, componentName)
+ }
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_A11Y_QS_SHORTCUT)
+ fun getA11yShortcutAutoAddables_withA11yQsShortcutFlagOff_emptyResult() {
+ val autoAddables = A11yShortcutAutoAddableList.getA11yShortcutAutoAddables(factory)
+
+ assertThat(autoAddables).isEmpty()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_A11Y_QS_SHORTCUT)
+ fun getA11yShortcutAutoAddables_withA11yQsShortcutFlagOn_correctAutoAddables() {
+ val expected =
+ setOf(
+ factory.create(
+ TileSpec.create(ColorCorrectionTile.TILE_SPEC),
+ AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME
+ ),
+ factory.create(
+ TileSpec.create(ColorInversionTile.TILE_SPEC),
+ AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME
+ ),
+ factory.create(
+ TileSpec.create(OneHandedModeTile.TILE_SPEC),
+ AccessibilityShortcutController.ONE_HANDED_COMPONENT_NAME
+ ),
+ factory.create(
+ TileSpec.create(ReduceBrightColorsTile.TILE_SPEC),
+ AccessibilityShortcutController.REDUCE_BRIGHT_COLORS_COMPONENT_NAME
+ ),
+ )
+
+ val autoAddables = A11yShortcutAutoAddableList.getA11yShortcutAutoAddables(factory)
+
+ assertThat(autoAddables).isNotEmpty()
+ assertThat(autoAddables).containsExactlyElementsIn(expected)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableTest.kt
new file mode 100644
index 0000000..3b33a43
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableTest.kt
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2023 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.pipeline.domain.autoaddable
+
+import android.content.ComponentName
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.accessibility.data.repository.FakeAccessibilityQsShortcutsRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class A11yShortcutAutoAddableTest : SysuiTestCase() {
+ private val testDispatcher = StandardTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ private val a11yQsShortcutsRepository = FakeAccessibilityQsShortcutsRepository()
+ private val underTest =
+ A11yShortcutAutoAddable(a11yQsShortcutsRepository, testDispatcher, SPEC, TARGET_COMPONENT)
+
+ @Test
+ fun settingNotSet_noSignal() =
+ testScope.runTest {
+ val signal by collectLastValue(underTest.autoAddSignal(USER_ID))
+
+ assertThat(signal).isNull() // null means no emitted value
+ }
+
+ @Test
+ fun settingSetWithTarget_addSignal() =
+ testScope.runTest {
+ val signal by collectLastValue(underTest.autoAddSignal(USER_ID))
+ assertThat(signal).isNull()
+
+ a11yQsShortcutsRepository.setA11yQsShortcutTargets(
+ USER_ID,
+ setOf(TARGET_COMPONENT_FLATTEN)
+ )
+
+ assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC))
+ }
+
+ @Test
+ fun settingSetWithoutTarget_removeSignal() =
+ testScope.runTest {
+ val signal by collectLastValue(flow = underTest.autoAddSignal(USER_ID))
+ assertThat(signal).isNull()
+
+ a11yQsShortcutsRepository.setA11yQsShortcutTargets(
+ USER_ID,
+ setOf(OTHER_COMPONENT_FLATTEN)
+ )
+
+ assertThat(signal).isEqualTo(AutoAddSignal.Remove(SPEC))
+ }
+
+ @Test
+ fun settingSetWithMultipleComponents_containsTarget_addSignal() =
+ testScope.runTest {
+ val signal by collectLastValue(underTest.autoAddSignal(USER_ID))
+ assertThat(signal).isNull()
+
+ a11yQsShortcutsRepository.setA11yQsShortcutTargets(
+ USER_ID,
+ setOf(OTHER_COMPONENT_FLATTEN, TARGET_COMPONENT_FLATTEN)
+ )
+
+ assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC))
+ }
+
+ @Test
+ fun settingSetWithMultipleComponents_doesNotContainTarget_removeSignal() =
+ testScope.runTest {
+ val signal by collectLastValue(underTest.autoAddSignal(USER_ID))
+ assertThat(signal).isNull()
+
+ a11yQsShortcutsRepository.setA11yQsShortcutTargets(
+ USER_ID,
+ setOf(OTHER_COMPONENT_FLATTEN, OTHER_COMPONENT_FLATTEN)
+ )
+
+ assertThat(signal).isEqualTo(AutoAddSignal.Remove(SPEC))
+ }
+
+ @Test
+ fun multipleChangesWithTarget_onlyOneAddSignal() =
+ testScope.runTest {
+ val signals by collectValues(underTest.autoAddSignal(USER_ID))
+ assertThat(signals).isEmpty()
+
+ repeat(3) {
+ a11yQsShortcutsRepository.setA11yQsShortcutTargets(
+ USER_ID,
+ setOf(TARGET_COMPONENT_FLATTEN)
+ )
+ }
+
+ assertThat(signals.size).isEqualTo(1)
+ assertThat(signals[0]).isEqualTo(AutoAddSignal.Add(SPEC))
+ }
+
+ @Test
+ fun multipleChangesWithoutTarget_onlyOneRemoveSignal() =
+ testScope.runTest {
+ val signals by collectValues(underTest.autoAddSignal(USER_ID))
+ assertThat(signals).isEmpty()
+
+ repeat(3) {
+ a11yQsShortcutsRepository.setA11yQsShortcutTargets(
+ USER_ID,
+ setOf("$OTHER_COMPONENT_FLATTEN$it")
+ )
+ }
+
+ assertThat(signals.size).isEqualTo(1)
+ assertThat(signals[0]).isEqualTo(AutoAddSignal.Remove(SPEC))
+ }
+
+ @Test
+ fun settingSetWithTargetForUsers_onlySignalInThatUser() =
+ testScope.runTest {
+ val otherUserId = USER_ID + 1
+ val signalTargetUser by collectLastValue(underTest.autoAddSignal(USER_ID))
+ val signalOtherUser by collectLastValue(underTest.autoAddSignal(otherUserId))
+ assertThat(signalTargetUser).isNull()
+ assertThat(signalOtherUser).isNull()
+
+ a11yQsShortcutsRepository.setA11yQsShortcutTargets(
+ USER_ID,
+ setOf(TARGET_COMPONENT_FLATTEN)
+ )
+
+ assertThat(signalTargetUser).isEqualTo(AutoAddSignal.Add(SPEC))
+ assertThat(signalOtherUser).isNull()
+ }
+
+ @Test
+ fun strategyAlways() {
+ assertThat(underTest.autoAddTracking).isEqualTo(AutoAddTracking.Always)
+ }
+
+ companion object {
+ private val SPEC = TileSpec.create("spec")
+ private val TARGET_COMPONENT = ComponentName("FakePkgName", "FakeClassName")
+ private val TARGET_COMPONENT_FLATTEN = TARGET_COMPONENT.flattenToString()
+ private val OTHER_COMPONENT_FLATTEN =
+ ComponentName("FakePkgName", "OtherClassName").flattenToString()
+ private const val USER_ID = 0
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt
index 8c2d221..35f9344 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt
@@ -16,6 +16,8 @@
package com.android.systemui.accessibility
+import com.android.systemui.accessibility.data.repository.AccessibilityQsShortcutsRepository
+import com.android.systemui.accessibility.data.repository.AccessibilityQsShortcutsRepositoryImpl
import com.android.systemui.accessibility.data.repository.ColorCorrectionRepository
import com.android.systemui.accessibility.data.repository.ColorCorrectionRepositoryImpl
import com.android.systemui.accessibility.data.repository.ColorInversionRepository
@@ -31,4 +33,9 @@
@Binds
fun colorInversionRepository(impl: ColorInversionRepositoryImpl): ColorInversionRepository
+
+ @Binds
+ fun accessibilityQsShortcutsRepository(
+ impl: AccessibilityQsShortcutsRepositoryImpl
+ ): AccessibilityQsShortcutsRepository
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepository.kt
new file mode 100644
index 0000000..401ac0f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepository.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2023 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.accessibility.data.repository
+
+import android.util.SparseArray
+import androidx.annotation.GuardedBy
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.flow.SharedFlow
+
+/** Provides data related to accessibility quick setting shortcut option. */
+interface AccessibilityQsShortcutsRepository {
+ /**
+ * Observable for the a11y features the user chooses in the Settings app to use the quick
+ * setting option.
+ */
+ fun a11yQsShortcutTargets(userId: Int): SharedFlow<Set<String>>
+}
+
+@SysUISingleton
+class AccessibilityQsShortcutsRepositoryImpl
+@Inject
+constructor(
+ private val userA11yQsShortcutsRepositoryFactory: UserA11yQsShortcutsRepository.Factory,
+) : AccessibilityQsShortcutsRepository {
+
+ @GuardedBy("userA11yQsShortcutsRepositories")
+ private val userA11yQsShortcutsRepositories = SparseArray<UserA11yQsShortcutsRepository>()
+
+ override fun a11yQsShortcutTargets(userId: Int): SharedFlow<Set<String>> {
+ return synchronized(userA11yQsShortcutsRepositories) {
+ if (userId !in userA11yQsShortcutsRepositories) {
+ val userA11yQsShortcutsRepository =
+ userA11yQsShortcutsRepositoryFactory.create(userId)
+ userA11yQsShortcutsRepositories.put(userId, userA11yQsShortcutsRepository)
+ }
+ userA11yQsShortcutsRepositories.get(userId).targets
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepository.kt
new file mode 100644
index 0000000..ed91f03
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepository.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2023 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.accessibility.data.repository
+
+import android.provider.Settings
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.shareIn
+
+/**
+ * Single user version of [AccessibilityQsShortcutsRepository]. It provides a similar interface as
+ * [TileSpecRepository], but focusing solely on the user it was created for. It observes the changes
+ * on the [Settings.Secure.ACCESSIBILITY_QS_TARGETS] for a given user
+ */
+class UserA11yQsShortcutsRepository
+@AssistedInject
+constructor(
+ @Assisted private val userId: Int,
+ private val secureSettings: SecureSettings,
+ @Application private val applicationScope: CoroutineScope,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+) {
+ val targets =
+ secureSettings
+ .observerFlow(userId, SETTING_NAME)
+ // Force an update
+ .onStart { emit(Unit) }
+ .map { getA11yQsShortcutTargets(userId) }
+ .flowOn(backgroundDispatcher)
+ .shareIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ replay = 1
+ )
+
+ private fun getA11yQsShortcutTargets(userId: Int): Set<String> {
+ val settingValue = secureSettings.getStringForUser(SETTING_NAME, userId) ?: ""
+ return settingValue.split(SETTING_SEPARATOR).filterNot { it.isEmpty() }.toSet()
+ }
+
+ companion object {
+ const val SETTING_NAME = Settings.Secure.ACCESSIBILITY_QS_TARGETS
+ const val SETTING_SEPARATOR = ":"
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ userId: Int,
+ ): UserA11yQsShortcutsRepository
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/BaseAutoAddableModule.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/BaseAutoAddableModule.kt
index adea26e..e1ec338 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/BaseAutoAddableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/BaseAutoAddableModule.kt
@@ -18,6 +18,8 @@
import android.content.res.Resources
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.pipeline.domain.autoaddable.A11yShortcutAutoAddable
+import com.android.systemui.qs.pipeline.domain.autoaddable.A11yShortcutAutoAddableList
import com.android.systemui.qs.pipeline.domain.autoaddable.AutoAddableSetting
import com.android.systemui.qs.pipeline.domain.autoaddable.AutoAddableSettingList
import com.android.systemui.qs.pipeline.domain.autoaddable.CastAutoAddable
@@ -51,6 +53,16 @@
)
.toSet()
}
+
+ @Provides
+ @ElementsIntoSet
+ fun providesA11yShortcutAutoAddable(
+ a11yShortcutAutoAddableFactory: A11yShortcutAutoAddable.Factory
+ ): Set<AutoAddable> {
+ return A11yShortcutAutoAddableList.getA11yShortcutAutoAddables(
+ a11yShortcutAutoAddableFactory
+ )
+ }
}
@Binds @IntoSet fun bindCastAutoAddable(impl: CastAutoAddable): AutoAddable
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddable.kt
new file mode 100644
index 0000000..2cebbe3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddable.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2023 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.pipeline.domain.autoaddable
+
+import android.content.ComponentName
+import android.provider.Settings
+import com.android.systemui.accessibility.data.repository.AccessibilityQsShortcutsRepository
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import java.util.Objects
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+/**
+ * [A11yShortcutAutoAddable] will auto add/remove qs tile of the accessibility framework feature
+ * based on the user's choices in the Settings app.
+ *
+ * The a11y feature component is added to [Settings.Secure.ACCESSIBILITY_QS_TARGETS] when the user
+ * selects to use qs tile as a shortcut for the a11 feature in the Settings app. The accessibility
+ * feature component is removed from [Settings.Secure.ACCESSIBILITY_QS_TARGETS] when the user
+ * doesn't want to use qs tile as a shortcut for the a11y feature in the Settings app.
+ *
+ * [A11yShortcutAutoAddable] tracks a [Settings.Secure.ACCESSIBILITY_QS_TARGETS] and when its value
+ * changes, it will emit a [AutoAddSignal.Add] for the [spec] if the [componentName] is a substring
+ * of the value; it will emit a [AutoAddSignal.Remove] for the [spec] if the [componentName] is not
+ * a substring of the value.
+ */
+class A11yShortcutAutoAddable
+@AssistedInject
+constructor(
+ private val a11yQsShortcutsRepository: AccessibilityQsShortcutsRepository,
+ @Background private val bgDispatcher: CoroutineDispatcher,
+ @Assisted private val spec: TileSpec,
+ @Assisted private val componentName: ComponentName
+) : AutoAddable {
+
+ override fun autoAddSignal(userId: Int): Flow<AutoAddSignal> {
+ return a11yQsShortcutsRepository
+ .a11yQsShortcutTargets(userId)
+ .map { it.contains(componentName.flattenToString()) }
+ .filterNotNull()
+ .distinctUntilChanged()
+ .map { if (it) AutoAddSignal.Add(spec) else AutoAddSignal.Remove(spec) }
+ .flowOn(bgDispatcher)
+ }
+
+ override val autoAddTracking = AutoAddTracking.Always
+
+ override val description =
+ "A11yShortcutAutoAddableSetting: $spec:$componentName ($autoAddTracking)"
+
+ override fun equals(other: Any?): Boolean {
+ return other is A11yShortcutAutoAddable &&
+ spec == other.spec &&
+ componentName == other.componentName
+ }
+
+ override fun hashCode(): Int {
+ return Objects.hash(spec, componentName)
+ }
+
+ override fun toString(): String {
+ return description
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(spec: TileSpec, componentName: ComponentName): A11yShortcutAutoAddable
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableList.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableList.kt
new file mode 100644
index 0000000..08e3920
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableList.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2023 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.pipeline.domain.autoaddable
+
+import android.view.accessibility.Flags
+import com.android.internal.accessibility.AccessibilityShortcutController
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.ColorCorrectionTile
+import com.android.systemui.qs.tiles.ColorInversionTile
+import com.android.systemui.qs.tiles.OneHandedModeTile
+import com.android.systemui.qs.tiles.ReduceBrightColorsTile
+
+object A11yShortcutAutoAddableList {
+
+ /**
+ * Generate a collection of [A11yShortcutAutoAddable] for the framework tiles related to
+ * accessibility features with shortcut options
+ */
+ fun getA11yShortcutAutoAddables(factory: A11yShortcutAutoAddable.Factory): Set<AutoAddable> {
+ return if (Flags.a11yQsShortcut()) {
+ setOf(
+ factory.create(
+ TileSpec.create(ColorCorrectionTile.TILE_SPEC),
+ AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME
+ ),
+ factory.create(
+ TileSpec.create(ColorInversionTile.TILE_SPEC),
+ AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME
+ ),
+ factory.create(
+ TileSpec.create(OneHandedModeTile.TILE_SPEC),
+ AccessibilityShortcutController.ONE_HANDED_COMPONENT_NAME
+ ),
+ factory.create(
+ TileSpec.create(ReduceBrightColorsTile.TILE_SPEC),
+ AccessibilityShortcutController.REDUCE_BRIGHT_COLORS_COMPONENT_NAME
+ ),
+ )
+ } else {
+ emptySet()
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityQsShortcutsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityQsShortcutsRepository.kt
new file mode 100644
index 0000000..e547da1
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityQsShortcutsRepository.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2023 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.accessibility.data.repository
+
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+
+class FakeAccessibilityQsShortcutsRepository : AccessibilityQsShortcutsRepository {
+
+ private val targetsPerUser = mutableMapOf<Int, MutableSharedFlow<Set<String>>>()
+
+ override fun a11yQsShortcutTargets(userId: Int): SharedFlow<Set<String>> {
+ return getFlow(userId).asSharedFlow()
+ }
+
+ /**
+ * Set the a11y qs shortcut targets. In real world, the A11y QS Shortcut targets are set by the
+ * Settings app not in SysUi
+ */
+ suspend fun setA11yQsShortcutTargets(userId: Int, targets: Set<String>) {
+ getFlow(userId).emit(targets)
+ }
+
+ private fun getFlow(userId: Int): MutableSharedFlow<Set<String>> =
+ targetsPerUser.getOrPut(userId) { MutableSharedFlow(replay = 1) }
+}