Merge "Enabled a11y for clock carousel view" into udc-dev am: 0856e6f914

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/ThemePicker/+/23814274

Change-Id: I0410268f243fa65814fb0ecd1c645e5e691b2714
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/res/values/config.xml b/res/values/config.xml
index d992bf3..ad81e67 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -19,4 +19,8 @@
     <item type="id" name="option_tile" />
     <!-- ID for the label of an option tile -->
     <item type="id" name="option_label" />
+
+    <!-- ID for the a11y actions on carousel -->
+    <item type="id" name="action_scroll_forward" />
+    <item type="id" name="action_scroll_backward" />
 </resources>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index b7574a3..680947d 100755
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -507,4 +507,22 @@
     [CHAR LIMIT=NONE].
     -->
     <string name="content_description_color_option">Color option <xliff:g name="color_number" example="1">%1$d</xliff:g></string>
+
+    <!--
+    Accessibility label for forward scrolling in the carousel of clock faces.
+    [CHAR LIMIT=128].
+    -->
+    <string name="scroll_forward_and_select">Swipe left to choose a different clock face</string>
+
+    <!--
+    Accessibility label for backward scrolling in the carousel of clock faces.
+    [CHAR LIMIT=128].
+    -->
+    <string name="scroll_backward_and_select">Swipe right to choose a different clock face</string>
+
+    <!--
+    Accessibility label for the carousel of clock faces.
+    [CHAR LIMIT=128].
+    -->
+    <string name="custom_clocks_label">Custom Clocks</string>
 </resources>
diff --git a/src/com/android/customization/picker/clock/ui/binder/CarouselAccessibilityDelegate.kt b/src/com/android/customization/picker/clock/ui/binder/CarouselAccessibilityDelegate.kt
new file mode 100644
index 0000000..5e3c26e
--- /dev/null
+++ b/src/com/android/customization/picker/clock/ui/binder/CarouselAccessibilityDelegate.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.customization.picker.clock.ui.binder
+
+import android.content.Context
+import android.os.Bundle
+import android.view.View
+import android.view.accessibility.AccessibilityNodeInfo
+import com.android.wallpaper.R
+
+class CarouselAccessibilityDelegate(
+    private val context: Context,
+    private val scrollForwardCallback: () -> Unit,
+    private val scrollBackwardCallback: () -> Unit
+) : View.AccessibilityDelegate() {
+
+    var contentDescriptionOfSelectedClock = ""
+
+    private val ACTION_SCROLL_BACKWARD = R.id.action_scroll_backward
+    private val ACTION_SCROLL_FORWARD = R.id.action_scroll_forward
+
+    override fun onInitializeAccessibilityNodeInfo(host: View?, info: AccessibilityNodeInfo?) {
+        super.onInitializeAccessibilityNodeInfo(host, info)
+        info?.isScrollable = true
+        info?.addAction(
+            AccessibilityNodeInfo.AccessibilityAction(
+                ACTION_SCROLL_FORWARD,
+                context.getString(R.string.scroll_forward_and_select)
+            )
+        )
+        info?.addAction(
+            AccessibilityNodeInfo.AccessibilityAction(
+                ACTION_SCROLL_BACKWARD,
+                context.getString(R.string.scroll_backward_and_select)
+            )
+        )
+        info?.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS)
+        // We need to specifically set the content description since for some reason the talkback
+        // service does not go to children of the clock carousel in the view hierarchy
+        info?.contentDescription = contentDescriptionOfSelectedClock
+    }
+
+    override fun performAccessibilityAction(host: View?, action: Int, args: Bundle?): Boolean {
+        when (action) {
+            ACTION_SCROLL_BACKWARD -> {
+                scrollBackwardCallback.invoke()
+                return true
+            }
+            ACTION_SCROLL_FORWARD -> {
+                scrollForwardCallback.invoke()
+                return true
+            }
+        }
+        return super.performAccessibilityAction(host, action, args)
+    }
+}
diff --git a/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt b/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt
index 13c80c8..89fac89 100644
--- a/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt
+++ b/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt
@@ -15,6 +15,7 @@
  */
 package com.android.customization.picker.clock.ui.binder
 
+import android.content.Context
 import android.view.ViewGroup
 import android.widget.FrameLayout
 import androidx.core.view.isVisible
@@ -27,6 +28,7 @@
 import com.android.customization.picker.clock.ui.view.ClockViewFactory
 import com.android.customization.picker.clock.ui.viewmodel.ClockCarouselViewModel
 import com.android.wallpaper.R
+import com.android.wallpaper.picker.customization.ui.section.ScreenPreviewClickView
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.launch
 
@@ -34,8 +36,10 @@
 
     @JvmStatic
     fun bind(
+        context: Context,
         carouselView: ClockCarouselView,
         singleClockView: ViewGroup,
+        screenPreviewClickView: ScreenPreviewClickView,
         viewModel: ClockCarouselViewModel,
         clockViewFactory: ClockViewFactory,
         lifecycleOwner: LifecycleOwner,
@@ -43,6 +47,20 @@
     ) {
         carouselView.setClockViewFactory(clockViewFactory)
         clockViewFactory.updateRegionDarkness()
+        val carouselAccessibilityDelegate =
+            CarouselAccessibilityDelegate(
+                context,
+                scrollForwardCallback = {
+                    // Callback code for scrolling forward
+                    carouselView.transitionToNext()
+                },
+                scrollBackwardCallback = {
+                    // Callback code for scrolling backward
+                    carouselView.transitionToPrevious()
+                }
+            )
+        screenPreviewClickView.accessibilityDelegate = carouselAccessibilityDelegate
+
         val singleClockHostView =
             singleClockView.requireViewById<FrameLayout>(R.id.single_clock_host_view)
         lifecycleOwner.lifecycleScope.launch {
@@ -71,6 +89,8 @@
 
                 launch {
                     viewModel.selectedIndex.collect { selectedIndex ->
+                        carouselAccessibilityDelegate.contentDescriptionOfSelectedClock =
+                            carouselView.getContentDescription(selectedIndex)
                         carouselView.setSelectedClockIndex(selectedIndex)
                     }
                 }
diff --git a/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt b/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt
index 73d5508..8764e54 100644
--- a/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt
+++ b/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt
@@ -16,6 +16,7 @@
 package com.android.customization.picker.clock.ui.view
 
 import android.content.Context
+import android.content.res.Resources
 import android.util.AttributeSet
 import android.view.LayoutInflater
 import android.view.View
@@ -60,6 +61,7 @@
         val clockCarousel = LayoutInflater.from(context).inflate(R.layout.clock_carousel, this)
         carousel = clockCarousel.requireViewById(R.id.carousel)
         motionLayout = clockCarousel.requireViewById(R.id.motion_container)
+        motionLayout.contentDescription = context.getString(R.string.custom_clocks_label)
     }
 
     /**
@@ -69,6 +71,24 @@
         clockViewFactory = factory
     }
 
+    fun transitionToNext() {
+        val index = (carousel.currentIndex + 1) % carousel.count
+        if (index < carousel.count && index > 0) {
+            carousel.transitionToIndex(index, 0)
+        }
+    }
+
+    fun transitionToPrevious() {
+        val index = (carousel.currentIndex - 1) % carousel.count
+        if (index < carousel.count && index > 0) {
+            carousel.transitionToIndex(index, 0)
+        }
+    }
+
+    fun getContentDescription(index: Int): String {
+        return adapter.getContentDescription(index, resources)
+    }
+
     fun setUpClockCarouselView(
         clockSize: ClockSize,
         clocks: List<ClockCarouselItemViewModel>,
@@ -304,6 +324,10 @@
         private val onClockSelected: (clock: ClockCarouselItemViewModel) -> Unit
     ) : Carousel.Adapter {
 
+        fun getContentDescription(index: Int, resources: Resources): String {
+            return clocks[index].getContentDescription(resources)
+        }
+
         override fun count(): Int {
             return clocks.size
         }
@@ -336,7 +360,7 @@
             val isMiddleView = isMiddleView(viewRoot.id)
 
             // Accessibility
-            viewRoot.contentDescription = clocks[index].getContentDescription(view.resources)
+            viewRoot.contentDescription = getContentDescription(index, view.resources)
             viewRoot.isSelected = isMiddleView
 
             when (clockSize) {
diff --git a/src/com/android/customization/picker/preview/ui/section/PreviewWithClockCarouselSectionController.kt b/src/com/android/customization/picker/preview/ui/section/PreviewWithClockCarouselSectionController.kt
index 4322009..8e91798 100644
--- a/src/com/android/customization/picker/preview/ui/section/PreviewWithClockCarouselSectionController.kt
+++ b/src/com/android/customization/picker/preview/ui/section/PreviewWithClockCarouselSectionController.kt
@@ -46,6 +46,7 @@
 import com.android.wallpaper.module.CurrentWallpaperInfoFactory
 import com.android.wallpaper.module.CustomizationSections
 import com.android.wallpaper.picker.customization.domain.interactor.WallpaperInteractor
+import com.android.wallpaper.picker.customization.ui.section.ScreenPreviewClickView
 import com.android.wallpaper.picker.customization.ui.section.ScreenPreviewSectionController
 import com.android.wallpaper.picker.customization.ui.section.ScreenPreviewView
 import com.android.wallpaper.util.DisplayUtils
@@ -100,6 +101,8 @@
     override fun createView(context: Context): ScreenPreviewView {
         val view = super.createView(context)
         if (screen == CustomizationSections.Screen.LOCK_SCREEN) {
+            val screenPreviewClickView: ScreenPreviewClickView =
+                view.findViewById(R.id.screen_preview_click_view)
             val clockColorAndSizeButtonStub: ViewStub =
                 view.requireViewById(R.id.clock_color_and_size_button)
             clockColorAndSizeButtonStub.layoutResource = R.layout.clock_color_and_size_button
@@ -162,8 +165,10 @@
                         bindJob =
                             lifecycleOwner.lifecycleScope.launch {
                                 ClockCarouselViewBinder.bind(
+                                    context = context,
                                     carouselView = carouselView,
                                     singleClockView = singleClockView,
+                                    screenPreviewClickView = screenPreviewClickView,
                                     viewModel = viewModel,
                                     clockViewFactory = clockViewFactory,
                                     lifecycleOwner = lifecycleOwner,