Add ActivatableNotificationViewBinder hierarchy

Bug: 271161129
Test: atest SystemUITests
Change-Id: Ia1a06a400ce5f978f6147b74b9a9afb7c787efdd
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 0000000..ae9f57f
--- /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 0000000..968ce0d
--- /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 7945470..75fcbd0 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.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 @@
  */
 @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 aa3ecb6..766ad88 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 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 @@
         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 0000000..54af107
--- /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 0000000..745ce77
--- /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 0000000..49cfb576
--- /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 0000000..f46d424
--- /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 0000000..5904c77
--- /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 0000000..5efaf04
--- /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 9638753e..c823189 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.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 @@
     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 @@
     }
 
     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 @@
 
 /** 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 0b41b63..fb19443 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 @@
 @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 5d4adda..b96001f 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.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.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.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 0000000..aff52f5
--- /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 0000000..c960230
--- /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 c36925a..e9a8f3f 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.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 @@
     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 @@
             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 0000000..8444c7b
--- /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