content: /*
   * 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 com.android.datetimepicker.time;
  
  import android.animation.AnimatorSet;
  import android.animation.ObjectAnimator;
  import android.annotation.SuppressLint;
  import android.content.Context;
  import android.content.res.Resources;
  import android.os.Bundle;
  import android.os.Handler;
  import android.text.format.DateUtils;
  import android.text.format.Time;
  import android.util.AttributeSet;
  import android.util.Log;
  import android.view.MotionEvent;
  import android.view.View;
  import android.view.View.OnTouchListener;
  import android.view.ViewConfiguration;
  import android.view.ViewGroup;
  import android.view.accessibility.AccessibilityEvent;
  import android.view.accessibility.AccessibilityManager;
  import android.view.accessibility.AccessibilityNodeInfo;
  import android.widget.FrameLayout;
  
  import com.android.datetimepicker.HapticFeedbackController;
  import com.android.datetimepicker.R;
  
  /**
   * The primary layout to hold the circular picker, and the am/pm buttons. This view well measure
   * itself to end up as a square. It also handles touches to be passed in to views that need to know
   * when they'd been touched.
   *
   * @deprecated This module is deprecated. Do not use this class.
   */
  public class RadialPickerLayout extends FrameLayout implements OnTouchListener {
      private static final String TAG = "RadialPickerLayout";
  
      private final int TOUCH_SLOP;
      private final int TAP_TIMEOUT;
  
      private static final int VISIBLE_DEGREES_STEP_SIZE = 30;
      private static final int HOUR_VALUE_TO_DEGREES_STEP_SIZE = VISIBLE_DEGREES_STEP_SIZE;
      private static final int MINUTE_VALUE_TO_DEGREES_STEP_SIZE = 6;
      private static final int HOUR_INDEX = TimePickerDialog.HOUR_INDEX;
      private static final int MINUTE_INDEX = TimePickerDialog.MINUTE_INDEX;
      private static final int AMPM_INDEX = TimePickerDialog.AMPM_INDEX;
      private static final int ENABLE_PICKER_INDEX = TimePickerDialog.ENABLE_PICKER_INDEX;
      private static final int AM = TimePickerDialog.AM;
      private static final int PM = TimePickerDialog.PM;
  
      private int mLastValueSelected;
  
      private HapticFeedbackController mHapticFeedbackController;
      private OnValueSelectedListener mListener;
      private boolean mTimeInitialized;
      private int mCurrentHoursOfDay;
      private int mCurrentMinutes;
      private boolean mIs24HourMode;
      private boolean mHideAmPm;
      private int mCurrentItemShowing;
  
      private CircleView mCircleView;
      private AmPmCirclesView mAmPmCirclesView;
      private RadialTextsView mHourRadialTextsView;
      private RadialTextsView mMinuteRadialTextsView;
      private RadialSelectorView mHourRadialSelectorView;
      private RadialSelectorView mMinuteRadialSelectorView;
      private View mGrayBox;
  
      private int[] mSnapPrefer30sMap;
      private boolean mInputEnabled;
      private int mIsTouchingAmOrPm = -1;
      private boolean mDoingMove;
      private boolean mDoingTouch;
      private int mDownDegrees;
      private float mDownX;
      private float mDownY;
      private AccessibilityManager mAccessibilityManager;
  
      private AnimatorSet mTransition;
      private Handler mHandler = new Handler();
  
      public interface OnValueSelectedListener {
          void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance);
      }
  
      public RadialPickerLayout(Context context, AttributeSet attrs) {
          super(context, attrs);
  
          setOnTouchListener(this);
          ViewConfiguration vc = ViewConfiguration.get(context);
          TOUCH_SLOP = vc.getScaledTouchSlop();
          TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
          mDoingMove = false;
  
          mCircleView = new CircleView(context);
          addView(mCircleView);
  
          mAmPmCirclesView = new AmPmCirclesView(context);
          addView(mAmPmCirclesView);
  
          mHourRadialTextsView = new RadialTextsView(context);
          addView(mHourRadialTextsView);
          mMinuteRadialTextsView = new RadialTextsView(context);
          addView(mMinuteRadialTextsView);
  
          mHourRadialSelectorView = new RadialSelectorView(context);
          addView(mHourRadialSelectorView);
          mMinuteRadialSelectorView = new RadialSelectorView(context);
          addView(mMinuteRadialSelectorView);
  
          // Prepare mapping to snap touchable degrees to selectable degrees.
          preparePrefer30sMap();
  
          mLastValueSelected = -1;
  
          mInputEnabled = true;
          mGrayBox = new View(context);
          mGrayBox.setLayoutParams(new ViewGroup.LayoutParams(
                  ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
          mGrayBox.setBackgroundColor(getResources().getColor(R.color.transparent_black));
          mGrayBox.setVisibility(View.INVISIBLE);
          addView(mGrayBox);
  
          mAccessibilityManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
  
          mTimeInitialized = false;
      }
  
      /**
       * 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 setOnValueSelectedListener(OnValueSelectedListener listener) {
          mListener = listener;
      }
  
      /**
       * Initialize the Layout with starting values.
       * @param context
       * @param initialHoursOfDay
       * @param initialMinutes
       * @param is24HourMode
       */
      public void initialize(Context context, HapticFeedbackController hapticFeedbackController,
              int initialHoursOfDay, int initialMinutes, boolean is24HourMode) {
          if (mTimeInitialized) {
              Log.e(TAG, "Time has already been initialized.");
              return;
          }
  
          mHapticFeedbackController = hapticFeedbackController;
          mIs24HourMode = is24HourMode;
          mHideAmPm = mAccessibilityManager.isTouchExplorationEnabled()? true : mIs24HourMode;
  
          // Initialize the circle and AM/PM circles if applicable.
          mCircleView.initialize(context, mHideAmPm);
          mCircleView.invalidate();
          if (!mHideAmPm) {
              mAmPmCirclesView.initialize(context, initialHoursOfDay < 12? AM : PM);
              mAmPmCirclesView.invalidate();
          }
  
          // Initialize the hours and minutes numbers.
          Resources res = context.getResources();
          int[] hours = {12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
          int[] hours_24 = {0, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23};
          int[] minutes = {0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55};
          String[] hoursTexts = new String[12];
          String[] innerHoursTexts = new String[12];
          String[] minutesTexts = new String[12];
          for (int i = 0; i < 12; i++) {
              hoursTexts[i] = is24HourMode?
                      String.format("%02d", hours_24[i]) : String.format("%d", hours[i]);
              innerHoursTexts[i] = String.format("%d", hours[i]);
              minutesTexts[i] = String.format("%02d", minutes[i]);
          }
          mHourRadialTextsView.initialize(res,
                  hoursTexts, (is24HourMode? innerHoursTexts : null), mHideAmPm, true);
          mHourRadialTextsView.invalidate();
          mMinuteRadialTextsView.initialize(res, minutesTexts, null, mHideAmPm, false);
          mMinuteRadialTextsView.invalidate();
  
          // Initialize the currently-selected hour and minute.
          setValueForItem(HOUR_INDEX, initialHoursOfDay);
          setValueForItem(MINUTE_INDEX, initialMinutes);
          int hourDegrees = (initialHoursOfDay % 12) * HOUR_VALUE_TO_DEGREES_STEP_SIZE;
          mHourRadialSelectorView.initialize(context, mHideAmPm, is24HourMode, true,
                  hourDegrees, isHourInnerCircle(initialHoursOfDay));
          int minuteDegrees = initialMinutes * MINUTE_VALUE_TO_DEGREES_STEP_SIZE;
          mMinuteRadialSelectorView.initialize(context, mHideAmPm, false, false,
                  minuteDegrees, false);
  
          mTimeInitialized = true;
      }
  
      /* package */ void setTheme(Context context, boolean themeDark) {
          mCircleView.setTheme(context, themeDark);
          mAmPmCirclesView.setTheme(context, themeDark);
          mHourRadialTextsView.setTheme(context, themeDark);
          mMinuteRadialTextsView.setTheme(context, themeDark);
          mHourRadialSelectorView.setTheme(context, themeDark);
          mMinuteRadialSelectorView.setTheme(context, themeDark);
     }
  
      public void setTime(int hours, int minutes) {
          setItem(HOUR_INDEX, hours);
          setItem(MINUTE_INDEX, minutes);
      }
  
      /**
       * Set either the hour or the minute. Will set the internal value, and set the selection.
       */
      private void setItem(int index, int value) {
          if (index == HOUR_INDEX) {
              setValueForItem(HOUR_INDEX, value);
              int hourDegrees = (value % 12) * HOUR_VALUE_TO_DEGREES_STEP_SIZE;
              mHourRadialSelectorView.setSelection(hourDegrees, isHourInnerCircle(value), false);
              mHourRadialSelectorView.invalidate();
          } else if (index == MINUTE_INDEX) {
              setValueForItem(MINUTE_INDEX, value);
              int minuteDegrees = value * MINUTE_VALUE_TO_DEGREES_STEP_SIZE;
              mMinuteRadialSelectorView.setSelection(minuteDegrees, false, false);
              mMinuteRadialSelectorView.invalidate();
          }
      }
  
      /**
       * Check if a given hour appears in the outer circle or the inner circle
       * @return true if the hour is in the inner circle, false if it's in the outer circle.
       */
      private boolean isHourInnerCircle(int hourOfDay) {
          // We'll have the 00 hours on the outside circle.
          return mIs24HourMode && (hourOfDay <= 12 && hourOfDay != 0);
      }
  
      public int getHours() {
          return mCurrentHoursOfDay;
      }
  
      public int getMinutes() {
          return mCurrentMinutes;
      }
  
      /**
       * If the hours are showing, return the current hour. If the minutes are showing, return the
       * current minute.
       */
      private int getCurrentlyShowingValue() {
          int currentIndex = getCurrentItemShowing();
          if (currentIndex == HOUR_INDEX) {
              return mCurrentHoursOfDay;
          } else if (currentIndex == MINUTE_INDEX) {
              return mCurrentMinutes;
          } else {
              return -1;
          }
      }
  
      public int getIsCurrentlyAmOrPm() {
          if (mCurrentHoursOfDay < 12) {
              return AM;
          } else if (mCurrentHoursOfDay < 24) {
              return PM;
          }
          return -1;
      }
  
      /**
       * Set the internal value for the hour, minute, or AM/PM.
       */
      private void setValueForItem(int index, int value) {
          if (index == HOUR_INDEX) {
              mCurrentHoursOfDay = value;
          } else if (index == MINUTE_INDEX){
              mCurrentMinutes = value;
          } else if (index == AMPM_INDEX) {
              if (value == AM) {
                  mCurrentHoursOfDay = mCurrentHoursOfDay % 12;
              } else if (value == PM) {
                  mCurrentHoursOfDay = (mCurrentHoursOfDay % 12) + 12;
              }
          }
      }
  
      /**
       * Set the internal value as either AM or PM, and update the AM/PM circle displays.
       * @param amOrPm
       */
      public void setAmOrPm(int amOrPm) {
          mAmPmCirclesView.setAmOrPm(amOrPm);
          mAmPmCirclesView.invalidate();
          setValueForItem(AMPM_INDEX, amOrPm);
      }
  
      /**
       * 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 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.
  
          // Our input will be 0 through 360.
          mSnapPrefer30sMap = new int[361];
  
          // 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.
              mSnapPrefer30sMap[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 int snapPrefer30s(int degrees) {
          if (mSnapPrefer30sMap == null) {
              return -1;
          }
          return mSnapPrefer30sMap[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 forceAboveOrBelow 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) {
          int stepSize = HOUR_VALUE_TO_DEGREES_STEP_SIZE;
          int floor = (degrees / stepSize) * stepSize;
          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;
      }
  
      /**
       * For the currently showing view (either hours or minutes), re-calculate the position for the
       * selector, and redraw it at that position. The input degrees will be snapped to a selectable
       * value.
       * @param degrees Degrees which should be selected.
       * @param isInnerCircle Whether the selection should be in the inner circle; will be ignored
       * if there is no inner circle.
       * @param forceToVisibleValue Even if the currently-showing circle allows for fine-grained
       * selection (i.e. minutes), force the selection to one of the visibly-showing values.
       * @param forceDrawDot The dot in the circle will generally only be shown when the selection
       * is on non-visible values, but use this to force the dot to be shown.
       * @return The value that was selected, i.e. 0-23 for hours, 0-59 for minutes.
       */
      private int reselectSelector(int degrees, boolean isInnerCircle,
              boolean forceToVisibleValue, boolean forceDrawDot) {
          if (degrees == -1) {
              return -1;
          }
          int currentShowing = getCurrentItemShowing();
  
          int stepSize;
          boolean allowFineGrained = !forceToVisibleValue && (currentShowing == MINUTE_INDEX);
          if (allowFineGrained) {
              degrees = snapPrefer30s(degrees);
          } else {
              degrees = snapOnly30s(degrees, 0);
          }
  
          RadialSelectorView radialSelectorView;
          if (currentShowing == HOUR_INDEX) {
              radialSelectorView = mHourRadialSelectorView;
              stepSize = HOUR_VALUE_TO_DEGREES_STEP_SIZE;
          } else {
              radialSelectorView = mMinuteRadialSelectorView;
              stepSize = MINUTE_VALUE_TO_DEGREES_STEP_SIZE;
          }
          radialSelectorView.setSelection(degrees, isInnerCircle, forceDrawDot);
          radialSelectorView.invalidate();
  
  
          if (currentShowing == HOUR_INDEX) {
              if (mIs24HourMode) {
                  if (degrees == 0 && isInnerCircle) {
                      degrees = 360;
                  } else if (degrees == 360 && !isInnerCircle) {
                      degrees = 0;
                  }
              } else if (degrees == 0) {
                  degrees = 360;
              }
          } else if (degrees == 360 && currentShowing == MINUTE_INDEX) {
              degrees = 0;
          }
  
          int value = degrees / stepSize;
          if (currentShowing == HOUR_INDEX && mIs24HourMode && !isInnerCircle && degrees != 0) {
              value += 12;
          }
          return value;
      }
  
      /**
       * Calculate the degrees within the circle that corresponds to the specified coordinates, if
       * the coordinates are within the range that will trigger a selection.
       * @param pointX The x coordinate.
       * @param pointY The y coordinate.
       * @param forceLegal Force the selection to be legal, regardless of how far the coordinates are
       * from the actual numbers.
       * @param isInnerCircle If the selection may be in the inner circle, pass in a size-1 boolean
       * array here, inside which the value will be true if the selection is in the inner circle,
       * and false if in the outer circle.
       * @return Degrees from 0 to 360, if the selection was within the legal range. -1 if not.
       */
      private int getDegreesFromCoords(float pointX, float pointY, boolean forceLegal,
              final Boolean[] isInnerCircle) {
          int currentItem = getCurrentItemShowing();
          if (currentItem == HOUR_INDEX) {
              return mHourRadialSelectorView.getDegreesFromCoords(
                      pointX, pointY, forceLegal, isInnerCircle);
          } else if (currentItem == MINUTE_INDEX) {
              return mMinuteRadialSelectorView.getDegreesFromCoords(
                      pointX, pointY, forceLegal, isInnerCircle);
          } else {
              return -1;
          }
      }
  
      /**
       * Get the item (hours or minutes) that is currently showing.
       */
      public int getCurrentItemShowing() {
          if (mCurrentItemShowing != HOUR_INDEX && mCurrentItemShowing != MINUTE_INDEX) {
              Log.e(TAG, "Current item showing was unfortunately set to "+mCurrentItemShowing);
              return -1;
          }
          return mCurrentItemShowing;
      }
  
      /**
       * Set either minutes or hours as showing.
       * @param animate True to animate the transition, false to show with no animation.
       */
      public void setCurrentItemShowing(int index, boolean animate) {
          if (index != HOUR_INDEX && index != MINUTE_INDEX) {
              Log.e(TAG, "TimePicker does not support view at index "+index);
              return;
          }
  
          int lastIndex = getCurrentItemShowing();
          mCurrentItemShowing = index;
  
          if (animate && (index != lastIndex)) {
              ObjectAnimator[] anims = new ObjectAnimator[4];
              if (index == MINUTE_INDEX) {
                  anims[0] = mHourRadialTextsView.getDisappearAnimator();
                  anims[1] = mHourRadialSelectorView.getDisappearAnimator();
                  anims[2] = mMinuteRadialTextsView.getReappearAnimator();
                  anims[3] = mMinuteRadialSelectorView.getReappearAnimator();
              } else if (index == HOUR_INDEX){
                  anims[0] = mHourRadialTextsView.getReappearAnimator();
                  anims[1] = mHourRadialSelectorView.getReappearAnimator();
                  anims[2] = mMinuteRadialTextsView.getDisappearAnimator();
                  anims[3] = mMinuteRadialSelectorView.getDisappearAnimator();
              }
  
              if (mTransition != null && mTransition.isRunning()) {
                  mTransition.end();
              }
              mTransition = new AnimatorSet();
              mTransition.playTogether(anims);
              mTransition.start();
          } else {
              int hourAlpha = (index == HOUR_INDEX) ? 255 : 0;
              int minuteAlpha = (index == MINUTE_INDEX) ? 255 : 0;
              mHourRadialTextsView.setAlpha(hourAlpha);
              mHourRadialSelectorView.setAlpha(hourAlpha);
              mMinuteRadialTextsView.setAlpha(minuteAlpha);
              mMinuteRadialSelectorView.setAlpha(minuteAlpha);
          }
  
      }
  
      @Override
      public boolean onTouch(View v, MotionEvent event) {
          final float eventX = event.getX();
          final float eventY = event.getY();
          int degrees;
          int value;
          final Boolean[] isInnerCircle = new Boolean[1];
          isInnerCircle[0] = false;
  
          switch(event.getAction()) {
              case MotionEvent.ACTION_DOWN:
                  if (!mInputEnabled) {
                      return true;
                  }
  
                  mDownX = eventX;
                  mDownY = eventY;
  
                  mLastValueSelected = -1;
                  mDoingMove = false;
                  mDoingTouch = true;
                  // If we're showing the AM/PM, check to see if the user is touching it.
                  if (!mHideAmPm) {
                      mIsTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY);
                  } else {
                      mIsTouchingAmOrPm = -1;
                  }
                  if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) {
                      // If the touch is on AM or PM, set it as "touched" after the TAP_TIMEOUT
                      // in case the user moves their finger quickly.
                      mHapticFeedbackController.tryVibrate();
                      mDownDegrees = -1;
                      mHandler.postDelayed(new Runnable() {
                          @Override
                          public void run() {
                              mAmPmCirclesView.setAmOrPmPressed(mIsTouchingAmOrPm);
                              mAmPmCirclesView.invalidate();
                          }
                      }, TAP_TIMEOUT);
                  } else {
                      // If we're in accessibility mode, force the touch to be legal. Otherwise,
                      // it will only register within the given touch target zone.
                      boolean forceLegal = mAccessibilityManager.isTouchExplorationEnabled();
                      // Calculate the degrees that is currently being touched.
                      mDownDegrees = getDegreesFromCoords(eventX, eventY, forceLegal, isInnerCircle);
                      if (mDownDegrees != -1) {
                          // If it's a legal touch, set that number as "selected" after the
                          // TAP_TIMEOUT in case the user moves their finger quickly.
                          mHapticFeedbackController.tryVibrate();
                          mHandler.postDelayed(new Runnable() {
                              @Override
                              public void run() {
                                  mDoingMove = true;
                                  int value = reselectSelector(mDownDegrees, isInnerCircle[0],
                                          false, true);
                                  mLastValueSelected = value;
                                  mListener.onValueSelected(getCurrentItemShowing(), value, false);
                              }
                          }, TAP_TIMEOUT);
                      }
                  }
                  return true;
              case MotionEvent.ACTION_MOVE:
                  if (!mInputEnabled) {
                      // We shouldn't be in this state, because input is disabled.
                      Log.e(TAG, "Input was disabled, but received ACTION_MOVE.");
                      return true;
                  }
  
                  float dY = Math.abs(eventY - mDownY);
                  float dX = Math.abs(eventX - mDownX);
  
                  if (!mDoingMove && dX <= TOUCH_SLOP && dY <= TOUCH_SLOP) {
                      // Hasn't registered down yet, just slight, accidental movement of finger.
                      break;
                  }
  
                  // If we're in the middle of touching down on AM or PM, check if we still are.
                  // If so, no-op. If not, remove its pressed state. Either way, no need to check
                  // for touches on the other circle.
                  if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) {
                      mHandler.removeCallbacksAndMessages(null);
                      int isTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY);
                      if (isTouchingAmOrPm != mIsTouchingAmOrPm) {
                          mAmPmCirclesView.setAmOrPmPressed(-1);
                          mAmPmCirclesView.invalidate();
                          mIsTouchingAmOrPm = -1;
                      }
                      break;
                  }
  
                  if (mDownDegrees == -1) {
                      // Original down was illegal, so no movement will register.
                      break;
                  }
  
                  // We're doing a move along the circle, so move the selection as appropriate.
                  mDoingMove = true;
                  mHandler.removeCallbacksAndMessages(null);
                  degrees = getDegreesFromCoords(eventX, eventY, true, isInnerCircle);
                  if (degrees != -1) {
                      value = reselectSelector(degrees, isInnerCircle[0], false, true);
                      if (value != mLastValueSelected) {
                          mHapticFeedbackController.tryVibrate();
                          mLastValueSelected = value;
                          mListener.onValueSelected(getCurrentItemShowing(), value, false);
                      }
                  }
                  return true;
              case MotionEvent.ACTION_UP:
                  if (!mInputEnabled) {
                      // If our touch input was disabled, tell the listener to re-enable us.
                      Log.d(TAG, "Input was disabled, but received ACTION_UP.");
                      mListener.onValueSelected(ENABLE_PICKER_INDEX, 1, false);
                      return true;
                  }
  
                  mHandler.removeCallbacksAndMessages(null);
                  mDoingTouch = false;
  
                  // If we're touching AM or PM, set it as selected, and tell the listener.
                  if (mIsTouchingAmOrPm == AM || mIsTouchingAmOrPm == PM) {
                      int isTouchingAmOrPm = mAmPmCirclesView.getIsTouchingAmOrPm(eventX, eventY);
                      mAmPmCirclesView.setAmOrPmPressed(-1);
                      mAmPmCirclesView.invalidate();
  
                      if (isTouchingAmOrPm == mIsTouchingAmOrPm) {
                          mAmPmCirclesView.setAmOrPm(isTouchingAmOrPm);
                          if (getIsCurrentlyAmOrPm() != isTouchingAmOrPm) {
                              mListener.onValueSelected(AMPM_INDEX, mIsTouchingAmOrPm, false);
                              setValueForItem(AMPM_INDEX, isTouchingAmOrPm);
                          }
                      }
                      mIsTouchingAmOrPm = -1;
                      break;
                  }
  
                  // If we have a legal degrees selected, set the value and tell the listener.
                  if (mDownDegrees != -1) {
                      degrees = getDegreesFromCoords(eventX, eventY, mDoingMove, isInnerCircle);
                      if (degrees != -1) {
                          value = reselectSelector(degrees, isInnerCircle[0], !mDoingMove, false);
                          if (getCurrentItemShowing() == HOUR_INDEX && !mIs24HourMode) {
                              int amOrPm = getIsCurrentlyAmOrPm();
                              if (amOrPm == AM && value == 12) {
                                  value = 0;
                              } else if (amOrPm == PM && value != 12) {
                                  value += 12;
                              }
                          }
                          setValueForItem(getCurrentItemShowing(), value);
                          mListener.onValueSelected(getCurrentItemShowing(), value, true);
                      }
                  }
                  mDoingMove = false;
                  return true;
              default:
                  break;
          }
          return false;
      }
  
      /**
       * Set touch input as enabled or disabled, for use with keyboard mode.
       */
      public boolean trySettingInputEnabled(boolean inputEnabled) {
          if (mDoingTouch && !inputEnabled) {
              // If we're trying to disable input, but we're in the middle of a touch event,
              // we'll allow the touch event to continue before disabling input.
              return false;
          }
          mInputEnabled = inputEnabled;
          mGrayBox.setVisibility(inputEnabled? View.INVISIBLE : View.VISIBLE);
          return true;
      }
  
      /**
       * 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 = getHours();
              time.minute = getMinutes();
              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 = getCurrentlyShowingValue();
              int stepSize = 0;
              int currentItemShowing = getCurrentItemShowing();
              if (currentItemShowing == HOUR_INDEX) {
                  stepSize = HOUR_VALUE_TO_DEGREES_STEP_SIZE;
                  value %= 12;
              } else if (currentItemShowing == MINUTE_INDEX) {
                  stepSize = MINUTE_VALUE_TO_DEGREES_STEP_SIZE;
              }
  
              int degrees = value * stepSize;
              degrees = snapOnly30s(degrees, changeMultiplier);
              value = degrees / stepSize;
              int maxValue = 0;
              int minValue = 0;
              if (currentItemShowing == HOUR_INDEX) {
                  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;
              }
              setItem(currentItemShowing, value);
              mListener.onValueSelected(currentItemShowing, value, false);
              return true;
          }
  
          return false;
      }
  }
  