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 android.widget;
  
  import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
  import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES;
  
  import android.annotation.TestApi;
  import android.content.Context;
  import android.content.res.TypedArray;
  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.inputmethod.EditorInfo;
  import android.view.inputmethod.InputMethodManager;
  
  import com.android.internal.R;
  
  import libcore.icu.LocaleData;
  
  import java.util.Calendar;
  
  /**
   * A delegate implementing the basic spinner-based TimePicker.
   */
  class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate {
      private static final boolean DEFAULT_ENABLED_STATE = true;
      private static final int HOURS_IN_HALF_DAY = 12;
  
      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 final Calendar mTempCalendar;
  
      private boolean mIsEnabled = DEFAULT_ENABLED_STATE;
      private boolean mHourWithTwoDigit;
      private char mHourFormat;
  
      private boolean mIs24HourView;
      private boolean mIsAm;
  
      public TimePickerSpinnerDelegate(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 int layoutResourceId = a.getResourceId(
                  R.styleable.TimePicker_legacyLayout, R.layout.time_picker_legacy);
          a.recycle();
  
          final LayoutInflater inflater = LayoutInflater.from(mContext);
          final View view = inflater.inflate(layoutResourceId, mDelegator, true);
          view.setSaveFromParentEnabled(false);
  
          // hour
          mHourSpinner = delegator.findViewById(R.id.hour);
          mHourSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
              public void onValueChange(NumberPicker spinner, int oldVal, int newVal) {
                  updateInputState();
                  if (!is24Hour()) {
                      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 = mHourSpinner.findViewById(R.id.numberpicker_input);
          mHourSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
  
          // divider (only for the new widget style)
          mDivider = mDelegator.findViewById(R.id.divider);
          if (mDivider != null) {
              setDividerText();
          }
  
          // minute
          mMinuteSpinner = 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 (!is24Hour() && newHour == HOURS_IN_HALF_DAY) {
                          mIsAm = !mIsAm;
                          updateAmPmControl();
                      }
                      mHourSpinner.setValue(newHour);
                  } else if (oldVal == minValue && newVal == maxValue) {
                      int newHour = mHourSpinner.getValue() - 1;
                      if (!is24Hour() && newHour == HOURS_IN_HALF_DAY - 1) {
                          mIsAm = !mIsAm;
                          updateAmPmControl();
                      }
                      mHourSpinner.setValue(newHour);
                  }
                  onTimeChanged();
              }
          });
          mMinuteSpinnerInput = 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 = getAmPmStrings(context);
  
          // am/pm
          final 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 = mAmPmSpinner.findViewById(R.id.numberpicker_input);
              mAmPmSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE);
          }
  
          if (isAmPmAtStart()) {
              // Move the am/pm view to the beginning
              ViewGroup amPmParent = 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();
  
          // set to current time
          mTempCalendar = Calendar.getInstance(mLocale);
          setHour(mTempCalendar.get(Calendar.HOUR_OF_DAY));
          setMinute(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);
          }
      }
  
      @Override
      public boolean validateInput() {
          return true;
      }
  
      private void getHourFormatData() {
          final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mLocale,
                  (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(mLocale,
                  "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(mLocale,
                  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 setDate(int hour, int minute) {
          setCurrentHour(hour, false);
          setCurrentMinute(minute, false);
  
          onTimeChanged();
      }
  
      @Override
      public void setHour(int hour) {
          setCurrentHour(hour, true);
      }
  
      private void setCurrentHour(int currentHour, boolean notifyTimeChanged) {
          // why was Integer used in the first place?
          if (currentHour == getHour()) {
              return;
          }
          resetAutofilledValue();
          if (!is24Hour()) {
              // 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 int getHour() {
          int currentHour = mHourSpinner.getValue();
          if (is24Hour()) {
              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 setMinute(int minute) {
          setCurrentMinute(minute, true);
      }
  
      private void setCurrentMinute(int minute, boolean notifyTimeChanged) {
          if (minute == getMinute()) {
              return;
          }
          resetAutofilledValue();
          mMinuteSpinner.setValue(minute);
          if (notifyTimeChanged) {
              onTimeChanged();
          }
      }
  
      @Override
      public int getMinute() {
          return mMinuteSpinner.getValue();
      }
  
      public void setIs24Hour(boolean is24Hour) {
          if (mIs24HourView == is24Hour) {
              return;
          }
          // cache the current hour since spinner range changes and BEFORE changing mIs24HourView!!
          int currentHour = getHour();
          // Order is important here.
          mIs24HourView = is24Hour;
          getHourFormatData();
          updateHourControl();
          // set value after spinner range is updated
          setCurrentHour(currentHour, false);
          updateMinuteControl();
          updateAmPmControl();
      }
  
      @Override
      public boolean is24Hour() {
          return mIs24HourView;
      }
  
      @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 Parcelable onSaveInstanceState(Parcelable superState) {
          return new SavedState(superState, getHour(), getMinute(), is24Hour());
      }
  
      @Override
      public void onRestoreInstanceState(Parcelable state) {
          if (state instanceof SavedState) {
              final SavedState ss = (SavedState) state;
              setHour(ss.getHour());
              setMinute(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, getHour());
          mTempCalendar.set(Calendar.MINUTE, getMinute());
          String selectedDateUtterance = DateUtils.formatDateTime(mContext,
                  mTempCalendar.getTimeInMillis(), flags);
          event.getText().add(selectedDateUtterance);
      }
  
      /** @hide */
      @Override
      @TestApi
      public View getHourView() {
          return mHourSpinnerInput;
      }
  
      /** @hide */
      @Override
      @TestApi
      public View getMinuteView() {
          return mMinuteSpinnerInput;
      }
  
      /** @hide */
      @Override
      @TestApi
      public View getAmView() {
          return mAmPmSpinnerInput;
      }
  
      /** @hide */
      @Override
      @TestApi
      public View getPmView() {
          return mAmPmSpinnerInput;
      }
  
      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 = mContext.getSystemService(InputMethodManager.class);
          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 (is24Hour()) {
              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);
      }
  
      private void onTimeChanged() {
          mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
          if (mOnTimeChangedListener != null) {
              mOnTimeChangedListener.onTimeChanged(mDelegator, getHour(),
                      getMinute());
          }
          if (mAutoFillChangeListener != null) {
              mAutoFillChangeListener.onTimeChanged(mDelegator, getHour(), getMinute());
          }
      }
  
      private void updateHourControl() {
          if (is24Hour()) {
              // '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 (is24Hour()) {
              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));
          }
      }
  
      public static String[] getAmPmStrings(Context context) {
          String[] result = new String[2];
          LocaleData d = LocaleData.get(context.getResources().getConfiguration().locale);
          result[0] = d.amPm[0].length() > 4 ? d.narrowAm : d.amPm[0];
          result[1] = d.amPm[1].length() > 4 ? d.narrowPm : d.amPm[1];
          return result;
      }
  }
  