diff options
17 files changed, 477 insertions, 28 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/AccessibilityRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/AccessibilityRepository.kt new file mode 100644 index 000000000000..ae9f57f1f1b0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/AccessibilityRepository.kt @@ -0,0 +1,55 @@ +/* + * 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.view.accessibility.AccessibilityManager +import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import dagger.Module +import dagger.Provides +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged + +/** Exposes accessibility-related state. */ +interface AccessibilityRepository { + /** @see [AccessibilityManager.isTouchExplorationEnabled] */ + val isTouchExplorationEnabled: Flow<Boolean> + + companion object { + operator fun invoke(a11yManager: AccessibilityManager): AccessibilityRepository = + AccessibilityRepositoryImpl(a11yManager) + } +} + +private class AccessibilityRepositoryImpl( + manager: AccessibilityManager, +) : AccessibilityRepository { + override val isTouchExplorationEnabled: Flow<Boolean> = + conflatedCallbackFlow { + val listener = TouchExplorationStateChangeListener(::trySend) + manager.addTouchExplorationStateChangeListener(listener) + trySend(manager.isTouchExplorationEnabled) + awaitClose { manager.removeTouchExplorationStateChangeListener(listener) } + } + .distinctUntilChanged() +} + +@Module +object AccessibilityRepositoryModule { + @Provides fun provideRepo(manager: AccessibilityManager) = AccessibilityRepository(manager) +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/AccessibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/AccessibilityInteractor.kt new file mode 100644 index 000000000000..968ce0dc8cb0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/AccessibilityInteractor.kt @@ -0,0 +1,33 @@ +/* + * 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.domain.interactor + +import com.android.systemui.accessibility.data.repository.AccessibilityRepository +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow + +@SysUISingleton +class AccessibilityInteractor +@Inject +constructor( + private val a11yRepo: AccessibilityRepository, +) { + /** @see [android.view.accessibility.AccessibilityManager.isTouchExplorationEnabled] */ + val isTouchExplorationEnabled: Flow<Boolean> + get() = a11yRepo.isTouchExplorationEnabled +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 7945470b424a..75fcbd098ca0 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -29,6 +29,7 @@ import com.android.keyguard.dagger.KeyguardBouncerComponent; import com.android.systemui.BootCompleteCache; import com.android.systemui.BootCompleteCacheImpl; import com.android.systemui.accessibility.AccessibilityModule; +import com.android.systemui.accessibility.data.repository.AccessibilityRepositoryModule; import com.android.systemui.appops.dagger.AppOpsModule; import com.android.systemui.assist.AssistModule; import com.android.systemui.biometrics.AlternateUdfpsTouchProvider; @@ -140,6 +141,7 @@ import javax.inject.Named; */ @Module(includes = { AccessibilityModule.class, + AccessibilityRepositoryModule.class, AppOpsModule.class, AssistModule.class, BiometricsModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java index aa3ecb6531c5..766ad88f8a55 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java @@ -196,7 +196,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView public void onTap() {} /** Sets the last action up time this view was touched. */ - void setLastActionUpTime(long eventTime) { + public void setLastActionUpTime(long eventTime) { mLastActionUpTime = eventTime; } @@ -705,7 +705,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView return mRefocusOnDismiss || isAccessibilityFocused(); } - void setTouchHandler(Gefingerpoken touchHandler) { + public void setTouchHandler(Gefingerpoken touchHandler) { mTouchHandler = touchHandler; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/ActivatableNotificationViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/ActivatableNotificationViewBinder.kt new file mode 100644 index 000000000000..54af1078741c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/ActivatableNotificationViewBinder.kt @@ -0,0 +1,98 @@ +/* + * 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.statusbar.notification.row.ui.viewbinder + +import android.view.MotionEvent +import android.view.View +import android.view.View.OnTouchListener +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.Gefingerpoken +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.notification.row.ActivatableNotificationView +import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModel +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.launch + +/** Binds an [ActivatableNotificationView] to its [view model][ActivatableNotificationViewModel]. */ +object ActivatableNotificationViewBinder { + + fun bind( + viewModel: ActivatableNotificationViewModel, + view: ActivatableNotificationView, + falsingManager: FalsingManager, + ) { + ExpandableOutlineViewBinder.bind(viewModel, view) + val touchHandler = TouchHandler(view, falsingManager) + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.isTouchable.collect { isTouchable -> + touchHandler.isTouchEnabled = isTouchable + } + } + view.registerListenersWhileAttached(touchHandler) + } + } + } + + private suspend fun ActivatableNotificationView.registerListenersWhileAttached( + touchHandler: TouchHandler, + ): Unit = + try { + setOnTouchListener(touchHandler) + setTouchHandler(touchHandler) + awaitCancellation() + } finally { + setTouchHandler(null) + setOnTouchListener(null) + } +} + +private class TouchHandler( + private val view: ActivatableNotificationView, + private val falsingManager: FalsingManager, +) : Gefingerpoken, OnTouchListener { + + var isTouchEnabled = false + + override fun onTouch(v: View, ev: MotionEvent): Boolean { + val result = false + if (ev.action == MotionEvent.ACTION_UP) { + view.setLastActionUpTime(ev.eventTime) + } + // With a11y, just do nothing. + if (!isTouchEnabled) { + return false + } + if (ev.action == MotionEvent.ACTION_UP) { + // If this is a false tap, capture the even so it doesn't result in a click. + val falseTap: Boolean = falsingManager.isFalseTap(FalsingManager.LOW_PENALTY) + if (!falseTap && v is ActivatableNotificationView) { + v.onTap() + } + return falseTap + } + return result + } + + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean = false + + /** Use [onTouch] instead. */ + override fun onTouchEvent(ev: MotionEvent): Boolean = false +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/ExpandableOutlineViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/ExpandableOutlineViewBinder.kt new file mode 100644 index 000000000000..745ce7743fe9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/ExpandableOutlineViewBinder.kt @@ -0,0 +1,13 @@ +package com.android.systemui.statusbar.notification.row.ui.viewbinder + +import com.android.systemui.statusbar.notification.row.ExpandableOutlineView +import com.android.systemui.statusbar.notification.row.ui.viewmodel.ExpandableOutlineViewModel as ViewModel + +object ExpandableOutlineViewBinder { + fun bind( + viewModel: ViewModel, + view: ExpandableOutlineView, + ) { + ExpandableViewBinder.bind(viewModel, view) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/ExpandableViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/ExpandableViewBinder.kt new file mode 100644 index 000000000000..49cfb5766ce8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/ExpandableViewBinder.kt @@ -0,0 +1,8 @@ +package com.android.systemui.statusbar.notification.row.ui.viewbinder + +import com.android.systemui.statusbar.notification.row.ExpandableView +import com.android.systemui.statusbar.notification.row.ui.viewmodel.ExpandableViewModel as ViewModel + +object ExpandableViewBinder { + fun bind(viewModel: ViewModel, view: ExpandableView) {} +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/ActivatableNotificationViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/ActivatableNotificationViewModel.kt new file mode 100644 index 000000000000..f46d42473863 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/ActivatableNotificationViewModel.kt @@ -0,0 +1,50 @@ +/* + * 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.statusbar.notification.row.ui.viewmodel + +import com.android.systemui.accessibility.domain.interactor.AccessibilityInteractor +import dagger.Module +import dagger.Provides +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +/** ViewModel for [com.android.systemui.statusbar.notification.row.ActivatableNotificationView]. */ +interface ActivatableNotificationViewModel : ExpandableOutlineViewModel { + /** Does the view react to touches? */ + val isTouchable: Flow<Boolean> + + companion object { + operator fun invoke( + a11yInteractor: AccessibilityInteractor, + ): ActivatableNotificationViewModel = ActivatableNotificationViewModelImpl(a11yInteractor) + } +} + +private class ActivatableNotificationViewModelImpl( + a11yInteractor: AccessibilityInteractor, +) : ActivatableNotificationViewModel { + override val isTouchable: Flow<Boolean> = + // If a11y touch exploration is enabled, then the activatable view should ignore touches + a11yInteractor.isTouchExplorationEnabled.map { !it } +} + +@Module +object ActivatableNotificationViewModelModule { + @Provides + fun provideViewModel(interactor: AccessibilityInteractor) = + ActivatableNotificationViewModel(interactor) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/ExpandableOutlineViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/ExpandableOutlineViewModel.kt new file mode 100644 index 000000000000..5904c77426a9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/ExpandableOutlineViewModel.kt @@ -0,0 +1,4 @@ +package com.android.systemui.statusbar.notification.row.ui.viewmodel + +/** ViewModel for [com.android.systemui.statusbar.notification.row.ExpandableOutlineView]. */ +interface ExpandableOutlineViewModel : ExpandableViewModel diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/ExpandableViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/ExpandableViewModel.kt new file mode 100644 index 000000000000..5efaf04aab9b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/ExpandableViewModel.kt @@ -0,0 +1,4 @@ +package com.android.systemui.statusbar.notification.row.ui.viewmodel + +/** ViewModel for [com.android.systemui.statusbar.notification.row.ExpandableView]. */ +interface ExpandableViewModel diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt index 9638753e2f74..c82318913ced 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.notification.shelf.ui.viewbinder import android.view.View -import android.view.accessibility.AccessibilityManager import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.flags.FeatureFlags @@ -27,9 +26,7 @@ import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.LegacyNotificationShelfControllerImpl import com.android.systemui.statusbar.NotificationShelf import com.android.systemui.statusbar.NotificationShelfController -import com.android.systemui.statusbar.notification.row.ActivatableNotificationViewController -import com.android.systemui.statusbar.notification.row.ExpandableOutlineViewController -import com.android.systemui.statusbar.notification.row.ExpandableViewController +import com.android.systemui.statusbar.notification.row.ui.viewbinder.ActivatableNotificationViewBinder import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel import com.android.systemui.statusbar.notification.stack.AmbientState import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController @@ -56,7 +53,6 @@ constructor( private val shelf: NotificationShelf, private val viewModel: NotificationShelfViewModel, featureFlags: FeatureFlags, - private val a11yManager: AccessibilityManager, private val falsingManager: FalsingManager, hostControllerLazy: Lazy<NotificationStackScrollLayoutController>, private val notificationIconAreaController: NotificationIconAreaController, @@ -76,15 +72,7 @@ constructor( } fun init() { - NotificationShelfViewBinder.bind(viewModel, shelf) - - ActivatableNotificationViewController( - shelf, - ExpandableOutlineViewController(shelf, ExpandableViewController(shelf)), - a11yManager, - falsingManager, - ) - .init() + NotificationShelfViewBinder.bind(viewModel, shelf, falsingManager) hostController.setShelf(shelf) hostController.setOnNotificationRemovedListener { child, _ -> view.requestRoundnessResetFor(child) @@ -113,7 +101,12 @@ constructor( /** Binds a [NotificationShelf] to its backend. */ object NotificationShelfViewBinder { - fun bind(viewModel: NotificationShelfViewModel, shelf: NotificationShelf) { + fun bind( + viewModel: NotificationShelfViewModel, + shelf: NotificationShelf, + falsingManager: FalsingManager, + ) { + ActivatableNotificationViewBinder.bind(viewModel, shelf, falsingManager) shelf.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.canModifyColorOfNotifications diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt index 0b41b637004f..fb1944315e10 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.shelf.ui.viewmodel import com.android.systemui.statusbar.NotificationShelf +import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModel import com.android.systemui.statusbar.notification.shelf.domain.interactor.NotificationShelfInteractor import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope import javax.inject.Inject @@ -29,7 +30,8 @@ class NotificationShelfViewModel @Inject constructor( private val interactor: NotificationShelfInteractor, -) { + activatableViewModel: ActivatableNotificationViewModel, +) : ActivatableNotificationViewModel by activatableViewModel { /** Is the shelf allowed to be clickable when it has content? */ val isClickable: Flow<Boolean> get() = interactor.isShowingOnKeyguard diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java index 5d4addab240a..b96001ffd74d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java @@ -21,9 +21,7 @@ import android.content.ContentResolver; import android.os.Handler; import android.view.LayoutInflater; import android.view.ViewStub; - import androidx.constraintlayout.motion.widget.MotionLayout; - import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.LockIconView; import com.android.systemui.R; @@ -52,6 +50,7 @@ import com.android.systemui.statusbar.OperatorNameViewController; import com.android.systemui.statusbar.core.StatusBarInitializer.OnStatusBarViewInitializedListener; import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler; import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfComponent; +import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModelModule; import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinderWrapperControllerImpl; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; import com.android.systemui.statusbar.phone.KeyguardBottomAreaView; @@ -75,18 +74,16 @@ import com.android.systemui.statusbar.window.StatusBarWindowStateController; import com.android.systemui.tuner.TunerService; import com.android.systemui.util.CarrierConfigTracker; import com.android.systemui.util.settings.SecureSettings; - -import java.util.concurrent.Executor; - -import javax.inject.Named; -import javax.inject.Provider; - import dagger.Binds; import dagger.Module; import dagger.Provides; import dagger.multibindings.IntoSet; +import java.util.concurrent.Executor; +import javax.inject.Named; +import javax.inject.Provider; -@Module(subcomponents = StatusBarFragmentComponent.class) +@Module(subcomponents = StatusBarFragmentComponent.class, + includes = { ActivatableNotificationViewModelModule.class }) public abstract class StatusBarViewModule { public static final String SHADE_HEADER = "large_screen_shade_header"; diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/data/repository/AccessibilityRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/accessibility/data/repository/AccessibilityRepositoryTest.kt new file mode 100644 index 000000000000..aff52f511171 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/data/repository/AccessibilityRepositoryTest.kt @@ -0,0 +1,85 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.accessibility.data.repository + +import android.testing.AndroidTestingRunner +import android.view.accessibility.AccessibilityManager +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.mockito.withArgCaptor +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class AccessibilityRepositoryTest : SysuiTestCase() { + + @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule() + + // mocks + @Mock private lateinit var a11yManager: AccessibilityManager + + // real impls + private val underTest by lazy { AccessibilityRepository(a11yManager) } + + @Test + fun isTouchExplorationEnabled_reflectsA11yManager_initFalse() = runTest { + whenever(a11yManager.isTouchExplorationEnabled).thenReturn(false) + val isTouchExplorationEnabled by collectLastValue(underTest.isTouchExplorationEnabled) + assertThat(isTouchExplorationEnabled).isFalse() + } + + @Test + fun isTouchExplorationEnabled_reflectsA11yManager_initTrue() = runTest { + whenever(a11yManager.isTouchExplorationEnabled).thenReturn(true) + val isTouchExplorationEnabled by collectLastValue(underTest.isTouchExplorationEnabled) + assertThat(isTouchExplorationEnabled).isTrue() + } + + @Test + fun isTouchExplorationEnabled_reflectsA11yManager_changeTrue() = runTest { + whenever(a11yManager.isTouchExplorationEnabled).thenReturn(false) + val isTouchExplorationEnabled by collectLastValue(underTest.isTouchExplorationEnabled) + runCurrent() + withArgCaptor { verify(a11yManager).addTouchExplorationStateChangeListener(capture()) } + .onTouchExplorationStateChanged(/* enabled = */ true) + assertThat(isTouchExplorationEnabled).isTrue() + } + + @Test + fun isTouchExplorationEnabled_reflectsA11yManager_changeFalse() = runTest { + whenever(a11yManager.isTouchExplorationEnabled).thenReturn(true) + val isTouchExplorationEnabled by collectLastValue(underTest.isTouchExplorationEnabled) + runCurrent() + withArgCaptor { verify(a11yManager).addTouchExplorationStateChangeListener(capture()) } + .onTouchExplorationStateChanged(/* enabled = */ false) + assertThat(isTouchExplorationEnabled).isFalse() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/ActivatableNotificationViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/ActivatableNotificationViewModelTest.kt new file mode 100644 index 000000000000..c9602307216d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/ActivatableNotificationViewModelTest.kt @@ -0,0 +1,76 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.statusbar.notification.row.ui.viewmodel + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.accessibility.data.repository.FakeAccessibilityRepository +import com.android.systemui.accessibility.domain.interactor.AccessibilityInteractor +import com.android.systemui.coroutines.collectLastValue +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class ActivatableNotificationViewModelTest : SysuiTestCase() { + + // fakes + private val a11yRepo = FakeAccessibilityRepository() + + // real impls + private val a11yInteractor = AccessibilityInteractor(a11yRepo) + private val underTest = ActivatableNotificationViewModel(a11yInteractor) + + @Test + fun isTouchable_whenA11yTouchExplorationDisabled() = runTest { + a11yRepo.isTouchExplorationEnabled.value = false + val isTouchable: Boolean? by collectLastValue(underTest.isTouchable) + assertThat(isTouchable).isTrue() + } + + @Test + fun isNotTouchable_whenA11yTouchExplorationEnabled() = runTest { + a11yRepo.isTouchExplorationEnabled.value = true + val isTouchable: Boolean? by collectLastValue(underTest.isTouchable) + assertThat(isTouchable).isFalse() + } + + @Test + fun isTouchable_whenA11yTouchExplorationChangesToDisabled() = runTest { + a11yRepo.isTouchExplorationEnabled.value = true + val isTouchable: Boolean? by collectLastValue(underTest.isTouchable) + runCurrent() + a11yRepo.isTouchExplorationEnabled.value = false + assertThat(isTouchable).isTrue() + } + + @Test + fun isNotTouchable_whenA11yTouchExplorationChangesToEnabled() = runTest { + a11yRepo.isTouchExplorationEnabled.value = false + val isTouchable: Boolean? by collectLastValue(underTest.isTouchable) + runCurrent() + a11yRepo.isTouchExplorationEnabled.value = true + assertThat(isTouchable).isFalse() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt index c36925a650de..e9a8f3f0d60c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt @@ -22,10 +22,13 @@ import android.os.PowerManager import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.accessibility.data.repository.FakeAccessibilityRepository +import com.android.systemui.accessibility.domain.interactor.AccessibilityInteractor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.statusbar.LockscreenShadeTransitionController +import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModel import com.android.systemui.statusbar.notification.shelf.domain.interactor.NotificationShelfInteractor import com.android.systemui.statusbar.phone.CentralSurfaces import com.android.systemui.util.mockito.any @@ -58,8 +61,11 @@ class NotificationShelfViewModelTest : SysuiTestCase() { private val keyguardRepository = FakeKeyguardRepository() private val deviceEntryFaceAuthRepository = FakeDeviceEntryFaceAuthRepository() private val systemClock = FakeSystemClock() + private val a11yRepo = FakeAccessibilityRepository() // real impls + private val a11yInteractor = AccessibilityInteractor(a11yRepo) + private val activatableViewModel = ActivatableNotificationViewModel(a11yInteractor) private val interactor by lazy { NotificationShelfInteractor( keyguardRepository, @@ -69,7 +75,7 @@ class NotificationShelfViewModelTest : SysuiTestCase() { keyguardTransitionController, ) } - private val underTest by lazy { NotificationShelfViewModel(interactor) } + private val underTest by lazy { NotificationShelfViewModel(interactor, activatableViewModel) } @Test fun canModifyColorOfNotifications_whenKeyguardNotShowing() = runTest { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityRepository.kt new file mode 100644 index 000000000000..8444c7b52d1e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityRepository.kt @@ -0,0 +1,23 @@ +/* + * 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.MutableStateFlow + +class FakeAccessibilityRepository( + override val isTouchExplorationEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false) +) : AccessibilityRepository |