Merge "Replace SeekBar with LabeledSeekBar for Accessibility-related settings in order to adjust the nonsense read-out by talkback."
diff --git a/res/layout/font_size_activity.xml b/res/layout/font_size_activity.xml
index ec064fa..479a5f5 100644
--- a/res/layout/font_size_activity.xml
+++ b/res/layout/font_size_activity.xml
@@ -70,7 +70,7 @@
                 android:focusable="true"
                 android:contentDescription="@string/font_size_make_smaller_desc" />
 
-            <SeekBar
+            <com.android.settings.widget.LabeledSeekBar
                 android:id="@+id/seek_bar"
                 android:layout_width="0dp"
                 android:layout_height="48dp"
diff --git a/res/layout/screen_zoom_activity.xml b/res/layout/screen_zoom_activity.xml
index 51fea69..47c6b19 100644
--- a/res/layout/screen_zoom_activity.xml
+++ b/res/layout/screen_zoom_activity.xml
@@ -69,7 +69,7 @@
                 android:focusable="true"
                 android:contentDescription="@string/screen_zoom_make_smaller_desc" />
 
-            <SeekBar
+            <com.android.settings.widget.LabeledSeekBar
                 android:id="@+id/seek_bar"
                 android:layout_width="0dp"
                 android:layout_height="48dp"
diff --git a/src/com/android/settings/PreviewSeekBarPreferenceFragment.java b/src/com/android/settings/PreviewSeekBarPreferenceFragment.java
index ea8b55a..ad32c2d 100644
--- a/src/com/android/settings/PreviewSeekBarPreferenceFragment.java
+++ b/src/com/android/settings/PreviewSeekBarPreferenceFragment.java
@@ -28,6 +28,7 @@
 import android.widget.SeekBar.OnSeekBarChangeListener;
 import android.widget.TextView;
 import com.android.settings.widget.DotsPageIndicator;
+import com.android.settings.widget.LabeledSeekBar;
 
 
 /**
@@ -97,7 +98,8 @@
         // seek bar.
         final int max = Math.max(1, mEntries.length - 1);
 
-        final SeekBar seekBar = (SeekBar) content.findViewById(R.id.seek_bar);
+        final LabeledSeekBar seekBar = (LabeledSeekBar) content.findViewById(R.id.seek_bar);
+        seekBar.setLabels(mEntries);
         seekBar.setMax(max);
         seekBar.setProgress(mInitialIndex);
         seekBar.setOnSeekBarChangeListener(new onPreviewSeekBarChangeListener());
diff --git a/src/com/android/settings/widget/LabeledSeekBar.java b/src/com/android/settings/widget/LabeledSeekBar.java
new file mode 100644
index 0000000..9463bf7
--- /dev/null
+++ b/src/com/android/settings/widget/LabeledSeekBar.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2016 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.settings.widget;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.support.v4.widget.ExploreByTouchHelper;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+import android.widget.SeekBar;
+
+import java.util.List;
+
+/**
+ * LabeledSeekBar represent a seek bar assigned with labeled, discrete values.
+ * It pretends to be a group of radio button for AccessibilityServices, in order to adjust the
+ * behavior of these services to keep the mental model of the visual discrete SeekBar.
+ */
+public class LabeledSeekBar extends SeekBar {
+
+    private class LabeledSeekBarExploreByTouchHelper extends ExploreByTouchHelper {
+
+        public LabeledSeekBarExploreByTouchHelper(View forView) {
+            super(forView);
+        }
+
+        @Override
+        protected int getVirtualViewAt(float x, float y) {
+            return getVirtualViewIdIndexFromX(x);
+        }
+
+        @Override
+        protected void getVisibleVirtualViews(List<Integer> list) {
+            for (int i = 0, c = LabeledSeekBar.this.getMax(); i <= c; ++i) {
+                list.add(i);
+            }
+        }
+
+        @Override
+        protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
+                Bundle arguments) {
+            if (virtualViewId == ExploreByTouchHelper.HOST_ID) {
+                // Do nothing
+                return false;
+            }
+
+            switch (action) {
+                case AccessibilityNodeInfoCompat.ACTION_CLICK:
+                    LabeledSeekBar.this.setProgress(virtualViewId);
+                    sendEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_CLICKED);
+                    return true;
+                default:
+                    return false;
+            }
+        }
+
+        @Override
+        protected void onPopulateNodeForVirtualView(
+                int virtualViewId, AccessibilityNodeInfoCompat node) {
+            node.setClassName(RadioButton.class.getName());
+            node.setBoundsInParent(getBoundsInParentFromVirtualViewId(virtualViewId));
+            node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
+            node.setContentDescription(mLabels[virtualViewId]);
+            node.setClickable(true);
+            node.setCheckable(true);
+            node.setChecked(virtualViewId == LabeledSeekBar.this.getProgress());
+        }
+
+        @Override
+        protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
+            event.setClassName(RadioButton.class.getName());
+            event.setContentDescription(mLabels[virtualViewId]);
+            event.setChecked(virtualViewId == LabeledSeekBar.this.getProgress());
+        }
+
+        @Override
+        protected void onPopulateNodeForHost(AccessibilityNodeInfoCompat node) {
+            node.setClassName(RadioGroup.class.getName());
+        }
+
+        @Override
+        protected void onPopulateEventForHost(AccessibilityEvent event) {
+            event.setClassName(RadioGroup.class.getName());
+        }
+
+        private int getHalfVirtualViewWidth() {
+            final int width = LabeledSeekBar.this.getWidth();
+            final int barWidth = width - LabeledSeekBar.this.getPaddingStart()
+                    - LabeledSeekBar.this.getPaddingEnd();
+            return Math.max(0, barWidth / (LabeledSeekBar.this.getMax() * 2));
+        }
+
+        private int getVirtualViewIdIndexFromX(float x) {
+            final int posBase = Math.max(0,
+                    ((int) x - LabeledSeekBar.this.getPaddingStart()) / getHalfVirtualViewWidth());
+            return (posBase + 1) / 2;
+        }
+
+        private Rect getBoundsInParentFromVirtualViewId(int virtualViewId) {
+            int left = (virtualViewId * 2 - 1) * getHalfVirtualViewWidth()
+                    + LabeledSeekBar.this.getPaddingStart();
+            int right = (virtualViewId * 2 + 1) * getHalfVirtualViewWidth()
+                    + LabeledSeekBar.this.getPaddingStart();
+
+            // Edge case
+            left = virtualViewId == 0 ? 0 : left;
+            right = virtualViewId == LabeledSeekBar.this.getMax()
+                    ? LabeledSeekBar.this.getWidth() : right;
+
+            final Rect r = new Rect();
+            r.set(left, 0, right, LabeledSeekBar.this.getHeight());
+            return r;
+        }
+    }
+
+    private String[] mLabels;
+
+    private ExploreByTouchHelper mAccessHelper;
+
+    public LabeledSeekBar(Context context, AttributeSet attrs) {
+        this(context, attrs, com.android.internal.R.attr.seekBarStyle);
+    }
+
+    public LabeledSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public LabeledSeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    public synchronized void setProgress(int progress) {
+        if (mAccessHelper != null) {
+            mAccessHelper.invalidateRoot();
+        }
+
+        super.setProgress(progress);
+    }
+
+    public void setLabels(String[] labels) {
+        mLabels = labels;
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        mAccessHelper = new LabeledSeekBarExploreByTouchHelper(this);
+        ViewCompat.setAccessibilityDelegate(this, mAccessHelper);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        ViewCompat.setAccessibilityDelegate(this, null);
+        mAccessHelper = null;
+        super.onDetachedFromWindow();
+    }
+
+    @Override
+    protected boolean dispatchHoverEvent(MotionEvent event) {
+        if (mAccessHelper != null && mAccessHelper.dispatchHoverEvent(event)) {
+            return true;
+        }
+
+        return super.dispatchHoverEvent(event);
+    }
+}