content: /*
   * Copyright (C) 2014 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.widget;
  
  import android.annotation.Nullable;
  import android.content.Context;
  import android.content.res.ColorStateList;
  import android.content.res.Configuration;
  import android.content.res.Resources;
  import android.content.res.TypedArray;
  import android.icu.text.DateFormat;
  import android.icu.text.DisplayContext;
  import android.icu.util.Calendar;
  import android.os.Parcelable;
  import android.util.AttributeSet;
  import android.util.StateSet;
  import android.view.HapticFeedbackConstants;
  import android.view.LayoutInflater;
  import android.view.View;
  import android.view.View.OnClickListener;
  import android.view.ViewGroup;
  import android.view.accessibility.AccessibilityEvent;
  import android.widget.DayPickerView.OnDaySelectedListener;
  import android.widget.YearPickerView.OnYearSelectedListener;
  
  import com.android.internal.R;
  
  import java.util.Locale;
  
  /**
   * A delegate for picking up a date (day / month / year).
   */
  class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate {
      private static final int USE_LOCALE = 0;
  
      private static final int UNINITIALIZED = -1;
      private static final int VIEW_MONTH_DAY = 0;
      private static final int VIEW_YEAR = 1;
  
      private static final int DEFAULT_START_YEAR = 1900;
      private static final int DEFAULT_END_YEAR = 2100;
  
      private static final int ANIMATION_DURATION = 300;
  
      private static final int[] ATTRS_TEXT_COLOR = new int[] {
              com.android.internal.R.attr.textColor};
      private static final int[] ATTRS_DISABLED_ALPHA = new int[] {
              com.android.internal.R.attr.disabledAlpha};
  
      private DateFormat mYearFormat;
      private DateFormat mMonthDayFormat;
  
      // Top-level container.
      private ViewGroup mContainer;
  
      // Header views.
      private TextView mHeaderYear;
      private TextView mHeaderMonthDay;
  
      // Picker views.
      private ViewAnimator mAnimator;
      private DayPickerView mDayPickerView;
      private YearPickerView mYearPickerView;
  
      // Accessibility strings.
      private String mSelectDay;
      private String mSelectYear;
  
      private int mCurrentView = UNINITIALIZED;
  
      private final Calendar mTempDate;
      private final Calendar mMinDate;
      private final Calendar mMaxDate;
  
      private int mFirstDayOfWeek = USE_LOCALE;
  
      public DatePickerCalendarDelegate(DatePicker delegator, Context context, AttributeSet attrs,
              int defStyleAttr, int defStyleRes) {
          super(delegator, context);
  
          final Locale locale = mCurrentLocale;
          mCurrentDate = Calendar.getInstance(locale);
          mTempDate = Calendar.getInstance(locale);
          mMinDate = Calendar.getInstance(locale);
          mMaxDate = Calendar.getInstance(locale);
  
          mMinDate.set(DEFAULT_START_YEAR, Calendar.JANUARY, 1);
          mMaxDate.set(DEFAULT_END_YEAR, Calendar.DECEMBER, 31);
  
          final Resources res = mDelegator.getResources();
          final TypedArray a = mContext.obtainStyledAttributes(attrs,
                  R.styleable.DatePicker, defStyleAttr, defStyleRes);
          final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
                  Context.LAYOUT_INFLATER_SERVICE);
          final int layoutResourceId = a.getResourceId(
                  R.styleable.DatePicker_internalLayout, R.layout.date_picker_material);
  
          // Set up and attach container.
          mContainer = (ViewGroup) inflater.inflate(layoutResourceId, mDelegator, false);
          mContainer.setSaveFromParentEnabled(false);
          mDelegator.addView(mContainer);
  
          // Set up header views.
          final ViewGroup header = mContainer.findViewById(R.id.date_picker_header);
          mHeaderYear = header.findViewById(R.id.date_picker_header_year);
          mHeaderYear.setOnClickListener(mOnHeaderClickListener);
          mHeaderMonthDay = header.findViewById(R.id.date_picker_header_date);
          mHeaderMonthDay.setOnClickListener(mOnHeaderClickListener);
  
          // For the sake of backwards compatibility, attempt to extract the text
          // color from the header month text appearance. If it's set, we'll let
          // that override the "real" header text color.
          ColorStateList headerTextColor = null;
  
          @SuppressWarnings("deprecation")
          final int monthHeaderTextAppearance = a.getResourceId(
                  R.styleable.DatePicker_headerMonthTextAppearance, 0);
          if (monthHeaderTextAppearance != 0) {
              final TypedArray textAppearance = mContext.obtainStyledAttributes(null,
                      ATTRS_TEXT_COLOR, 0, monthHeaderTextAppearance);
              final ColorStateList legacyHeaderTextColor = textAppearance.getColorStateList(0);
              headerTextColor = applyLegacyColorFixes(legacyHeaderTextColor);
              textAppearance.recycle();
          }
  
          if (headerTextColor == null) {
              headerTextColor = a.getColorStateList(R.styleable.DatePicker_headerTextColor);
          }
  
          if (headerTextColor != null) {
              mHeaderYear.setTextColor(headerTextColor);
              mHeaderMonthDay.setTextColor(headerTextColor);
          }
  
          // Set up header background, if available.
          if (a.hasValueOrEmpty(R.styleable.DatePicker_headerBackground)) {
              header.setBackground(a.getDrawable(R.styleable.DatePicker_headerBackground));
          }
  
          a.recycle();
  
          // Set up picker container.
          mAnimator = mContainer.findViewById(R.id.animator);
  
          // Set up day picker view.
          mDayPickerView = mAnimator.findViewById(R.id.date_picker_day_picker);
          mDayPickerView.setFirstDayOfWeek(mFirstDayOfWeek);
          mDayPickerView.setMinDate(mMinDate.getTimeInMillis());
          mDayPickerView.setMaxDate(mMaxDate.getTimeInMillis());
          mDayPickerView.setDate(mCurrentDate.getTimeInMillis());
          mDayPickerView.setOnDaySelectedListener(mOnDaySelectedListener);
  
          // Set up year picker view.
          mYearPickerView = mAnimator.findViewById(R.id.date_picker_year_picker);
          mYearPickerView.setRange(mMinDate, mMaxDate);
          mYearPickerView.setYear(mCurrentDate.get(Calendar.YEAR));
          mYearPickerView.setOnYearSelectedListener(mOnYearSelectedListener);
  
          // Set up content descriptions.
          mSelectDay = res.getString(R.string.select_day);
          mSelectYear = res.getString(R.string.select_year);
  
          // Initialize for current locale. This also initializes the date, so no
          // need to call onDateChanged.
          onLocaleChanged(mCurrentLocale);
  
          setCurrentView(VIEW_MONTH_DAY);
      }
  
      /**
       * The legacy text color might have been poorly defined. Ensures that it
       * has an appropriate activated state, using the selected state if one
       * exists or modifying the default text color otherwise.
       *
       * @param color a legacy text color, or {@code null}
       * @return a color state list with an appropriate activated state, or
       *         {@code null} if a valid activated state could not be generated
       */
      @Nullable
      private ColorStateList applyLegacyColorFixes(@Nullable ColorStateList color) {
          if (color == null || color.hasState(R.attr.state_activated)) {
              return color;
          }
  
          final int activatedColor;
          final int defaultColor;
          if (color.hasState(R.attr.state_selected)) {
              activatedColor = color.getColorForState(StateSet.get(
                      StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_SELECTED), 0);
              defaultColor = color.getColorForState(StateSet.get(
                      StateSet.VIEW_STATE_ENABLED), 0);
          } else {
              activatedColor = color.getDefaultColor();
  
              // Generate a non-activated color using the disabled alpha.
              final TypedArray ta = mContext.obtainStyledAttributes(ATTRS_DISABLED_ALPHA);
              final float disabledAlpha = ta.getFloat(0, 0.30f);
              defaultColor = multiplyAlphaComponent(activatedColor, disabledAlpha);
          }
  
          if (activatedColor == 0 || defaultColor == 0) {
              // We somehow failed to obtain the colors.
              return null;
          }
  
          final int[][] stateSet = new int[][] {{ R.attr.state_activated }, {}};
          final int[] colors = new int[] { activatedColor, defaultColor };
          return new ColorStateList(stateSet, colors);
      }
  
      private int multiplyAlphaComponent(int color, float alphaMod) {
          final int srcRgb = color & 0xFFFFFF;
          final int srcAlpha = (color >> 24) & 0xFF;
          final int dstAlpha = (int) (srcAlpha * alphaMod + 0.5f);
          return srcRgb | (dstAlpha << 24);
      }
  
      /**
       * Listener called when the user selects a day in the day picker view.
       */
      private final OnDaySelectedListener mOnDaySelectedListener = new OnDaySelectedListener() {
          @Override
          public void onDaySelected(DayPickerView view, Calendar day) {
              mCurrentDate.setTimeInMillis(day.getTimeInMillis());
              onDateChanged(true, true);
          }
      };
  
      /**
       * Listener called when the user selects a year in the year picker view.
       */
      private final OnYearSelectedListener mOnYearSelectedListener = new OnYearSelectedListener() {
          @Override
          public void onYearChanged(YearPickerView view, int year) {
              // If the newly selected month / year does not contain the
              // currently selected day number, change the selected day number
              // to the last day of the selected month or year.
              // e.g. Switching from Mar to Apr when Mar 31 is selected -> Apr 30
              // e.g. Switching from 2012 to 2013 when Feb 29, 2012 is selected -> Feb 28, 2013
              final int day = mCurrentDate.get(Calendar.DAY_OF_MONTH);
              final int month = mCurrentDate.get(Calendar.MONTH);
              final int daysInMonth = getDaysInMonth(month, year);
              if (day > daysInMonth) {
                  mCurrentDate.set(Calendar.DAY_OF_MONTH, daysInMonth);
              }
  
              mCurrentDate.set(Calendar.YEAR, year);
              onDateChanged(true, true);
  
              // Automatically switch to day picker.
              setCurrentView(VIEW_MONTH_DAY);
  
              // Switch focus back to the year text.
              mHeaderYear.requestFocus();
          }
      };
  
      /**
       * Listener called when the user clicks on a header item.
       */
      private final OnClickListener mOnHeaderClickListener = v -> {
          tryVibrate();
  
          switch (v.getId()) {
              case R.id.date_picker_header_year:
                  setCurrentView(VIEW_YEAR);
                  break;
              case R.id.date_picker_header_date:
                  setCurrentView(VIEW_MONTH_DAY);
                  break;
          }
      };
  
      @Override
      protected void onLocaleChanged(Locale locale) {
          final TextView headerYear = mHeaderYear;
          if (headerYear == null) {
              // Abort, we haven't initialized yet. This method will get called
              // again later after everything has been set up.
              return;
          }
  
          // Update the date formatter.
          mMonthDayFormat = DateFormat.getInstanceForSkeleton("EMMMd", locale);
          mMonthDayFormat.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE);
          mYearFormat = DateFormat.getInstanceForSkeleton("y", locale);
  
          // Update the header text.
          onCurrentDateChanged(false);
      }
  
      private void onCurrentDateChanged(boolean announce) {
          if (mHeaderYear == null) {
              // Abort, we haven't initialized yet. This method will get called
              // again later after everything has been set up.
              return;
          }
  
          final String year = mYearFormat.format(mCurrentDate.getTime());
          mHeaderYear.setText(year);
  
          final String monthDay = mMonthDayFormat.format(mCurrentDate.getTime());
          mHeaderMonthDay.setText(monthDay);
  
          // TODO: This should use live regions.
          if (announce) {
              mAnimator.announceForAccessibility(getFormattedCurrentDate());
          }
      }
  
      private void setCurrentView(final int viewIndex) {
          switch (viewIndex) {
              case VIEW_MONTH_DAY:
                  mDayPickerView.setDate(mCurrentDate.getTimeInMillis());
  
                  if (mCurrentView != viewIndex) {
                      mHeaderMonthDay.setActivated(true);
                      mHeaderYear.setActivated(false);
                      mAnimator.setDisplayedChild(VIEW_MONTH_DAY);
                      mCurrentView = viewIndex;
                  }
  
                  mAnimator.announceForAccessibility(mSelectDay);
                  break;
              case VIEW_YEAR:
                  final int year = mCurrentDate.get(Calendar.YEAR);
                  mYearPickerView.setYear(year);
                  mYearPickerView.post(() -> {
                      mYearPickerView.requestFocus();
                      final View selected = mYearPickerView.getSelectedView();
                      if (selected != null) {
                          selected.requestFocus();
                      }
                  });
  
                  if (mCurrentView != viewIndex) {
                      mHeaderMonthDay.setActivated(false);
                      mHeaderYear.setActivated(true);
                      mAnimator.setDisplayedChild(VIEW_YEAR);
                      mCurrentView = viewIndex;
                  }
  
                  mAnimator.announceForAccessibility(mSelectYear);
                  break;
          }
      }
  
      @Override
      public void init(int year, int month, int dayOfMonth,
              DatePicker.OnDateChangedListener callBack) {
          setDate(year, month, dayOfMonth);
          onDateChanged(false, false);
  
          mOnDateChangedListener = callBack;
      }
  
      @Override
      public void updateDate(int year, int month, int dayOfMonth) {
          setDate(year, month, dayOfMonth);
          onDateChanged(false, true);
      }
  
      private void setDate(int year, int month, int dayOfMonth) {
          mCurrentDate.set(Calendar.YEAR, year);
          mCurrentDate.set(Calendar.MONTH, month);
          mCurrentDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
          resetAutofilledValue();
      }
  
      private void onDateChanged(boolean fromUser, boolean callbackToClient) {
          final int year = mCurrentDate.get(Calendar.YEAR);
  
          if (callbackToClient
                  && (mOnDateChangedListener != null || mAutoFillChangeListener != null)) {
              final int monthOfYear = mCurrentDate.get(Calendar.MONTH);
              final int dayOfMonth = mCurrentDate.get(Calendar.DAY_OF_MONTH);
              if (mOnDateChangedListener != null) {
                  mOnDateChangedListener.onDateChanged(mDelegator, year, monthOfYear, dayOfMonth);
              }
              if (mAutoFillChangeListener != null) {
                  mAutoFillChangeListener.onDateChanged(mDelegator, year, monthOfYear, dayOfMonth);
              }
          }
  
          mDayPickerView.setDate(mCurrentDate.getTimeInMillis());
          mYearPickerView.setYear(year);
  
          onCurrentDateChanged(fromUser);
  
          if (fromUser) {
              tryVibrate();
          }
      }
  
      @Override
      public int getYear() {
          return mCurrentDate.get(Calendar.YEAR);
      }
  
      @Override
      public int getMonth() {
          return mCurrentDate.get(Calendar.MONTH);
      }
  
      @Override
      public int getDayOfMonth() {
          return mCurrentDate.get(Calendar.DAY_OF_MONTH);
      }
  
      @Override
      public void setMinDate(long minDate) {
          mTempDate.setTimeInMillis(minDate);
          if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR)
                  && mTempDate.get(Calendar.DAY_OF_YEAR) == mMinDate.get(Calendar.DAY_OF_YEAR)) {
              // Same day, no-op.
              return;
          }
          if (mCurrentDate.before(mTempDate)) {
              mCurrentDate.setTimeInMillis(minDate);
              onDateChanged(false, true);
          }
          mMinDate.setTimeInMillis(minDate);
          mDayPickerView.setMinDate(minDate);
          mYearPickerView.setRange(mMinDate, mMaxDate);
      }
  
      @Override
      public Calendar getMinDate() {
          return mMinDate;
      }
  
      @Override
      public void setMaxDate(long maxDate) {
          mTempDate.setTimeInMillis(maxDate);
          if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR)
                  && mTempDate.get(Calendar.DAY_OF_YEAR) == mMaxDate.get(Calendar.DAY_OF_YEAR)) {
              // Same day, no-op.
              return;
          }
          if (mCurrentDate.after(mTempDate)) {
              mCurrentDate.setTimeInMillis(maxDate);
              onDateChanged(false, true);
          }
          mMaxDate.setTimeInMillis(maxDate);
          mDayPickerView.setMaxDate(maxDate);
          mYearPickerView.setRange(mMinDate, mMaxDate);
      }
  
      @Override
      public Calendar getMaxDate() {
          return mMaxDate;
      }
  
      @Override
      public void setFirstDayOfWeek(int firstDayOfWeek) {
          mFirstDayOfWeek = firstDayOfWeek;
  
          mDayPickerView.setFirstDayOfWeek(firstDayOfWeek);
      }
  
      @Override
      public int getFirstDayOfWeek() {
          if (mFirstDayOfWeek != USE_LOCALE) {
              return mFirstDayOfWeek;
          }
          return mCurrentDate.getFirstDayOfWeek();
      }
  
      @Override
      public void setEnabled(boolean enabled) {
          mContainer.setEnabled(enabled);
          mDayPickerView.setEnabled(enabled);
          mYearPickerView.setEnabled(enabled);
          mHeaderYear.setEnabled(enabled);
          mHeaderMonthDay.setEnabled(enabled);
      }
  
      @Override
      public boolean isEnabled() {
          return mContainer.isEnabled();
      }
  
      @Override
      public CalendarView getCalendarView() {
          throw new UnsupportedOperationException("Not supported by calendar-mode DatePicker");
      }
  
      @Override
      public void setCalendarViewShown(boolean shown) {
          // No-op for compatibility with the old DatePicker.
      }
  
      @Override
      public boolean getCalendarViewShown() {
          return false;
      }
  
      @Override
      public void setSpinnersShown(boolean shown) {
          // No-op for compatibility with the old DatePicker.
      }
  
      @Override
      public boolean getSpinnersShown() {
          return false;
      }
  
      @Override
      public void onConfigurationChanged(Configuration newConfig) {
          setCurrentLocale(newConfig.locale);
      }
  
      @Override
      public Parcelable onSaveInstanceState(Parcelable superState) {
          final int year = mCurrentDate.get(Calendar.YEAR);
          final int month = mCurrentDate.get(Calendar.MONTH);
          final int day = mCurrentDate.get(Calendar.DAY_OF_MONTH);
  
          int listPosition = -1;
          int listPositionOffset = -1;
  
          if (mCurrentView == VIEW_MONTH_DAY) {
              listPosition = mDayPickerView.getMostVisiblePosition();
          } else if (mCurrentView == VIEW_YEAR) {
              listPosition = mYearPickerView.getFirstVisiblePosition();
              listPositionOffset = mYearPickerView.getFirstPositionOffset();
          }
  
          return new SavedState(superState, year, month, day, mMinDate.getTimeInMillis(),
                  mMaxDate.getTimeInMillis(), mCurrentView, listPosition, listPositionOffset);
      }
  
      @Override
      public void onRestoreInstanceState(Parcelable state) {
          if (state instanceof SavedState) {
              final SavedState ss = (SavedState) state;
  
              // TODO: Move instance state into DayPickerView, YearPickerView.
              mCurrentDate.set(ss.getSelectedYear(), ss.getSelectedMonth(), ss.getSelectedDay());
              mMinDate.setTimeInMillis(ss.getMinDate());
              mMaxDate.setTimeInMillis(ss.getMaxDate());
  
              onCurrentDateChanged(false);
  
              final int currentView = ss.getCurrentView();
              setCurrentView(currentView);
  
              final int listPosition = ss.getListPosition();
              if (listPosition != -1) {
                  if (currentView == VIEW_MONTH_DAY) {
                      mDayPickerView.setPosition(listPosition);
                  } else if (currentView == VIEW_YEAR) {
                      final int listPositionOffset = ss.getListPositionOffset();
                      mYearPickerView.setSelectionFromTop(listPosition, listPositionOffset);
                  }
              }
          }
      }
  
      @Override
      public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
          onPopulateAccessibilityEvent(event);
          return true;
      }
  
      public CharSequence getAccessibilityClassName() {
          return DatePicker.class.getName();
      }
  
      public static int getDaysInMonth(int month, int year) {
          switch (month) {
              case Calendar.JANUARY:
              case Calendar.MARCH:
              case Calendar.MAY:
              case Calendar.JULY:
              case Calendar.AUGUST:
              case Calendar.OCTOBER:
              case Calendar.DECEMBER:
                  return 31;
              case Calendar.APRIL:
              case Calendar.JUNE:
              case Calendar.SEPTEMBER:
              case Calendar.NOVEMBER:
                  return 30;
              case Calendar.FEBRUARY:
                  return (year % 4 == 0) ? 29 : 28;
              default:
                  throw new IllegalArgumentException("Invalid Month");
          }
      }
  
      private void tryVibrate() {
          mDelegator.performHapticFeedback(HapticFeedbackConstants.CALENDAR_DATE);
      }
  }
  