Update TimePicker widget and its related dialog

- the old TimePicker widget is still there for obvious layout compatibility reasons
- add a new delegate implementation for having a new UI based on a radial picker
- use the new delegate only for the TimePickerDialog (which does not need to be
the same)
- added support for Theming and light/dark Themes
- added support for I18N (hour formatting and time separator and also position of
AM/PM indicator coming from Unicode CLDR)
- added support for RTL
- verified support for Keyboard
- verified that CTS tests for TimePicker are passing (for both the legacy and the
new widgets)

Also added a new HapticFeedbackConstants.CLOCK_TICK and its related code for
enabling ticks vibration.

Change-Id: Ib9b53a152bd9e97383dc391ef8c26da91217298f
diff --git a/api/current.txt b/api/current.txt
index 0bdc8a5..fba6f1a 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -26637,6 +26637,7 @@
   }
 
   public class HapticFeedbackConstants {
+    field public static final int CLOCK_TICK = 4; // 0x4
     field public static final int FLAG_IGNORE_GLOBAL_SETTING = 2; // 0x2
     field public static final int FLAG_IGNORE_VIEW_SETTING = 1; // 0x1
     field public static final int KEYBOARD_TAP = 3; // 0x3
@@ -31796,8 +31797,6 @@
     ctor public NumberPicker(android.content.Context, android.util.AttributeSet);
     ctor public NumberPicker(android.content.Context, android.util.AttributeSet, int);
     ctor public NumberPicker(android.content.Context, android.util.AttributeSet, int, int);
-    method public int computeVerticalScrollOffset();
-    method public int computeVerticalScrollRange();
     method public java.lang.String[] getDisplayedValues();
     method public int getMaxValue();
     method public int getMinValue();
diff --git a/core/java/android/app/TimePickerDialog.java b/core/java/android/app/TimePickerDialog.java
index 952227f..a85c61f 100644
--- a/core/java/android/app/TimePickerDialog.java
+++ b/core/java/android/app/TimePickerDialog.java
@@ -16,17 +16,19 @@
 
 package android.app;
 
-import com.android.internal.R;
-
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.DialogInterface.OnClickListener;
 import android.os.Bundle;
+import android.util.TypedValue;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.TimePicker;
 import android.widget.TimePicker.OnTimeChangedListener;
 
+import com.android.internal.R;
+
+
 /**
  * A dialog that prompts the user for the time of day using a {@link TimePicker}.
  *
@@ -38,7 +40,7 @@
 
     /**
      * The callback interface used to indicate the user is done filling in
-     * the time (they clicked on the 'Set' button).
+     * the time (they clicked on the 'Done' button).
      */
     public interface OnTimeSetListener {
 
@@ -55,7 +57,7 @@
     private static final String IS_24_HOUR = "is24hour";
 
     private final TimePicker mTimePicker;
-    private final OnTimeSetListener mCallback;
+    private final OnTimeSetListener mTimeSetCallback;
 
     int mInitialHourOfDay;
     int mInitialMinute;
@@ -74,6 +76,16 @@
         this(context, 0, callBack, hourOfDay, minute, is24HourView);
     }
 
+    static int resolveDialogTheme(Context context, int resid) {
+        if (resid == 0) {
+            TypedValue outValue = new TypedValue();
+            context.getTheme().resolveAttribute(R.attr.timePickerDialogTheme, outValue, true);
+            return outValue.resourceId;
+        } else {
+            return resid;
+        }
+    }
+
     /**
      * @param context Parent.
      * @param theme the theme to apply to this dialog
@@ -86,17 +98,13 @@
             int theme,
             OnTimeSetListener callBack,
             int hourOfDay, int minute, boolean is24HourView) {
-        super(context, theme);
-        mCallback = callBack;
+        super(context, resolveDialogTheme(context, theme));
+        mTimeSetCallback = callBack;
         mInitialHourOfDay = hourOfDay;
         mInitialMinute = minute;
         mIs24HourView = is24HourView;
 
-        setIcon(0);
-        setTitle(R.string.time_picker_dialog_title);
-
         Context themeContext = getContext();
-        setButton(BUTTON_POSITIVE, themeContext.getText(R.string.date_time_done), this);
 
         LayoutInflater inflater =
                 (LayoutInflater) themeContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
@@ -104,7 +112,18 @@
         setView(view);
         mTimePicker = (TimePicker) view.findViewById(R.id.timePicker);
 
-        // initialize state
+        // Initialize state
+        mTimePicker.setLegacyMode(false /* will show new UI */);
+        mTimePicker.setShowDoneButton(true);
+        mTimePicker.setDismissCallback(new TimePicker.TimePickerDismissCallback() {
+            @Override
+            public void dismiss(TimePicker view, boolean isCancel, int hourOfDay, int minute) {
+                if (!isCancel) {
+                    mTimeSetCallback.onTimeSet(view, hourOfDay, minute);
+                }
+                TimePickerDialog.this.dismiss();
+            }
+        });
         mTimePicker.setIs24HourView(mIs24HourView);
         mTimePicker.setCurrentHour(mInitialHourOfDay);
         mTimePicker.setCurrentMinute(mInitialMinute);
@@ -125,9 +144,9 @@
     }
 
     private void tryNotifyTimeSet() {
-        if (mCallback != null) {
+        if (mTimeSetCallback != null) {
             mTimePicker.clearFocus();
-            mCallback.onTimeSet(mTimePicker, mTimePicker.getCurrentHour(),
+            mTimeSetCallback.onTimeSet(mTimePicker, mTimePicker.getCurrentHour(),
                     mTimePicker.getCurrentMinute());
         }
     }
diff --git a/core/java/android/view/HapticFeedbackConstants.java b/core/java/android/view/HapticFeedbackConstants.java
index 8f40260..26f47f9 100644
--- a/core/java/android/view/HapticFeedbackConstants.java
+++ b/core/java/android/view/HapticFeedbackConstants.java
@@ -41,6 +41,11 @@
     public static final int KEYBOARD_TAP = 3;
 
     /**
+     * The user has pressed either an hour or minute tick of a Clock.
+     */
+    public static final int CLOCK_TICK = 4;
+
+    /**
      * This is a private constant.  Feel free to renumber as desired.
      * @hide
      */
diff --git a/core/java/android/widget/LegacyTimePickerDelegate.java b/core/java/android/widget/LegacyTimePickerDelegate.java
new file mode 100644
index 0000000..1634d5f
--- /dev/null
+++ b/core/java/android/widget/LegacyTimePickerDelegate.java
@@ -0,0 +1,638 @@
+/*
+ * Copyright (C) 2013 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.widget;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import com.android.internal.R;
+
+import java.text.DateFormatSymbols;
+import java.util.Calendar;
+import java.util.Locale;
+
+import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
+import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES;
+
+/**
+ * A delegate implementing the basic TimePicker
+ */
+class LegacyTimePickerDelegate extends TimePicker.AbstractTimePickerDelegate {
+
+    private static final boolean DEFAULT_ENABLED_STATE = true;
+
+    private static final int HOURS_IN_HALF_DAY = 12;
+
+    // state
+    private boolean mIs24HourView;
+
+    private boolean mIsAm;
+
+    // ui components
+    private final NumberPicker mHourSpinner;
+
+    private final NumberPicker mMinuteSpinner;
+
+    private final NumberPicker mAmPmSpinner;
+
+    private final EditText mHourSpinnerInput;
+
+    private final EditText mMinuteSpinnerInput;
+
+    private final EditText mAmPmSpinnerInput;
+
+    private final TextView mDivider;
+
+    // Note that the legacy implementation of the TimePicker is
+    // using a button for toggling between AM/PM while the new
+    // version uses a NumberPicker spinner. Therefore the code
+    // accommodates these two cases to be backwards compatible.
+    private final Button mAmPmButton;
+
+    private final String[] mAmPmStrings;
+
+    private boolean mIsEnabled = DEFAULT_ENABLED_STATE;
+
+    private Calendar mTempCalendar;
+
+    private boolean mHourWithTwoDigit;
+    private char mHourFormat;
+
+    /**
+     * A no-op callback used in the constructor to avoid null checks later in
+     * the code.
+     */
+    private static final TimePicker.OnTimeChangedListener NO_OP_CHANGE_LISTENER =
+            new TimePicker.OnTimeChangedListener() {
+                public void onTimeChanged(TimePicker view, int hourOfDay, int minute) {
+                }
+            };
+
+    public LegacyTimePickerDelegate(TimePicker delegator, Context context, AttributeSet attrs,
+                                    int defStyleAttr, int defStyleRes) {
+        super(delegator, context);
+
+        // process style attributes
+        final TypedArray attributesArray = mContext.obtainStyledAttributes(
+                attrs, R.styleable.TimePicker, defStyleAttr, defStyleRes);
+        final int layoutResourceId = attributesArray.getResourceId(
+                R.styleable.TimePicker_legacyLayout, R.layout.time_picker_legacy);
+        attributesArray.recycle();
+
+        final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+        inflater.inflate(layoutResourceId, mDelegator, true);
+
+        // hour
+        mHourSpinner = (NumberPicker) delegator.findViewById(R.id.hour);
+        mHourSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
+            public void onValueChange(NumberPicker spinner, int oldVal, int newVal) {
+                updateInputState();
+                if (!is24HourView()) {
+                    if ((oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) ||
+                            (oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1)) {
+                        mIsAm = !mIsAm;
+                        updateAmPmControl();
+                    }
+                }
+                onTimeChanged();
+            }
+        });
+        mHourSpinnerInput = (EditText) mHourSpinner.findViewById(R.id.numberpicker_input);
+        mHourSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
+
+        // divider (only for the new widget style)
+        mDivider = (TextView) mDelegator.findViewById(R.id.divider);
+        if (mDivider != null) {
+            setDividerText();
+        }
+
+        // minute
+        mMinuteSpinner = (NumberPicker) mDelegator.findViewById(R.id.minute);
+        mMinuteSpinner.setMinValue(0);
+        mMinuteSpinner.setMaxValue(59);
+        mMinuteSpinner.setOnLongPressUpdateInterval(100);
+        mMinuteSpinner.setFormatter(NumberPicker.getTwoDigitFormatter());
+        mMinuteSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
+            public void onValueChange(NumberPicker spinner, int oldVal, int newVal) {
+                updateInputState();
+                int minValue = mMinuteSpinner.getMinValue();
+                int maxValue = mMinuteSpinner.getMaxValue();
+                if (oldVal == maxValue && newVal == minValue) {
+                    int newHour = mHourSpinner.getValue() + 1;
+                    if (!is24HourView() && newHour == HOURS_IN_HALF_DAY) {
+                        mIsAm = !mIsAm;
+                        updateAmPmControl();
+                    }
+                    mHourSpinner.setValue(newHour);
+                } else if (oldVal == minValue && newVal == maxValue) {
+                    int newHour = mHourSpinner.getValue() - 1;
+                    if (!is24HourView() && newHour == HOURS_IN_HALF_DAY - 1) {
+                        mIsAm = !mIsAm;
+                        updateAmPmControl();
+                    }
+                    mHourSpinner.setValue(newHour);
+                }
+                onTimeChanged();
+            }
+        });
+        mMinuteSpinnerInput = (EditText) mMinuteSpinner.findViewById(R.id.numberpicker_input);
+        mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
+
+            /* Get the localized am/pm strings and use them in the spinner */
+        mAmPmStrings = new DateFormatSymbols().getAmPmStrings();
+
+        // am/pm
+        View amPmView = mDelegator.findViewById(R.id.amPm);
+        if (amPmView instanceof Button) {
+            mAmPmSpinner = null;
+            mAmPmSpinnerInput = null;
+            mAmPmButton = (Button) amPmView;
+            mAmPmButton.setOnClickListener(new View.OnClickListener() {
+                public void onClick(View button) {
+                    button.requestFocus();
+                    mIsAm = !mIsAm;
+                    updateAmPmControl();
+                    onTimeChanged();
+                }
+            });
+        } else {
+            mAmPmButton = null;
+            mAmPmSpinner = (NumberPicker) amPmView;
+            mAmPmSpinner.setMinValue(0);
+            mAmPmSpinner.setMaxValue(1);
+            mAmPmSpinner.setDisplayedValues(mAmPmStrings);
+            mAmPmSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
+                public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
+                    updateInputState();
+                    picker.requestFocus();
+                    mIsAm = !mIsAm;
+                    updateAmPmControl();
+                    onTimeChanged();
+                }
+            });
+            mAmPmSpinnerInput = (EditText) mAmPmSpinner.findViewById(R.id.numberpicker_input);
+            mAmPmSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE);
+        }
+
+        if (isAmPmAtStart()) {
+            // Move the am/pm view to the beginning
+            ViewGroup amPmParent = (ViewGroup) delegator.findViewById(R.id.timePickerLayout);
+            amPmParent.removeView(amPmView);
+            amPmParent.addView(amPmView, 0);
+            // Swap layout margins if needed. They may be not symmetrical (Old Standard Theme
+            // for example and not for Holo Theme)
+            ViewGroup.MarginLayoutParams lp =
+                    (ViewGroup.MarginLayoutParams) amPmView.getLayoutParams();
+            final int startMargin = lp.getMarginStart();
+            final int endMargin = lp.getMarginEnd();
+            if (startMargin != endMargin) {
+                lp.setMarginStart(endMargin);
+                lp.setMarginEnd(startMargin);
+            }
+        }
+
+        getHourFormatData();
+
+        // update controls to initial state
+        updateHourControl();
+        updateMinuteControl();
+        updateAmPmControl();
+
+        setOnTimeChangedListener(NO_OP_CHANGE_LISTENER);
+
+        // set to current time
+        setCurrentHour(mTempCalendar.get(Calendar.HOUR_OF_DAY));
+        setCurrentMinute(mTempCalendar.get(Calendar.MINUTE));
+
+        if (!isEnabled()) {
+            setEnabled(false);
+        }
+
+        // set the content descriptions
+        setContentDescriptions();
+
+        // If not explicitly specified this view is important for accessibility.
+        if (mDelegator.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+            mDelegator.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+        }
+    }
+
+    private void getHourFormatData() {
+        final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale,
+                (mIs24HourView) ? "Hm" : "hm");
+        final int lengthPattern = bestDateTimePattern.length();
+        mHourWithTwoDigit = false;
+        char hourFormat = '\0';
+        // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save
+        // the hour format that we found.
+        for (int i = 0; i < lengthPattern; i++) {
+            final char c = bestDateTimePattern.charAt(i);
+            if (c == 'H' || c == 'h' || c == 'K' || c == 'k') {
+                mHourFormat = c;
+                if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) {
+                    mHourWithTwoDigit = true;
+                }
+                break;
+            }
+        }
+    }
+
+    private boolean isAmPmAtStart() {
+        final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale,
+                "hm" /* skeleton */);
+
+        return bestDateTimePattern.startsWith("a");
+    }
+
+    /**
+     * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":".
+     *
+     * See http://unicode.org/cldr/trac/browser/trunk/common/main
+     *
+     * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the
+     * separator as the character which is just after the hour marker in the returned pattern.
+     */
+    private void setDividerText() {
+        final String skeleton = (mIs24HourView) ? "Hm" : "hm";
+        final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale,
+                skeleton);
+        final String separatorText;
+        int hourIndex = bestDateTimePattern.lastIndexOf('H');
+        if (hourIndex == -1) {
+            hourIndex = bestDateTimePattern.lastIndexOf('h');
+        }
+        if (hourIndex == -1) {
+            // Default case
+            separatorText = ":";
+        } else {
+            int minuteIndex = bestDateTimePattern.indexOf('m', hourIndex + 1);
+            if  (minuteIndex == -1) {
+                separatorText = Character.toString(bestDateTimePattern.charAt(hourIndex + 1));
+            } else {
+                separatorText = bestDateTimePattern.substring(hourIndex + 1, minuteIndex);
+            }
+        }
+        mDivider.setText(separatorText);
+    }
+
+    @Override
+    public void setCurrentHour(Integer currentHour) {
+        setCurrentHour(currentHour, true);
+    }
+
+    private void setCurrentHour(Integer currentHour, boolean notifyTimeChanged) {
+        // why was Integer used in the first place?
+        if (currentHour == null || currentHour == getCurrentHour()) {
+            return;
+        }
+        if (!is24HourView()) {
+            // convert [0,23] ordinal to wall clock display
+            if (currentHour >= HOURS_IN_HALF_DAY) {
+                mIsAm = false;
+                if (currentHour > HOURS_IN_HALF_DAY) {
+                    currentHour = currentHour - HOURS_IN_HALF_DAY;
+                }
+            } else {
+                mIsAm = true;
+                if (currentHour == 0) {
+                    currentHour = HOURS_IN_HALF_DAY;
+                }
+            }
+            updateAmPmControl();
+        }
+        mHourSpinner.setValue(currentHour);
+        if (notifyTimeChanged) {
+            onTimeChanged();
+        }
+    }
+
+    @Override
+    public Integer getCurrentHour() {
+        int currentHour = mHourSpinner.getValue();
+        if (is24HourView()) {
+            return currentHour;
+        } else if (mIsAm) {
+            return currentHour % HOURS_IN_HALF_DAY;
+        } else {
+            return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY;
+        }
+    }
+
+    @Override
+    public void setCurrentMinute(Integer currentMinute) {
+        if (currentMinute == getCurrentMinute()) {
+            return;
+        }
+        mMinuteSpinner.setValue(currentMinute);
+        onTimeChanged();
+    }
+
+    @Override
+    public Integer getCurrentMinute() {
+        return mMinuteSpinner.getValue();
+    }
+
+    @Override
+    public void setIs24HourView(Boolean is24HourView) {
+        if (mIs24HourView == is24HourView) {
+            return;
+        }
+        // cache the current hour since spinner range changes and BEFORE changing mIs24HourView!!
+        int currentHour = getCurrentHour();
+        // Order is important here.
+        mIs24HourView = is24HourView;
+        getHourFormatData();
+        updateHourControl();
+        // set value after spinner range is updated
+        setCurrentHour(currentHour, false);
+        updateMinuteControl();
+        updateAmPmControl();
+    }
+
+    @Override
+    public boolean is24HourView() {
+        return mIs24HourView;
+    }
+
+    @Override
+    public void setOnTimeChangedListener(TimePicker.OnTimeChangedListener onTimeChangedListener) {
+        mOnTimeChangedListener = onTimeChangedListener;
+    }
+
+    @Override
+    public void setEnabled(boolean enabled) {
+        mMinuteSpinner.setEnabled(enabled);
+        if (mDivider != null) {
+            mDivider.setEnabled(enabled);
+        }
+        mHourSpinner.setEnabled(enabled);
+        if (mAmPmSpinner != null) {
+            mAmPmSpinner.setEnabled(enabled);
+        } else {
+            mAmPmButton.setEnabled(enabled);
+        }
+        mIsEnabled = enabled;
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return mIsEnabled;
+    }
+
+    @Override
+    public void setShowDoneButton(boolean showDoneButton) {
+        // Nothing to do
+    }
+
+    @Override
+    public void setDismissCallback(TimePicker.TimePickerDismissCallback callback) {
+        // Nothing to do
+    }
+
+    @Override
+    public int getBaseline() {
+        return mHourSpinner.getBaseline();
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        setCurrentLocale(newConfig.locale);
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState(Parcelable superState) {
+        return new SavedState(superState, getCurrentHour(), getCurrentMinute());
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        SavedState ss = (SavedState) state;
+        setCurrentHour(ss.getHour());
+        setCurrentMinute(ss.getMinute());
+    }
+
+    @Override
+    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+        onPopulateAccessibilityEvent(event);
+        return true;
+    }
+
+    @Override
+    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
+        int flags = DateUtils.FORMAT_SHOW_TIME;
+        if (mIs24HourView) {
+            flags |= DateUtils.FORMAT_24HOUR;
+        } else {
+            flags |= DateUtils.FORMAT_12HOUR;
+        }
+        mTempCalendar.set(Calendar.HOUR_OF_DAY, getCurrentHour());
+        mTempCalendar.set(Calendar.MINUTE, getCurrentMinute());
+        String selectedDateUtterance = DateUtils.formatDateTime(mContext,
+                mTempCalendar.getTimeInMillis(), flags);
+        event.getText().add(selectedDateUtterance);
+    }
+
+    @Override
+    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+        event.setClassName(TimePicker.class.getName());
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+        info.setClassName(TimePicker.class.getName());
+    }
+
+    private void updateInputState() {
+        // Make sure that if the user changes the value and the IME is active
+        // for one of the inputs if this widget, the IME is closed. If the user
+        // changed the value via the IME and there is a next input the IME will
+        // be shown, otherwise the user chose another means of changing the
+        // value and having the IME up makes no sense.
+        InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
+        if (inputMethodManager != null) {
+            if (inputMethodManager.isActive(mHourSpinnerInput)) {
+                mHourSpinnerInput.clearFocus();
+                inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
+            } else if (inputMethodManager.isActive(mMinuteSpinnerInput)) {
+                mMinuteSpinnerInput.clearFocus();
+                inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
+            } else if (inputMethodManager.isActive(mAmPmSpinnerInput)) {
+                mAmPmSpinnerInput.clearFocus();
+                inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
+            }
+        }
+    }
+
+    private void updateAmPmControl() {
+        if (is24HourView()) {
+            if (mAmPmSpinner != null) {
+                mAmPmSpinner.setVisibility(View.GONE);
+            } else {
+                mAmPmButton.setVisibility(View.GONE);
+            }
+        } else {
+            int index = mIsAm ? Calendar.AM : Calendar.PM;
+            if (mAmPmSpinner != null) {
+                mAmPmSpinner.setValue(index);
+                mAmPmSpinner.setVisibility(View.VISIBLE);
+            } else {
+                mAmPmButton.setText(mAmPmStrings[index]);
+                mAmPmButton.setVisibility(View.VISIBLE);
+            }
+        }
+        mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
+    }
+
+    /**
+     * Sets the current locale.
+     *
+     * @param locale The current locale.
+     */
+    @Override
+    public void setCurrentLocale(Locale locale) {
+        super.setCurrentLocale(locale);
+        mTempCalendar = Calendar.getInstance(locale);
+    }
+
+    private void onTimeChanged() {
+        mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
+        if (mOnTimeChangedListener != null) {
+            mOnTimeChangedListener.onTimeChanged(mDelegator, getCurrentHour(),
+                    getCurrentMinute());
+        }
+    }
+
+    private void updateHourControl() {
+        if (is24HourView()) {
+            // 'k' means 1-24 hour
+            if (mHourFormat == 'k') {
+                mHourSpinner.setMinValue(1);
+                mHourSpinner.setMaxValue(24);
+            } else {
+                mHourSpinner.setMinValue(0);
+                mHourSpinner.setMaxValue(23);
+            }
+        } else {
+            // 'K' means 0-11 hour
+            if (mHourFormat == 'K') {
+                mHourSpinner.setMinValue(0);
+                mHourSpinner.setMaxValue(11);
+            } else {
+                mHourSpinner.setMinValue(1);
+                mHourSpinner.setMaxValue(12);
+            }
+        }
+        mHourSpinner.setFormatter(mHourWithTwoDigit ? NumberPicker.getTwoDigitFormatter() : null);
+    }
+
+    private void updateMinuteControl() {
+        if (is24HourView()) {
+            mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE);
+        } else {
+            mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
+        }
+    }
+
+    private void setContentDescriptions() {
+        // Minute
+        trySetContentDescription(mMinuteSpinner, R.id.increment,
+                R.string.time_picker_increment_minute_button);
+        trySetContentDescription(mMinuteSpinner, R.id.decrement,
+                R.string.time_picker_decrement_minute_button);
+        // Hour
+        trySetContentDescription(mHourSpinner, R.id.increment,
+                R.string.time_picker_increment_hour_button);
+        trySetContentDescription(mHourSpinner, R.id.decrement,
+                R.string.time_picker_decrement_hour_button);
+        // AM/PM
+        if (mAmPmSpinner != null) {
+            trySetContentDescription(mAmPmSpinner, R.id.increment,
+                    R.string.time_picker_increment_set_pm_button);
+            trySetContentDescription(mAmPmSpinner, R.id.decrement,
+                    R.string.time_picker_decrement_set_am_button);
+        }
+    }
+
+    private void trySetContentDescription(View root, int viewId, int contDescResId) {
+        View target = root.findViewById(viewId);
+        if (target != null) {
+            target.setContentDescription(mContext.getString(contDescResId));
+        }
+    }
+
+    /**
+     * Used to save / restore state of time picker
+     */
+    private static class SavedState extends View.BaseSavedState {
+
+        private final int mHour;
+
+        private final int mMinute;
+
+        private SavedState(Parcelable superState, int hour, int minute) {
+            super(superState);
+            mHour = hour;
+            mMinute = minute;
+        }
+
+        private SavedState(Parcel in) {
+            super(in);
+            mHour = in.readInt();
+            mMinute = in.readInt();
+        }
+
+        public int getHour() {
+            return mHour;
+        }
+
+        public int getMinute() {
+            return mMinute;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            super.writeToParcel(dest, flags);
+            dest.writeInt(mHour);
+            dest.writeInt(mMinute);
+        }
+
+        @SuppressWarnings({"unused", "hiding"})
+        public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() {
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+    }
+}
+
diff --git a/core/java/android/widget/RadialTimePickerView.java b/core/java/android/widget/RadialTimePickerView.java
new file mode 100644
index 0000000..1c9ab61
--- /dev/null
+++ b/core/java/android/widget/RadialTimePickerView.java
@@ -0,0 +1,1396 @@
+/*
+ * Copyright (C) 2013 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.widget;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.Keyframe;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+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.Log;
+import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import com.android.internal.R;
+
+import java.text.DateFormatSymbols;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Locale;
+
+/**
+ * View to show a clock circle picker (with one or two picking circles)
+ *
+ * @hide
+ */
+public class RadialTimePickerView extends View implements View.OnTouchListener {
+    private static final String TAG = "ClockView";
+
+    private static final boolean DEBUG = false;
+
+    private static final int DEBUG_COLOR = 0x20FF0000;
+    private static final int DEBUG_TEXT_COLOR = 0x60FF0000;
+    private static final int DEBUG_STROKE_WIDTH = 2;
+
+    private static final int HOURS = 0;
+    private static final int MINUTES = 1;
+    private static final int HOURS_INNER = 2;
+    private static final int AMPM = 3;
+
+    private static final int SELECTOR_CIRCLE = 0;
+    private static final int SELECTOR_DOT = 1;
+    private static final int SELECTOR_LINE = 2;
+
+    private static final int AM = 0;
+    private static final int PM = 1;
+
+    // Opaque alpha level
+    private static final int ALPHA_OPAQUE = 255;
+
+    // Transparent alpha level
+    private static final int ALPHA_TRANSPARENT = 0;
+
+    // Alpha level of color for selector.
+    private static final int ALPHA_SELECTOR = 51;
+
+    // Alpha level of color for selected circle.
+    private static final int ALPHA_AMPM_SELECTED = ALPHA_SELECTOR;
+
+    // Alpha level of color for pressed circle.
+    private static final int ALPHA_AMPM_PRESSED = 175;
+
+    private static final float COSINE_30_DEGREES = ((float) Math.sqrt(3)) * 0.5f;
+    private static final float SINE_30_DEGREES = 0.5f;
+
+    private static final int DEGREES_FOR_ONE_HOUR = 30;
+    private static final int DEGREES_FOR_ONE_MINUTE = 6;
+
+    private static final int[] HOURS_NUMBERS = {12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
+    private static final int[] HOURS_NUMBERS_24 = {0, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23};
+    private static final int[] MINUTES_NUMBERS = {0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55};
+
+    private static final int CENTER_RADIUS = 2;
+
+    private static int[] sSnapPrefer30sMap = new int[361];
+
+    private final String[] mHours12Texts = new String[12];
+    private final String[] mOuterHours24Texts = new String[12];
+    private final String[] mInnerHours24Texts = new String[12];
+    private final String[] mMinutesTexts = new String[12];
+
+    private final String[] mAmPmText = new String[2];
+
+    private final Paint[] mPaint = new Paint[2];
+    private final Paint mPaintCenter = new Paint();
+    private final Paint[][] mPaintSelector = new Paint[2][3];
+    private final Paint mPaintAmPmText = new Paint();
+    private final Paint[] mPaintAmPmCircle = new Paint[2];
+
+    private final Paint mPaintBackground = new Paint();
+    private final Paint mPaintDisabled = new Paint();
+    private final Paint mPaintDebug = new Paint();
+
+    private Typeface mTypeface;
+
+    private boolean mIs24HourMode;
+    private boolean mShowHours;
+    private boolean mIsOnInnerCircle;
+
+    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 mAmPmCircleRadius;
+    private float mAmPmYCenter;
+
+    private float mAmPmCircleRadiusMultiplier;
+    private int mAmPmTextColor;
+
+    private float mLeftIndicatorXCenter;
+    private float mRightIndicatorXCenter;
+
+    private int mAmPmUnselectedColor;
+    private int mAmPmSelectedColor;
+
+    private int mAmOrPm;
+    private int mAmOrPmPressed;
+
+    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>();
+
+    public interface OnValueSelectedListener {
+        void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance);
+    }
+
+    static {
+        // Prepare mapping to snap touchable degrees to selectable degrees.
+        preparePrefer30sMap();
+    }
+
+    /**
+     * Split up the 360 degrees of the circle among the 60 selectable values. Assigns a larger
+     * selectable area to each of the 12 visible values, such that the ratio of space apportioned
+     * to a visible value : space apportioned to a non-visible value will be 14 : 4.
+     * E.g. the output of 30 degrees should have a higher range of input associated with it than
+     * the output of 24 degrees, because 30 degrees corresponds to a visible number on the clock
+     * circle (5 on the minutes, 1 or 13 on the hours).
+     */
+    private static void preparePrefer30sMap() {
+        // We'll split up the visible output and the non-visible output such that each visible
+        // output will correspond to a range of 14 associated input degrees, and each non-visible
+        // output will correspond to a range of 4 associate input degrees, so visible numbers
+        // are more than 3 times easier to get than non-visible numbers:
+        // {354-359,0-7}:0, {8-11}:6, {12-15}:12, {16-19}:18, {20-23}:24, {24-37}:30, etc.
+        //
+        // If an output of 30 degrees should correspond to a range of 14 associated degrees, then
+        // we'll need any input between 24 - 37 to snap to 30. Working out from there, 20-23 should
+        // snap to 24, while 38-41 should snap to 36. This is somewhat counter-intuitive, that you
+        // can be touching 36 degrees but have the selection snapped to 30 degrees; however, this
+        // inconsistency isn't noticeable at such fine-grained degrees, and it affords us the
+        // ability to aggressively prefer the visible values by a factor of more than 3:1, which
+        // greatly contributes to the selectability of these values.
+
+        // The first output is 0, and each following output will increment by 6 {0, 6, 12, ...}.
+        int snappedOutputDegrees = 0;
+        // Count of how many inputs we've designated to the specified output.
+        int count = 1;
+        // How many input we expect for a specified output. This will be 14 for output divisible
+        // by 30, and 4 for the remaining output. We'll special case the outputs of 0 and 360, so
+        // the caller can decide which they need.
+        int expectedCount = 8;
+        // Iterate through the input.
+        for (int degrees = 0; degrees < 361; degrees++) {
+            // Save the input-output mapping.
+            sSnapPrefer30sMap[degrees] = snappedOutputDegrees;
+            // If this is the last input for the specified output, calculate the next output and
+            // the next expected count.
+            if (count == expectedCount) {
+                snappedOutputDegrees += 6;
+                if (snappedOutputDegrees == 360) {
+                    expectedCount = 7;
+                } else if (snappedOutputDegrees % 30 == 0) {
+                    expectedCount = 14;
+                } else {
+                    expectedCount = 4;
+                }
+                count = 1;
+            } else {
+                count++;
+            }
+        }
+    }
+
+    /**
+     * Returns mapping of any input degrees (0 to 360) to one of 60 selectable output degrees,
+     * where the degrees corresponding to visible numbers (i.e. those divisible by 30) will be
+     * weighted heavier than the degrees corresponding to non-visible numbers.
+     * See {@link #preparePrefer30sMap()} documentation for the rationale and generation of the
+     * mapping.
+     */
+    private static int snapPrefer30s(int degrees) {
+        if (sSnapPrefer30sMap == null) {
+            return -1;
+        }
+        return sSnapPrefer30sMap[degrees];
+    }
+
+    /**
+     * Returns mapping of any input degrees (0 to 360) to one of 12 visible output degrees (all
+     * multiples of 30), where the input will be "snapped" to the closest visible degrees.
+     * @param degrees The input degrees
+     * @param forceHigherOrLower The output may be forced to either the higher or lower step, or may
+     * be allowed to snap to whichever is closer. Use 1 to force strictly higher, -1 to force
+     * strictly lower, and 0 to snap to the closer one.
+     * @return output degrees, will be a multiple of 30
+     */
+    private static int snapOnly30s(int degrees, int forceHigherOrLower) {
+        final int stepSize = DEGREES_FOR_ONE_HOUR;
+        int floor = (degrees / stepSize) * stepSize;
+        final int ceiling = floor + stepSize;
+        if (forceHigherOrLower == 1) {
+            degrees = ceiling;
+        } else if (forceHigherOrLower == -1) {
+            if (degrees == floor) {
+                floor -= stepSize;
+            }
+            degrees = floor;
+        } else {
+            if ((degrees - floor) < (ceiling - degrees)) {
+                degrees = floor;
+            } else {
+                degrees = ceiling;
+            }
+        }
+        return degrees;
+    }
+
+    public RadialTimePickerView(Context context, AttributeSet attrs)  {
+        this(context, attrs, R.attr.timePickerStyle);
+    }
+
+    public RadialTimePickerView(Context context, AttributeSet attrs, int defStyle)  {
+        super(context, attrs);
+
+        // process style attributes
+        final TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.TimePicker,
+                defStyle, 0);
+
+        final Resources res = getResources();
+
+        mAmPmUnselectedColor = a.getColor(R.styleable.TimePicker_amPmUnselectedBackgroundColor,
+                res.getColor(
+                        R.color.timepicker_default_ampm_unselected_background_color_holo_light));
+
+        mAmPmSelectedColor = a.getColor(R.styleable.TimePicker_amPmSelectedBackgroundColor,
+                res.getColor(R.color.timepicker_default_ampm_selected_background_color_holo_light));
+
+        mAmPmTextColor = a.getColor(R.styleable.TimePicker_amPmTextColor,
+                res.getColor(R.color.timepicker_default_text_color_holo_light));
+
+        final int numbersTextColor = a.getColor(R.styleable.TimePicker_numbersTextColor,
+                res.getColor(R.color.timepicker_default_text_color_holo_light));
+
+        mTypeface = Typeface.create("sans-serif", Typeface.NORMAL);
+
+        mPaint[HOURS] = new Paint();
+        mPaint[HOURS].setColor(numbersTextColor);
+        mPaint[HOURS].setAntiAlias(true);
+        mPaint[HOURS].setTextAlign(Paint.Align.CENTER);
+
+        mPaint[MINUTES] = new Paint();
+        mPaint[MINUTES].setColor(numbersTextColor);
+        mPaint[MINUTES].setAntiAlias(true);
+        mPaint[MINUTES].setTextAlign(Paint.Align.CENTER);
+
+        mPaintCenter.setColor(numbersTextColor);
+        mPaintCenter.setAntiAlias(true);
+        mPaintCenter.setTextAlign(Paint.Align.CENTER);
+
+        mPaintSelector[HOURS][SELECTOR_CIRCLE] = new Paint();
+        mPaintSelector[HOURS][SELECTOR_CIRCLE].setColor(
+                a.getColor(R.styleable.TimePicker_numbersSelectorColor, R.color.holo_blue_light));
+        mPaintSelector[HOURS][SELECTOR_CIRCLE].setAntiAlias(true);
+
+        mPaintSelector[HOURS][SELECTOR_DOT] = new Paint();
+        mPaintSelector[HOURS][SELECTOR_DOT].setColor(
+                a.getColor(R.styleable.TimePicker_numbersSelectorColor, R.color.holo_blue_light));
+        mPaintSelector[HOURS][SELECTOR_DOT].setAntiAlias(true);
+
+        mPaintSelector[HOURS][SELECTOR_LINE] = new Paint();
+        mPaintSelector[HOURS][SELECTOR_LINE].setColor(
+                a.getColor(R.styleable.TimePicker_numbersSelectorColor, R.color.holo_blue_light));
+        mPaintSelector[HOURS][SELECTOR_LINE].setAntiAlias(true);
+        mPaintSelector[HOURS][SELECTOR_LINE].setStrokeWidth(2);
+
+        mPaintSelector[MINUTES][SELECTOR_CIRCLE] = new Paint();
+        mPaintSelector[MINUTES][SELECTOR_CIRCLE].setColor(
+                a.getColor(R.styleable.TimePicker_numbersSelectorColor, R.color.holo_blue_light));
+        mPaintSelector[MINUTES][SELECTOR_CIRCLE].setAntiAlias(true);
+
+        mPaintSelector[MINUTES][SELECTOR_DOT] = new Paint();
+        mPaintSelector[MINUTES][SELECTOR_DOT].setColor(
+                a.getColor(R.styleable.TimePicker_numbersSelectorColor, R.color.holo_blue_light));
+        mPaintSelector[MINUTES][SELECTOR_DOT].setAntiAlias(true);
+
+        mPaintSelector[MINUTES][SELECTOR_LINE] = new Paint();
+        mPaintSelector[MINUTES][SELECTOR_LINE].setColor(
+                a.getColor(R.styleable.TimePicker_numbersSelectorColor, R.color.holo_blue_light));
+        mPaintSelector[MINUTES][SELECTOR_LINE].setAntiAlias(true);
+        mPaintSelector[MINUTES][SELECTOR_LINE].setStrokeWidth(2);
+
+        mPaintAmPmText.setColor(mAmPmTextColor);
+        mPaintAmPmText.setTypeface(mTypeface);
+        mPaintAmPmText.setAntiAlias(true);
+        mPaintAmPmText.setTextAlign(Paint.Align.CENTER);
+
+        mPaintAmPmCircle[AM] = new Paint();
+        mPaintAmPmCircle[AM].setAntiAlias(true);
+        mPaintAmPmCircle[PM] = new Paint();
+        mPaintAmPmCircle[PM].setAntiAlias(true);
+
+        mPaintBackground.setColor(
+                a.getColor(R.styleable.TimePicker_numbersBackgroundColor, Color.WHITE));
+        mPaintBackground.setAntiAlias(true);
+
+        final int disabledColor = a.getColor(R.styleable.TimePicker_disabledColor,
+                res.getColor(R.color.timepicker_default_disabled_color_holo_light));
+        mPaintDisabled.setColor(disabledColor);
+        mPaintDisabled.setAntiAlias(true);
+
+        if (DEBUG) {
+            mPaintDebug.setColor(DEBUG_COLOR);
+            mPaintDebug.setAntiAlias(true);
+            mPaintDebug.setStrokeWidth(DEBUG_STROKE_WIDTH);
+            mPaintDebug.setStyle(Paint.Style.STROKE);
+            mPaintDebug.setTextAlign(Paint.Align.CENTER);
+        }
+
+        mShowHours = true;
+        mIs24HourMode = false;
+        mAmOrPm = AM;
+        mAmOrPmPressed = -1;
+
+        initHoursAndMinutesText();
+        initData();
+
+        mTransitionMidRadiusMultiplier =  Float.parseFloat(
+                res.getString(R.string.timepicker_transition_mid_radius_multiplier));
+        mTransitionEndRadiusMultiplier = Float.parseFloat(
+                res.getString(R.string.timepicker_transition_end_radius_multiplier));
+
+        mTextGridHeights[HOURS] = new float[7];
+        mTextGridHeights[MINUTES] = new float[7];
+
+        mSelectionRadiusMultiplier = Float.parseFloat(
+                res.getString(R.string.timepicker_selection_radius_multiplier));
+
+        setOnTouchListener(this);
+
+        // Initial values
+        final Calendar calendar = Calendar.getInstance(Locale.getDefault());
+        final int currentHour = calendar.get(Calendar.HOUR_OF_DAY);
+        final int currentMinute = calendar.get(Calendar.MINUTE);
+
+        setCurrentHour(currentHour);
+        setCurrentMinute(currentMinute);
+
+        setHapticFeedbackEnabled(true);
+    }
+
+    /**
+     * Measure the view to end up as a square, based on the minimum of the height and width.
+     */
+    @Override
+    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
+        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        int measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
+        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+        int minDimension = Math.min(measuredWidth, measuredHeight);
+
+        super.onMeasure(MeasureSpec.makeMeasureSpec(minDimension, widthMode),
+                MeasureSpec.makeMeasureSpec(minDimension, heightMode));
+    }
+
+    public void initialize(int hour, int minute, boolean is24HourMode) {
+        mIs24HourMode = is24HourMode;
+        setCurrentHour(hour);
+        setCurrentMinute(minute);
+    }
+
+    public void setCurrentItemShowing(int item, boolean animate) {
+        switch (item){
+            case HOURS:
+                showHours(animate);
+                break;
+            case MINUTES:
+                showMinutes(animate);
+                break;
+            default:
+                Log.e(TAG, "ClockView does not support showing item " + item);
+        }
+    }
+
+    public int getCurrentItemShowing() {
+        return mShowHours ? HOURS : MINUTES;
+    }
+
+    public void setOnValueSelectedListener(OnValueSelectedListener listener) {
+        mListener = listener;
+    }
+
+    public void setCurrentHour(int hour) {
+        final int degrees = (hour % 12) * DEGREES_FOR_ONE_HOUR;
+        mSelectionDegrees[HOURS] = degrees;
+        mSelectionDegrees[HOURS_INNER] = degrees;
+        mAmOrPm = ((hour % 24) < 12) ? AM : PM;
+        if (mIs24HourMode) {
+            mIsOnInnerCircle = (mAmOrPm == AM);
+        } else {
+            mIsOnInnerCircle = false;
+        }
+        initData();
+        updateLayoutData();
+        invalidate();
+    }
+
+    // Return hours in 0-23 range
+    public int getCurrentHour() {
+        int hours =
+                mSelectionDegrees[mIsOnInnerCircle ? HOURS_INNER : HOURS] / DEGREES_FOR_ONE_HOUR;
+        if (mIs24HourMode) {
+            if (mIsOnInnerCircle) {
+                hours = hours % 12;
+            } else {
+                if (hours != 0) {
+                    hours += 12;
+                }
+            }
+        } else {
+            hours = hours % 12;
+            if (hours == 0) {
+                if (mAmOrPm == PM) {
+                    hours = 12;
+                }
+            } else {
+                if (mAmOrPm == PM) {
+                    hours += 12;
+                }
+            }
+        }
+        return hours;
+    }
+
+    public void setCurrentMinute(int minute) {
+        mSelectionDegrees[MINUTES] = (minute % 60) * DEGREES_FOR_ONE_MINUTE;
+        invalidate();
+    }
+
+    // Returns minutes in 0-59 range
+    public int getCurrentMinute() {
+        return (mSelectionDegrees[MINUTES] / DEGREES_FOR_ONE_MINUTE);
+    }
+
+    public void setAmOrPm(int val) {
+        mAmOrPm = (val % 2);
+        invalidate();
+    }
+
+    public int getAmOrPm() {
+        return mAmOrPm;
+    }
+
+    public void swapAmPm() {
+        mAmOrPm = (mAmOrPm == AM) ? PM : AM;
+        invalidate();
+    }
+
+    public void showHours(boolean animate) {
+        if (mShowHours) return;
+        mShowHours = true;
+        if (animate) {
+            startMinutesToHoursAnimation();
+        }
+        initData();
+        updateLayoutData();
+        invalidate();
+    }
+
+    public void showMinutes(boolean animate) {
+        if (!mShowHours) return;
+        mShowHours = false;
+        if (animate) {
+            startHoursToMinutesAnimation();
+        }
+        initData();
+        updateLayoutData();
+        invalidate();
+    }
+
+    private void initHoursAndMinutesText() {
+        // Initialize the hours and minutes numbers.
+        for (int i = 0; i < 12; i++) {
+            mHours12Texts[i] = String.format("%d", HOURS_NUMBERS[i]);
+            mOuterHours24Texts[i] = String.format("%02d", HOURS_NUMBERS_24[i]);
+            mInnerHours24Texts[i] = String.format("%d", HOURS_NUMBERS[i]);
+            mMinutesTexts[i] = String.format("%02d", MINUTES_NUMBERS[i]);
+        }
+
+        String[] amPmTexts = new DateFormatSymbols().getAmPmStrings();
+        mAmPmText[AM] = amPmTexts[0];
+        mAmPmText[PM] = amPmTexts[1];
+    }
+
+    private void initData() {
+        if (mIs24HourMode) {
+            mOuterTextHours = mOuterHours24Texts;
+            mInnerTextHours = mInnerHours24Texts;
+        } else {
+            mOuterTextHours = mHours12Texts;
+            mInnerTextHours = null;
+        }
+
+        mOuterTextMinutes = mMinutesTexts;
+
+        final Resources res = getResources();
+
+        if (mShowHours) {
+            if (mIs24HourMode) {
+                mCircleRadiusMultiplier[HOURS] = Float.parseFloat(
+                        res.getString(R.string.timepicker_circle_radius_multiplier_24HourMode));
+                mNumbersRadiusMultiplier[HOURS] = Float.parseFloat(
+                        res.getString(R.string.timepicker_numbers_radius_multiplier_outer));
+                mTextSizeMultiplier[HOURS] = Float.parseFloat(
+                        res.getString(R.string.timepicker_text_size_multiplier_outer));
+
+                mNumbersRadiusMultiplier[HOURS_INNER] = Float.parseFloat(
+                        res.getString(R.string.timepicker_numbers_radius_multiplier_inner));
+                mTextSizeMultiplier[HOURS_INNER] = Float.parseFloat(
+                        res.getString(R.string.timepicker_text_size_multiplier_inner));
+            } else {
+                mCircleRadiusMultiplier[HOURS] = Float.parseFloat(
+                        res.getString(R.string.timepicker_circle_radius_multiplier));
+                mNumbersRadiusMultiplier[HOURS] = Float.parseFloat(
+                        res.getString(R.string.timepicker_numbers_radius_multiplier_normal));
+                mTextSizeMultiplier[HOURS] = Float.parseFloat(
+                        res.getString(R.string.timepicker_text_size_multiplier_normal));
+            }
+        } else {
+            mCircleRadiusMultiplier[MINUTES] = Float.parseFloat(
+                    res.getString(R.string.timepicker_circle_radius_multiplier));
+            mNumbersRadiusMultiplier[MINUTES] = Float.parseFloat(
+                    res.getString(R.string.timepicker_numbers_radius_multiplier_normal));
+            mTextSizeMultiplier[MINUTES] = Float.parseFloat(
+                    res.getString(R.string.timepicker_text_size_multiplier_normal));
+        }
+
+        mAnimationRadiusMultiplier[HOURS] = 1;
+        mAnimationRadiusMultiplier[HOURS_INNER] = 1;
+        mAnimationRadiusMultiplier[MINUTES] = 1;
+
+        mAmPmCircleRadiusMultiplier = Float.parseFloat(
+                res.getString(R.string.timepicker_ampm_circle_radius_multiplier));
+
+        mPaint[HOURS].setAlpha(mShowHours ? ALPHA_OPAQUE : ALPHA_TRANSPARENT);
+        mPaint[MINUTES].setAlpha(mShowHours ? ALPHA_TRANSPARENT : ALPHA_OPAQUE);
+
+        mPaintSelector[HOURS][SELECTOR_CIRCLE].setAlpha(
+                mShowHours ?ALPHA_SELECTOR : ALPHA_TRANSPARENT);
+        mPaintSelector[HOURS][SELECTOR_DOT].setAlpha(
+                mShowHours ? ALPHA_OPAQUE : ALPHA_TRANSPARENT);
+        mPaintSelector[HOURS][SELECTOR_LINE].setAlpha(
+                mShowHours ? ALPHA_SELECTOR : ALPHA_TRANSPARENT);
+
+        mPaintSelector[MINUTES][SELECTOR_CIRCLE].setAlpha(
+                mShowHours ? ALPHA_TRANSPARENT : ALPHA_SELECTOR);
+        mPaintSelector[MINUTES][SELECTOR_DOT].setAlpha(
+                mShowHours ? ALPHA_TRANSPARENT : ALPHA_OPAQUE);
+        mPaintSelector[MINUTES][SELECTOR_LINE].setAlpha(
+                mShowHours ? ALPHA_TRANSPARENT : ALPHA_SELECTOR);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        updateLayoutData();
+    }
+
+    private void updateLayoutData() {
+        mXCenter = getWidth() / 2;
+        mYCenter = getHeight() / 2;
+
+        final int min = Math.min(mXCenter, mYCenter);
+
+        mCircleRadius[HOURS] = min * mCircleRadiusMultiplier[HOURS];
+        mCircleRadius[HOURS_INNER] = min * mCircleRadiusMultiplier[HOURS];
+        mCircleRadius[MINUTES] = min * mCircleRadiusMultiplier[MINUTES];
+
+        if (!mIs24HourMode) {
+            // We'll need to draw the AM/PM circles, so the main circle will need to have
+            // a slightly higher center. To keep the entire view centered vertically, we'll
+            // have to push it up by half the radius of the AM/PM circles.
+            int amPmCircleRadius = (int) (mCircleRadius[HOURS] * mAmPmCircleRadiusMultiplier);
+            mYCenter -= amPmCircleRadius / 2;
+        }
+
+        mMinHypotenuseForInnerNumber = (int) (mCircleRadius[HOURS]
+                * mNumbersRadiusMultiplier[HOURS_INNER]) - mSelectionRadius[HOURS];
+        mMaxHypotenuseForOuterNumber = (int) (mCircleRadius[HOURS]
+                * mNumbersRadiusMultiplier[HOURS]) + mSelectionRadius[HOURS];
+        mHalfwayHypotenusePoint = (int) (mCircleRadius[HOURS]
+                * ((mNumbersRadiusMultiplier[HOURS] + mNumbersRadiusMultiplier[HOURS_INNER]) / 2));
+
+        mTextSize[HOURS] = mCircleRadius[HOURS] * mTextSizeMultiplier[HOURS];
+        mTextSize[MINUTES] = mCircleRadius[MINUTES] * mTextSizeMultiplier[MINUTES];
+
+        if (mIs24HourMode) {
+            mInnerTextSize = mCircleRadius[HOURS] * mTextSizeMultiplier[HOURS_INNER];
+        }
+
+        calculateGridSizesHours();
+        calculateGridSizesMinutes();
+
+        mSelectionRadius[HOURS] = (int) (mCircleRadius[HOURS] * mSelectionRadiusMultiplier);
+        mSelectionRadius[HOURS_INNER] = mSelectionRadius[HOURS];
+        mSelectionRadius[MINUTES] = (int) (mCircleRadius[MINUTES] * mSelectionRadiusMultiplier);
+
+        mAmPmCircleRadius = (int) (mCircleRadius[HOURS] * mAmPmCircleRadiusMultiplier);
+        mPaintAmPmText.setTextSize(mAmPmCircleRadius * 3 / 4);
+
+        // Line up the vertical center of the AM/PM circles with the bottom of the main circle.
+        mAmPmYCenter = mYCenter + mCircleRadius[HOURS];
+
+        // Line up the horizontal edges of the AM/PM circles with the horizontal edges
+        // of the main circle
+        mLeftIndicatorXCenter = mXCenter - mCircleRadius[HOURS] + mAmPmCircleRadius;
+        mRightIndicatorXCenter = mXCenter + mCircleRadius[HOURS] - mAmPmCircleRadius;
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        canvas.save();
+
+        calculateGridSizesHours();
+        calculateGridSizesMinutes();
+
+        drawCircleBackground(canvas);
+
+        drawTextElements(canvas, mTextSize[HOURS], mTypeface, mOuterTextHours,
+                mTextGridWidths[HOURS], mTextGridHeights[HOURS], mPaint[HOURS]);
+
+        if (mIs24HourMode && mInnerTextHours != null) {
+            drawTextElements(canvas, mInnerTextSize, mTypeface, mInnerTextHours,
+                    mInnerTextGridWidths, mInnerTextGridHeights, mPaint[HOURS]);
+        }
+
+        drawTextElements(canvas, mTextSize[MINUTES], mTypeface, mOuterTextMinutes,
+                mTextGridWidths[MINUTES], mTextGridHeights[MINUTES], mPaint[MINUTES]);
+
+        drawCenter(canvas);
+        drawSelector(canvas);
+        if (!mIs24HourMode) {
+            drawAmPm(canvas);
+        }
+
+        if(!mInputEnabled) {
+            // Draw outer view rectangle
+            mRectF.set(0, 0, getWidth(), getHeight());
+            canvas.drawRect(mRectF, mPaintDisabled);
+        }
+
+        if (DEBUG) {
+            drawDebug(canvas);
+        }
+
+        canvas.restore();
+    }
+
+    private void drawCircleBackground(Canvas canvas) {
+        canvas.drawCircle(mXCenter, mYCenter, mCircleRadius[HOURS], mPaintBackground);
+    }
+
+    private void drawCenter(Canvas canvas) {
+        canvas.drawCircle(mXCenter, mYCenter, CENTER_RADIUS, mPaintCenter);
+    }
+
+    private void drawSelector(Canvas canvas) {
+        drawSelector(canvas, mIsOnInnerCircle ? HOURS_INNER : HOURS);
+        drawSelector(canvas, MINUTES);
+    }
+
+    private void drawAmPm(Canvas canvas) {
+        final boolean isLayoutRtl = isLayoutRtl();
+
+        int amColor = mAmPmUnselectedColor;
+        int amAlpha = ALPHA_OPAQUE;
+        int pmColor = mAmPmUnselectedColor;
+        int pmAlpha = ALPHA_OPAQUE;
+        if (mAmOrPm == AM) {
+            amColor = mAmPmSelectedColor;
+            amAlpha = ALPHA_AMPM_SELECTED;
+        } else if (mAmOrPm == PM) {
+            pmColor = mAmPmSelectedColor;
+            pmAlpha = ALPHA_AMPM_SELECTED;
+        }
+        if (mAmOrPmPressed == AM) {
+            amColor = mAmPmSelectedColor;
+            amAlpha = ALPHA_AMPM_PRESSED;
+        } else if (mAmOrPmPressed == PM) {
+            pmColor = mAmPmSelectedColor;
+            pmAlpha = ALPHA_AMPM_PRESSED;
+        }
+
+        // Draw the two circles
+        mPaintAmPmCircle[AM].setColor(amColor);
+        mPaintAmPmCircle[AM].setAlpha(amAlpha);
+        canvas.drawCircle(isLayoutRtl ? mRightIndicatorXCenter : mLeftIndicatorXCenter,
+                mAmPmYCenter, mAmPmCircleRadius, mPaintAmPmCircle[AM]);
+
+        mPaintAmPmCircle[PM].setColor(pmColor);
+        mPaintAmPmCircle[PM].setAlpha(pmAlpha);
+        canvas.drawCircle(isLayoutRtl ? mLeftIndicatorXCenter : mRightIndicatorXCenter,
+                mAmPmYCenter, mAmPmCircleRadius, mPaintAmPmCircle[PM]);
+
+        // Draw the AM/PM texts on top
+        mPaintAmPmText.setColor(mAmPmTextColor);
+        float textYCenter = mAmPmYCenter -
+                (int) (mPaintAmPmText.descent() + mPaintAmPmText.ascent()) / 2;
+
+        canvas.drawText(isLayoutRtl ? mAmPmText[PM] : mAmPmText[AM], mLeftIndicatorXCenter,
+                textYCenter, mPaintAmPmText);
+        canvas.drawText(isLayoutRtl ? mAmPmText[AM] : mAmPmText[PM], mRightIndicatorXCenter,
+                textYCenter, mPaintAmPmText);
+    }
+
+    private void drawSelector(Canvas canvas, int index) {
+        // Calculate the current radius at which to place the selection circle.
+        mLineLength[index] = (int) (mCircleRadius[index]
+                * mNumbersRadiusMultiplier[index] * mAnimationRadiusMultiplier[index]);
+
+        double selectionRadians = Math.toRadians(mSelectionDegrees[index]);
+
+        int pointX = mXCenter + (int) (mLineLength[index] * Math.sin(selectionRadians));
+        int pointY = mYCenter - (int) (mLineLength[index] * Math.cos(selectionRadians));
+
+        // Draw the selection circle
+        canvas.drawCircle(pointX, pointY, mSelectionRadius[index],
+                mPaintSelector[index % 2][SELECTOR_CIRCLE]);
+
+        // Draw the dot if needed
+        if (mSelectionDegrees[index] % 30 != 0) {
+            // We're not on a direct tick
+            canvas.drawCircle(pointX, pointY, (mSelectionRadius[index] * 2 / 7),
+                    mPaintSelector[index % 2][SELECTOR_DOT]);
+        } else {
+            // We're not drawing the dot, so shorten the line to only go as far as the edge of the
+            // selection circle
+            int lineLength = mLineLength[index] - mSelectionRadius[index];
+            pointX = mXCenter + (int) (lineLength * Math.sin(selectionRadians));
+            pointY = mYCenter - (int) (lineLength * Math.cos(selectionRadians));
+        }
+
+        // Draw the line
+        canvas.drawLine(mXCenter, mYCenter, pointX, pointY,
+                mPaintSelector[index % 2][SELECTOR_LINE]);
+    }
+
+    private void drawDebug(Canvas canvas) {
+        // Draw outer numbers circle
+        final float outerRadius = mCircleRadius[HOURS] * mNumbersRadiusMultiplier[HOURS];
+        canvas.drawCircle(mXCenter, mYCenter, outerRadius, mPaintDebug);
+
+        // Draw inner numbers circle
+        final float innerRadius = mCircleRadius[HOURS] * mNumbersRadiusMultiplier[HOURS_INNER];
+        canvas.drawCircle(mXCenter, mYCenter, innerRadius, mPaintDebug);
+
+        // Draw outer background circle
+        canvas.drawCircle(mXCenter, mYCenter, mCircleRadius[HOURS], mPaintDebug);
+
+        // Draw outer rectangle for circles
+        float left = mXCenter - outerRadius;
+        float top = mYCenter - outerRadius;
+        float right = mXCenter + outerRadius;
+        float bottom = mYCenter + outerRadius;
+        mRectF = new RectF(left, top, right, bottom);
+        canvas.drawRect(mRectF, 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);
+
+        // Draw outer view rectangle
+        mRectF.set(0, 0, getWidth(), getHeight());
+        canvas.drawRect(mRectF, mPaintDebug);
+
+        // Draw selected time
+        final String selected = String.format("%02d:%02d", getCurrentHour(), getCurrentMinute());
+
+        ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT);
+        TextView tv = new TextView(getContext());
+        tv.setLayoutParams(lp);
+        tv.setText(selected);
+        tv.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+        Paint paint = tv.getPaint();
+        paint.setColor(DEBUG_TEXT_COLOR);
+
+        final int width = tv.getMeasuredWidth();
+
+        float height = paint.descent() - paint.ascent();
+        float x = mXCenter - width / 2;
+        float y = mYCenter + 1.5f * height;
+
+        canvas.drawText(selected.toString(), x, y, paint);
+    }
+
+    private void calculateGridSizesHours() {
+        // Calculate the text positions
+        float numbersRadius = mCircleRadius[HOURS]
+                * mNumbersRadiusMultiplier[HOURS] * mAnimationRadiusMultiplier[HOURS];
+
+        // Calculate the positions for the 12 numbers in the main circle.
+        calculateGridSizes(mPaint[HOURS], numbersRadius, mXCenter, mYCenter,
+                mTextSize[HOURS], mTextGridHeights[HOURS], mTextGridWidths[HOURS]);
+
+        // If we have an inner circle, calculate those positions too.
+        if (mIs24HourMode) {
+            float innerNumbersRadius = mCircleRadius[HOURS_INNER]
+                    * mNumbersRadiusMultiplier[HOURS_INNER]
+                    * mAnimationRadiusMultiplier[HOURS_INNER];
+
+            calculateGridSizes(mPaint[HOURS], innerNumbersRadius, mXCenter, mYCenter,
+                    mInnerTextSize, mInnerTextGridHeights, mInnerTextGridWidths);
+        }
+    }
+
+    private void calculateGridSizesMinutes() {
+        // Calculate the text positions
+        float numbersRadius = mCircleRadius[MINUTES]
+                * mNumbersRadiusMultiplier[MINUTES] * mAnimationRadiusMultiplier[MINUTES];
+
+        // Calculate the positions for the 12 numbers in the main circle.
+        calculateGridSizes(mPaint[MINUTES], numbersRadius, mXCenter, mYCenter,
+                mTextSize[MINUTES], mTextGridHeights[MINUTES], mTextGridWidths[MINUTES]);
+    }
+
+
+    /**
+     * Using the trigonometric Unit Circle, calculate the positions that the text will need to be
+     * drawn at based on the specified circle radius. Place the values in the textGridHeights and
+     * textGridWidths parameters.
+     */
+    private static void calculateGridSizes(Paint paint, float numbersRadius, float xCenter,
+            float yCenter, float textSize, float[] textGridHeights, float[] textGridWidths) {
+        /*
+         * The numbers need to be drawn in a 7x7 grid, representing the points on the Unit Circle.
+         */
+        final float offset1 = numbersRadius;
+        // cos(30) = a / r => r * cos(30)
+        final float offset2 = numbersRadius * COSINE_30_DEGREES;
+        // sin(30) = o / r => r * sin(30)
+        final float offset3 = numbersRadius * SINE_30_DEGREES;
+
+        paint.setTextSize(textSize);
+        // We'll need yTextBase to be slightly lower to account for the text's baseline.
+        yCenter -= (paint.descent() + paint.ascent()) / 2;
+
+        textGridHeights[0] = yCenter - offset1;
+        textGridWidths[0] = xCenter - offset1;
+        textGridHeights[1] = yCenter - offset2;
+        textGridWidths[1] = xCenter - offset2;
+        textGridHeights[2] = yCenter - offset3;
+        textGridWidths[2] = xCenter - offset3;
+        textGridHeights[3] = yCenter;
+        textGridWidths[3] = xCenter;
+        textGridHeights[4] = yCenter + offset3;
+        textGridWidths[4] = xCenter + offset3;
+        textGridHeights[5] = yCenter + offset2;
+        textGridWidths[5] = xCenter + offset2;
+        textGridHeights[6] = yCenter + offset1;
+        textGridWidths[6] = xCenter + offset1;
+    }
+
+    /**
+     * Draw the 12 text values at the positions specified by the textGrid parameters.
+     */
+    private void drawTextElements(Canvas canvas, float textSize, Typeface typeface, String[] texts,
+            float[] textGridWidths, float[] textGridHeights, Paint paint) {
+        paint.setTextSize(textSize);
+        paint.setTypeface(typeface);
+        canvas.drawText(texts[0], textGridWidths[3], textGridHeights[0], paint);
+        canvas.drawText(texts[1], textGridWidths[4], textGridHeights[1], paint);
+        canvas.drawText(texts[2], textGridWidths[5], textGridHeights[2], paint);
+        canvas.drawText(texts[3], textGridWidths[6], textGridHeights[3], paint);
+        canvas.drawText(texts[4], textGridWidths[5], textGridHeights[4], paint);
+        canvas.drawText(texts[5], textGridWidths[4], textGridHeights[5], paint);
+        canvas.drawText(texts[6], textGridWidths[3], textGridHeights[6], paint);
+        canvas.drawText(texts[7], textGridWidths[2], textGridHeights[5], paint);
+        canvas.drawText(texts[8], textGridWidths[1], textGridHeights[4], paint);
+        canvas.drawText(texts[9], textGridWidths[0], textGridHeights[3], paint);
+        canvas.drawText(texts[10], textGridWidths[1], textGridHeights[2], paint);
+        canvas.drawText(texts[11], textGridWidths[2], textGridHeights[1], paint);
+    }
+
+    // Used for animating the hours by changing their radius
+    private void setAnimationRadiusMultiplierHours(float animationRadiusMultiplier) {
+        mAnimationRadiusMultiplier[HOURS] = animationRadiusMultiplier;
+        mAnimationRadiusMultiplier[HOURS_INNER] = animationRadiusMultiplier;
+    }
+
+    // Used for animating the minutes by changing their radius
+    private void setAnimationRadiusMultiplierMinutes(float animationRadiusMultiplier) {
+        mAnimationRadiusMultiplier[MINUTES] = animationRadiusMultiplier;
+    }
+
+    private static ObjectAnimator getRadiusDisappearAnimator(Object target,
+            String radiusPropertyName, InvalidateUpdateListener updateListener,
+            float midRadiusMultiplier, float endRadiusMultiplier) {
+        Keyframe kf0, kf1, kf2;
+        float midwayPoint = 0.2f;
+        int duration = 500;
+
+        kf0 = Keyframe.ofFloat(0f, 1);
+        kf1 = Keyframe.ofFloat(midwayPoint, midRadiusMultiplier);
+        kf2 = Keyframe.ofFloat(1f, endRadiusMultiplier);
+        PropertyValuesHolder radiusDisappear = PropertyValuesHolder.ofKeyframe(
+                radiusPropertyName, kf0, kf1, kf2);
+
+        ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(
+                target, radiusDisappear).setDuration(duration);
+        animator.addUpdateListener(updateListener);
+        return animator;
+    }
+
+    private static ObjectAnimator getRadiusReappearAnimator(Object target,
+            String radiusPropertyName, InvalidateUpdateListener updateListener,
+            float midRadiusMultiplier, float endRadiusMultiplier) {
+        Keyframe kf0, kf1, kf2, kf3;
+        float midwayPoint = 0.2f;
+        int duration = 500;
+
+        // Set up animator for reappearing.
+        float delayMultiplier = 0.25f;
+        float transitionDurationMultiplier = 1f;
+        float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier;
+        int totalDuration = (int) (duration * totalDurationMultiplier);
+        float delayPoint = (delayMultiplier * duration) / totalDuration;
+        midwayPoint = 1 - (midwayPoint * (1 - delayPoint));
+
+        kf0 = Keyframe.ofFloat(0f, endRadiusMultiplier);
+        kf1 = Keyframe.ofFloat(delayPoint, endRadiusMultiplier);
+        kf2 = Keyframe.ofFloat(midwayPoint, midRadiusMultiplier);
+        kf3 = Keyframe.ofFloat(1f, 1);
+        PropertyValuesHolder radiusReappear = PropertyValuesHolder.ofKeyframe(
+                radiusPropertyName, kf0, kf1, kf2, kf3);
+
+        ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(
+                target, radiusReappear).setDuration(totalDuration);
+        animator.addUpdateListener(updateListener);
+        return animator;
+    }
+
+    private static ObjectAnimator getFadeOutAnimator(Object target, int startAlpha, int endAlpha,
+                InvalidateUpdateListener updateListener) {
+        int duration = 500;
+        ObjectAnimator animator = ObjectAnimator.ofInt(target, "alpha", startAlpha, endAlpha);
+        animator.setDuration(duration);
+        animator.addUpdateListener(updateListener);
+
+        return animator;
+    }
+
+    private static ObjectAnimator getFadeInAnimator(Object target, int startAlpha, int endAlpha,
+                InvalidateUpdateListener updateListener) {
+        Keyframe kf0, kf1, kf2;
+        int duration = 500;
+
+        // Set up animator for reappearing.
+        float delayMultiplier = 0.25f;
+        float transitionDurationMultiplier = 1f;
+        float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier;
+        int totalDuration = (int) (duration * totalDurationMultiplier);
+        float delayPoint = (delayMultiplier * duration) / totalDuration;
+
+        kf0 = Keyframe.ofInt(0f, startAlpha);
+        kf1 = Keyframe.ofInt(delayPoint, startAlpha);
+        kf2 = Keyframe.ofInt(1f, endAlpha);
+        PropertyValuesHolder fadeIn = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1, kf2);
+
+        ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(
+                target, fadeIn).setDuration(totalDuration);
+        animator.addUpdateListener(updateListener);
+        return animator;
+    }
+
+    private class InvalidateUpdateListener implements ValueAnimator.AnimatorUpdateListener {
+        @Override
+        public void onAnimationUpdate(ValueAnimator animation) {
+            RadialTimePickerView.this.invalidate();
+        }
+    }
+
+    private void startHoursToMinutesAnimation() {
+        if (mHoursToMinutesAnims.size() == 0) {
+            mHoursToMinutesAnims.add(getRadiusDisappearAnimator(this,
+                    "animationRadiusMultiplierHours", mInvalidateUpdateListener,
+                    mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier));
+            mHoursToMinutesAnims.add(getFadeOutAnimator(mPaint[HOURS],
+                    ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener));
+            mHoursToMinutesAnims.add(getFadeOutAnimator(mPaintSelector[HOURS][SELECTOR_CIRCLE],
+                    ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener));
+            mHoursToMinutesAnims.add(getFadeOutAnimator(mPaintSelector[HOURS][SELECTOR_DOT],
+                    ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener));
+            mHoursToMinutesAnims.add(getFadeOutAnimator(mPaintSelector[HOURS][SELECTOR_LINE],
+                    ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener));
+
+            mHoursToMinutesAnims.add(getRadiusReappearAnimator(this,
+                    "animationRadiusMultiplierMinutes", mInvalidateUpdateListener,
+                    mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier));
+            mHoursToMinutesAnims.add(getFadeInAnimator(mPaint[MINUTES],
+                    ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener));
+            mHoursToMinutesAnims.add(getFadeInAnimator(mPaintSelector[MINUTES][SELECTOR_CIRCLE],
+                    ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener));
+            mHoursToMinutesAnims.add(getFadeInAnimator(mPaintSelector[MINUTES][SELECTOR_DOT],
+                    ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener));
+            mHoursToMinutesAnims.add(getFadeInAnimator(mPaintSelector[MINUTES][SELECTOR_LINE],
+                    ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener));
+        }
+
+        if (mTransition != null && mTransition.isRunning()) {
+            mTransition.end();
+        }
+        mTransition = new AnimatorSet();
+        mTransition.playTogether(mHoursToMinutesAnims);
+        mTransition.start();
+    }
+
+    private void startMinutesToHoursAnimation() {
+        if (mMinuteToHoursAnims.size() == 0) {
+            mMinuteToHoursAnims.add(getRadiusDisappearAnimator(this,
+                    "animationRadiusMultiplierMinutes", mInvalidateUpdateListener,
+                    mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier));
+            mMinuteToHoursAnims.add(getFadeOutAnimator(mPaint[MINUTES],
+                    ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener));
+            mMinuteToHoursAnims.add(getFadeOutAnimator(mPaintSelector[MINUTES][SELECTOR_CIRCLE],
+                    ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener));
+            mMinuteToHoursAnims.add(getFadeOutAnimator(mPaintSelector[MINUTES][SELECTOR_DOT],
+                    ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener));
+            mMinuteToHoursAnims.add(getFadeOutAnimator(mPaintSelector[MINUTES][SELECTOR_LINE],
+                    ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener));
+
+            mMinuteToHoursAnims.add(getRadiusReappearAnimator(this,
+                    "animationRadiusMultiplierHours", mInvalidateUpdateListener,
+                    mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier));
+            mMinuteToHoursAnims.add(getFadeInAnimator(mPaint[HOURS],
+                    ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener));
+            mMinuteToHoursAnims.add(getFadeInAnimator(mPaintSelector[HOURS][SELECTOR_CIRCLE],
+                    ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener));
+            mMinuteToHoursAnims.add(getFadeInAnimator(mPaintSelector[HOURS][SELECTOR_DOT],
+                    ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener));
+            mMinuteToHoursAnims.add(getFadeInAnimator(mPaintSelector[HOURS][SELECTOR_LINE],
+                    ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener));
+        }
+
+        if (mTransition != null && mTransition.isRunning()) {
+            mTransition.end();
+        }
+        mTransition = new AnimatorSet();
+        mTransition.playTogether(mMinuteToHoursAnims);
+        mTransition.start();
+    }
+
+    private int getDegreesFromXY(float x, float y) {
+        final double hypotenuse = Math.sqrt(
+                (y - mYCenter) * (y - mYCenter) + (x - mXCenter) * (x - mXCenter));
+
+        // Basic check if we're outside the range of the disk
+        if (hypotenuse > mCircleRadius[HOURS]) {
+            return -1;
+        }
+        // Check
+        if (mIs24HourMode && mShowHours) {
+            if (hypotenuse >= mMinHypotenuseForInnerNumber
+                    && hypotenuse <= mHalfwayHypotenusePoint) {
+                mIsOnInnerCircle = true;
+            } else if (hypotenuse <= mMaxHypotenuseForOuterNumber
+                    && hypotenuse >= mHalfwayHypotenusePoint) {
+                mIsOnInnerCircle = false;
+            } else {
+                return -1;
+            }
+        } else {
+            final int index =  (mShowHours) ? HOURS : MINUTES;
+            final float length = (mCircleRadius[index] * mNumbersRadiusMultiplier[index]);
+            final int distanceToNumber = (int) Math.abs(hypotenuse - length);
+            final int maxAllowedDistance =
+                    (int) (mCircleRadius[index] * (1 - mNumbersRadiusMultiplier[index]));
+            if (distanceToNumber > maxAllowedDistance) {
+                return -1;
+            }
+        }
+
+        final float opposite = Math.abs(y - mYCenter);
+        double degrees = Math.toDegrees(Math.asin(opposite / hypotenuse));
+
+        // 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;
+        }
+        return (int) degrees;
+    }
+
+    private int getIsTouchingAmOrPm(float x, float y) {
+        final boolean isLayoutRtl = isLayoutRtl();
+        int squaredYDistance = (int) ((y - mAmPmYCenter) * (y - mAmPmYCenter));
+
+        int distanceToAmCenter = (int) Math.sqrt(
+                (x - mLeftIndicatorXCenter) * (x - mLeftIndicatorXCenter) + squaredYDistance);
+        if (distanceToAmCenter <= mAmPmCircleRadius) {
+            return (isLayoutRtl ? PM : AM);
+        }
+
+        int distanceToPmCenter = (int) Math.sqrt(
+                (x - mRightIndicatorXCenter) * (x - mRightIndicatorXCenter) + squaredYDistance);
+        if (distanceToPmCenter <= mAmPmCircleRadius) {
+            return (isLayoutRtl ? AM : PM);
+        }
+
+        // Neither was close enough.
+        return -1;
+    }
+
+    @Override
+    public boolean onTouch(View v, MotionEvent event) {
+        if(!mInputEnabled) {
+            return true;
+        }
+
+        final float eventX = event.getX();
+        final float eventY = event.getY();
+
+        int degrees;
+        int snapDegrees;
+        boolean result = false;
+
+        switch(event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+            case MotionEvent.ACTION_MOVE:
+                mAmOrPmPressed = getIsTouchingAmOrPm(eventX, eventY);
+                if (mAmOrPmPressed != -1) {
+                    result = true;
+                } else {
+                    degrees = getDegreesFromXY(eventX, eventY);
+                    if (degrees != -1) {
+                        snapDegrees = (mShowHours ?
+                                snapOnly30s(degrees, 0) : snapPrefer30s(degrees)) % 360;
+                        if (mShowHours) {
+                            mSelectionDegrees[HOURS] = snapDegrees;
+                            mSelectionDegrees[HOURS_INNER] = snapDegrees;
+                        } else {
+                            mSelectionDegrees[MINUTES] = snapDegrees;
+                        }
+                        performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
+                        if (mListener != null) {
+                            if (mShowHours) {
+                                mListener.onValueSelected(HOURS, getCurrentHour(), false);
+                            } else  {
+                                mListener.onValueSelected(MINUTES, getCurrentMinute(), false);
+                            }
+                        }
+                        result = true;
+                    }
+                }
+                invalidate();
+                return result;
+
+            case MotionEvent.ACTION_UP:
+                mAmOrPmPressed = getIsTouchingAmOrPm(eventX, eventY);
+                if (mAmOrPmPressed != -1) {
+                    if (mAmOrPm != mAmOrPmPressed) {
+                        swapAmPm();
+                    }
+                    mAmOrPmPressed = -1;
+                    if (mListener != null) {
+                        mListener.onValueSelected(AMPM, getCurrentHour(), true);
+                    }
+                    result = true;
+                } else {
+                    degrees = getDegreesFromXY(eventX, eventY);
+                    if (degrees != -1) {
+                        snapDegrees = (mShowHours ?
+                                snapOnly30s(degrees, 0) : snapPrefer30s(degrees)) % 360;
+                        if (mShowHours) {
+                            mSelectionDegrees[HOURS] = snapDegrees;
+                            mSelectionDegrees[HOURS_INNER] = snapDegrees;
+                        } else {
+                            mSelectionDegrees[MINUTES] = snapDegrees;
+                        }
+                        if (mListener != null) {
+                            if (mShowHours) {
+                                mListener.onValueSelected(HOURS, getCurrentHour(), true);
+                            } else  {
+                                mListener.onValueSelected(MINUTES, getCurrentMinute(), true);
+                            }
+                        }
+                        result = true;
+                    }
+                }
+                if (result) {
+                    invalidate();
+                }
+                return result;
+
+            default:
+                break;
+        }
+        return false;
+    }
+
+    /**
+     * 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.ACTION_SCROLL_FORWARD);
+        info.addAction(AccessibilityNodeInfo.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);
+            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.
+     */
+    @SuppressLint("NewApi")
+    @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 = 0;
+            int stepSize = 0;
+            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;
+            int maxValue = 0;
+            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;
+    }
+
+    public void setInputEnabled(boolean inputEnabled) {
+        mInputEnabled = inputEnabled;
+        invalidate();
+    }
+}
diff --git a/core/java/android/widget/TimePicker.java b/core/java/android/widget/TimePicker.java
index 7926ab0..485fecf 100644
--- a/core/java/android/widget/TimePicker.java
+++ b/core/java/android/widget/TimePicker.java
@@ -17,29 +17,22 @@
 package android.widget;
 
 import android.annotation.Widget;
+import android.app.Dialog;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.content.res.TypedArray;
-import android.os.Parcel;
 import android.os.Parcelable;
-import android.text.format.DateFormat;
-import android.text.format.DateUtils;
 import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
+import android.view.KeyEvent;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityNodeInfo;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.NumberPicker.OnValueChangeListener;
 
 import com.android.internal.R;
 
-import java.text.DateFormatSymbols;
-import java.util.Calendar;
 import java.util.Locale;
 
+import static android.os.Build.VERSION_CODES.KITKAT;
+
 /**
  * A view for selecting the time of day, in either 24 hour or AM/PM mode. The
  * hour, each minute digit, and AM/PM (if applicable) can be conrolled by
@@ -59,6 +52,11 @@
 
     private TimePickerDelegate mDelegate;
 
+    private AttributeSet mAttrs;
+    private int mDefStyleAttr;
+    private int mDefStyleRes;
+    private Context mContext;
+
     /**
      * The callback interface used to indicate the time has been adjusted.
      */
@@ -86,7 +84,45 @@
 
     public TimePicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
-        mDelegate = new LegacyTimePickerDelegate(this, context, attrs, defStyleAttr, defStyleRes);
+
+        mContext = context;
+        mAttrs = attrs;
+        mDefStyleAttr = defStyleAttr;
+        mDefStyleRes = defStyleRes;
+
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TimePicker,
+                mDefStyleAttr, mDefStyleRes);
+
+        // Create the correct UI delegate. Default is the legacy one.
+        final boolean isLegacyMode = shouldForceLegacyMode() ?
+                true : a.getBoolean(R.styleable.TimePicker_legacyMode, true);
+        setLegacyMode(isLegacyMode);
+    }
+
+    private boolean shouldForceLegacyMode() {
+        final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
+        return targetSdkVersion < KITKAT;
+    }
+
+    private TimePickerDelegate createLegacyUIDelegate(Context context, AttributeSet attrs,
+            int defStyleAttr, int defStyleRes) {
+        return new LegacyTimePickerDelegate(this, context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    private TimePickerDelegate createNewUIDelegate(Context context, AttributeSet attrs,
+            int defStyleAttr, int defStyleRes) {
+        return new android.widget.TimePickerDelegate(this, context, attrs, defStyleAttr,
+                defStyleRes);
+    }
+
+    /**
+     * @hide
+     */
+    public void setLegacyMode(boolean isLegacyMode) {
+        removeAllViewsInLayout();
+        mDelegate = isLegacyMode ?
+                createLegacyUIDelegate(mContext, mAttrs, mDefStyleAttr, mDefStyleRes) :
+                createNewUIDelegate(mContext, mAttrs, mDefStyleAttr, mDefStyleRes);
     }
 
     /**
@@ -156,6 +192,20 @@
         return mDelegate.isEnabled();
     }
 
+    /**
+     * @hide
+     */
+    public void setShowDoneButton(boolean showDoneButton) {
+        mDelegate.setShowDoneButton(showDoneButton);
+    }
+
+    /**
+     * @hide
+     */
+    public void setDismissCallback(TimePickerDismissCallback callback) {
+        mDelegate.setDismissCallback(callback);
+    }
+
     @Override
     public int getBaseline() {
         return mDelegate.getBaseline();
@@ -175,7 +225,7 @@
 
     @Override
     protected void onRestoreInstanceState(Parcelable state) {
-        SavedState ss = (SavedState) state;
+        BaseSavedState ss = (BaseSavedState) state;
         super.onRestoreInstanceState(ss.getSuperState());
         mDelegate.onRestoreInstanceState(ss);
     }
@@ -208,7 +258,7 @@
      * TimePicker implementations. This would need to be implemented by the TimePicker delegates
      * for the real behavior.
      */
-    private interface TimePickerDelegate {
+    interface TimePickerDelegate {
         void setCurrentHour(Integer currentHour);
         Integer getCurrentHour();
 
@@ -223,6 +273,9 @@
         void setEnabled(boolean enabled);
         boolean isEnabled();
 
+        void setShowDoneButton(boolean showDoneButton);
+        void setDismissCallback(TimePickerDismissCallback callback);
+
         int getBaseline();
 
         void onConfigurationChanged(Configuration newConfig);
@@ -237,605 +290,43 @@
     }
 
     /**
-     * A delegate implementing the basic TimePicker
+     * A callback interface for dismissing the TimePicker when included into a Dialog
+     *
+     * @hide
      */
-    private static class LegacyTimePickerDelegate implements TimePickerDelegate {
-        // the delegator
-        private TimePicker mDelegator;
+    public static interface TimePickerDismissCallback {
+        void dismiss(TimePicker view, boolean isCancel, int hourOfDay, int minute);
+    }
 
-        private static final boolean DEFAULT_ENABLED_STATE = true;
+    /**
+     * An abstract class which can be used as a start for TimePicker implementations
+     */
+    abstract static class AbstractTimePickerDelegate implements TimePickerDelegate {
+        // The delegator
+        protected TimePicker mDelegator;
 
-        private static final int HOURS_IN_HALF_DAY = 12;
+        // The context
+        protected Context mContext;
 
-        // state
-        private boolean mIs24HourView;
+        // The current locale
+        protected Locale mCurrentLocale;
 
-        private boolean mIsAm;
+        // Callbacks
+        protected  OnTimeChangedListener mOnTimeChangedListener;
 
-        // ui components
-        private final NumberPicker mHourSpinner;
-
-        private final NumberPicker mMinuteSpinner;
-
-        private final NumberPicker mAmPmSpinner;
-
-        private final EditText mHourSpinnerInput;
-
-        private final EditText mMinuteSpinnerInput;
-
-        private final EditText mAmPmSpinnerInput;
-
-        private final TextView mDivider;
-
-        // Note that the legacy implementation of the TimePicker is
-        // using a button for toggling between AM/PM while the new
-        // version uses a NumberPicker spinner. Therefore the code
-        // accommodates these two cases to be backwards compatible.
-        private final Button mAmPmButton;
-
-        private final String[] mAmPmStrings;
-
-        private boolean mIsEnabled = DEFAULT_ENABLED_STATE;
-
-        // callbacks
-        private OnTimeChangedListener mOnTimeChangedListener;
-
-        private Calendar mTempCalendar;
-
-        private Locale mCurrentLocale;
-
-        private boolean mHourWithTwoDigit;
-        private char mHourFormat;
-
-        /**
-         * A no-op callback used in the constructor to avoid null checks later in
-         * the code.
-         */
-        private static final OnTimeChangedListener NO_OP_CHANGE_LISTENER =
-                new OnTimeChangedListener() {
-                    public void onTimeChanged(TimePicker view, int hourOfDay, int minute) {
-                }
-        };
-
-        public LegacyTimePickerDelegate(TimePicker delegator, Context context, AttributeSet attrs,
-                int defStyleAttr, int defStyleRes) {
+        public AbstractTimePickerDelegate(TimePicker delegator, Context context) {
             mDelegator = delegator;
+            mContext = context;
 
             // initialization based on locale
             setCurrentLocale(Locale.getDefault());
-
-            // process style attributes
-            final TypedArray attributesArray = context.obtainStyledAttributes(
-                    attrs, R.styleable.TimePicker, defStyleAttr, defStyleRes);
-            int layoutResourceId = attributesArray.getResourceId(
-                    R.styleable.TimePicker_internalLayout, R.layout.time_picker);
-            attributesArray.recycle();
-
-            LayoutInflater inflater = (LayoutInflater) context.getSystemService(
-                    Context.LAYOUT_INFLATER_SERVICE);
-            inflater.inflate(layoutResourceId, mDelegator, true);
-
-            // hour
-            mHourSpinner = (NumberPicker) delegator.findViewById(R.id.hour);
-            mHourSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
-                public void onValueChange(NumberPicker spinner, int oldVal, int newVal) {
-                    updateInputState();
-                    if (!is24HourView()) {
-                        if ((oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) ||
-                                (oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1)) {
-                            mIsAm = !mIsAm;
-                            updateAmPmControl();
-                        }
-                    }
-                    onTimeChanged();
-                }
-            });
-            mHourSpinnerInput = (EditText) mHourSpinner.findViewById(R.id.numberpicker_input);
-            mHourSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
-
-            // divider (only for the new widget style)
-            mDivider = (TextView) mDelegator.findViewById(R.id.divider);
-            if (mDivider != null) {
-                setDividerText();
-            }
-
-            // minute
-            mMinuteSpinner = (NumberPicker) mDelegator.findViewById(R.id.minute);
-            mMinuteSpinner.setMinValue(0);
-            mMinuteSpinner.setMaxValue(59);
-            mMinuteSpinner.setOnLongPressUpdateInterval(100);
-            mMinuteSpinner.setFormatter(NumberPicker.getTwoDigitFormatter());
-            mMinuteSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
-                public void onValueChange(NumberPicker spinner, int oldVal, int newVal) {
-                    updateInputState();
-                    int minValue = mMinuteSpinner.getMinValue();
-                    int maxValue = mMinuteSpinner.getMaxValue();
-                    if (oldVal == maxValue && newVal == minValue) {
-                        int newHour = mHourSpinner.getValue() + 1;
-                        if (!is24HourView() && newHour == HOURS_IN_HALF_DAY) {
-                            mIsAm = !mIsAm;
-                            updateAmPmControl();
-                        }
-                        mHourSpinner.setValue(newHour);
-                    } else if (oldVal == minValue && newVal == maxValue) {
-                        int newHour = mHourSpinner.getValue() - 1;
-                        if (!is24HourView() && newHour == HOURS_IN_HALF_DAY - 1) {
-                            mIsAm = !mIsAm;
-                            updateAmPmControl();
-                        }
-                        mHourSpinner.setValue(newHour);
-                    }
-                    onTimeChanged();
-                }
-            });
-            mMinuteSpinnerInput = (EditText) mMinuteSpinner.findViewById(R.id.numberpicker_input);
-            mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
-
-            /* Get the localized am/pm strings and use them in the spinner */
-            mAmPmStrings = new DateFormatSymbols().getAmPmStrings();
-
-            // am/pm
-            View amPmView = mDelegator.findViewById(R.id.amPm);
-            if (amPmView instanceof Button) {
-                mAmPmSpinner = null;
-                mAmPmSpinnerInput = null;
-                mAmPmButton = (Button) amPmView;
-                mAmPmButton.setOnClickListener(new OnClickListener() {
-                    public void onClick(View button) {
-                        button.requestFocus();
-                        mIsAm = !mIsAm;
-                        updateAmPmControl();
-                        onTimeChanged();
-                    }
-                });
-            } else {
-                mAmPmButton = null;
-                mAmPmSpinner = (NumberPicker) amPmView;
-                mAmPmSpinner.setMinValue(0);
-                mAmPmSpinner.setMaxValue(1);
-                mAmPmSpinner.setDisplayedValues(mAmPmStrings);
-                mAmPmSpinner.setOnValueChangedListener(new OnValueChangeListener() {
-                    public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
-                        updateInputState();
-                        picker.requestFocus();
-                        mIsAm = !mIsAm;
-                        updateAmPmControl();
-                        onTimeChanged();
-                    }
-                });
-                mAmPmSpinnerInput = (EditText) mAmPmSpinner.findViewById(R.id.numberpicker_input);
-                mAmPmSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE);
-            }
-
-            if (isAmPmAtStart()) {
-                // Move the am/pm view to the beginning
-                ViewGroup amPmParent = (ViewGroup) delegator.findViewById(R.id.timePickerLayout);
-                amPmParent.removeView(amPmView);
-                amPmParent.addView(amPmView, 0);
-                // Swap layout margins if needed. They may be not symmetrical (Old Standard Theme
-                // for example and not for Holo Theme)
-                ViewGroup.MarginLayoutParams lp =
-                        (ViewGroup.MarginLayoutParams) amPmView.getLayoutParams();
-                final int startMargin = lp.getMarginStart();
-                final int endMargin = lp.getMarginEnd();
-                if (startMargin != endMargin) {
-                    lp.setMarginStart(endMargin);
-                    lp.setMarginEnd(startMargin);
-                }
-            }
-
-            getHourFormatData();
-
-            // update controls to initial state
-            updateHourControl();
-            updateMinuteControl();
-            updateAmPmControl();
-
-            setOnTimeChangedListener(NO_OP_CHANGE_LISTENER);
-
-            // set to current time
-            setCurrentHour(mTempCalendar.get(Calendar.HOUR_OF_DAY));
-            setCurrentMinute(mTempCalendar.get(Calendar.MINUTE));
-
-            if (!isEnabled()) {
-                setEnabled(false);
-            }
-
-            // set the content descriptions
-            setContentDescriptions();
-
-            // If not explicitly specified this view is important for accessibility.
-            if (mDelegator.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
-                mDelegator.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
-            }
         }
 
-        private void getHourFormatData() {
-            final Locale defaultLocale = Locale.getDefault();
-            final String bestDateTimePattern = DateFormat.getBestDateTimePattern(defaultLocale,
-                    (mIs24HourView) ? "Hm" : "hm");
-            final int lengthPattern = bestDateTimePattern.length();
-            mHourWithTwoDigit = false;
-            char hourFormat = '\0';
-            // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save
-            // the hour format that we found.
-            for (int i = 0; i < lengthPattern; i++) {
-                final char c = bestDateTimePattern.charAt(i);
-                if (c == 'H' || c == 'h' || c == 'K' || c == 'k') {
-                    mHourFormat = c;
-                    if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) {
-                        mHourWithTwoDigit = true;
-                    }
-                    break;
-                }
-            }
-        }
-
-        private boolean isAmPmAtStart() {
-            final Locale defaultLocale = Locale.getDefault();
-            final String bestDateTimePattern = DateFormat.getBestDateTimePattern(defaultLocale,
-                    "hm" /* skeleton */);
-
-            return bestDateTimePattern.startsWith("a");
-        }
-
-        /**
-         * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":".
-         *
-         * See http://unicode.org/cldr/trac/browser/trunk/common/main
-         *
-         * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the
-         * separator as the character which is just after the hour marker in the returned pattern.
-         */
-        private void setDividerText() {
-            final Locale defaultLocale = Locale.getDefault();
-            final String skeleton = (mIs24HourView) ? "Hm" : "hm";
-            final String bestDateTimePattern = DateFormat.getBestDateTimePattern(defaultLocale,
-                    skeleton);
-            final String separatorText;
-            int hourIndex = bestDateTimePattern.lastIndexOf('H');
-            if (hourIndex == -1) {
-                hourIndex = bestDateTimePattern.lastIndexOf('h');
-            }
-            if (hourIndex == -1) {
-                // Default case
-                separatorText = ":";
-            } else {
-                int minuteIndex = bestDateTimePattern.indexOf('m', hourIndex + 1);
-                if  (minuteIndex == -1) {
-                    separatorText = Character.toString(bestDateTimePattern.charAt(hourIndex + 1));
-                } else {
-                    separatorText = bestDateTimePattern.substring(hourIndex + 1, minuteIndex);
-                }
-            }
-            mDivider.setText(separatorText);
-        }
-
-        @Override
-        public void setCurrentHour(Integer currentHour) {
-            setCurrentHour(currentHour, true);
-        }
-
-        private void setCurrentHour(Integer currentHour, boolean notifyTimeChanged) {
-            // why was Integer used in the first place?
-            if (currentHour == null || currentHour == getCurrentHour()) {
-                return;
-            }
-            if (!is24HourView()) {
-                // convert [0,23] ordinal to wall clock display
-                if (currentHour >= HOURS_IN_HALF_DAY) {
-                    mIsAm = false;
-                    if (currentHour > HOURS_IN_HALF_DAY) {
-                        currentHour = currentHour - HOURS_IN_HALF_DAY;
-                    }
-                } else {
-                    mIsAm = true;
-                    if (currentHour == 0) {
-                        currentHour = HOURS_IN_HALF_DAY;
-                    }
-                }
-                updateAmPmControl();
-            }
-            mHourSpinner.setValue(currentHour);
-            if (notifyTimeChanged) {
-                onTimeChanged();
-            }
-        }
-
-        @Override
-        public Integer getCurrentHour() {
-            int currentHour = mHourSpinner.getValue();
-            if (is24HourView()) {
-                return currentHour;
-            } else if (mIsAm) {
-                return currentHour % HOURS_IN_HALF_DAY;
-            } else {
-                return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY;
-            }
-        }
-
-        @Override
-        public void setCurrentMinute(Integer currentMinute) {
-            if (currentMinute == getCurrentMinute()) {
-                return;
-            }
-            mMinuteSpinner.setValue(currentMinute);
-            onTimeChanged();
-        }
-
-        @Override
-        public Integer getCurrentMinute() {
-            return mMinuteSpinner.getValue();
-        }
-
-        @Override
-        public void setIs24HourView(Boolean is24HourView) {
-            if (mIs24HourView == is24HourView) {
-                return;
-            }
-            // cache the current hour since spinner range changes and BEFORE changing mIs24HourView!!
-            int currentHour = getCurrentHour();
-            // Order is important here.
-            mIs24HourView = is24HourView;
-            getHourFormatData();
-            updateHourControl();
-            // set value after spinner range is updated - be aware that because mIs24HourView has
-            // changed then getCurrentHour() is not equal to the currentHour we cached before so
-            // explicitly ask for *not* propagating any onTimeChanged()
-            setCurrentHour(currentHour, false /* no onTimeChanged() */);
-            updateMinuteControl();
-            updateAmPmControl();
-        }
-
-        @Override
-        public boolean is24HourView() {
-            return mIs24HourView;
-        }
-
-        @Override
-        public void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener) {
-            mOnTimeChangedListener = onTimeChangedListener;
-        }
-
-        @Override
-        public void setEnabled(boolean enabled) {
-            mMinuteSpinner.setEnabled(enabled);
-            if (mDivider != null) {
-                mDivider.setEnabled(enabled);
-            }
-            mHourSpinner.setEnabled(enabled);
-            if (mAmPmSpinner != null) {
-                mAmPmSpinner.setEnabled(enabled);
-            } else {
-                mAmPmButton.setEnabled(enabled);
-            }
-            mIsEnabled = enabled;
-        }
-
-        @Override
-        public boolean isEnabled() {
-            return mIsEnabled;
-        }
-
-        @Override
-        public int getBaseline() {
-            return mHourSpinner.getBaseline();
-        }
-
-        @Override
-        public void onConfigurationChanged(Configuration newConfig) {
-            setCurrentLocale(newConfig.locale);
-        }
-
-        @Override
-        public Parcelable onSaveInstanceState(Parcelable superState) {
-            return new SavedState(superState, getCurrentHour(), getCurrentMinute());
-        }
-
-        @Override
-        public void onRestoreInstanceState(Parcelable state) {
-            SavedState ss = (SavedState) state;
-            setCurrentHour(ss.getHour());
-            setCurrentMinute(ss.getMinute());
-        }
-
-        @Override
-        public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
-            onPopulateAccessibilityEvent(event);
-            return true;
-        }
-
-        @Override
-        public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
-            int flags = DateUtils.FORMAT_SHOW_TIME;
-            if (mIs24HourView) {
-                flags |= DateUtils.FORMAT_24HOUR;
-            } else {
-                flags |= DateUtils.FORMAT_12HOUR;
-            }
-            mTempCalendar.set(Calendar.HOUR_OF_DAY, getCurrentHour());
-            mTempCalendar.set(Calendar.MINUTE, getCurrentMinute());
-            String selectedDateUtterance = DateUtils.formatDateTime(mDelegator.getContext(),
-                    mTempCalendar.getTimeInMillis(), flags);
-            event.getText().add(selectedDateUtterance);
-        }
-
-        @Override
-        public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
-            event.setClassName(TimePicker.class.getName());
-        }
-
-        @Override
-        public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
-            info.setClassName(TimePicker.class.getName());
-        }
-
-        private void updateInputState() {
-            // Make sure that if the user changes the value and the IME is active
-            // for one of the inputs if this widget, the IME is closed. If the user
-            // changed the value via the IME and there is a next input the IME will
-            // be shown, otherwise the user chose another means of changing the
-            // value and having the IME up makes no sense.
-            InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
-            if (inputMethodManager != null) {
-                if (inputMethodManager.isActive(mHourSpinnerInput)) {
-                    mHourSpinnerInput.clearFocus();
-                    inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
-                } else if (inputMethodManager.isActive(mMinuteSpinnerInput)) {
-                    mMinuteSpinnerInput.clearFocus();
-                    inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
-                } else if (inputMethodManager.isActive(mAmPmSpinnerInput)) {
-                    mAmPmSpinnerInput.clearFocus();
-                    inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
-                }
-            }
-    }
-
-        private void updateAmPmControl() {
-            if (is24HourView()) {
-                if (mAmPmSpinner != null) {
-                    mAmPmSpinner.setVisibility(View.GONE);
-                } else {
-                    mAmPmButton.setVisibility(View.GONE);
-                }
-            } else {
-                int index = mIsAm ? Calendar.AM : Calendar.PM;
-                if (mAmPmSpinner != null) {
-                    mAmPmSpinner.setValue(index);
-                    mAmPmSpinner.setVisibility(View.VISIBLE);
-                } else {
-                    mAmPmButton.setText(mAmPmStrings[index]);
-                    mAmPmButton.setVisibility(View.VISIBLE);
-                }
-            }
-            mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
-        }
-
-        /**
-         * Sets the current locale.
-         *
-         * @param locale The current locale.
-         */
-        private void setCurrentLocale(Locale locale) {
+        public void setCurrentLocale(Locale locale) {
             if (locale.equals(mCurrentLocale)) {
                 return;
             }
             mCurrentLocale = locale;
-            mTempCalendar = Calendar.getInstance(locale);
         }
-
-        private void onTimeChanged() {
-            mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
-            if (mOnTimeChangedListener != null) {
-                mOnTimeChangedListener.onTimeChanged(mDelegator, getCurrentHour(),
-                        getCurrentMinute());
-            }
-        }
-
-        private void updateHourControl() {
-            if (is24HourView()) {
-                // 'k' means 1-24 hour
-                if (mHourFormat == 'k') {
-                    mHourSpinner.setMinValue(1);
-                    mHourSpinner.setMaxValue(24);
-                } else {
-                    mHourSpinner.setMinValue(0);
-                    mHourSpinner.setMaxValue(23);
-                }
-            } else {
-                // 'K' means 0-11 hour
-                if (mHourFormat == 'K') {
-                    mHourSpinner.setMinValue(0);
-                    mHourSpinner.setMaxValue(11);
-                } else {
-                    mHourSpinner.setMinValue(1);
-                    mHourSpinner.setMaxValue(12);
-                }
-            }
-            mHourSpinner.setFormatter(mHourWithTwoDigit ? NumberPicker.getTwoDigitFormatter() : null);
-        }
-
-        private void updateMinuteControl() {
-            if (is24HourView()) {
-                mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE);
-            } else {
-                mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
-            }
-        }
-
-        private void setContentDescriptions() {
-            // Minute
-            trySetContentDescription(mMinuteSpinner, R.id.increment,
-                    R.string.time_picker_increment_minute_button);
-            trySetContentDescription(mMinuteSpinner, R.id.decrement,
-                    R.string.time_picker_decrement_minute_button);
-            // Hour
-            trySetContentDescription(mHourSpinner, R.id.increment,
-                    R.string.time_picker_increment_hour_button);
-            trySetContentDescription(mHourSpinner, R.id.decrement,
-                    R.string.time_picker_decrement_hour_button);
-            // AM/PM
-            if (mAmPmSpinner != null) {
-                trySetContentDescription(mAmPmSpinner, R.id.increment,
-                        R.string.time_picker_increment_set_pm_button);
-                trySetContentDescription(mAmPmSpinner, R.id.decrement,
-                        R.string.time_picker_decrement_set_am_button);
-            }
-        }
-
-        private void trySetContentDescription(View root, int viewId, int contDescResId) {
-            View target = root.findViewById(viewId);
-            if (target != null) {
-                target.setContentDescription(mDelegator.getContext().getString(contDescResId));
-            }
-        }
-    }
-
-    /**
-     * Used to save / restore state of time picker
-     */
-    private static class SavedState extends BaseSavedState {
-
-        private final int mHour;
-
-        private final int mMinute;
-
-        private SavedState(Parcelable superState, int hour, int minute) {
-            super(superState);
-            mHour = hour;
-            mMinute = minute;
-        }
-
-        private SavedState(Parcel in) {
-            super(in);
-            mHour = in.readInt();
-            mMinute = in.readInt();
-        }
-
-        public int getHour() {
-            return mHour;
-        }
-
-        public int getMinute() {
-            return mMinute;
-        }
-
-        @Override
-        public void writeToParcel(Parcel dest, int flags) {
-            super.writeToParcel(dest, flags);
-            dest.writeInt(mHour);
-            dest.writeInt(mMinute);
-        }
-
-        @SuppressWarnings({"unused", "hiding"})
-        public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() {
-            public SavedState createFromParcel(Parcel in) {
-                return new SavedState(in);
-            }
-
-            public SavedState[] newArray(int size) {
-                return new SavedState[size];
-            }
-        };
     }
 }
diff --git a/core/java/android/widget/TimePickerDelegate.java b/core/java/android/widget/TimePickerDelegate.java
new file mode 100644
index 0000000..182d370
--- /dev/null
+++ b/core/java/android/widget/TimePickerDelegate.java
@@ -0,0 +1,1380 @@
+/*
+ * Copyright (C) 2013 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.widget;
+
+import android.animation.Keyframe;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.text.format.DateFormat;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.HapticFeedbackConstants;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.android.internal.R;
+
+import java.text.DateFormatSymbols;
+import java.util.ArrayList;
+import java.util.Calendar;
+
+/**
+ * A view for selecting the time of day, in either 24 hour or AM/PM mode.
+ */
+class TimePickerDelegate extends TimePicker.AbstractTimePickerDelegate implements
+        RadialTimePickerView.OnValueSelectedListener {
+
+    private static final String TAG = "TimePickerDelegate";
+
+    // Index used by RadialPickerLayout
+    private static final int HOUR_INDEX = 0;
+    private static final int MINUTE_INDEX = 1;
+
+    // NOT a real index for the purpose of what's showing.
+    private static final int AMPM_INDEX = 2;
+
+    // Also NOT a real index, just used for keyboard mode.
+    private static final int ENABLE_PICKER_INDEX = 3;
+
+    private static final int AM = 0;
+    private static final int PM = 1;
+
+    private static final boolean DEFAULT_ENABLED_STATE = true;
+    private boolean mIsEnabled = DEFAULT_ENABLED_STATE;
+
+    private static final int HOURS_IN_HALF_DAY = 12;
+
+    // Delay in ms before starting the pulse animation
+    private static final int PULSE_ANIMATOR_DELAY = 300;
+
+    // Duration in ms of the pulse animation
+    private static final int PULSE_ANIMATOR_DURATION = 544;
+
+    private static int[] TEXT_APPEARANCE_TIME_LABEL_ATTR =
+            new int[] { R.attr.timePickerHeaderTimeLabelTextAppearance };
+
+    private final View mMainView;
+    private TextView mHourView;
+    private TextView mMinuteView;
+    private TextView mAmPmTextView;
+    private RadialTimePickerView mRadialTimePickerView;
+    private TextView mSeparatorView;
+
+    private ViewGroup mLayoutButtons;
+
+    private int mHeaderSelectedColor;
+    private int mHeaderUnSelectedColor;
+    private String mAmText;
+    private String mPmText;
+
+    private boolean mAllowAutoAdvance;
+    private int mInitialHourOfDay;
+    private int mInitialMinute;
+    private boolean mIs24HourView;
+
+    // For hardware IME input.
+    private char mPlaceholderText;
+    private String mDoublePlaceholderText;
+    private String mDeletedKeyFormat;
+    private boolean mInKbMode;
+    private ArrayList<Integer> mTypedTimes = new ArrayList<Integer>();
+    private Node mLegalTimesTree;
+    private int mAmKeyCode;
+    private int mPmKeyCode;
+
+    // For showing the done button when in a Dialog
+    private Button mDoneButton;
+    private boolean mShowDoneButton;
+    private TimePicker.TimePickerDismissCallback mDismissCallback;
+
+    // Accessibility strings.
+    private String mHourPickerDescription;
+    private String mSelectHours;
+    private String mMinutePickerDescription;
+    private String mSelectMinutes;
+
+    public TimePickerDelegate(TimePicker delegator, Context context, AttributeSet attrs,
+                              int defStyleAttr, int defStyleRes) {
+        super(delegator, context);
+
+        // process style attributes
+        final TypedArray a = mContext.obtainStyledAttributes(attrs,
+                R.styleable.TimePicker, defStyleAttr, defStyleRes);
+
+        final Resources res = mContext.getResources();
+
+        mHourPickerDescription = res.getString(R.string.hour_picker_description);
+        mSelectHours = res.getString(R.string.select_hours);
+        mMinutePickerDescription = res.getString(R.string.minute_picker_description);
+        mSelectMinutes = res.getString(R.string.select_minutes);
+
+        mHeaderSelectedColor = a.getColor(R.styleable.TimePicker_headerSelectedTextColor,
+                android.R.color.holo_blue_light);
+
+        mHeaderUnSelectedColor = getUnselectedColor(
+                R.color.timepicker_default_text_color_holo_light);
+        if (mHeaderUnSelectedColor == -1) {
+            mHeaderUnSelectedColor = a.getColor(R.styleable.TimePicker_headerUnselectedTextColor,
+                    R.color.timepicker_default_text_color_holo_light);
+        }
+
+        final int headerBackgroundColor = a.getColor(
+                R.styleable.TimePicker_headerBackgroundColor, 0);
+
+        a.recycle();
+
+        final int layoutResourceId = a.getResourceId(
+                R.styleable.TimePicker_internalLayout, R.layout.time_picker_holo);
+
+        final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+
+        mMainView = inflater.inflate(layoutResourceId, null);
+        mDelegator.addView(mMainView);
+
+        if (headerBackgroundColor != 0) {
+            RelativeLayout header = (RelativeLayout) mMainView.findViewById(R.id.time_header);
+            header.setBackgroundColor(headerBackgroundColor);
+        }
+
+        mHourView = (TextView) mMainView.findViewById(R.id.hours);
+        mMinuteView = (TextView) mMainView.findViewById(R.id.minutes);
+        mAmPmTextView = (TextView) mMainView.findViewById(R.id.ampm_label);
+        mSeparatorView = (TextView) mMainView.findViewById(R.id.separator);
+        mRadialTimePickerView = (RadialTimePickerView) mMainView.findViewById(R.id.radial_picker);
+
+        mLayoutButtons = (ViewGroup) mMainView.findViewById(R.id.layout_buttons);
+        mDoneButton = (Button) mMainView.findViewById(R.id.done_button);
+
+        String[] amPmTexts = new DateFormatSymbols().getAmPmStrings();
+        mAmText = amPmTexts[0];
+        mPmText = amPmTexts[1];
+
+        setupListeners();
+
+        mAllowAutoAdvance = true;
+
+        // Set up for keyboard mode.
+        mDoublePlaceholderText = res.getString(R.string.time_placeholder);
+        mDeletedKeyFormat = res.getString(R.string.deleted_key);
+        mPlaceholderText = mDoublePlaceholderText.charAt(0);
+        mAmKeyCode = mPmKeyCode = -1;
+        generateLegalTimesTree();
+
+        // Initialize with current time
+        final Calendar calendar = Calendar.getInstance(mCurrentLocale);
+        final int currentHour = calendar.get(Calendar.HOUR_OF_DAY);
+        final int currentMinute = calendar.get(Calendar.MINUTE);
+        initialize(currentHour, currentMinute, false /* 12h */, HOUR_INDEX, false);
+    }
+
+    private int getUnselectedColor(int defColor) {
+        int result = -1;
+        final Resources.Theme theme = mContext.getTheme();
+        final TypedValue outValue = new TypedValue();
+        theme.resolveAttribute(R.attr.timePickerHeaderTimeLabelTextAppearance, outValue, true);
+        final int appearanceResId = outValue.resourceId;
+        TypedArray appearance = null;
+        if (appearanceResId != -1) {
+            appearance = theme.obtainStyledAttributes(appearanceResId,
+                    com.android.internal.R.styleable.TextAppearance);
+        }
+        if (appearance != null) {
+            result = appearance.getColor(
+                    com.android.internal.R.styleable.TextAppearance_textColor, defColor);
+            appearance.recycle();
+        }
+        return result;
+    }
+
+    private void initialize(int hourOfDay, int minute, boolean is24HourView, int index,
+                            boolean showDoneButton) {
+        mInitialHourOfDay = hourOfDay;
+        mInitialMinute = minute;
+        mIs24HourView = is24HourView;
+        mInKbMode = false;
+        mShowDoneButton = showDoneButton;
+        updateUI(index);
+    }
+
+    private void setupListeners() {
+        KeyboardListener keyboardListener = new KeyboardListener();
+        mDelegator.setOnKeyListener(keyboardListener);
+
+        mHourView.setOnKeyListener(keyboardListener);
+        mMinuteView.setOnKeyListener(keyboardListener);
+        mAmPmTextView.setOnKeyListener(keyboardListener);
+        mRadialTimePickerView.setOnValueSelectedListener(this);
+        mRadialTimePickerView.setOnKeyListener(keyboardListener);
+
+        mHourView.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                setCurrentItemShowing(HOUR_INDEX, true, false, true);
+                tryVibrate();
+            }
+        });
+        mMinuteView.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                setCurrentItemShowing(MINUTE_INDEX, true, false, true);
+                tryVibrate();
+            }
+        });
+        mDoneButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                if (mInKbMode && isTypedTimeFullyLegal()) {
+                    finishKbMode(false);
+                } else {
+                    tryVibrate();
+                }
+                if (mDismissCallback != null) {
+                    mDismissCallback.dismiss(mDelegator, false, getCurrentHour(),
+                            getCurrentMinute());
+                }
+            }
+        });
+        mDoneButton.setOnKeyListener(keyboardListener);
+    }
+
+    private void updateUI(int index) {
+        // Update RadialPicker values
+        updateRadialPicker(index);
+        // Enable or disable the AM/PM view.
+        updateHeaderAmPm();
+        // Show or hide Done button
+        updateDoneButton();
+        // Update Hour and Minutes
+        updateHeaderHour(mInitialHourOfDay, true);
+        // Update time separator
+        updateHeaderSeparator();
+        // Update Minutes
+        updateHeaderMinute(mInitialMinute);
+        // Invalidate everything
+        mDelegator.invalidate();
+    }
+
+    private void updateRadialPicker(int index) {
+        mRadialTimePickerView.initialize(mInitialHourOfDay, mInitialMinute, mIs24HourView);
+        setCurrentItemShowing(index, false, true, true);
+    }
+
+    private int computeMaxWidthOfNumbers(int max) {
+        TextView tempView = new TextView(mContext);
+        TypedArray a = mContext.obtainStyledAttributes(TEXT_APPEARANCE_TIME_LABEL_ATTR);
+        final int textAppearanceResId = a.getResourceId(0, 0);
+        tempView.setTextAppearance(mContext, (textAppearanceResId != 0) ?
+                textAppearanceResId : R.style.TextAppearance_Holo_TimePicker_TimeLabel);
+        a.recycle();
+        ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT);
+        tempView.setLayoutParams(lp);
+        int maxWidth = 0;
+        for (int minutes = 0; minutes < max; minutes++) {
+            final String text = String.format("%02d", minutes);
+            tempView.setText(text);
+            tempView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
+            maxWidth = Math.max(maxWidth, tempView.getMeasuredWidth());
+        }
+        return maxWidth;
+    }
+
+    private void updateHeaderAmPm() {
+        if (mIs24HourView) {
+            mAmPmTextView.setVisibility(View.GONE);
+        } else {
+            mAmPmTextView.setVisibility(View.VISIBLE);
+            final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale,
+                    "hm");
+
+            boolean amPmOnLeft = bestDateTimePattern.startsWith("a");
+            if (TextUtils.getLayoutDirectionFromLocale(mCurrentLocale) ==
+                    View.LAYOUT_DIRECTION_RTL) {
+                amPmOnLeft = !amPmOnLeft;
+            }
+
+            RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams)
+                    mAmPmTextView.getLayoutParams();
+
+            if (amPmOnLeft) {
+                layoutParams.rightMargin = computeMaxWidthOfNumbers(12 /* for hours */);
+                layoutParams.removeRule(RelativeLayout.RIGHT_OF);
+                layoutParams.addRule(RelativeLayout.LEFT_OF, R.id.separator);
+            } else {
+                layoutParams.leftMargin = computeMaxWidthOfNumbers(60 /* for minutes */);
+                layoutParams.removeRule(RelativeLayout.LEFT_OF);
+                layoutParams.addRule(RelativeLayout.RIGHT_OF, R.id.separator);
+            }
+
+            updateAmPmDisplay(mInitialHourOfDay < 12 ? AM : PM);
+            mAmPmTextView.setOnClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    tryVibrate();
+                    int amOrPm = mRadialTimePickerView.getAmOrPm();
+                    if (amOrPm == AM) {
+                        amOrPm = PM;
+                    } else if (amOrPm == PM){
+                        amOrPm = AM;
+                    }
+                    updateAmPmDisplay(amOrPm);
+                    mRadialTimePickerView.setAmOrPm(amOrPm);
+                }
+            });
+        }
+    }
+
+    private void updateDoneButton() {
+        mLayoutButtons.setVisibility(mShowDoneButton ? View.VISIBLE : View.GONE);
+    }
+
+    /**
+     * Set the current hour.
+     */
+    @Override
+    public void setCurrentHour(Integer currentHour) {
+        if (mInitialHourOfDay == currentHour) {
+            return;
+        }
+        mInitialHourOfDay = currentHour;
+        updateHeaderHour(currentHour, true /* accessibility announce */);
+        updateHeaderAmPm();
+        mRadialTimePickerView.setCurrentHour(currentHour);
+        mRadialTimePickerView.setAmOrPm(mInitialHourOfDay < 12 ? AM : PM);
+        mDelegator.invalidate();
+        onTimeChanged();
+    }
+
+    /**
+     * @return The current hour in the range (0-23).
+     */
+    @Override
+    public Integer getCurrentHour() {
+        int currentHour = mRadialTimePickerView.getCurrentHour();
+        if (mIs24HourView) {
+            return currentHour;
+        } else {
+            switch(mRadialTimePickerView.getAmOrPm()) {
+                case PM:
+                    return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY;
+                case AM:
+                default:
+                    return currentHour % HOURS_IN_HALF_DAY;
+            }
+        }
+    }
+
+    /**
+     * Set the current minute (0-59).
+     */
+    @Override
+    public void setCurrentMinute(Integer currentMinute) {
+        if (mInitialMinute == currentMinute) {
+            return;
+        }
+        mInitialMinute = currentMinute;
+        updateHeaderMinute(currentMinute);
+        mRadialTimePickerView.setCurrentMinute(currentMinute);
+        mDelegator.invalidate();
+        onTimeChanged();
+    }
+
+    /**
+     * @return The current minute.
+     */
+    @Override
+    public Integer getCurrentMinute() {
+        return mRadialTimePickerView.getCurrentMinute();
+    }
+
+    /**
+     * Set whether in 24 hour or AM/PM mode.
+     *
+     * @param is24HourView True = 24 hour mode. False = AM/PM.
+     */
+    @Override
+    public void setIs24HourView(Boolean is24HourView) {
+        if (is24HourView == mIs24HourView) {
+            return;
+        }
+        mIs24HourView = is24HourView;
+        generateLegalTimesTree();
+        int hour = mRadialTimePickerView.getCurrentHour();
+        mInitialHourOfDay = hour;
+        updateHeaderHour(hour, false /* no accessibility announce */);
+        updateHeaderAmPm();
+        updateRadialPicker(mRadialTimePickerView.getCurrentItemShowing());
+        mDelegator.invalidate();
+    }
+
+    /**
+     * @return true if this is in 24 hour view else false.
+     */
+    @Override
+    public boolean is24HourView() {
+        return mIs24HourView;
+    }
+
+    @Override
+    public void setOnTimeChangedListener(TimePicker.OnTimeChangedListener callback) {
+        mOnTimeChangedListener = callback;
+    }
+
+    @Override
+    public void setEnabled(boolean enabled) {
+        mHourView.setEnabled(enabled);
+        mMinuteView.setEnabled(enabled);
+        mAmPmTextView.setEnabled(enabled);
+        mRadialTimePickerView.setEnabled(enabled);
+        mIsEnabled = enabled;
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return mIsEnabled;
+    }
+
+    @Override
+    public void setShowDoneButton(boolean showDoneButton) {
+        mShowDoneButton = showDoneButton;
+        updateDoneButton();
+    }
+
+    @Override
+    public void setDismissCallback(TimePicker.TimePickerDismissCallback callback) {
+        mDismissCallback = callback;
+    }
+
+    @Override
+    public int getBaseline() {
+        // does not support baseline alignment
+        return -1;
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        updateUI(mRadialTimePickerView.getCurrentItemShowing());
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState(Parcelable superState) {
+        return new SavedState(superState, getCurrentHour(), getCurrentMinute(),
+                is24HourView(), inKbMode(), getTypedTimes(), getCurrentItemShowing(),
+                isShowDoneButton());
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        SavedState ss = (SavedState) state;
+        setInKbMode(ss.inKbMode());
+        setTypedTimes(ss.getTypesTimes());
+        initialize(ss.getHour(), ss.getMinute(), ss.is24HourMode(), ss.getCurrentItemShowing(),
+                ss.isShowDoneButton());
+        mRadialTimePickerView.invalidate();
+        if (mInKbMode) {
+            tryStartingKbMode(-1);
+            mHourView.invalidate();
+        }
+    }
+
+    @Override
+    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+        return mRadialTimePickerView.dispatchPopulateAccessibilityEvent(event);
+    }
+
+    @Override
+    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
+        mRadialTimePickerView.onPopulateAccessibilityEvent(event);
+    }
+
+    @Override
+    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+        mRadialTimePickerView.onInitializeAccessibilityEvent(event);
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+        mRadialTimePickerView.onInitializeAccessibilityNodeInfo(info);
+    }
+
+    /**
+     * Set whether in keyboard mode or not.
+     *
+     * @param inKbMode True means in keyboard mode.
+     */
+    private void setInKbMode(boolean inKbMode) {
+        mInKbMode = inKbMode;
+    }
+
+    /**
+     * @return true if in keyboard mode
+     */
+    private boolean inKbMode() {
+        return mInKbMode;
+    }
+
+    private void setTypedTimes(ArrayList<Integer> typeTimes) {
+        mTypedTimes = typeTimes;
+    }
+
+    /**
+     * @return an array of typed times
+     */
+    private ArrayList<Integer> getTypedTimes() {
+        return mTypedTimes;
+    }
+
+    /**
+     * @return the index of the current item showing
+     */
+    private int getCurrentItemShowing() {
+        return mRadialTimePickerView.getCurrentItemShowing();
+    }
+
+    private boolean isShowDoneButton() {
+        return mShowDoneButton;
+    }
+
+    /**
+     * Propagate the time change
+     */
+    private void onTimeChanged() {
+        mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
+        if (mOnTimeChangedListener != null) {
+            mOnTimeChangedListener.onTimeChanged(mDelegator,
+                    getCurrentHour(), getCurrentMinute());
+        }
+    }
+
+    /**
+     * Used to save / restore state of time picker
+     */
+    private static class SavedState extends View.BaseSavedState {
+
+        private final int mHour;
+        private final int mMinute;
+        private final boolean mIs24HourMode;
+        private final boolean mInKbMode;
+        private final ArrayList<Integer> mTypedTimes;
+        private final int mCurrentItemShowing;
+        private final boolean mShowDoneButton;
+
+        private SavedState(Parcelable superState, int hour, int minute, boolean is24HourMode,
+                           boolean isKbMode, ArrayList<Integer> typedTimes,
+                           int currentItemShowing, boolean showDoneButton) {
+            super(superState);
+            mHour = hour;
+            mMinute = minute;
+            mIs24HourMode = is24HourMode;
+            mInKbMode = isKbMode;
+            mTypedTimes = typedTimes;
+            mCurrentItemShowing = currentItemShowing;
+            mShowDoneButton = showDoneButton;
+        }
+
+        private SavedState(Parcel in) {
+            super(in);
+            mHour = in.readInt();
+            mMinute = in.readInt();
+            mIs24HourMode = (in.readInt() == 1);
+            mInKbMode = (in.readInt() == 1);
+            mTypedTimes = in.readArrayList(getClass().getClassLoader());
+            mCurrentItemShowing = in.readInt();
+            mShowDoneButton = (in.readInt() == 1);
+        }
+
+        public int getHour() {
+            return mHour;
+        }
+
+        public int getMinute() {
+            return mMinute;
+        }
+
+        public boolean is24HourMode() {
+            return mIs24HourMode;
+        }
+
+        public boolean inKbMode() {
+            return mInKbMode;
+        }
+
+        public ArrayList<Integer> getTypesTimes() {
+            return mTypedTimes;
+        }
+
+        public int getCurrentItemShowing() {
+            return mCurrentItemShowing;
+        }
+
+        public boolean isShowDoneButton() {
+            return mShowDoneButton;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            super.writeToParcel(dest, flags);
+            dest.writeInt(mHour);
+            dest.writeInt(mMinute);
+            dest.writeInt(mIs24HourMode ? 1 : 0);
+            dest.writeInt(mInKbMode ? 1 : 0);
+            dest.writeList(mTypedTimes);
+            dest.writeInt(mCurrentItemShowing);
+            dest.writeInt(mShowDoneButton ? 1 : 0);
+        }
+
+        @SuppressWarnings({"unused", "hiding"})
+        public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() {
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+    }
+
+    private void tryVibrate() {
+        mDelegator.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
+    }
+
+    private void updateAmPmDisplay(int amOrPm) {
+        if (amOrPm == AM) {
+            mAmPmTextView.setText(mAmText);
+            mRadialTimePickerView.announceForAccessibility(mAmText);
+        } else if (amOrPm == PM){
+            mAmPmTextView.setText(mPmText);
+            mRadialTimePickerView.announceForAccessibility(mPmText);
+        } else {
+            mAmPmTextView.setText(mDoublePlaceholderText);
+        }
+    }
+
+    /**
+     * Called by the picker for updating the header display.
+     */
+    @Override
+    public void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance) {
+        if (pickerIndex == HOUR_INDEX) {
+            updateHeaderHour(newValue, false);
+            String announcement = String.format("%d", newValue);
+            if (mAllowAutoAdvance && autoAdvance) {
+                setCurrentItemShowing(MINUTE_INDEX, true, true, false);
+                announcement += ". " + mSelectMinutes;
+            } else {
+                mRadialTimePickerView.setContentDescription(
+                        mHourPickerDescription + ": " + newValue);
+            }
+
+            mRadialTimePickerView.announceForAccessibility(announcement);
+        } else if (pickerIndex == MINUTE_INDEX){
+            updateHeaderMinute(newValue);
+            mRadialTimePickerView.setContentDescription(mMinutePickerDescription + ": " + newValue);
+        } else if (pickerIndex == AMPM_INDEX) {
+            updateAmPmDisplay(newValue);
+        } else if (pickerIndex == ENABLE_PICKER_INDEX) {
+            if (!isTypedTimeFullyLegal()) {
+                mTypedTimes.clear();
+            }
+            finishKbMode(true);
+        }
+    }
+
+    private void updateHeaderHour(int value, boolean announce) {
+        final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale,
+                (mIs24HourView) ? "Hm" : "hm");
+        final int lengthPattern = bestDateTimePattern.length();
+        boolean hourWithTwoDigit = false;
+        char hourFormat = '\0';
+        // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save
+        // the hour format that we found.
+        for (int i = 0; i < lengthPattern; i++) {
+            final char c = bestDateTimePattern.charAt(i);
+            if (c == 'H' || c == 'h' || c == 'K' || c == 'k') {
+                hourFormat = c;
+                if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) {
+                    hourWithTwoDigit = true;
+                }
+                break;
+            }
+        }
+        final String format;
+        if (hourWithTwoDigit) {
+            format = "%02d";
+        } else {
+            format = "%d";
+        }
+        if (mIs24HourView) {
+            // 'k' means 1-24 hour
+            if (hourFormat == 'k' && value == 0) {
+                value = 24;
+            }
+        } else {
+            // 'K' means 0-11 hour
+            value = modulo12(value, hourFormat == 'K');
+        }
+        CharSequence text = String.format(format, value);
+        mHourView.setText(text);
+        if (announce) {
+            mRadialTimePickerView.announceForAccessibility(text);
+        }
+    }
+
+    private static int modulo12(int n, boolean startWithZero) {
+        int value = n % 12;
+        if (value == 0 && !startWithZero) {
+            value = 12;
+        }
+        return value;
+    }
+
+    /**
+     * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":".
+     *
+     * See http://unicode.org/cldr/trac/browser/trunk/common/main
+     *
+     * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the
+     * separator as the character which is just after the hour marker in the returned pattern.
+     */
+    private void updateHeaderSeparator() {
+        final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale,
+                (mIs24HourView) ? "Hm" : "hm");
+        final String separatorText;
+        // See http://www.unicode.org/reports/tr35/tr35-dates.html for hour formats
+        final char[] hourFormats = {'H', 'h', 'K', 'k'};
+        int hIndex = lastIndexOfAny(bestDateTimePattern, hourFormats);
+        if (hIndex == -1) {
+            // Default case
+            separatorText = ":";
+        } else {
+            separatorText = Character.toString(bestDateTimePattern.charAt(hIndex + 1));
+        }
+        mSeparatorView.setText(separatorText);
+    }
+
+    static private int lastIndexOfAny(String str, char[] any) {
+        final int lengthAny = any.length;
+        if (lengthAny > 0) {
+            for (int i = str.length() - 1; i >= 0; i--) {
+                char c = str.charAt(i);
+                for (int j = 0; j < lengthAny; j++) {
+                    if (c == any[j]) {
+                        return i;
+                    }
+                }
+            }
+        }
+        return -1;
+    }
+
+    private void updateHeaderMinute(int value) {
+        if (value == 60) {
+            value = 0;
+        }
+        CharSequence text = String.format(mCurrentLocale, "%02d", value);
+        mRadialTimePickerView.announceForAccessibility(text);
+        mMinuteView.setText(text);
+    }
+
+    /**
+     * Show either Hours or Minutes.
+     */
+    private void setCurrentItemShowing(int index, boolean animateCircle, boolean delayLabelAnimate,
+                                       boolean announce) {
+        mRadialTimePickerView.setCurrentItemShowing(index, animateCircle);
+
+        TextView labelToAnimate;
+        if (index == HOUR_INDEX) {
+            int hours = mRadialTimePickerView.getCurrentHour();
+            if (!mIs24HourView) {
+                hours = hours % 12;
+            }
+            mRadialTimePickerView.setContentDescription(mHourPickerDescription + ": " + hours);
+            if (announce) {
+                mRadialTimePickerView.announceForAccessibility(mSelectHours);
+            }
+            labelToAnimate = mHourView;
+        } else {
+            int minutes = mRadialTimePickerView.getCurrentMinute();
+            mRadialTimePickerView.setContentDescription(mMinutePickerDescription + ": " + minutes);
+            if (announce) {
+                mRadialTimePickerView.announceForAccessibility(mSelectMinutes);
+            }
+            labelToAnimate = mMinuteView;
+        }
+
+        int hourColor = (index == HOUR_INDEX) ? mHeaderSelectedColor : mHeaderUnSelectedColor;
+        int minuteColor = (index == MINUTE_INDEX) ? mHeaderSelectedColor : mHeaderUnSelectedColor;
+        mHourView.setTextColor(hourColor);
+        mMinuteView.setTextColor(minuteColor);
+
+        ObjectAnimator pulseAnimator = getPulseAnimator(labelToAnimate, 0.85f, 1.1f);
+        if (delayLabelAnimate) {
+            pulseAnimator.setStartDelay(PULSE_ANIMATOR_DELAY);
+        }
+        pulseAnimator.start();
+    }
+
+    /**
+     * For keyboard mode, processes key events.
+     *
+     * @param keyCode the pressed key.
+     *
+     * @return true if the key was successfully processed, false otherwise.
+     */
+    private boolean processKeyUp(int keyCode) {
+        if (keyCode == KeyEvent.KEYCODE_ESCAPE || keyCode == KeyEvent.KEYCODE_BACK) {
+            if (mDismissCallback != null) {
+                mDismissCallback.dismiss(mDelegator, true, getCurrentHour(), getCurrentMinute());
+            }
+            return true;
+        } else if (keyCode == KeyEvent.KEYCODE_TAB) {
+            if(mInKbMode) {
+                if (isTypedTimeFullyLegal()) {
+                    finishKbMode(true);
+                }
+                return true;
+            }
+        } else if (keyCode == KeyEvent.KEYCODE_ENTER) {
+            if (mInKbMode) {
+                if (!isTypedTimeFullyLegal()) {
+                    return true;
+                }
+                finishKbMode(false);
+            }
+            if (mOnTimeChangedListener != null) {
+                mOnTimeChangedListener.onTimeChanged(mDelegator,
+                        mRadialTimePickerView.getCurrentHour(),
+                        mRadialTimePickerView.getCurrentMinute());
+            }
+            if (mDismissCallback != null) {
+                mDismissCallback.dismiss(mDelegator, false, getCurrentHour(), getCurrentMinute());
+            }
+            return true;
+        } else if (keyCode == KeyEvent.KEYCODE_DEL) {
+            if (mInKbMode) {
+                if (!mTypedTimes.isEmpty()) {
+                    int deleted = deleteLastTypedKey();
+                    String deletedKeyStr;
+                    if (deleted == getAmOrPmKeyCode(AM)) {
+                        deletedKeyStr = mAmText;
+                    } else if (deleted == getAmOrPmKeyCode(PM)) {
+                        deletedKeyStr = mPmText;
+                    } else {
+                        deletedKeyStr = String.format("%d", getValFromKeyCode(deleted));
+                    }
+                    mRadialTimePickerView.announceForAccessibility(
+                            String.format(mDeletedKeyFormat, deletedKeyStr));
+                    updateDisplay(true);
+                }
+            }
+        } else if (keyCode == KeyEvent.KEYCODE_0 || keyCode == KeyEvent.KEYCODE_1
+                || keyCode == KeyEvent.KEYCODE_2 || keyCode == KeyEvent.KEYCODE_3
+                || keyCode == KeyEvent.KEYCODE_4 || keyCode == KeyEvent.KEYCODE_5
+                || keyCode == KeyEvent.KEYCODE_6 || keyCode == KeyEvent.KEYCODE_7
+                || keyCode == KeyEvent.KEYCODE_8 || keyCode == KeyEvent.KEYCODE_9
+                || (!mIs24HourView &&
+                (keyCode == getAmOrPmKeyCode(AM) || keyCode == getAmOrPmKeyCode(PM)))) {
+            if (!mInKbMode) {
+                if (mRadialTimePickerView == null) {
+                    // Something's wrong, because time picker should definitely not be null.
+                    Log.e(TAG, "Unable to initiate keyboard mode, TimePicker was null.");
+                    return true;
+                }
+                mTypedTimes.clear();
+                tryStartingKbMode(keyCode);
+                return true;
+            }
+            // We're already in keyboard mode.
+            if (addKeyIfLegal(keyCode)) {
+                updateDisplay(false);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Try to start keyboard mode with the specified key.
+     *
+     * @param keyCode The key to use as the first press. Keyboard mode will not be started if the
+     * key is not legal to start with. Or, pass in -1 to get into keyboard mode without a starting
+     * key.
+     */
+    private void tryStartingKbMode(int keyCode) {
+        if (keyCode == -1 || addKeyIfLegal(keyCode)) {
+            mInKbMode = true;
+            mDoneButton.setEnabled(false);
+            updateDisplay(false);
+            mRadialTimePickerView.setInputEnabled(false);
+        }
+    }
+
+    private boolean addKeyIfLegal(int keyCode) {
+        // If we're in 24hour mode, we'll need to check if the input is full. If in AM/PM mode,
+        // we'll need to see if AM/PM have been typed.
+        if ((mIs24HourView && mTypedTimes.size() == 4) ||
+                (!mIs24HourView && isTypedTimeFullyLegal())) {
+            return false;
+        }
+
+        mTypedTimes.add(keyCode);
+        if (!isTypedTimeLegalSoFar()) {
+            deleteLastTypedKey();
+            return false;
+        }
+
+        int val = getValFromKeyCode(keyCode);
+        mRadialTimePickerView.announceForAccessibility(String.format("%d", val));
+        // Automatically fill in 0's if AM or PM was legally entered.
+        if (isTypedTimeFullyLegal()) {
+            if (!mIs24HourView && mTypedTimes.size() <= 3) {
+                mTypedTimes.add(mTypedTimes.size() - 1, KeyEvent.KEYCODE_0);
+                mTypedTimes.add(mTypedTimes.size() - 1, KeyEvent.KEYCODE_0);
+            }
+            mDoneButton.setEnabled(true);
+        }
+
+        return true;
+    }
+
+    /**
+     * Traverse the tree to see if the keys that have been typed so far are legal as is,
+     * or may become legal as more keys are typed (excluding backspace).
+     */
+    private boolean isTypedTimeLegalSoFar() {
+        Node node = mLegalTimesTree;
+        for (int keyCode : mTypedTimes) {
+            node = node.canReach(keyCode);
+            if (node == null) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Check if the time that has been typed so far is completely legal, as is.
+     */
+    private boolean isTypedTimeFullyLegal() {
+        if (mIs24HourView) {
+            // For 24-hour mode, the time is legal if the hours and minutes are each legal. Note:
+            // getEnteredTime() will ONLY call isTypedTimeFullyLegal() when NOT in 24hour mode.
+            int[] values = getEnteredTime(null);
+            return (values[0] >= 0 && values[1] >= 0 && values[1] < 60);
+        } else {
+            // For AM/PM mode, the time is legal if it contains an AM or PM, as those can only be
+            // legally added at specific times based on the tree's algorithm.
+            return (mTypedTimes.contains(getAmOrPmKeyCode(AM)) ||
+                    mTypedTimes.contains(getAmOrPmKeyCode(PM)));
+        }
+    }
+
+    private int deleteLastTypedKey() {
+        int deleted = mTypedTimes.remove(mTypedTimes.size() - 1);
+        if (!isTypedTimeFullyLegal()) {
+            mDoneButton.setEnabled(false);
+        }
+        return deleted;
+    }
+
+    /**
+     * Get out of keyboard mode. If there is nothing in typedTimes, revert to TimePicker's time.
+     * @param updateDisplays If true, update the displays with the relevant time.
+     */
+    private void finishKbMode(boolean updateDisplays) {
+        mInKbMode = false;
+        if (!mTypedTimes.isEmpty()) {
+            int values[] = getEnteredTime(null);
+            mRadialTimePickerView.setCurrentHour(values[0]);
+            mRadialTimePickerView.setCurrentMinute(values[1]);
+            if (!mIs24HourView) {
+                mRadialTimePickerView.setAmOrPm(values[2]);
+            }
+            mTypedTimes.clear();
+        }
+        if (updateDisplays) {
+            updateDisplay(false);
+            mRadialTimePickerView.setInputEnabled(true);
+        }
+    }
+
+    /**
+     * Update the hours, minutes, and AM/PM displays with the typed times. If the typedTimes is
+     * empty, either show an empty display (filled with the placeholder text), or update from the
+     * timepicker's values.
+     *
+     * @param allowEmptyDisplay if true, then if the typedTimes is empty, use the placeholder text.
+     * Otherwise, revert to the timepicker's values.
+     */
+    private void updateDisplay(boolean allowEmptyDisplay) {
+        if (!allowEmptyDisplay && mTypedTimes.isEmpty()) {
+            int hour = mRadialTimePickerView.getCurrentHour();
+            int minute = mRadialTimePickerView.getCurrentMinute();
+            updateHeaderHour(hour, true);
+            updateHeaderMinute(minute);
+            if (!mIs24HourView) {
+                updateAmPmDisplay(hour < 12 ? AM : PM);
+            }
+            setCurrentItemShowing(mRadialTimePickerView.getCurrentItemShowing(), true, true, true);
+            mDoneButton.setEnabled(true);
+        } else {
+            boolean[] enteredZeros = {false, false};
+            int[] values = getEnteredTime(enteredZeros);
+            String hourFormat = enteredZeros[0] ? "%02d" : "%2d";
+            String minuteFormat = (enteredZeros[1]) ? "%02d" : "%2d";
+            String hourStr = (values[0] == -1) ? mDoublePlaceholderText :
+                    String.format(hourFormat, values[0]).replace(' ', mPlaceholderText);
+            String minuteStr = (values[1] == -1) ? mDoublePlaceholderText :
+                    String.format(minuteFormat, values[1]).replace(' ', mPlaceholderText);
+            mHourView.setText(hourStr);
+            mHourView.setTextColor(mHeaderUnSelectedColor);
+            mMinuteView.setText(minuteStr);
+            mMinuteView.setTextColor(mHeaderUnSelectedColor);
+            if (!mIs24HourView) {
+                updateAmPmDisplay(values[2]);
+            }
+        }
+    }
+
+    private int getValFromKeyCode(int keyCode) {
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_0:
+                return 0;
+            case KeyEvent.KEYCODE_1:
+                return 1;
+            case KeyEvent.KEYCODE_2:
+                return 2;
+            case KeyEvent.KEYCODE_3:
+                return 3;
+            case KeyEvent.KEYCODE_4:
+                return 4;
+            case KeyEvent.KEYCODE_5:
+                return 5;
+            case KeyEvent.KEYCODE_6:
+                return 6;
+            case KeyEvent.KEYCODE_7:
+                return 7;
+            case KeyEvent.KEYCODE_8:
+                return 8;
+            case KeyEvent.KEYCODE_9:
+                return 9;
+            default:
+                return -1;
+        }
+    }
+
+    /**
+     * Get the currently-entered time, as integer values of the hours and minutes typed.
+     *
+     * @param enteredZeros A size-2 boolean array, which the caller should initialize, and which
+     * may then be used for the caller to know whether zeros had been explicitly entered as either
+     * hours of minutes. This is helpful for deciding whether to show the dashes, or actual 0's.
+     *
+     * @return A size-3 int array. The first value will be the hours, the second value will be the
+     * minutes, and the third will be either AM or PM.
+     */
+    private int[] getEnteredTime(boolean[] enteredZeros) {
+        int amOrPm = -1;
+        int startIndex = 1;
+        if (!mIs24HourView && isTypedTimeFullyLegal()) {
+            int keyCode = mTypedTimes.get(mTypedTimes.size() - 1);
+            if (keyCode == getAmOrPmKeyCode(AM)) {
+                amOrPm = AM;
+            } else if (keyCode == getAmOrPmKeyCode(PM)){
+                amOrPm = PM;
+            }
+            startIndex = 2;
+        }
+        int minute = -1;
+        int hour = -1;
+        for (int i = startIndex; i <= mTypedTimes.size(); i++) {
+            int val = getValFromKeyCode(mTypedTimes.get(mTypedTimes.size() - i));
+            if (i == startIndex) {
+                minute = val;
+            } else if (i == startIndex+1) {
+                minute += 10 * val;
+                if (enteredZeros != null && val == 0) {
+                    enteredZeros[1] = true;
+                }
+            } else if (i == startIndex+2) {
+                hour = val;
+            } else if (i == startIndex+3) {
+                hour += 10 * val;
+                if (enteredZeros != null && val == 0) {
+                    enteredZeros[0] = true;
+                }
+            }
+        }
+
+        int[] ret = {hour, minute, amOrPm};
+        return ret;
+    }
+
+    /**
+     * Get the keycode value for AM and PM in the current language.
+     */
+    private int getAmOrPmKeyCode(int amOrPm) {
+        // Cache the codes.
+        if (mAmKeyCode == -1 || mPmKeyCode == -1) {
+            // Find the first character in the AM/PM text that is unique.
+            KeyCharacterMap kcm = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
+            char amChar;
+            char pmChar;
+            for (int i = 0; i < Math.max(mAmText.length(), mPmText.length()); i++) {
+                amChar = mAmText.toLowerCase(mCurrentLocale).charAt(i);
+                pmChar = mPmText.toLowerCase(mCurrentLocale).charAt(i);
+                if (amChar != pmChar) {
+                    KeyEvent[] events = kcm.getEvents(new char[]{amChar, pmChar});
+                    // There should be 4 events: a down and up for both AM and PM.
+                    if (events != null && events.length == 4) {
+                        mAmKeyCode = events[0].getKeyCode();
+                        mPmKeyCode = events[2].getKeyCode();
+                    } else {
+                        Log.e(TAG, "Unable to find keycodes for AM and PM.");
+                    }
+                    break;
+                }
+            }
+        }
+        if (amOrPm == AM) {
+            return mAmKeyCode;
+        } else if (amOrPm == PM) {
+            return mPmKeyCode;
+        }
+
+        return -1;
+    }
+
+    /**
+     * Create a tree for deciding what keys can legally be typed.
+     */
+    private void generateLegalTimesTree() {
+        // Create a quick cache of numbers to their keycodes.
+        final int k0 = KeyEvent.KEYCODE_0;
+        final int k1 = KeyEvent.KEYCODE_1;
+        final int k2 = KeyEvent.KEYCODE_2;
+        final int k3 = KeyEvent.KEYCODE_3;
+        final int k4 = KeyEvent.KEYCODE_4;
+        final int k5 = KeyEvent.KEYCODE_5;
+        final int k6 = KeyEvent.KEYCODE_6;
+        final int k7 = KeyEvent.KEYCODE_7;
+        final int k8 = KeyEvent.KEYCODE_8;
+        final int k9 = KeyEvent.KEYCODE_9;
+
+        // The root of the tree doesn't contain any numbers.
+        mLegalTimesTree = new Node();
+        if (mIs24HourView) {
+            // We'll be re-using these nodes, so we'll save them.
+            Node minuteFirstDigit = new Node(k0, k1, k2, k3, k4, k5);
+            Node minuteSecondDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9);
+            // The first digit must be followed by the second digit.
+            minuteFirstDigit.addChild(minuteSecondDigit);
+
+            // The first digit may be 0-1.
+            Node firstDigit = new Node(k0, k1);
+            mLegalTimesTree.addChild(firstDigit);
+
+            // When the first digit is 0-1, the second digit may be 0-5.
+            Node secondDigit = new Node(k0, k1, k2, k3, k4, k5);
+            firstDigit.addChild(secondDigit);
+            // We may now be followed by the first minute digit. E.g. 00:09, 15:58.
+            secondDigit.addChild(minuteFirstDigit);
+
+            // When the first digit is 0-1, and the second digit is 0-5, the third digit may be 6-9.
+            Node thirdDigit = new Node(k6, k7, k8, k9);
+            // The time must now be finished. E.g. 0:55, 1:08.
+            secondDigit.addChild(thirdDigit);
+
+            // When the first digit is 0-1, the second digit may be 6-9.
+            secondDigit = new Node(k6, k7, k8, k9);
+            firstDigit.addChild(secondDigit);
+            // We must now be followed by the first minute digit. E.g. 06:50, 18:20.
+            secondDigit.addChild(minuteFirstDigit);
+
+            // The first digit may be 2.
+            firstDigit = new Node(k2);
+            mLegalTimesTree.addChild(firstDigit);
+
+            // When the first digit is 2, the second digit may be 0-3.
+            secondDigit = new Node(k0, k1, k2, k3);
+            firstDigit.addChild(secondDigit);
+            // We must now be followed by the first minute digit. E.g. 20:50, 23:09.
+            secondDigit.addChild(minuteFirstDigit);
+
+            // When the first digit is 2, the second digit may be 4-5.
+            secondDigit = new Node(k4, k5);
+            firstDigit.addChild(secondDigit);
+            // We must now be followd by the last minute digit. E.g. 2:40, 2:53.
+            secondDigit.addChild(minuteSecondDigit);
+
+            // The first digit may be 3-9.
+            firstDigit = new Node(k3, k4, k5, k6, k7, k8, k9);
+            mLegalTimesTree.addChild(firstDigit);
+            // We must now be followed by the first minute digit. E.g. 3:57, 8:12.
+            firstDigit.addChild(minuteFirstDigit);
+        } else {
+            // We'll need to use the AM/PM node a lot.
+            // Set up AM and PM to respond to "a" and "p".
+            Node ampm = new Node(getAmOrPmKeyCode(AM), getAmOrPmKeyCode(PM));
+
+            // The first hour digit may be 1.
+            Node firstDigit = new Node(k1);
+            mLegalTimesTree.addChild(firstDigit);
+            // We'll allow quick input of on-the-hour times. E.g. 1pm.
+            firstDigit.addChild(ampm);
+
+            // When the first digit is 1, the second digit may be 0-2.
+            Node secondDigit = new Node(k0, k1, k2);
+            firstDigit.addChild(secondDigit);
+            // Also for quick input of on-the-hour times. E.g. 10pm, 12am.
+            secondDigit.addChild(ampm);
+
+            // When the first digit is 1, and the second digit is 0-2, the third digit may be 0-5.
+            Node thirdDigit = new Node(k0, k1, k2, k3, k4, k5);
+            secondDigit.addChild(thirdDigit);
+            // The time may be finished now. E.g. 1:02pm, 1:25am.
+            thirdDigit.addChild(ampm);
+
+            // When the first digit is 1, the second digit is 0-2, and the third digit is 0-5,
+            // the fourth digit may be 0-9.
+            Node fourthDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9);
+            thirdDigit.addChild(fourthDigit);
+            // The time must be finished now. E.g. 10:49am, 12:40pm.
+            fourthDigit.addChild(ampm);
+
+            // When the first digit is 1, and the second digit is 0-2, the third digit may be 6-9.
+            thirdDigit = new Node(k6, k7, k8, k9);
+            secondDigit.addChild(thirdDigit);
+            // The time must be finished now. E.g. 1:08am, 1:26pm.
+            thirdDigit.addChild(ampm);
+
+            // When the first digit is 1, the second digit may be 3-5.
+            secondDigit = new Node(k3, k4, k5);
+            firstDigit.addChild(secondDigit);
+
+            // When the first digit is 1, and the second digit is 3-5, the third digit may be 0-9.
+            thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9);
+            secondDigit.addChild(thirdDigit);
+            // The time must be finished now. E.g. 1:39am, 1:50pm.
+            thirdDigit.addChild(ampm);
+
+            // The hour digit may be 2-9.
+            firstDigit = new Node(k2, k3, k4, k5, k6, k7, k8, k9);
+            mLegalTimesTree.addChild(firstDigit);
+            // We'll allow quick input of on-the-hour-times. E.g. 2am, 5pm.
+            firstDigit.addChild(ampm);
+
+            // When the first digit is 2-9, the second digit may be 0-5.
+            secondDigit = new Node(k0, k1, k2, k3, k4, k5);
+            firstDigit.addChild(secondDigit);
+
+            // When the first digit is 2-9, and the second digit is 0-5, the third digit may be 0-9.
+            thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9);
+            secondDigit.addChild(thirdDigit);
+            // The time must be finished now. E.g. 2:57am, 9:30pm.
+            thirdDigit.addChild(ampm);
+        }
+    }
+
+    /**
+     * Simple node class to be used for traversal to check for legal times.
+     * mLegalKeys represents the keys that can be typed to get to the node.
+     * mChildren are the children that can be reached from this node.
+     */
+    private class Node {
+        private int[] mLegalKeys;
+        private ArrayList<Node> mChildren;
+
+        public Node(int... legalKeys) {
+            mLegalKeys = legalKeys;
+            mChildren = new ArrayList<Node>();
+        }
+
+        public void addChild(Node child) {
+            mChildren.add(child);
+        }
+
+        public boolean containsKey(int key) {
+            for (int i = 0; i < mLegalKeys.length; i++) {
+                if (mLegalKeys[i] == key) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        public Node canReach(int key) {
+            if (mChildren == null) {
+                return null;
+            }
+            for (Node child : mChildren) {
+                if (child.containsKey(key)) {
+                    return child;
+                }
+            }
+            return null;
+        }
+    }
+
+    private class KeyboardListener implements View.OnKeyListener {
+        @Override
+        public boolean onKey(View v, int keyCode, KeyEvent event) {
+            if (event.getAction() == KeyEvent.ACTION_UP) {
+                return processKeyUp(keyCode);
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Render an animator to pulsate a view in place.
+     *
+     * @param labelToAnimate the view to pulsate.
+     * @return The animator object. Use .start() to begin.
+     */
+    private static ObjectAnimator getPulseAnimator(View labelToAnimate, float decreaseRatio,
+            float increaseRatio) {
+        final Keyframe k0 = Keyframe.ofFloat(0f, 1f);
+        final Keyframe k1 = Keyframe.ofFloat(0.275f, decreaseRatio);
+        final Keyframe k2 = Keyframe.ofFloat(0.69f, increaseRatio);
+        final Keyframe k3 = Keyframe.ofFloat(1f, 1f);
+
+        PropertyValuesHolder scaleX = PropertyValuesHolder.ofKeyframe("scaleX", k0, k1, k2, k3);
+        PropertyValuesHolder scaleY = PropertyValuesHolder.ofKeyframe("scaleY", k0, k1, k2, k3);
+        ObjectAnimator pulseAnimator =
+                ObjectAnimator.ofPropertyValuesHolder(labelToAnimate, scaleX, scaleY);
+        pulseAnimator.setDuration(PULSE_ANIMATOR_DURATION);
+
+        return pulseAnimator;
+    }
+}
diff --git a/core/res/res/layout-land/time_picker_holo.xml b/core/res/res/layout-land/time_picker_holo.xml
new file mode 100644
index 0000000..f5ce1ec
--- /dev/null
+++ b/core/res/res/layout-land/time_picker_holo.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 2013, 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.
+*/
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="horizontal"
+        android:focusable="true"
+        android:layout_marginLeft="@dimen/timepicker_minimum_margin_sides"
+        android:layout_marginRight="@dimen/timepicker_minimum_margin_sides"
+        android:layout_marginTop="@dimen/timepicker_minimum_margin_top_bottom"
+        android:layout_marginBottom="@dimen/timepicker_minimum_margin_top_bottom">
+    <LinearLayout
+            android:layout_width="@dimen/timepicker_left_side_width"
+            android:layout_height="match_parent"
+            android:orientation="vertical">
+        <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="0dip"
+                android:layout_weight="1"
+                android:background="?android:attr/timePickerHeaderBackgroundColor">
+            <include
+                    layout="@layout/time_header_label"
+                    android:layout_width="match_parent"
+                    android:layout_height="@dimen/timepicker_header_height"
+                    android:layout_gravity="center" />
+        </FrameLayout>
+        <LinearLayout
+                android:id="@+id/layout_buttons"
+                style="?android:attr/buttonBarStyle"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical"
+                android:background="?android:attr/timePickerHeaderBackgroundColor"
+                android:divider="?android:attr/dividerHorizontal"
+                android:showDividers="beginning">
+            <Button
+                    android:id="@+id/done_button"
+                    style="?android:attr/buttonBarButtonStyle"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:minHeight="48dp"
+                    android:text="@string/done_label"
+                    android:textSize="@dimen/timepicker_done_label_size" />
+        </LinearLayout>
+    </LinearLayout>
+    <android.widget.RadialTimePickerView
+            android:id="@+id/radial_picker"
+            android:layout_width="@dimen/timepicker_radial_picker_dimen"
+            android:layout_height="match_parent"
+            android:layout_gravity="center"
+            android:focusable="true"
+            android:focusableInTouchMode="true" />
+</LinearLayout>
\ No newline at end of file
diff --git a/core/res/res/layout/time_header_label.xml b/core/res/res/layout/time_header_label.xml
new file mode 100644
index 0000000..00cb81b
--- /dev/null
+++ b/core/res/res/layout/time_header_label.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2013 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
+  -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                android:id="@+id/time_header"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_gravity="center" >
+
+    <TextView
+            android:id="@+id/hours"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_toLeftOf="@+id/separator"
+            android:layout_alignBaseline="@+id/separator"
+            android:textAppearance="?android:attr/timePickerHeaderTimeLabelTextAppearance"/>
+
+    <TextView
+            android:id="@+id/separator"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:paddingLeft="@dimen/timepicker_separator_padding"
+            android:paddingRight="@dimen/timepicker_separator_padding"
+            android:layout_centerInParent="true"
+            android:textAppearance="?android:attr/timePickerHeaderTimeLabelTextAppearance"
+            android:importantForAccessibility="no" />
+
+    <TextView
+            android:id="@+id/minutes"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_toRightOf="@+id/separator"
+            android:layout_alignBaseline="@+id/separator"
+            android:textAppearance="?android:attr/timePickerHeaderTimeLabelTextAppearance" />
+
+    <TextView
+            android:id="@+id/ampm_label"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:paddingLeft="@dimen/timepicker_ampm_left_padding"
+            android:paddingRight="@dimen/timepicker_ampm_left_padding"
+            android:layout_toRightOf="@+id/separator"
+            android:layout_alignBaseline="@+id/separator"
+            android:textAppearance="?android:attr/timePickerHeaderAmPmLabelTextAppearance"
+            android:importantForAccessibility="no" />
+
+</RelativeLayout>
diff --git a/core/res/res/layout/time_picker_holo.xml b/core/res/res/layout/time_picker_holo.xml
index c6b7d3a..0890fe5 100644
--- a/core/res/res/layout/time_picker_holo.xml
+++ b/core/res/res/layout/time_picker_holo.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
 **
-** Copyright 2011, The Android Open Source Project
+** Copyright 2013, 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.
@@ -17,70 +17,38 @@
 */
 -->
 
-<!-- Layout of time picker -->
-
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/timePickerLayout"
-    android:orientation="horizontal"
-    android:layout_gravity="center_horizontal"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content"
-    android:paddingStart="8dip"
-    android:paddingEnd="8dip">
-
-    <LinearLayout android:orientation="horizontal"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:paddingStart="8dip"
-        android:paddingEnd="8dip"
-        android:layoutDirection="ltr">
-
-        <!-- hour -->
-        <NumberPicker
-            android:id="@+id/hour"
+          android:layout_width="wrap_content"
+          android:layout_height="match_parent"
+          android:orientation="vertical"
+          android:focusable="true" >
+    <include
+            layout="@layout/time_header_label"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/timepicker_header_height"
+            android:layout_gravity="center" />
+    <android.widget.RadialTimePickerView
+            android:id="@+id/radial_picker"
             android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="16dip"
-            android:layout_marginBottom="16dip"
+            android:layout_height="@dimen/timepicker_radial_picker_dimen"
+            android:layout_gravity="center"
             android:focusable="true"
-            android:focusableInTouchMode="true"
-            />
-
-        <!-- divider -->
-        <TextView
-            android:id="@+id/divider"
-            android:layout_width="wrap_content"
+            android:focusableInTouchMode="true" />
+    <LinearLayout
+            android:id="@+id/layout_buttons"
+            style="?android:attr/buttonBarStyle"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:layout_marginStart="6dip"
-            android:layout_marginEnd="6dip"
-            android:layout_gravity="center_vertical"
-            android:importantForAccessibility="no"
-            />
-
-        <!-- minute -->
-        <NumberPicker
-            android:id="@+id/minute"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="16dip"
-            android:layout_marginBottom="16dip"
-            android:focusable="true"
-            android:focusableInTouchMode="true"
-            />
-
+            android:orientation="vertical"
+            android:divider="?android:attr/dividerHorizontal"
+            android:showDividers="beginning">
+        <Button
+                android:id="@+id/done_button"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:minHeight="48dp"
+                android:text="@string/done_label"
+                android:textSize="@dimen/timepicker_done_label_size"
+                style="?android:attr/buttonBarButtonStyle" />
     </LinearLayout>
-
-    <!-- AM / PM -->
-    <NumberPicker
-        android:id="@+id/amPm"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="16dip"
-        android:layout_marginBottom="16dip"
-        android:layout_marginStart="8dip"
-        android:layout_marginEnd="8dip"
-        android:focusable="true"
-        android:focusableInTouchMode="true"
-        />
-
 </LinearLayout>
diff --git a/core/res/res/layout/time_picker.xml b/core/res/res/layout/time_picker_legacy.xml
similarity index 100%
rename from core/res/res/layout/time_picker.xml
rename to core/res/res/layout/time_picker_legacy.xml
diff --git a/core/res/res/layout/time_picker_legacy_holo.xml b/core/res/res/layout/time_picker_legacy_holo.xml
new file mode 100644
index 0000000..c6b7d3a
--- /dev/null
+++ b/core/res/res/layout/time_picker_legacy_holo.xml
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 2011, 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.
+*/
+-->
+
+<!-- Layout of time picker -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/timePickerLayout"
+    android:orientation="horizontal"
+    android:layout_gravity="center_horizontal"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:paddingStart="8dip"
+    android:paddingEnd="8dip">
+
+    <LinearLayout android:orientation="horizontal"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:paddingStart="8dip"
+        android:paddingEnd="8dip"
+        android:layoutDirection="ltr">
+
+        <!-- hour -->
+        <NumberPicker
+            android:id="@+id/hour"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="16dip"
+            android:layout_marginBottom="16dip"
+            android:focusable="true"
+            android:focusableInTouchMode="true"
+            />
+
+        <!-- divider -->
+        <TextView
+            android:id="@+id/divider"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="6dip"
+            android:layout_marginEnd="6dip"
+            android:layout_gravity="center_vertical"
+            android:importantForAccessibility="no"
+            />
+
+        <!-- minute -->
+        <NumberPicker
+            android:id="@+id/minute"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="16dip"
+            android:layout_marginBottom="16dip"
+            android:focusable="true"
+            android:focusableInTouchMode="true"
+            />
+
+    </LinearLayout>
+
+    <!-- AM / PM -->
+    <NumberPicker
+        android:id="@+id/amPm"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="16dip"
+        android:layout_marginBottom="16dip"
+        android:layout_marginStart="8dip"
+        android:layout_marginEnd="8dip"
+        android:focusable="true"
+        android:focusableInTouchMode="true"
+        />
+
+</LinearLayout>
diff --git a/core/res/res/values-land/dimens.xml b/core/res/res/values-land/dimens.xml
index 8f1bd9a..de1ae74 100644
--- a/core/res/res/values-land/dimens.xml
+++ b/core/res/res/values-land/dimens.xml
@@ -61,4 +61,7 @@
     Landscape's layout allows this to be smaller than for portrait. -->
     <dimen name="kg_squashed_layout_threshold">400dp</dimen>
 
+    <!-- New TimePicker dimensions. -->
+    <dimen name="timepicker_left_side_width">250dip</dimen>
+
 </resources>
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 5f5b2bc..b3e349e 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -611,6 +611,18 @@
         <!-- The TimePicker style. -->
         <attr name="timePickerStyle" format="reference" />
 
+        <!-- The TimePicker Header background color . -->
+        <attr name="timePickerHeaderBackgroundColor" format="reference" />
+
+        <!-- The TimePicker Header time label text appearance -->
+        <attr name="timePickerHeaderTimeLabelTextAppearance" format="reference" />
+
+        <!-- TimePicker Header am pm label text appearance -->
+        <attr name="timePickerHeaderAmPmLabelTextAppearance" format="reference" />
+
+        <!-- The TimePicker dialog theme. -->
+        <attr name="timePickerDialogTheme" format="reference" />
+
         <!-- The DatePicker style. -->
         <attr name="datePickerStyle" format="reference" />
 
@@ -3944,6 +3956,30 @@
     <declare-styleable name="TimePicker">
         <!-- @hide The layout of the time picker. -->
         <attr name="internalLayout" />
+        <!-- @hide The layout of the legacy time picker. -->
+        <attr name="legacyLayout" format="reference" />
+        <!-- @hide Enables or disable the use of the legacy layout for the TimePicker. -->
+        <attr name="legacyMode" format="boolean" />
+        <!-- @hide The color when the non legacy TimePicker is disabled. -->
+        <attr name="disabledColor" format="color|reference" />
+        <!-- @hide The color for selected text of the non legacy TimePicker. -->
+        <attr name="headerSelectedTextColor" format="color|reference" />
+        <!-- @hide The color for unselected text of the non legacy TimePicker. -->
+        <attr name="headerUnselectedTextColor" format="color|reference" />
+        <!-- @hide The background color for the header of the non legacy TimePicker. -->
+        <attr name="headerBackgroundColor" format="color|reference" />
+        <!-- @hide The color for the hours/minutes numbers of the non legacy TimePicker. -->
+        <attr name="numbersTextColor" format="color|reference" />
+        <!-- @hide The background color for the hours/minutes numbers of the non legacy TimePicker. -->
+        <attr name="numbersBackgroundColor" format="color|reference" />
+        <!-- @hide The color for the AM/PM selectors of the non legacy TimePicker. -->
+        <attr name="amPmTextColor" format="color|reference" />
+        <!-- @hide The background color for the AM/PM selectors of the non legacy TimePicker when unselected. -->
+        <attr name="amPmUnselectedBackgroundColor" format="color|reference" />
+        <!-- @hide The background color for the AM/PM selectors of the non legacy TimePicker when selected. -->
+        <attr name="amPmSelectedBackgroundColor" format="color|reference" />
+        <!-- @hide The color for the hours/minutes selector of the non legacy TimePicker. -->
+        <attr name="numbersSelectorColor" format="color|reference" />
     </declare-styleable>
 
     <!-- ========================= -->
diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml
index 81ee3af..14a817e 100644
--- a/core/res/res/values/colors.xml
+++ b/core/res/res/values/colors.xml
@@ -201,5 +201,22 @@
     <color name="keyguard_avatar_frame_pressed_color">#ff35b5e5</color>
 
     <color name="accessibility_focus_highlight">#80ffff00</color>
+
+    <!-- New TimePicker colors -->
+    <color name="timepicker_default_background_holo_light">@android:color/white</color>
+    <color name="timepicker_default_background_holo_dark">#ff303030</color>
+
+    <color name="timepicker_default_text_color_holo_light">#8c8c8c</color>
+    <color name="timepicker_default_text_color_holo_dark">@android:color/white</color>
+
+    <color name="timepicker_default_disabled_color_holo_light">#7f000000</color>
+    <color name="timepicker_default_disabled_color_holo_dark">#7f08c8c8</color>
+
+    <color name="timepicker_default_ampm_selected_background_color_holo_light">@android:color/holo_blue_light</color>
+    <color name="timepicker_default_ampm_selected_background_color_holo_dark">@android:color/holo_blue_light</color>
+
+    <color name="timepicker_default_ampm_unselected_background_color_holo_light">@android:color/white</color>
+    <color name="timepicker_default_ampm_unselected_background_color_holo_dark">@android:color/transparent</color>
+
 </resources>
 
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index b3cb2a1..f881cf4 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -508,6 +508,12 @@
         <item>40</item>
     </integer-array>
 
+    <!-- Vibrator pattern for feedback when selecting an hour/minute tick of a Clock -->
+    <integer-array name="config_clockTickVibePattern">
+        <item>125</item>
+        <item>5</item>
+    </integer-array>
+
     <!-- Vibrator pattern for feedback about booting with safe mode disabled -->
     <integer-array name="config_safeModeDisabledVibePattern">
         <item>0</item>
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index e902354..82c088e 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -356,4 +356,29 @@
     <!-- Outline width for video subtitles. -->
     <dimen name="subtitle_outline_width">2dp</dimen>
 
+    <!-- New TimePicker dimensions. -->
+    <item name="timepicker_circle_radius_multiplier" format="float" type="string">0.82</item>
+    <item name="timepicker_circle_radius_multiplier_24HourMode" format="float" type="string">0.85</item>
+    <item name="timepicker_selection_radius_multiplier" format="float" type="string">0.16</item>
+    <item name="timepicker_ampm_circle_radius_multiplier" format="float" type="string">0.19</item>
+    <item name="timepicker_numbers_radius_multiplier_normal" format="float" type="string">0.81</item>
+    <item name="timepicker_numbers_radius_multiplier_inner" format="float" type="string">0.60</item>
+    <item name="timepicker_numbers_radius_multiplier_outer" format="float" type="string">0.83</item>
+    <item name="timepicker_text_size_multiplier_normal" format="float" type="string">0.17</item>
+    <item name="timepicker_text_size_multiplier_inner" format="float" type="string">0.14</item>
+    <item name="timepicker_text_size_multiplier_outer" format="float" type="string">0.11</item>
+    <item name="timepicker_transition_mid_radius_multiplier" format="float" type="string">0.95</item>
+    <item name="timepicker_transition_end_radius_multiplier" format="float" type="string">1.3</item>
+
+    <dimen name="timepicker_time_label_size">60sp</dimen>
+    <dimen name="timepicker_extra_time_label_margin">-30dp</dimen>
+    <dimen name="timepicker_ampm_label_size">16sp</dimen>
+    <dimen name="timepicker_done_label_size">14sp</dimen>
+    <dimen name="timepicker_ampm_left_padding">6dip</dimen>
+    <dimen name="timepicker_separator_padding">4dip</dimen>
+    <dimen name="timepicker_header_height">96dip</dimen>
+    <dimen name="timepicker_minimum_margin_sides">48dip</dimen>
+    <dimen name="timepicker_minimum_margin_top_bottom">24dip</dimen>
+    <dimen name="timepicker_radial_picker_dimen">270dip</dimen>
+
 </resources>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index bcde4d9..315ef20 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -4480,4 +4480,55 @@
     <!-- Toast bar message when hiding the transient navigation bar [CHAR LIMIT=45] -->
     <string name="transient_navigation_confirmation">Swipe down from the top to exit full screen</string>
 
+    <!-- Label for button to confirm chosen date or time [CHAR LIMIT=30] -->
+    <string name="done_label">Done</string>
+    <!--
+         Content description for the hour selector in the time picker, which displays
+         selectable hours of the day along the inside edge of a circle, as in an analog clock.
+         [CHAR LIMIT=50]
+    -->
+    <string name="hour_picker_description">Hours circular slider</string>
+    <!--
+         Content description for the minute selector in the time picker, which displays
+         selectable five-minute intervals along the inside edge of a circle, as in an analog clock.
+         [CHAR LIMIT=50]
+    -->
+    <string name="minute_picker_description">Minutes circular slider</string>
+    <!-- Accessibility announcement for hour circular picker [CHAR LIMIT=NONE] -->
+    <string name="select_hours">Select hours</string>
+    <!-- Accessibility announcement for minute circular picker [CHAR LIMIT=NONE] -->
+    <string name="select_minutes">Select minutes</string>
+
+    <!--
+        Content description for the month and day selector in the date picker, which displays
+        a selectable grid of days laid out by month.
+        [CHAR LIMIT=50]
+     -->
+    <string name="day_picker_description">Month grid of days</string>
+    <!--
+        Content description for the year selector in the date picker, which displays
+        a scrolling, vertical list of years.
+        [CHAR LIMIT=50]
+     -->
+    <string name="year_picker_description">Year list</string>
+    <!-- Accessibility announcement for the day picker [CHAR LIMIT=NONE] -->
+    <string name="select_day">Select month and day</string>
+    <!-- Accessibility announcement for the year picker [CHAR LIMIT=NONE] -->
+    <string name="select_year">Select year</string>
+    <!-- Accessibility description for the item that is currently selected. -->
+    <string name="item_is_selected"><xliff:g id="item" example="2013">%1$s</xliff:g> selected</string>
+    <!-- Accessibility announcement when a number that had been typed in is deleted [CHAR_LIMIT=NONE] -->
+    <string name="deleted_key"><xliff:g id="key" example="4">%1$s</xliff:g> deleted</string>
+
+    <!-- DO NOT TRANSLATE -->
+    <string name="time_placeholder">--</string>
+
+    <!-- DO NOT TRANSLATE -->
+    <string name="radial_numbers_typeface">sans-serif</string>
+    <!-- DO NOT TRANSLATE -->
+    <string name="sans_serif">sans-serif</string>
+
+    <!-- DO NOT TRANSLATE -->
+    <string name="day_of_week_label_typeface">sans-serif</string>
+
 </resources>
diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml
index d3ed0d0..78dd838 100644
--- a/core/res/res/values/styles.xml
+++ b/core/res/res/values/styles.xml
@@ -555,7 +555,7 @@
     </style>
 
     <style name="Widget.TimePicker">
-        <item name="android:internalLayout">@android:layout/time_picker</item>
+        <item name="android:legacyLayout">@android:layout/time_picker_legacy</item>
     </style>
 
     <style name="Widget.DatePicker">
@@ -1706,7 +1706,18 @@
     </style>
 
     <style name="Widget.Holo.TimePicker" parent="Widget.TimePicker">
+        <item name="android:legacyLayout">@android:layout/time_picker_legacy_holo</item>
         <item name="android:internalLayout">@android:layout/time_picker_holo</item>
+        <item name="android:disabledColor">@android:color/timepicker_default_disabled_color_holo_dark</item>
+        <item name="android:headerSelectedTextColor">@android:color/holo_blue_light</item>
+        <item name="android:headerUnselectedTextColor">@android:color/timepicker_default_text_color_holo_dark</item>
+        <item name="android:headerBackgroundColor">@android:color/timepicker_default_background_holo_dark</item>
+        <item name="android:numbersTextColor">@android:color/timepicker_default_text_color_holo_dark</item>
+        <item name="android:numbersBackgroundColor">@android:color/timepicker_default_background_holo_dark</item>
+        <item name="android:amPmTextColor">@android:color/timepicker_default_text_color_holo_dark</item>
+        <item name="android:amPmUnselectedBackgroundColor">@android:color/timepicker_default_background_holo_dark</item>
+        <item name="android:amPmSelectedBackgroundColor">@android:color/holo_blue_light</item>
+        <item name="android:numbersSelectorColor">@android:color/holo_blue_light</item>
     </style>
 
     <style name="Widget.Holo.DatePicker" parent="Widget.DatePicker">
@@ -2127,7 +2138,19 @@
     <style name="Widget.Holo.Light.NumberPicker" parent="Widget.Holo.NumberPicker">
     </style>
 
-    <style name="Widget.Holo.Light.TimePicker" parent="Widget.Holo.TimePicker">
+    <style name="Widget.Holo.Light.TimePicker" parent="Widget.TimePicker">
+        <item name="android:legacyLayout">@android:layout/time_picker_legacy_holo</item>
+        <item name="android:internalLayout">@android:layout/time_picker_holo</item>
+        <item name="android:disabledColor">@android:color/timepicker_default_disabled_color_holo_light</item>
+        <item name="android:headerSelectedTextColor">@android:color/holo_blue_light</item>
+        <item name="android:headerUnselectedTextColor">@android:color/timepicker_default_text_color_holo_light</item>
+        <item name="android:headerBackgroundColor">@android:color/timepicker_default_background_holo_light</item>
+        <item name="android:numbersTextColor">@android:color/timepicker_default_text_color_holo_light</item>
+        <item name="android:numbersBackgroundColor">@android:color/timepicker_default_background_holo_light</item>
+        <item name="android:amPmTextColor">@android:color/timepicker_default_text_color_holo_light</item>
+        <item name="android:amPmUnselectedBackgroundColor">@android:color/timepicker_default_background_holo_light</item>
+        <item name="android:amPmSelectedBackgroundColor">@android:color/holo_blue_light</item>
+        <item name="android:numbersSelectorColor">@android:color/holo_blue_light</item>
     </style>
 
     <style name="Widget.Holo.Light.DatePicker" parent="Widget.Holo.DatePicker">
@@ -2542,4 +2565,34 @@
         <item name="android:textColor">#80ffffff</item>
     </style>
 
+    <style name="TextAppearance.TimePicker.TimeLabel" parent="TextAppearance">
+    </style>
+
+    <style name="TextAppearance.TimePicker.AmPmLabel" parent="TextAppearance">
+    </style>
+
+    <style name="TextAppearance.Holo.TimePicker.TimeLabel" parent="TextAppearance.Holo">
+        <item name="android:textSize">@dimen/timepicker_time_label_size</item>
+        <item name="android:textColor">@android:color/timepicker_default_text_color_holo_dark</item>
+    </style>
+
+    <style name="TextAppearance.Holo.TimePicker.AmPmLabel" parent="TextAppearance.Holo">
+        <item name="android:textSize">@dimen/timepicker_ampm_label_size</item>
+        <item name="android:textAllCaps">true</item>
+        <item name="android:textColor">@android:color/timepicker_default_text_color_holo_dark</item>
+        <item name="android:textStyle">bold</item>
+    </style>
+
+    <style name="TextAppearance.Holo.Light.TimePicker.TimeLabel" parent="TextAppearance.Holo.Light">
+        <item name="android:textSize">@dimen/timepicker_time_label_size</item>
+        <item name="android:textColor">@color/timepicker_default_text_color_holo_light</item>
+    </style>
+
+    <style name="TextAppearance.Holo.Light.TimePicker.AmPmLabel" parent="TextAppearance.Holo.Light">
+        <item name="android:textSize">@dimen/timepicker_ampm_label_size</item>
+        <item name="android:textAllCaps">true</item>
+        <item name="android:textColor">@color/timepicker_default_text_color_holo_light</item>
+        <item name="android:textStyle">bold</item>
+    </style>
+
 </resources>
diff --git a/core/res/res/values/styles_device_defaults.xml b/core/res/res/values/styles_device_defaults.xml
index a6512d0..eca639f 100644
--- a/core/res/res/values/styles_device_defaults.xml
+++ b/core/res/res/values/styles_device_defaults.xml
@@ -735,4 +735,22 @@
     <style name="Widget.DeviceDefault.MediaRouteButton" parent="Widget.Holo.MediaRouteButton" />
     <style name="Widget.DeviceDefault.Light.MediaRouteButton" parent="Widget.Holo.Light.MediaRouteButton" />
 
+    <style name="TextAppearance.DeviceDefault.TimePicker.TimeLabel" parent="TextAppearance.Holo.TimePicker.TimeLabel">
+    </style>
+
+    <style name="TextAppearance.DeviceDefault.Light.TimePicker.TimeLabel" parent="TextAppearance.Holo.Light.TimePicker.TimeLabel">
+    </style>
+
+    <style name="TextAppearance.DeviceDefault.TimePicker.AmPmLabel" parent="TextAppearance.Holo.TimePicker.AmPmLabel">
+    </style>
+
+    <style name="TextAppearance.DeviceDefault.Light.TimePicker.AmPmLabel" parent="TextAppearance.Holo.Light.TimePicker.AmPmLabel">
+    </style>
+
+    <style name="Theme.DeviceDefault.Dialog.TimePicker" parent="Theme.Holo.Dialog.TimePicker">
+    </style>
+
+    <style name="Theme.DeviceDefault.Light.Dialog.TimePicker" parent="Theme.Holo.Light.Dialog.TimePicker">
+    </style>
+
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 12ab193..e66dcd1 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1161,7 +1161,7 @@
   <java-symbol type="layout" name="tab_content" />
   <java-symbol type="layout" name="tab_indicator_holo" />
   <java-symbol type="layout" name="textview_hint" />
-  <java-symbol type="layout" name="time_picker" />
+  <java-symbol type="layout" name="time_picker_legacy" />
   <java-symbol type="layout" name="time_picker_dialog" />
   <java-symbol type="layout" name="toast_bar" />
   <java-symbol type="layout" name="transient_notification" />
@@ -1735,4 +1735,55 @@
   <java-symbol type="dimen" name="subtitle_shadow_radius" />
   <java-symbol type="dimen" name="subtitle_shadow_offset" />
   <java-symbol type="dimen" name="subtitle_outline_width" />
+
+  <!-- From the new TimePicker -->
+  <java-symbol type="attr" name="timePickerHeaderBackgroundColor" />
+  <java-symbol type="attr" name="timePickerDialogTheme" />
+  <java-symbol type="attr" name="headerSelectedTextColor" />
+  <java-symbol type="attr" name="headerUnselectedTextColor" />
+  <java-symbol type="attr" name="numbersTextColor" />
+  <java-symbol type="attr" name="numbersBackgroundColor" />
+  <java-symbol type="attr" name="amPmTextColor" />
+  <java-symbol type="attr" name="amPmUnselectedBackgroundColor" />
+  <java-symbol type="attr" name="amPmSelectedBackgroundColor" />
+  <java-symbol type="attr" name="numbersSelectorColor" />
+  <java-symbol type="attr" name="timePickerHeaderTimeLabelTextAppearance" />
+  <java-symbol type="style" name="TextAppearance.Holo.TimePicker.TimeLabel" />
+  <java-symbol type="layout" name="time_picker_holo" />
+  <java-symbol type="layout" name="time_header_label" />
+  <java-symbol type="id" name="time_header" />
+  <java-symbol type="id" name="hours" />
+  <java-symbol type="id" name="minutes" />
+  <java-symbol type="id" name="ampm_label" />
+  <java-symbol type="id" name="radial_picker" />
+  <java-symbol type="id" name="separator" />
+  <java-symbol type="id" name="layout_buttons" />
+  <java-symbol type="id" name="done_button" />
+  <java-symbol type="string" name="done_label" />
+  <java-symbol type="string" name="hour_picker_description" />
+  <java-symbol type="string" name="minute_picker_description" />
+  <java-symbol type="string" name="select_hours" />
+  <java-symbol type="string" name="select_minutes" />
+  <java-symbol type="string" name="time_placeholder" />
+  <java-symbol type="string" name="timepicker_circle_radius_multiplier" />
+  <java-symbol type="string" name="timepicker_circle_radius_multiplier_24HourMode" />
+  <java-symbol type="string" name="timepicker_ampm_circle_radius_multiplier" />
+  <java-symbol type="string" name="deleted_key" />
+  <java-symbol type="string" name="sans_serif" />
+  <java-symbol type="string" name="radial_numbers_typeface" />
+  <java-symbol type="string" name="timepicker_text_size_multiplier_inner" />
+  <java-symbol type="string" name="timepicker_text_size_multiplier_outer" />
+  <java-symbol type="string" name="timepicker_text_size_multiplier_normal" />
+  <java-symbol type="string" name="timepicker_numbers_radius_multiplier_outer" />
+  <java-symbol type="string" name="timepicker_selection_radius_multiplier" />
+  <java-symbol type="string" name="timepicker_numbers_radius_multiplier_inner" />
+  <java-symbol type="string" name="timepicker_numbers_radius_multiplier_normal" />
+  <java-symbol type="string" name="timepicker_transition_mid_radius_multiplier" />
+  <java-symbol type="string" name="timepicker_transition_end_radius_multiplier" />
+  <java-symbol type="color" name="timepicker_default_text_color_holo_light" />
+  <java-symbol type="color" name="timepicker_default_disabled_color_holo_light" />
+  <java-symbol type="color" name="timepicker_default_ampm_unselected_background_color_holo_light" />
+  <java-symbol type="color" name="timepicker_default_ampm_selected_background_color_holo_light" />
+  <java-symbol type="array" name="config_clockTickVibePattern" />
+
 </resources>
diff --git a/core/res/res/values/themes.xml b/core/res/res/values/themes.xml
index ccd59c8..8715c80 100644
--- a/core/res/res/values/themes.xml
+++ b/core/res/res/values/themes.xml
@@ -380,6 +380,18 @@
         <!-- TimePicker style -->
         <item name="timePickerStyle">@style/Widget.TimePicker</item>
 
+        <!-- TimePicker background color -->
+        <item name="timePickerHeaderBackgroundColor">@android:color/darker_gray</item>
+
+        <!-- TimePicker Header time label text appearance -->
+        <item name="timePickerHeaderTimeLabelTextAppearance">@style/TextAppearance.TimePicker.TimeLabel</item>
+
+        <!-- TimePicker Header am pm label text appearance -->
+        <item name="timePickerHeaderAmPmLabelTextAppearance">@style/TextAppearance.TimePicker.AmPmLabel</item>
+
+        <!-- TimePicker dialog theme -->
+        <item name="timePickerDialogTheme">@android:style/Theme.Dialog.TimePicker</item>
+
         <!-- DatePicker style -->
         <item name="datePickerStyle">@style/Widget.DatePicker</item>
 
@@ -686,7 +698,15 @@
         <item name="textAppearanceListItem">@android:style/TextAppearance.Large.Inverse</item>
         <item name="textAppearanceListItemSmall">@android:style/TextAppearance.Large.Inverse</item>
     </style>
-    
+
+    <!-- Default heme for the TimePicker dialog windows, which is used by the
+         {@link android.app.TimePickerDialog} class. -->
+    <style name="Theme.Dialog.TimePicker">
+        <item name="windowBackground">@android:color/transparent</item>
+        <item name="windowTitleStyle">@android:style/DialogWindowTitle</item>
+        <item name="windowContentOverlay">@null</item>
+    </style>
+
     <!-- Default dark theme for panel windows (on API level 10 and lower).  This removes all
          extraneous window decorations, so you basically have an empty rectangle in which
          to place your content.  It makes the window floating, with a transparent
@@ -1201,6 +1221,18 @@
         <!-- TimePicker style -->
         <item name="timePickerStyle">@style/Widget.Holo.TimePicker</item>
 
+        <!-- TimePicker background color -->
+        <item name="timePickerHeaderBackgroundColor">@android:color/timepicker_default_background_holo_dark</item>
+
+        <!-- TimePicker Header time label text appearance -->
+        <item name="timePickerHeaderTimeLabelTextAppearance">@style/TextAppearance.Holo.TimePicker.TimeLabel</item>
+
+        <!-- TimePicker Header am pm label text appearance -->
+        <item name="timePickerHeaderAmPmLabelTextAppearance">@style/TextAppearance.Holo.TimePicker.AmPmLabel</item>
+
+        <!-- TimePicker dialog theme -->
+        <item name="timePickerDialogTheme">@android:style/Theme.Holo.Dialog.TimePicker</item>
+
         <!-- DatePicker style -->
         <item name="datePickerStyle">@style/Widget.Holo.DatePicker</item>
 
@@ -1517,6 +1549,18 @@
         <!-- TimePicker style -->
         <item name="timePickerStyle">@style/Widget.Holo.Light.TimePicker</item>
 
+        <!-- TimePicker Header background color -->
+        <item name="timePickerHeaderBackgroundColor">@android:color/timepicker_default_background_holo_light</item>
+
+        <!-- TimePicker Header time label text appearance -->
+        <item name="timePickerHeaderTimeLabelTextAppearance">@style/TextAppearance.Holo.Light.TimePicker.TimeLabel</item>
+
+        <!-- TimePicker Header am pm label text appearance -->
+        <item name="timePickerHeaderAmPmLabelTextAppearance">@style/TextAppearance.Holo.Light.TimePicker.AmPmLabel</item>
+
+        <!-- TimePicker dialog theme -->
+        <item name="timePickerDialogTheme">@android:style/Theme.Holo.Light.Dialog.TimePicker</item>
+
         <!-- DatePicker style -->
         <item name="datePickerStyle">@style/Widget.Holo.Light.DatePicker</item>
 
@@ -1711,6 +1755,14 @@
         <item name="android:windowMinWidthMinor">@android:dimen/dialog_min_width_minor</item>
     </style>
 
+    <!-- Holo theme for the TimePicker dialog windows, which is used by the
+         {@link android.app.TimePickerDialog} class. -->
+    <style name="Theme.Holo.Dialog.TimePicker">
+        <item name="windowBackground">@android:color/transparent</item>
+        <item name="windowTitleStyle">@android:style/DialogWindowTitle.Holo</item>
+        <item name="windowContentOverlay">@null</item>
+    </style>
+
     <!-- Theme for a window that will be displayed either full-screen on
          smaller screens (small, normal) or as a dialog on larger screens
          (large, xlarge). -->
@@ -1826,6 +1878,14 @@
         <item name="android:windowMinWidthMinor">@android:dimen/dialog_min_width_minor</item>
     </style>
 
+    <!-- Holo Light theme for the TimePicker dialog windows, which is used by the
+         {@link android.app.TimePickerDialog} class. -->
+    <style name="Theme.Holo.Light.Dialog.TimePicker">
+        <item name="windowBackground">@android:color/transparent</item>
+        <item name="windowTitleStyle">@android:style/DialogWindowTitle.Holo.Light</item>
+        <item name="windowContentOverlay">@null</item>
+    </style>
+
     <!-- Theme for a presentation window on a secondary display. -->
     <style name="Theme.Holo.Light.Dialog.Presentation" parent="@android:style/Theme.Holo.Light.NoActionBar.Fullscreen" >
     </style>
diff --git a/core/res/res/values/themes_device_defaults.xml b/core/res/res/values/themes_device_defaults.xml
index 87b1c9d..327867a 100644
--- a/core/res/res/values/themes_device_defaults.xml
+++ b/core/res/res/values/themes_device_defaults.xml
@@ -194,6 +194,15 @@
         <!-- TimePicker style -->
         <item name="timePickerStyle">@style/Widget.DeviceDefault.TimePicker</item>
 
+        <!-- TimePicker Header time label text appearance -->
+        <item name="timePickerHeaderTimeLabelTextAppearance">@style/TextAppearance.DeviceDefault.TimePicker.TimeLabel</item>
+
+        <!-- TimePicker Header am pm label text appearance -->
+        <item name="timePickerHeaderAmPmLabelTextAppearance">@style/TextAppearance.DeviceDefault.TimePicker.AmPmLabel</item>
+
+        <!-- TimePicker dialog theme -->
+        <item name="timePickerDialogTheme">@android:style/Theme.DeviceDefault.Dialog.TimePicker</item>
+
         <!-- DatePicker style -->
         <item name="datePickerStyle">@style/Widget.DeviceDefault.DatePicker</item>
 
@@ -357,6 +366,15 @@
         <!-- TimePicker style -->
         <item name="timePickerStyle">@style/Widget.DeviceDefault.Light.TimePicker</item>
 
+        <!-- TimePicker Header time label text appearance -->
+        <item name="timePickerHeaderTimeLabelTextAppearance">@style/TextAppearance.DeviceDefault.Light.TimePicker.TimeLabel</item>
+
+        <!-- TimePicker Header am pm label text appearance -->
+        <item name="timePickerHeaderAmPmLabelTextAppearance">@style/TextAppearance.DeviceDefault.Light.TimePicker.AmPmLabel</item>
+
+        <!-- TimePicker dialog theme -->
+        <item name="timePickerDialogTheme">@android:style/Theme.DeviceDefault.Light.Dialog.TimePicker</item>
+
         <!-- DatePicker style -->
         <item name="datePickerStyle">@style/Widget.DeviceDefault.Light.DatePicker</item>
 
diff --git a/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java b/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java
index 9f9b6d6..248112f 100644
--- a/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java
+++ b/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java
@@ -221,6 +221,9 @@
     // Vibrator pattern for a short vibration.
     long[] mKeyboardTapVibePattern;
 
+    // Vibrator pattern for a short vibration when tapping on an hour/minute tick of a Clock.
+    long[] mClockTickVibePattern;
+
     // Vibrator pattern for haptic feedback during boot when safe mode is disabled.
     long[] mSafeModeDisabledVibePattern;
 
@@ -958,6 +961,8 @@
                 com.android.internal.R.array.config_virtualKeyVibePattern);
         mKeyboardTapVibePattern = getLongIntArray(mContext.getResources(),
                 com.android.internal.R.array.config_keyboardTapVibePattern);
+        mClockTickVibePattern = getLongIntArray(mContext.getResources(),
+                com.android.internal.R.array.config_clockTickVibePattern);
         mSafeModeDisabledVibePattern = getLongIntArray(mContext.getResources(),
                 com.android.internal.R.array.config_safeModeDisabledVibePattern);
         mSafeModeEnabledVibePattern = getLongIntArray(mContext.getResources(),
@@ -4945,6 +4950,9 @@
             case HapticFeedbackConstants.KEYBOARD_TAP:
                 pattern = mKeyboardTapVibePattern;
                 break;
+            case HapticFeedbackConstants.CLOCK_TICK:
+                pattern = mClockTickVibePattern;
+                break;
             case HapticFeedbackConstants.SAFE_MODE_DISABLED:
                 pattern = mSafeModeDisabledVibePattern;
                 break;