Add support for Explore by Touch to RadialTimePickerView

Also adds IntArray, which is like LongArray for integers, and prevents
the AM/PM label text in the time picker header from wrapping.

BUG: 17468036
Change-Id: I7120089885709f23e20368927e4b3ed9db2e5393
diff --git a/core/java/android/util/IntArray.java b/core/java/android/util/IntArray.java
new file mode 100644
index 0000000..e8d3947
--- /dev/null
+++ b/core/java/android/util/IntArray.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2014 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 android.util;
+
+import com.android.internal.util.ArrayUtils;
+
+import libcore.util.EmptyArray;
+
+/**
+ * Implements a growing array of int primitives.
+ *
+ * @hide
+ */
+public class IntArray implements Cloneable {
+    private static final int MIN_CAPACITY_INCREMENT = 12;
+
+    private int[] mValues;
+    private int mSize;
+
+    /**
+     * Creates an empty IntArray with the default initial capacity.
+     */
+    public IntArray() {
+        this(10);
+    }
+
+    /**
+     * Creates an empty IntArray with the specified initial capacity.
+     */
+    public IntArray(int initialCapacity) {
+        if (initialCapacity == 0) {
+            mValues = EmptyArray.INT;
+        } else {
+            mValues = ArrayUtils.newUnpaddedIntArray(initialCapacity);
+        }
+        mSize = 0;
+    }
+
+    /**
+     * Appends the specified value to the end of this array.
+     */
+    public void add(int value) {
+        add(mSize, value);
+    }
+
+    /**
+     * Inserts a value at the specified position in this array.
+     *
+     * @throws IndexOutOfBoundsException when index < 0 || index > size()
+     */
+    public void add(int index, int value) {
+        if (index < 0 || index > mSize) {
+            throw new IndexOutOfBoundsException();
+        }
+
+        ensureCapacity(1);
+
+        if (mSize - index != 0) {
+            System.arraycopy(mValues, index, mValues, index + 1, mSize - index);
+        }
+
+        mValues[index] = value;
+        mSize++;
+    }
+
+    /**
+     * Adds the values in the specified array to this array.
+     */
+    public void addAll(IntArray values) {
+        final int count = values.mSize;
+        ensureCapacity(count);
+
+        System.arraycopy(values.mValues, 0, mValues, mSize, count);
+        mSize += count;
+    }
+
+    /**
+     * Ensures capacity to append at least <code>count</code> values.
+     */
+    private void ensureCapacity(int count) {
+        final int currentSize = mSize;
+        final int minCapacity = currentSize + count;
+        if (minCapacity >= mValues.length) {
+            final int targetCap = currentSize + (currentSize < (MIN_CAPACITY_INCREMENT / 2) ?
+                    MIN_CAPACITY_INCREMENT : currentSize >> 1);
+            final int newCapacity = targetCap > minCapacity ? targetCap : minCapacity;
+            final int[] newValues = ArrayUtils.newUnpaddedIntArray(newCapacity);
+            System.arraycopy(mValues, 0, newValues, 0, currentSize);
+            mValues = newValues;
+        }
+    }
+
+    /**
+     * Removes all values from this array.
+     */
+    public void clear() {
+        mSize = 0;
+    }
+
+    @Override
+    public IntArray clone() throws CloneNotSupportedException {
+        final IntArray clone = (IntArray) super.clone();
+        clone.mValues = mValues.clone();
+        return clone;
+    }
+
+    /**
+     * Returns the value at the specified position in this array.
+     */
+    public int get(int index) {
+        if (index >= mSize) {
+            throw new ArrayIndexOutOfBoundsException(mSize, index);
+        }
+        return mValues[index];
+    }
+
+    /**
+     * Returns the index of the first occurrence of the specified value in this
+     * array, or -1 if this array does not contain the value.
+     */
+    public int indexOf(int value) {
+        final int n = mSize;
+        for (int i = 0; i < n; i++) {
+            if (mValues[i] == value) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Removes the value at the specified index from this array.
+     */
+    public void remove(int index) {
+        if (index >= mSize) {
+            throw new ArrayIndexOutOfBoundsException(mSize, index);
+        }
+        System.arraycopy(mValues, index + 1, mValues, index, mSize - index - 1);
+        mSize--;
+    }
+
+    /**
+     * Returns the number of values in this array.
+     */
+    public int size() {
+        return mSize;
+    }
+}
diff --git a/core/java/android/widget/RadialTimePickerView.java b/core/java/android/widget/RadialTimePickerView.java
index d15f2d6..24fc2bb 100644
--- a/core/java/android/widget/RadialTimePickerView.java
+++ b/core/java/android/widget/RadialTimePickerView.java
@@ -28,13 +28,13 @@
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Paint;
+import android.graphics.Rect;
 import android.graphics.Typeface;
-import android.graphics.RectF;
 import android.os.Bundle;
-import android.text.format.DateUtils;
-import android.text.format.Time;
 import android.util.AttributeSet;
+import android.util.IntArray;
 import android.util.Log;
+import android.util.MathUtils;
 import android.util.TypedValue;
 import android.view.HapticFeedbackConstants;
 import android.view.MotionEvent;
@@ -42,8 +42,10 @@
 import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
 
 import com.android.internal.R;
+import com.android.internal.widget.ExploreByTouchHelper;
 
 import java.util.ArrayList;
 import java.util.Calendar;
@@ -97,6 +99,9 @@
 
     private static int[] sSnapPrefer30sMap = new int[361];
 
+    private final InvalidateUpdateListener mInvalidateUpdateListener =
+            new InvalidateUpdateListener();
+
     private final String[] mHours12Texts = new String[12];
     private final String[] mOuterHours24Texts = new String[12];
     private final String[] mInnerHours24Texts = new String[12];
@@ -115,7 +120,39 @@
     private final Paint mPaintBackground = new Paint();
     private final Paint mPaintDebug = new Paint();
 
-    private Typeface mTypeface;
+    private final Typeface mTypeface;
+
+    private final float[] mCircleRadius = new float[3];
+
+    private final float[] mTextSize = new float[2];
+
+    private final float[][] mTextGridHeights = new float[2][7];
+    private final float[][] mTextGridWidths = new float[2][7];
+
+    private final float[] mInnerTextGridHeights = new float[7];
+    private final float[] mInnerTextGridWidths = new float[7];
+
+    private final float[] mCircleRadiusMultiplier = new float[2];
+    private final float[] mNumbersRadiusMultiplier = new float[3];
+
+    private final float[] mTextSizeMultiplier = new float[3];
+
+    private final float[] mAnimationRadiusMultiplier = new float[3];
+
+    private final float mTransitionMidRadiusMultiplier;
+    private final float mTransitionEndRadiusMultiplier;
+
+    private final int[] mLineLength = new int[3];
+    private final int[] mSelectionRadius = new int[3];
+    private final float mSelectionRadiusMultiplier;
+    private final int[] mSelectionDegrees = new int[3];
+
+    private final ArrayList<Animator> mHoursToMinutesAnims = new ArrayList<Animator>();
+    private final ArrayList<Animator> mMinuteToHoursAnims = new ArrayList<Animator>();
+
+    private final RadialPickerTouchHelper mTouchHelper;
+
+    private float mInnerTextSize;
 
     private boolean mIs24HourMode;
     private boolean mShowHours;
@@ -129,52 +166,21 @@
     private int mXCenter;
     private int mYCenter;
 
-    private float[] mCircleRadius = new float[3];
-
     private int mMinHypotenuseForInnerNumber;
     private int mMaxHypotenuseForOuterNumber;
     private int mHalfwayHypotenusePoint;
 
-    private float[] mTextSize = new float[2];
-    private float mInnerTextSize;
-
-    private float[][] mTextGridHeights = new float[2][7];
-    private float[][] mTextGridWidths = new float[2][7];
-
-    private float[] mInnerTextGridHeights = new float[7];
-    private float[] mInnerTextGridWidths = new float[7];
-
     private String[] mOuterTextHours;
     private String[] mInnerTextHours;
     private String[] mOuterTextMinutes;
-
-    private float[] mCircleRadiusMultiplier = new float[2];
-    private float[] mNumbersRadiusMultiplier = new float[3];
-
-    private float[] mTextSizeMultiplier = new float[3];
-
-    private float[] mAnimationRadiusMultiplier = new float[3];
-
-    private float mTransitionMidRadiusMultiplier;
-    private float mTransitionEndRadiusMultiplier;
-
     private AnimatorSet mTransition;
-    private InvalidateUpdateListener mInvalidateUpdateListener = new InvalidateUpdateListener();
-
-    private int[] mLineLength = new int[3];
-    private int[] mSelectionRadius = new int[3];
-    private float mSelectionRadiusMultiplier;
-    private int[] mSelectionDegrees = new int[3];
 
     private int mAmOrPm;
     private int mDisabledAlpha;
 
-    private RectF mRectF = new RectF();
-    private boolean mInputEnabled = true;
     private OnValueSelectedListener mListener;
 
-    private final ArrayList<Animator> mHoursToMinutesAnims = new ArrayList<Animator>();
-    private final ArrayList<Animator> mMinuteToHoursAnims = new ArrayList<Animator>();
+    private boolean mInputEnabled = true;
 
     public interface OnValueSelectedListener {
         void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance);
@@ -282,11 +288,21 @@
         return degrees;
     }
 
+    @SuppressWarnings("unused")
+    public RadialTimePickerView(Context context)  {
+        this(context, null);
+    }
+
     public RadialTimePickerView(Context context, AttributeSet attrs)  {
         this(context, attrs, R.attr.timePickerStyle);
     }
 
-    public RadialTimePickerView(Context context, AttributeSet attrs, int defStyle)  {
+    public RadialTimePickerView(Context context, AttributeSet attrs, int defStyleAttr)  {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public RadialTimePickerView(
+            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)  {
         super(context, attrs);
 
         // Pull disabled alpha from theme.
@@ -297,7 +313,7 @@
         // process style attributes
         final Resources res = getResources();
         final TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.TimePicker,
-                defStyle, 0);
+                defStyleAttr, defStyleRes);
 
         mTypeface = Typeface.create("sans-serif", Typeface.NORMAL);
 
@@ -382,6 +398,14 @@
         mIs24HourMode = false;
         mAmOrPm = AM;
 
+        // Set up accessibility components.
+        mTouchHelper = new RadialPickerTouchHelper();
+        setAccessibilityDelegate(mTouchHelper);
+
+        if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+            setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+        }
+
         initHoursAndMinutesText();
         initData();
 
@@ -406,8 +430,8 @@
         final int currentHour = calendar.get(Calendar.HOUR_OF_DAY);
         final int currentMinute = calendar.get(Calendar.MINUTE);
 
-        setCurrentHour(currentHour);
-        setCurrentMinute(currentMinute);
+        setCurrentHourInternal(currentHour, false, false);
+        setCurrentMinuteInternal(currentMinute, false);
 
         setHapticFeedbackEnabled(true);
     }
@@ -429,8 +453,9 @@
 
     public void initialize(int hour, int minute, boolean is24HourMode) {
         mIs24HourMode = is24HourMode;
-        setCurrentHour(hour);
-        setCurrentMinute(minute);
+
+        setCurrentHourInternal(hour, false, false);
+        setCurrentMinuteInternal(minute, false);
     }
 
     public void setCurrentItemShowing(int item, boolean animate) {
@@ -460,17 +485,39 @@
      * @param hour the current hour between 0 and 23 (inclusive)
      */
     public void setCurrentHour(int hour) {
+        setCurrentHourInternal(hour, true, false);
+    }
+
+    /**
+     * Sets the current hour.
+     *
+     * @param hour The current hour
+     * @param callback Whether the value listener should be invoked
+     * @param autoAdvance Whether the listener should auto-advance to the next
+     *                    selection mode, e.g. hour to minutes
+     */
+    private void setCurrentHourInternal(int hour, boolean callback, boolean autoAdvance) {
         final int degrees = (hour % 12) * DEGREES_FOR_ONE_HOUR;
         mSelectionDegrees[HOURS] = degrees;
         mSelectionDegrees[HOURS_INNER] = degrees;
 
         // 0 is 12 AM (midnight) and 12 is 12 PM (noon).
-        mAmOrPm = (hour == 0 || (hour % 24) < 12) ? AM : PM;
-        mIsOnInnerCircle = mIs24HourMode && hour >= 1 && hour <= 12;
+        final int amOrPm = (hour == 0 || (hour % 24) < 12) ? AM : PM;
+        final boolean isOnInnerCircle = mIs24HourMode && hour >= 1 && hour <= 12;
+        if (mAmOrPm != amOrPm || mIsOnInnerCircle != isOnInnerCircle) {
+            mAmOrPm = amOrPm;
+            mIsOnInnerCircle = isOnInnerCircle;
 
-        initData();
-        updateLayoutData();
+            initData();
+            updateLayoutData();
+            mTouchHelper.invalidateRoot();
+        }
+
         invalidate();
+
+        if (callback && mListener != null) {
+            mListener.onValueSelected(HOURS, hour, autoAdvance);
+        }
     }
 
     /**
@@ -479,15 +526,19 @@
      * @return the current hour between 0 and 23 (inclusive)
      */
     public int getCurrentHour() {
-        int hour = (mSelectionDegrees[mIsOnInnerCircle ?
-                HOURS_INNER : HOURS] / DEGREES_FOR_ONE_HOUR) % 12;
+        return getHourForDegrees(
+                mSelectionDegrees[mIsOnInnerCircle ? HOURS_INNER : HOURS], mIsOnInnerCircle);
+    }
+
+    private int getHourForDegrees(int degrees, boolean innerCircle) {
+        int hour = (degrees / DEGREES_FOR_ONE_HOUR) % 12;
         if (mIs24HourMode) {
             // Convert the 12-hour value into 24-hour time based on where the
             // selector is positioned.
-            if (mIsOnInnerCircle && hour == 0) {
+            if (innerCircle && hour == 0) {
                 // Inner circle is 1 through 12.
                 hour = 12;
-            } else if (!mIsOnInnerCircle && hour != 0) {
+            } else if (!innerCircle && hour != 0) {
                 // Outer circle is 13 through 23 and 0.
                 hour += 12;
             }
@@ -497,19 +548,49 @@
         return hour;
     }
 
+    private int getDegreesForHour(int hour) {
+        // Convert to be 0-11.
+        if (mIs24HourMode) {
+            if (hour >= 12) {
+                hour -= 12;
+            }
+        } else if (hour == 12) {
+            hour = 0;
+        }
+        return hour * DEGREES_FOR_ONE_HOUR;
+    }
+
     public void setCurrentMinute(int minute) {
+        setCurrentMinuteInternal(minute, true);
+    }
+
+    private void setCurrentMinuteInternal(int minute, boolean callback) {
         mSelectionDegrees[MINUTES] = (minute % 60) * DEGREES_FOR_ONE_MINUTE;
+
         invalidate();
+
+        if (callback && mListener != null) {
+            mListener.onValueSelected(MINUTES, minute, false);
+        }
     }
 
     // Returns minutes in 0-59 range
     public int getCurrentMinute() {
-        return (mSelectionDegrees[MINUTES] / DEGREES_FOR_ONE_MINUTE);
+        return getMinuteForDegrees(mSelectionDegrees[MINUTES]);
+    }
+
+    private int getMinuteForDegrees(int degrees) {
+        return degrees / DEGREES_FOR_ONE_MINUTE;
+    }
+
+    private int getDegreesForMinute(int minute) {
+        return minute * DEGREES_FOR_ONE_MINUTE;
     }
 
     public void setAmOrPm(int val) {
         mAmOrPm = (val % 2);
         invalidate();
+        mTouchHelper.invalidateRoot();
     }
 
     public int getAmOrPm() {
@@ -648,6 +729,8 @@
         mSelectionRadius[HOURS] = (int) (mCircleRadius[HOURS] * mSelectionRadiusMultiplier);
         mSelectionRadius[HOURS_INNER] = mSelectionRadius[HOURS];
         mSelectionRadius[MINUTES] = (int) (mCircleRadius[MINUTES] * mSelectionRadiusMultiplier);
+
+        mTouchHelper.invalidateRoot();
     }
 
     @Override
@@ -769,20 +852,17 @@
         float top = mYCenter - outerRadius;
         float right = mXCenter + outerRadius;
         float bottom = mYCenter + outerRadius;
-        mRectF = new RectF(left, top, right, bottom);
-        canvas.drawRect(mRectF, mPaintDebug);
+        canvas.drawRect(left, top, right, bottom, mPaintDebug);
 
         // Draw outer rectangle for background
         left = mXCenter - mCircleRadius[HOURS];
         top = mYCenter - mCircleRadius[HOURS];
         right = mXCenter + mCircleRadius[HOURS];
         bottom = mYCenter + mCircleRadius[HOURS];
-        mRectF.set(left, top, right, bottom);
-        canvas.drawRect(mRectF, mPaintDebug);
+        canvas.drawRect(left, top, right, bottom, mPaintDebug);
 
         // Draw outer view rectangle
-        mRectF.set(0, 0, getWidth(), getHeight());
-        canvas.drawRect(mRectF, mPaintDebug);
+        canvas.drawRect(0, 0, getWidth(), getHeight(), mPaintDebug);
 
         // Draw selected time
         final String selected = String.format("%02d:%02d", getCurrentHour(), getCurrentMinute());
@@ -896,12 +976,14 @@
     }
 
     // Used for animating the hours by changing their radius
+    @SuppressWarnings("unused")
     private void setAnimationRadiusMultiplierHours(float animationRadiusMultiplier) {
         mAnimationRadiusMultiplier[HOURS] = animationRadiusMultiplier;
         mAnimationRadiusMultiplier[HOURS_INNER] = animationRadiusMultiplier;
     }
 
     // Used for animating the minutes by changing their radius
+    @SuppressWarnings("unused")
     private void setAnimationRadiusMultiplierMinutes(float animationRadiusMultiplier) {
         mAnimationRadiusMultiplier[MINUTES] = animationRadiusMultiplier;
     }
@@ -1094,21 +1176,25 @@
         }
 
         final float opposite = Math.abs(y - mYCenter);
-        double degrees = Math.toDegrees(Math.asin(opposite / hypotenuse));
+        int degrees = (int) (Math.toDegrees(Math.asin(opposite / hypotenuse)) + 0.5);
 
         // Now we have to translate to the correct quadrant.
-        boolean rightSide = (x > mXCenter);
-        boolean topSide = (y < mYCenter);
-        if (rightSide && topSide) {
-            degrees = 90 - degrees;
-        } else if (rightSide && !topSide) {
-            degrees = 90 + degrees;
-        } else if (!rightSide && !topSide) {
-            degrees = 270 - degrees;
-        } else if (!rightSide && topSide) {
-            degrees = 270 + degrees;
+        final boolean rightSide = (x > mXCenter);
+        final boolean topSide = (y < mYCenter);
+        if (rightSide) {
+            if (topSide) {
+                degrees = 90 - degrees;
+            } else {
+                degrees = 90 + degrees;
+            }
+        } else {
+            if (topSide) {
+                degrees = 270 + degrees;
+            } else {
+                degrees = 270 - degrees;
+            }
         }
-        return (int) degrees;
+        return degrees;
     }
 
     @Override
@@ -1176,104 +1262,13 @@
         return result;
     }
 
-    /**
-     * Necessary for accessibility, to ensure we support "scrolling" forward and backward
-     * in the circle.
-     */
     @Override
-    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
-        super.onInitializeAccessibilityNodeInfo(info);
-        info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
-        info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
-    }
-
-    /**
-     * Announce the currently-selected time when launched.
-     */
-    @Override
-    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
-        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
-            // Clear the event's current text so that only the current time will be spoken.
-            event.getText().clear();
-            Time time = new Time();
-            time.hour = getCurrentHour();
-            time.minute = getCurrentMinute();
-            long millis = time.normalize(true);
-            int flags = DateUtils.FORMAT_SHOW_TIME;
-            if (mIs24HourMode) {
-                flags |= DateUtils.FORMAT_24HOUR;
-            }
-            String timeString = DateUtils.formatDateTime(getContext(), millis, flags);
-            event.getText().add(timeString);
+    public boolean dispatchHoverEvent(MotionEvent event) {
+        // First right-of-refusal goes the touch exploration helper.
+        if (mTouchHelper.dispatchHoverEvent(event)) {
             return true;
         }
-        return super.dispatchPopulateAccessibilityEvent(event);
-    }
-
-    /**
-     * When scroll forward/backward events are received, jump the time to the higher/lower
-     * discrete, visible value on the circle.
-     */
-    @Override
-    public boolean performAccessibilityAction(int action, Bundle arguments) {
-        if (super.performAccessibilityAction(action, arguments)) {
-            return true;
-        }
-
-        int changeMultiplier = 0;
-        if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) {
-            changeMultiplier = 1;
-        } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) {
-            changeMultiplier = -1;
-        }
-        if (changeMultiplier != 0) {
-            int value;
-            final int stepSize;
-            if (mShowHours) {
-                stepSize = DEGREES_FOR_ONE_HOUR;
-                value = getCurrentHour() % 12;
-            } else {
-                stepSize = DEGREES_FOR_ONE_MINUTE;
-                value = getCurrentMinute();
-            }
-
-            int degrees = value * stepSize;
-            degrees = snapOnly30s(degrees, changeMultiplier);
-            value = degrees / stepSize;
-            final int maxValue;
-            int minValue = 0;
-            if (mShowHours) {
-                if (mIs24HourMode) {
-                    maxValue = 23;
-                } else {
-                    maxValue = 12;
-                    minValue = 1;
-                }
-            } else {
-                maxValue = 55;
-            }
-            if (value > maxValue) {
-                // If we scrolled forward past the highest number, wrap around to the lowest.
-                value = minValue;
-            } else if (value < minValue) {
-                // If we scrolled backward past the lowest number, wrap around to the highest.
-                value = maxValue;
-            }
-            if (mShowHours) {
-                setCurrentHour(value);
-                if (mListener != null) {
-                    mListener.onValueSelected(HOURS, value, false);
-                }
-            } else {
-                setCurrentMinute(value);
-                if (mListener != null) {
-                    mListener.onValueSelected(MINUTES, value, false);
-                }
-            }
-            return true;
-        }
-
-        return false;
+        return super.dispatchHoverEvent(event);
     }
 
     public void setInputEnabled(boolean inputEnabled) {
@@ -1281,6 +1276,265 @@
         invalidate();
     }
 
+    private class RadialPickerTouchHelper extends ExploreByTouchHelper {
+        private final Rect mTempRect = new Rect();
+
+        private final int TYPE_HOUR = 1;
+        private final int TYPE_MINUTE = 2;
+
+        private final int SHIFT_TYPE = 0;
+        private final int MASK_TYPE = 0xF;
+
+        private final int SHIFT_VALUE = 8;
+        private final int MASK_VALUE = 0xFF;
+
+        /** Increment in which virtual views are exposed for minutes. */
+        private final int MINUTE_INCREMENT = 5;
+
+        public RadialPickerTouchHelper() {
+            super(RadialTimePickerView.this);
+        }
+
+        @Override
+        public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+            super.onInitializeAccessibilityNodeInfo(host, info);
+
+            info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
+            info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
+        }
+
+        @Override
+        public boolean performAccessibilityAction(View host, int action, Bundle arguments) {
+            if (super.performAccessibilityAction(host, action, arguments)) {
+                return true;
+            }
+
+            switch (action) {
+                case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
+                    adjustPicker(1);
+                    return true;
+                case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
+                    adjustPicker(-1);
+                    return true;
+            }
+
+            return false;
+        }
+
+        private void adjustPicker(int step) {
+            final int stepSize;
+            final int initialValue;
+            final int maxValue;
+            final int minValue;
+            if (mShowHours) {
+                stepSize = DEGREES_FOR_ONE_HOUR;
+                initialValue = getCurrentHour() % 12;
+
+                if (mIs24HourMode) {
+                    maxValue = 23;
+                    minValue = 0;
+                } else {
+                    maxValue = 12;
+                    minValue = 1;
+                }
+            } else {
+                stepSize = DEGREES_FOR_ONE_MINUTE;
+                initialValue = getCurrentMinute();
+
+                maxValue = 55;
+                minValue = 0;
+            }
+
+            final int steppedValue = snapOnly30s(initialValue * stepSize, step) / stepSize;
+            final int clampedValue = MathUtils.constrain(steppedValue, minValue, maxValue);
+            if (mShowHours) {
+                setCurrentHour(clampedValue);
+            } else {
+                setCurrentMinute(clampedValue);
+            }
+        }
+
+        @Override
+        protected int getVirtualViewAt(float x, float y) {
+            final int id;
+            final int degrees = getDegreesFromXY(x, y);
+            if (degrees != -1) {
+                final int snapDegrees = snapOnly30s(degrees, 0) % 360;
+                if (mShowHours) {
+                    final int hour = getHourForDegrees(snapDegrees, mIsOnInnerCircle);
+                    id = makeId(TYPE_HOUR, hour);
+                } else {
+                    final int current = getCurrentMinute();
+                    final int touched = getMinuteForDegrees(degrees);
+                    final int snapped = getMinuteForDegrees(snapDegrees);
+
+                    // If the touched minute is closer to the current minute
+                    // than it is to the snapped minute, return current.
+                    final int minute;
+                    if (Math.abs(current - touched) < Math.abs(snapped - touched)) {
+                        minute = current;
+                    } else {
+                        minute = snapped;
+                    }
+                    id = makeId(TYPE_MINUTE, minute);
+                }
+            } else {
+                id = INVALID_ID;
+            }
+
+            return id;
+        }
+
+        @Override
+        protected void getVisibleVirtualViews(IntArray virtualViewIds) {
+            if (mShowHours) {
+                final int min = mIs24HourMode ? 0 : 1;
+                final int max = mIs24HourMode ? 23 : 12;
+                for (int i = min; i <= max ; i++) {
+                    virtualViewIds.add(makeId(TYPE_HOUR, i));
+                }
+            } else {
+                final int current = getCurrentMinute();
+                for (int i = 0; i < 60; i += MINUTE_INCREMENT) {
+                    virtualViewIds.add(makeId(TYPE_MINUTE, i));
+
+                    // If the current minute falls between two increments,
+                    // insert an extra node for it.
+                    if (current > i && current < i + MINUTE_INCREMENT) {
+                        virtualViewIds.add(makeId(TYPE_MINUTE, current));
+                    }
+                }
+            }
+        }
+
+        @Override
+        protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
+            event.setClassName(getClass().getName());
+
+            final int type = getTypeFromId(virtualViewId);
+            final int value = getValueFromId(virtualViewId);
+            final CharSequence description = getVirtualViewDescription(type, value);
+            event.setContentDescription(description);
+        }
+
+        @Override
+        protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) {
+            node.setClassName(getClass().getName());
+            node.addAction(AccessibilityAction.ACTION_CLICK);
+
+            final int type = getTypeFromId(virtualViewId);
+            final int value = getValueFromId(virtualViewId);
+            final CharSequence description = getVirtualViewDescription(type, value);
+            node.setContentDescription(description);
+
+            getBoundsForVirtualView(virtualViewId, mTempRect);
+            node.setBoundsInParent(mTempRect);
+
+            final boolean selected = isVirtualViewSelected(type, value);
+            node.setSelected(selected);
+        }
+
+        @Override
+        protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
+                Bundle arguments) {
+            if (action == AccessibilityNodeInfo.ACTION_CLICK) {
+                final int type = getTypeFromId(virtualViewId);
+                final int value = getValueFromId(virtualViewId);
+                if (type == TYPE_HOUR) {
+                    final int hour = mIs24HourMode ? value : hour12To24(value, mAmOrPm);
+                    setCurrentHour(hour);
+                    return true;
+                } else if (type == TYPE_MINUTE) {
+                    setCurrentMinute(value);
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        private int hour12To24(int hour12, int amOrPm) {
+            int hour24 = hour12;
+            if (hour12 == 12) {
+                if (amOrPm == AM) {
+                    hour24 = 0;
+                }
+            } else if (amOrPm == PM) {
+                hour24 += 12;
+            }
+            return hour24;
+        }
+
+        private void getBoundsForVirtualView(int virtualViewId, Rect bounds) {
+            final float radius;
+            final int type = getTypeFromId(virtualViewId);
+            final int value = getValueFromId(virtualViewId);
+            final float centerRadius;
+            final float degrees;
+            if (type == TYPE_HOUR) {
+                final boolean innerCircle = mIs24HourMode && value > 0 && value <= 12;
+                if (innerCircle) {
+                    centerRadius = mCircleRadius[HOURS_INNER] * mNumbersRadiusMultiplier[HOURS_INNER];
+                    radius = mSelectionRadius[HOURS_INNER];
+                } else {
+                    centerRadius = mCircleRadius[HOURS] * mNumbersRadiusMultiplier[HOURS];
+                    radius = mSelectionRadius[HOURS];
+                }
+
+                degrees = getDegreesForHour(value);
+            } else if (type == TYPE_MINUTE) {
+                centerRadius = mCircleRadius[MINUTES] * mNumbersRadiusMultiplier[MINUTES];
+                degrees = getDegreesForMinute(value);
+                radius = mSelectionRadius[MINUTES];
+            } else {
+                // This should never happen.
+                centerRadius = 0;
+                degrees = 0;
+                radius = 0;
+            }
+
+            final double radians = Math.toRadians(degrees);
+            final float xCenter = mXCenter + centerRadius * (float) Math.sin(radians);
+            final float yCenter = mYCenter - centerRadius * (float) Math.cos(radians);
+
+            bounds.set((int) (xCenter - radius), (int) (yCenter - radius),
+                    (int) (xCenter + radius), (int) (yCenter + radius));
+        }
+
+        private CharSequence getVirtualViewDescription(int type, int value) {
+            final CharSequence description;
+            if (type == TYPE_HOUR || type == TYPE_MINUTE) {
+                description = Integer.toString(value);
+            } else {
+                description = null;
+            }
+            return description;
+        }
+
+        private boolean isVirtualViewSelected(int type, int value) {
+            final boolean selected;
+            if (type == TYPE_HOUR) {
+                selected = getCurrentHour() == value;
+            } else if (type == TYPE_MINUTE) {
+                selected = getCurrentMinute() == value;
+            } else {
+                selected = false;
+            }
+            return selected;
+        }
+
+        private int makeId(int type, int value) {
+            return type << SHIFT_TYPE | value << SHIFT_VALUE;
+        }
+
+        private int getTypeFromId(int id) {
+            return id >>> SHIFT_TYPE & MASK_TYPE;
+        }
+
+        private int getValueFromId(int id) {
+            return id >>> SHIFT_VALUE & MASK_VALUE;
+        }
+    }
+
     private static class IntHolder {
         private int mValue;
 
diff --git a/core/java/android/widget/SimpleMonthView.java b/core/java/android/widget/SimpleMonthView.java
index a76241e..59baabae 100644
--- a/core/java/android/widget/SimpleMonthView.java
+++ b/core/java/android/widget/SimpleMonthView.java
@@ -31,6 +31,7 @@
 import android.text.format.DateUtils;
 import android.text.format.Time;
 import android.util.AttributeSet;
+import android.util.IntArray;
 import android.util.MathUtils;
 import android.view.MotionEvent;
 import android.view.View;
@@ -610,7 +611,7 @@
         }
 
         @Override
-        protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
+        protected void getVisibleVirtualViews(IntArray virtualViewIds) {
             for (int day = 1; day <= mNumCells; day++) {
                 virtualViewIds.add(day);
             }
diff --git a/core/java/android/widget/TimePickerClockDelegate.java b/core/java/android/widget/TimePickerClockDelegate.java
index eca3048..78ee247 100644
--- a/core/java/android/widget/TimePickerClockDelegate.java
+++ b/core/java/android/widget/TimePickerClockDelegate.java
@@ -611,15 +611,12 @@
             if (mAllowAutoAdvance && autoAdvance) {
                 updateHeaderHour(newValue, false);
                 setCurrentItemShowing(MINUTE_INDEX, true, false);
-                mRadialTimePickerView.announceForAccessibility(newValue + ". " + mSelectMinutes);
+                mDelegator.announceForAccessibility(newValue + ". " + mSelectMinutes);
             } else {
                 updateHeaderHour(newValue, true);
-                mRadialTimePickerView.setContentDescription(
-                        mHourPickerDescription + ": " + newValue);
             }
         } else if (pickerIndex == MINUTE_INDEX){
             updateHeaderMinute(newValue, true);
-            mRadialTimePickerView.setContentDescription(mMinutePickerDescription + ": " + newValue);
         } else if (pickerIndex == AMPM_INDEX) {
             updateAmPmLabelStates(newValue);
         } else if (pickerIndex == ENABLE_PICKER_INDEX) {
@@ -744,19 +741,12 @@
         mRadialTimePickerView.setCurrentItemShowing(index, animateCircle);
 
         if (index == HOUR_INDEX) {
-            int hours = mRadialTimePickerView.getCurrentHour();
-            if (!mIs24HourView) {
-                hours = hours % 12;
-            }
-            mRadialTimePickerView.setContentDescription(mHourPickerDescription + ": " + hours);
             if (announce) {
-                mRadialTimePickerView.announceForAccessibility(mSelectHours);
+                mDelegator.announceForAccessibility(mSelectHours);
             }
         } else {
-            int minutes = mRadialTimePickerView.getCurrentMinute();
-            mRadialTimePickerView.setContentDescription(mMinutePickerDescription + ": " + minutes);
             if (announce) {
-                mRadialTimePickerView.announceForAccessibility(mSelectMinutes);
+                mDelegator.announceForAccessibility(mSelectMinutes);
             }
         }
 
@@ -789,7 +779,7 @@
                     } else {
                         deletedKeyStr = String.format("%d", getValFromKeyCode(deleted));
                     }
-                    mRadialTimePickerView.announceForAccessibility(
+                    mDelegator.announceForAccessibility(
                             String.format(mDeletedKeyFormat, deletedKeyStr));
                     updateDisplay(true);
                 }
@@ -851,7 +841,7 @@
         }
 
         int val = getValFromKeyCode(keyCode);
-        mRadialTimePickerView.announceForAccessibility(String.format("%d", val));
+        mDelegator.announceForAccessibility(String.format("%d", val));
         // Automatically fill in 0's if AM or PM was legally entered.
         if (isTypedTimeFullyLegal()) {
             if (!mIs24HourView && mTypedTimes.size() <= 3) {
diff --git a/core/java/com/android/internal/widget/ExploreByTouchHelper.java b/core/java/com/android/internal/widget/ExploreByTouchHelper.java
index 4689179..0e046cb 100644
--- a/core/java/com/android/internal/widget/ExploreByTouchHelper.java
+++ b/core/java/com/android/internal/widget/ExploreByTouchHelper.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.graphics.Rect;
 import android.os.Bundle;
+import android.util.IntArray;
 import android.view.accessibility.*;
 import android.view.MotionEvent;
 import android.view.View;
@@ -26,11 +27,9 @@
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
 import android.view.accessibility.AccessibilityNodeProvider;
 
-import java.util.LinkedList;
-import java.util.List;
-
 /**
  * ExploreByTouchHelper is a utility class for implementing accessibility
  * support in custom {@link android.view.View}s that represent a collection of View-like
@@ -58,14 +57,16 @@
     private static final Rect INVALID_PARENT_BOUNDS = new Rect(
             Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE);
 
-    // Temporary, reusable data structures.
-    private final Rect mTempScreenRect = new Rect();
-    private final Rect mTempParentRect = new Rect();
-    private final Rect mTempVisibleRect = new Rect();
-    private final int[] mTempGlobalRect = new int[2];
+    // Lazily-created temporary data structures used when creating nodes.
+    private Rect mTempScreenRect;
+    private Rect mTempParentRect;
+    private int[] mTempGlobalRect;
 
-    /** View's context **/
-    private Context mContext;
+    /** Lazily-created temporary data structure used to compute visibility. */
+    private Rect mTempVisibleRect;
+
+    /** Lazily-created temporary data structure used to obtain child IDs. */
+    private IntArray mTempArray;
 
     /** System accessibility manager, used to check state and send events. */
     private final AccessibilityManager mManager;
@@ -73,6 +74,9 @@
     /** View whose internal structure is exposed through this helper. */
     private final View mView;
 
+    /** Context of the host view. **/
+    private final Context mContext;
+
     /** Node provider that handles creating nodes and performing actions. */
     private ExploreByTouchNodeProvider mNodeProvider;
 
@@ -332,11 +336,17 @@
         onInitializeAccessibilityNodeInfo(mView, node);
 
         // Add the virtual descendants.
-        final LinkedList<Integer> virtualViewIds = new LinkedList<Integer>();
+        if (mTempArray == null) {
+            mTempArray = new IntArray();
+        } else {
+            mTempArray.clear();
+        }
+        final IntArray virtualViewIds = mTempArray;
         getVisibleVirtualViews(virtualViewIds);
 
-        for (Integer childVirtualViewId : virtualViewIds) {
-            node.addChild(mView, childVirtualViewId);
+        final int N = virtualViewIds.size();
+        for (int i = 0; i < N; i++) {
+            node.addChild(mView, virtualViewIds.get(i));
         }
 
         return node;
@@ -371,6 +381,11 @@
      * @return An {@link AccessibilityNodeInfo} for the specified item.
      */
     private AccessibilityNodeInfo createNodeForChild(int virtualViewId) {
+        ensureTempRects();
+        final Rect tempParentRect = mTempParentRect;
+        final int[] tempGlobalRect = mTempGlobalRect;
+        final Rect tempScreenRect = mTempScreenRect;
+
         final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain();
 
         // Ensure the client has good defaults.
@@ -387,8 +402,8 @@
                     + "populateNodeForVirtualViewId()");
         }
 
-        node.getBoundsInParent(mTempParentRect);
-        if (mTempParentRect.equals(INVALID_PARENT_BOUNDS)) {
+        node.getBoundsInParent(tempParentRect);
+        if (tempParentRect.equals(INVALID_PARENT_BOUNDS)) {
             throw new RuntimeException("Callbacks must set parent bounds in "
                     + "populateNodeForVirtualViewId()");
         }
@@ -411,29 +426,35 @@
         // Manage internal accessibility focus state.
         if (mFocusedVirtualViewId == virtualViewId) {
             node.setAccessibilityFocused(true);
-            node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
+            node.addAction(AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
         } else {
             node.setAccessibilityFocused(false);
-            node.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
+            node.addAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
         }
 
         // Set the visibility based on the parent bound.
-        if (intersectVisibleToUser(mTempParentRect)) {
+        if (intersectVisibleToUser(tempParentRect)) {
             node.setVisibleToUser(true);
-            node.setBoundsInParent(mTempParentRect);
+            node.setBoundsInParent(tempParentRect);
         }
 
         // Calculate screen-relative bound.
-        mView.getLocationOnScreen(mTempGlobalRect);
-        final int offsetX = mTempGlobalRect[0];
-        final int offsetY = mTempGlobalRect[1];
-        mTempScreenRect.set(mTempParentRect);
-        mTempScreenRect.offset(offsetX, offsetY);
-        node.setBoundsInScreen(mTempScreenRect);
+        mView.getLocationOnScreen(tempGlobalRect);
+        final int offsetX = tempGlobalRect[0];
+        final int offsetY = tempGlobalRect[1];
+        tempScreenRect.set(tempParentRect);
+        tempScreenRect.offset(offsetX, offsetY);
+        node.setBoundsInScreen(tempScreenRect);
 
         return node;
     }
 
+    private void ensureTempRects() {
+        mTempGlobalRect = new int[2];
+        mTempParentRect = new Rect();
+        mTempScreenRect = new Rect();
+    }
+
     private boolean performAction(int virtualViewId, int action, Bundle arguments) {
         switch (virtualViewId) {
             case View.NO_ID:
@@ -451,13 +472,13 @@
         switch (action) {
             case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
             case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
-                return manageFocusForChild(virtualViewId, action, arguments);
+                return manageFocusForChild(virtualViewId, action);
             default:
                 return onPerformActionForVirtualView(virtualViewId, action, arguments);
         }
     }
 
-    private boolean manageFocusForChild(int virtualViewId, int action, Bundle arguments) {
+    private boolean manageFocusForChild(int virtualViewId, int action) {
         switch (action) {
             case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
                 return requestAccessibilityFocus(virtualViewId);
@@ -503,12 +524,16 @@
         }
 
         // If no portion of the parent is visible, this view is not visible.
-        if (!mView.getLocalVisibleRect(mTempVisibleRect)) {
+        if (mTempVisibleRect == null) {
+            mTempVisibleRect = new Rect();
+        }
+        final Rect tempVisibleRect = mTempVisibleRect;
+        if (!mView.getLocalVisibleRect(tempVisibleRect)) {
             return false;
         }
 
         // Check if the view intersects the visible portion of the parent.
-        return localRect.intersect(mTempVisibleRect);
+        return localRect.intersect(tempVisibleRect);
     }
 
     /**
@@ -588,7 +613,7 @@
      *
      * @param virtualViewIds The list to populate with visible items
      */
-    protected abstract void getVisibleVirtualViews(List<Integer> virtualViewIds);
+    protected abstract void getVisibleVirtualViews(IntArray virtualViewIds);
 
     /**
      * Populates an {@link AccessibilityEvent} with information about the
diff --git a/core/res/res/layout/time_header_label.xml b/core/res/res/layout/time_header_label.xml
index 84b2b0c..efb3628 100644
--- a/core/res/res/layout/time_header_label.xml
+++ b/core/res/res/layout/time_header_label.xml
@@ -56,7 +56,9 @@
                 android:paddingStart="@dimen/timepicker_ampm_horizontal_padding"
                 android:paddingTop="@dimen/timepicker_ampm_vertical_padding"
                 android:paddingEnd="@dimen/timepicker_ampm_horizontal_padding"
-                android:paddingBottom="@dimen/timepicker_am_bottom_padding" />
+                android:paddingBottom="@dimen/timepicker_am_bottom_padding"
+                android:lines="1"
+                android:ellipsize="none" />
             <CheckedTextView
                 android:id="@+id/pm_label"
                 android:layout_width="wrap_content"
@@ -64,7 +66,9 @@
                 android:paddingStart="@dimen/timepicker_ampm_horizontal_padding"
                 android:paddingTop="@dimen/timepicker_pm_top_padding"
                 android:paddingEnd="@dimen/timepicker_ampm_horizontal_padding"
-                android:paddingBottom="@dimen/timepicker_ampm_vertical_padding" />
+                android:paddingBottom="@dimen/timepicker_ampm_vertical_padding"
+                android:lines="1"
+                android:ellipsize="none" />
         </LinearLayout>
     </RelativeLayout>
 </FrameLayout>