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.Resources;
  import android.content.res.TypedArray;
  import android.graphics.Canvas;
  import android.graphics.Paint;
  import android.graphics.Paint.Align;
  import android.graphics.Paint.Style;
  import android.graphics.Rect;
  import android.graphics.Typeface;
  import android.icu.text.DisplayContext;
  import android.icu.text.SimpleDateFormat;
  import android.icu.util.Calendar;
  import android.os.Bundle;
  import android.text.TextPaint;
  import android.text.format.DateFormat;
  import android.util.AttributeSet;
  import android.util.IntArray;
  import android.util.MathUtils;
  import android.util.StateSet;
  import android.view.KeyEvent;
  import android.view.MotionEvent;
  import android.view.PointerIcon;
  import android.view.View;
  import android.view.ViewParent;
  import android.view.accessibility.AccessibilityEvent;
  import android.view.accessibility.AccessibilityNodeInfo;
  import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
  
  import com.android.internal.R;
  import com.android.internal.widget.ExploreByTouchHelper;
  
  import libcore.icu.LocaleData;
  
  import java.text.NumberFormat;
  import java.util.Locale;
  
  /**
   * A calendar-like view displaying a specified month and the appropriate selectable day numbers
   * within the specified month.
   */
  class SimpleMonthView extends View {
      private static final int DAYS_IN_WEEK = 7;
      private static final int MAX_WEEKS_IN_MONTH = 6;
  
      private static final int DEFAULT_SELECTED_DAY = -1;
      private static final int DEFAULT_WEEK_START = Calendar.SUNDAY;
  
      private static final String MONTH_YEAR_FORMAT = "MMMMy";
  
      private static final int SELECTED_HIGHLIGHT_ALPHA = 0xB0;
  
      private final TextPaint mMonthPaint = new TextPaint();
      private final TextPaint mDayOfWeekPaint = new TextPaint();
      private final TextPaint mDayPaint = new TextPaint();
      private final Paint mDaySelectorPaint = new Paint();
      private final Paint mDayHighlightPaint = new Paint();
      private final Paint mDayHighlightSelectorPaint = new Paint();
  
      /** Array of single-character weekday labels ordered by column index. */
      private final String[] mDayOfWeekLabels = new String[7];
  
      private final Calendar mCalendar;
      private final Locale mLocale;
  
      private final MonthViewTouchHelper mTouchHelper;
  
      private final NumberFormat mDayFormatter;
  
      // Desired dimensions.
      private final int mDesiredMonthHeight;
      private final int mDesiredDayOfWeekHeight;
      private final int mDesiredDayHeight;
      private final int mDesiredCellWidth;
      private final int mDesiredDaySelectorRadius;
  
      private String mMonthYearLabel;
  
      private int mMonth;
      private int mYear;
  
      // Dimensions as laid out.
      private int mMonthHeight;
      private int mDayOfWeekHeight;
      private int mDayHeight;
      private int mCellWidth;
      private int mDaySelectorRadius;
  
      private int mPaddedWidth;
      private int mPaddedHeight;
  
      /** The day of month for the selected day, or -1 if no day is selected. */
      private int mActivatedDay = -1;
  
      /**
       * The day of month for today, or -1 if the today is not in the current
       * month.
       */
      private int mToday = DEFAULT_SELECTED_DAY;
  
      /** The first day of the week (ex. Calendar.SUNDAY) indexed from one. */
      private int mWeekStart = DEFAULT_WEEK_START;
  
      /** The number of days (ex. 28) in the current month. */
      private int mDaysInMonth;
  
      /**
       * The day of week (ex. Calendar.SUNDAY) for the first day of the current
       * month.
       */
      private int mDayOfWeekStart;
  
      /** The day of month for the first (inclusive) enabled day. */
      private int mEnabledDayStart = 1;
  
      /** The day of month for the last (inclusive) enabled day. */
      private int mEnabledDayEnd = 31;
  
      /** Optional listener for handling day click actions. */
      private OnDayClickListener mOnDayClickListener;
  
      private ColorStateList mDayTextColor;
  
      private int mHighlightedDay = -1;
      private int mPreviouslyHighlightedDay = -1;
      private boolean mIsTouchHighlighted = false;
  
      public SimpleMonthView(Context context) {
          this(context, null);
      }
  
      public SimpleMonthView(Context context, AttributeSet attrs) {
          this(context, attrs, R.attr.datePickerStyle);
      }
  
      public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr) {
          this(context, attrs, defStyleAttr, 0);
      }
  
      public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
          super(context, attrs, defStyleAttr, defStyleRes);
  
          final Resources res = context.getResources();
          mDesiredMonthHeight = res.getDimensionPixelSize(R.dimen.date_picker_month_height);
          mDesiredDayOfWeekHeight = res.getDimensionPixelSize(R.dimen.date_picker_day_of_week_height);
          mDesiredDayHeight = res.getDimensionPixelSize(R.dimen.date_picker_day_height);
          mDesiredCellWidth = res.getDimensionPixelSize(R.dimen.date_picker_day_width);
          mDesiredDaySelectorRadius = res.getDimensionPixelSize(
                  R.dimen.date_picker_day_selector_radius);
  
          // Set up accessibility components.
          mTouchHelper = new MonthViewTouchHelper(this);
          setAccessibilityDelegate(mTouchHelper);
          setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
  
          mLocale = res.getConfiguration().locale;
          mCalendar = Calendar.getInstance(mLocale);
  
          mDayFormatter = NumberFormat.getIntegerInstance(mLocale);
  
          updateMonthYearLabel();
          updateDayOfWeekLabels();
  
          initPaints(res);
      }
  
      private void updateMonthYearLabel() {
          final String format = DateFormat.getBestDateTimePattern(mLocale, MONTH_YEAR_FORMAT);
          final SimpleDateFormat formatter = new SimpleDateFormat(format, mLocale);
          formatter.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE);
          mMonthYearLabel = formatter.format(mCalendar.getTime());
      }
  
      private void updateDayOfWeekLabels() {
          // Use tiny (e.g. single-character) weekday names from ICU. The indices
          // for this list correspond to Calendar days, e.g. SUNDAY is index 1.
          final String[] tinyWeekdayNames = LocaleData.get(mLocale).tinyWeekdayNames;
          for (int i = 0; i < DAYS_IN_WEEK; i++) {
              mDayOfWeekLabels[i] = tinyWeekdayNames[(mWeekStart + i - 1) % DAYS_IN_WEEK + 1];
          }
      }
  
      /**
       * Applies the specified text appearance resource to a paint, returning the
       * text color if one is set in the text appearance.
       *
       * @param p the paint to modify
       * @param resId the resource ID of the text appearance
       * @return the text color, if available
       */
      private ColorStateList applyTextAppearance(Paint p, int resId) {
          final TypedArray ta = mContext.obtainStyledAttributes(null,
                  R.styleable.TextAppearance, 0, resId);
  
          final String fontFamily = ta.getString(R.styleable.TextAppearance_fontFamily);
          if (fontFamily != null) {
              p.setTypeface(Typeface.create(fontFamily, 0));
          }
  
          p.setTextSize(ta.getDimensionPixelSize(
                  R.styleable.TextAppearance_textSize, (int) p.getTextSize()));
  
          final ColorStateList textColor = ta.getColorStateList(R.styleable.TextAppearance_textColor);
          if (textColor != null) {
              final int enabledColor = textColor.getColorForState(ENABLED_STATE_SET, 0);
              p.setColor(enabledColor);
          }
  
          ta.recycle();
  
          return textColor;
      }
  
      public int getMonthHeight() {
          return mMonthHeight;
      }
  
      public int getCellWidth() {
          return mCellWidth;
      }
  
      public void setMonthTextAppearance(int resId) {
          applyTextAppearance(mMonthPaint, resId);
  
          invalidate();
      }
  
      public void setDayOfWeekTextAppearance(int resId) {
          applyTextAppearance(mDayOfWeekPaint, resId);
          invalidate();
      }
  
      public void setDayTextAppearance(int resId) {
          final ColorStateList textColor = applyTextAppearance(mDayPaint, resId);
          if (textColor != null) {
              mDayTextColor = textColor;
          }
  
          invalidate();
      }
  
      /**
       * Sets up the text and style properties for painting.
       */
      private void initPaints(Resources res) {
          final String monthTypeface = res.getString(R.string.date_picker_month_typeface);
          final String dayOfWeekTypeface = res.getString(R.string.date_picker_day_of_week_typeface);
          final String dayTypeface = res.getString(R.string.date_picker_day_typeface);
  
          final int monthTextSize = res.getDimensionPixelSize(
                  R.dimen.date_picker_month_text_size);
          final int dayOfWeekTextSize = res.getDimensionPixelSize(
                  R.dimen.date_picker_day_of_week_text_size);
          final int dayTextSize = res.getDimensionPixelSize(
                  R.dimen.date_picker_day_text_size);
  
          mMonthPaint.setAntiAlias(true);
          mMonthPaint.setTextSize(monthTextSize);
          mMonthPaint.setTypeface(Typeface.create(monthTypeface, 0));
          mMonthPaint.setTextAlign(Align.CENTER);
          mMonthPaint.setStyle(Style.FILL);
  
          mDayOfWeekPaint.setAntiAlias(true);
          mDayOfWeekPaint.setTextSize(dayOfWeekTextSize);
          mDayOfWeekPaint.setTypeface(Typeface.create(dayOfWeekTypeface, 0));
          mDayOfWeekPaint.setTextAlign(Align.CENTER);
          mDayOfWeekPaint.setStyle(Style.FILL);
  
          mDaySelectorPaint.setAntiAlias(true);
          mDaySelectorPaint.setStyle(Style.FILL);
  
          mDayHighlightPaint.setAntiAlias(true);
          mDayHighlightPaint.setStyle(Style.FILL);
  
          mDayHighlightSelectorPaint.setAntiAlias(true);
          mDayHighlightSelectorPaint.setStyle(Style.FILL);
  
          mDayPaint.setAntiAlias(true);
          mDayPaint.setTextSize(dayTextSize);
          mDayPaint.setTypeface(Typeface.create(dayTypeface, 0));
          mDayPaint.setTextAlign(Align.CENTER);
          mDayPaint.setStyle(Style.FILL);
      }
  
      void setMonthTextColor(ColorStateList monthTextColor) {
          final int enabledColor = monthTextColor.getColorForState(ENABLED_STATE_SET, 0);
          mMonthPaint.setColor(enabledColor);
          invalidate();
      }
  
      void setDayOfWeekTextColor(ColorStateList dayOfWeekTextColor) {
          final int enabledColor = dayOfWeekTextColor.getColorForState(ENABLED_STATE_SET, 0);
          mDayOfWeekPaint.setColor(enabledColor);
          invalidate();
      }
  
      void setDayTextColor(ColorStateList dayTextColor) {
          mDayTextColor = dayTextColor;
          invalidate();
      }
  
      void setDaySelectorColor(ColorStateList dayBackgroundColor) {
          final int activatedColor = dayBackgroundColor.getColorForState(
                  StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED), 0);
          mDaySelectorPaint.setColor(activatedColor);
          mDayHighlightSelectorPaint.setColor(activatedColor);
          mDayHighlightSelectorPaint.setAlpha(SELECTED_HIGHLIGHT_ALPHA);
          invalidate();
      }
  
      void setDayHighlightColor(ColorStateList dayHighlightColor) {
          final int pressedColor = dayHighlightColor.getColorForState(
                  StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_PRESSED), 0);
          mDayHighlightPaint.setColor(pressedColor);
          invalidate();
      }
  
      public void setOnDayClickListener(OnDayClickListener listener) {
          mOnDayClickListener = listener;
      }
  
      @Override
      public boolean dispatchHoverEvent(MotionEvent event) {
          // First right-of-refusal goes the touch exploration helper.
          return mTouchHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event);
      }
  
      @Override
      public boolean onTouchEvent(MotionEvent event) {
          final int x = (int) (event.getX() + 0.5f);
          final int y = (int) (event.getY() + 0.5f);
  
          final int action = event.getAction();
          switch (action) {
              case MotionEvent.ACTION_DOWN:
              case MotionEvent.ACTION_MOVE:
                  final int touchedItem = getDayAtLocation(x, y);
                  mIsTouchHighlighted = true;
                  if (mHighlightedDay != touchedItem) {
                      mHighlightedDay = touchedItem;
                      mPreviouslyHighlightedDay = touchedItem;
                      invalidate();
                  }
                  if (action == MotionEvent.ACTION_DOWN && touchedItem < 0) {
                      // Touch something that's not an item, reject event.
                      return false;
                  }
                  break;
  
              case MotionEvent.ACTION_UP:
                  final int clickedDay = getDayAtLocation(x, y);
                  onDayClicked(clickedDay);
                  // Fall through.
              case MotionEvent.ACTION_CANCEL:
                  // Reset touched day on stream end.
                  mHighlightedDay = -1;
                  mIsTouchHighlighted = false;
                  invalidate();
                  break;
          }
          return true;
      }
  
      @Override
      public boolean onKeyDown(int keyCode, KeyEvent event) {
          // We need to handle focus change within the SimpleMonthView because we are simulating
          // multiple Views. The arrow keys will move between days until there is no space (no
          // day to the left, top, right, or bottom). Focus forward and back jumps out of the
          // SimpleMonthView, skipping over other SimpleMonthViews in the parent ViewPager
          // to the next focusable View in the hierarchy.
          boolean focusChanged = false;
          switch (event.getKeyCode()) {
              case KeyEvent.KEYCODE_DPAD_LEFT:
                  if (event.hasNoModifiers()) {
                      focusChanged = moveOneDay(isLayoutRtl());
                  }
                  break;
              case KeyEvent.KEYCODE_DPAD_RIGHT:
                  if (event.hasNoModifiers()) {
                      focusChanged = moveOneDay(!isLayoutRtl());
                  }
                  break;
              case KeyEvent.KEYCODE_DPAD_UP:
                  if (event.hasNoModifiers()) {
                      ensureFocusedDay();
                      if (mHighlightedDay > 7) {
                          mHighlightedDay -= 7;
                          focusChanged = true;
                      }
                  }
                  break;
              case KeyEvent.KEYCODE_DPAD_DOWN:
                  if (event.hasNoModifiers()) {
                      ensureFocusedDay();
                      if (mHighlightedDay <= mDaysInMonth - 7) {
                          mHighlightedDay += 7;
                          focusChanged = true;
                      }
                  }
                  break;
              case KeyEvent.KEYCODE_DPAD_CENTER:
              case KeyEvent.KEYCODE_ENTER:
                  if (mHighlightedDay != -1) {
                      onDayClicked(mHighlightedDay);
                      return true;
                  }
                  break;
              case KeyEvent.KEYCODE_TAB: {
                  int focusChangeDirection = 0;
                  if (event.hasNoModifiers()) {
                      focusChangeDirection = View.FOCUS_FORWARD;
                  } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
                      focusChangeDirection = View.FOCUS_BACKWARD;
                  }
                  if (focusChangeDirection != 0) {
                      final ViewParent parent = getParent();
                      // move out of the ViewPager next/previous
                      View nextFocus = this;
                      do {
                          nextFocus = nextFocus.focusSearch(focusChangeDirection);
                      } while (nextFocus != null && nextFocus != this &&
                              nextFocus.getParent() == parent);
                      if (nextFocus != null) {
                          nextFocus.requestFocus();
                          return true;
                      }
                  }
                  break;
              }
          }
          if (focusChanged) {
              invalidate();
              return true;
          } else {
              return super.onKeyDown(keyCode, event);
          }
      }
  
      private boolean moveOneDay(boolean positive) {
          ensureFocusedDay();
          boolean focusChanged = false;
          if (positive) {
              if (!isLastDayOfWeek(mHighlightedDay) && mHighlightedDay < mDaysInMonth) {
                  mHighlightedDay++;
                  focusChanged = true;
              }
          } else {
              if (!isFirstDayOfWeek(mHighlightedDay) && mHighlightedDay > 1) {
                  mHighlightedDay--;
                  focusChanged = true;
              }
          }
          return focusChanged;
      }
  
      @Override
      protected void onFocusChanged(boolean gainFocus, @FocusDirection int direction,
              @Nullable Rect previouslyFocusedRect) {
          if (gainFocus) {
              // If we've gained focus through arrow keys, we should find the day closest
              // to the focus rect. If we've gained focus through forward/back, we should
              // focus on the selected day if there is one.
              final int offset = findDayOffset();
              switch(direction) {
                  case View.FOCUS_RIGHT: {
                      int row = findClosestRow(previouslyFocusedRect);
                      mHighlightedDay = row == 0 ? 1 : (row * DAYS_IN_WEEK) - offset + 1;
                      break;
                  }
                  case View.FOCUS_LEFT: {
                      int row = findClosestRow(previouslyFocusedRect) + 1;
                      mHighlightedDay = Math.min(mDaysInMonth, (row * DAYS_IN_WEEK) - offset);
                      break;
                  }
                  case View.FOCUS_DOWN: {
                      final int col = findClosestColumn(previouslyFocusedRect);
                      final int day = col - offset + 1;
                      mHighlightedDay = day < 1 ? day + DAYS_IN_WEEK : day;
                      break;
                  }
                  case View.FOCUS_UP: {
                      final int col = findClosestColumn(previouslyFocusedRect);
                      final int maxWeeks = (offset + mDaysInMonth) / DAYS_IN_WEEK;
                      final int day = col - offset + (DAYS_IN_WEEK * maxWeeks) + 1;
                      mHighlightedDay = day > mDaysInMonth ? day - DAYS_IN_WEEK : day;
                      break;
                  }
              }
              ensureFocusedDay();
              invalidate();
          }
          super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
      }
  
      /**
       * Returns the row (0 indexed) closest to previouslyFocusedRect or center if null.
       */
      private int findClosestRow(@Nullable Rect previouslyFocusedRect) {
          if (previouslyFocusedRect == null) {
              return 3;
          } else if (mDayHeight == 0) {
              return 0; // There hasn't been a layout, so just choose the first row
          } else {
              int centerY = previouslyFocusedRect.centerY();
  
              final TextPaint p = mDayPaint;
              final int headerHeight = mMonthHeight + mDayOfWeekHeight;
              final int rowHeight = mDayHeight;
  
              // Text is vertically centered within the row height.
              final float halfLineHeight = (p.ascent() + p.descent()) / 2f;
              final int rowCenter = headerHeight + rowHeight / 2;
  
              centerY -= rowCenter - halfLineHeight;
              int row = Math.round(centerY / (float) rowHeight);
              final int maxDay = findDayOffset() + mDaysInMonth;
              final int maxRows = (maxDay / DAYS_IN_WEEK) - ((maxDay % DAYS_IN_WEEK == 0) ? 1 : 0);
  
              row = MathUtils.constrain(row, 0, maxRows);
              return row;
          }
      }
  
      /**
       * Returns the column (0 indexed) closest to the previouslyFocusedRect or center if null.
       * The 0 index is related to the first day of the week.
       */
      private int findClosestColumn(@Nullable Rect previouslyFocusedRect) {
          if (previouslyFocusedRect == null) {
              return DAYS_IN_WEEK / 2;
          } else if (mCellWidth == 0) {
              return 0; // There hasn't been a layout, so we can just choose the first column
          } else {
              int centerX = previouslyFocusedRect.centerX() - mPaddingLeft;
              final int columnFromLeft =
                      MathUtils.constrain(centerX / mCellWidth, 0, DAYS_IN_WEEK - 1);
              return isLayoutRtl() ? DAYS_IN_WEEK - columnFromLeft - 1: columnFromLeft;
          }
      }
  
      @Override
      public void getFocusedRect(Rect r) {
          if (mHighlightedDay > 0) {
              getBoundsForDay(mHighlightedDay, r);
          } else {
              super.getFocusedRect(r);
          }
      }
  
      @Override
      protected void onFocusLost() {
          if (!mIsTouchHighlighted) {
              // Unhighlight a day.
              mPreviouslyHighlightedDay = mHighlightedDay;
              mHighlightedDay = -1;
              invalidate();
          }
          super.onFocusLost();
      }
  
      /**
       * Ensure some day is highlighted. If a day isn't highlighted, it chooses the selected day,
       * if possible, or the first day of the month if not.
       */
      private void ensureFocusedDay() {
          if (mHighlightedDay != -1) {
              return;
          }
          if (mPreviouslyHighlightedDay != -1) {
              mHighlightedDay = mPreviouslyHighlightedDay;
              return;
          }
          if (mActivatedDay != -1) {
              mHighlightedDay = mActivatedDay;
              return;
          }
          mHighlightedDay = 1;
      }
  
      private boolean isFirstDayOfWeek(int day) {
          final int offset = findDayOffset();
          return (offset + day - 1) % DAYS_IN_WEEK == 0;
      }
  
      private boolean isLastDayOfWeek(int day) {
          final int offset = findDayOffset();
          return (offset + day) % DAYS_IN_WEEK == 0;
      }
  
      @Override
      protected void onDraw(Canvas canvas) {
          final int paddingLeft = getPaddingLeft();
          final int paddingTop = getPaddingTop();
          canvas.translate(paddingLeft, paddingTop);
  
          drawMonth(canvas);
          drawDaysOfWeek(canvas);
          drawDays(canvas);
  
          canvas.translate(-paddingLeft, -paddingTop);
      }
  
      private void drawMonth(Canvas canvas) {
          final float x = mPaddedWidth / 2f;
  
          // Vertically centered within the month header height.
          final float lineHeight = mMonthPaint.ascent() + mMonthPaint.descent();
          final float y = (mMonthHeight - lineHeight) / 2f;
  
          canvas.drawText(mMonthYearLabel, x, y, mMonthPaint);
      }
  
      public String getMonthYearLabel() {
          return mMonthYearLabel;
      }
  
      private void drawDaysOfWeek(Canvas canvas) {
          final TextPaint p = mDayOfWeekPaint;
          final int headerHeight = mMonthHeight;
          final int rowHeight = mDayOfWeekHeight;
          final int colWidth = mCellWidth;
  
          // Text is vertically centered within the day of week height.
          final float halfLineHeight = (p.ascent() + p.descent()) / 2f;
          final int rowCenter = headerHeight + rowHeight / 2;
  
          for (int col = 0; col < DAYS_IN_WEEK; col++) {
              final int colCenter = colWidth * col + colWidth / 2;
              final int colCenterRtl;
              if (isLayoutRtl()) {
                  colCenterRtl = mPaddedWidth - colCenter;
              } else {
                  colCenterRtl = colCenter;
              }
  
              final String label = mDayOfWeekLabels[col];
              canvas.drawText(label, colCenterRtl, rowCenter - halfLineHeight, p);
          }
      }
  
      /**
       * Draws the month days.
       */
      private void drawDays(Canvas canvas) {
          final TextPaint p = mDayPaint;
          final int headerHeight = mMonthHeight + mDayOfWeekHeight;
          final int rowHeight = mDayHeight;
          final int colWidth = mCellWidth;
  
          // Text is vertically centered within the row height.
          final float halfLineHeight = (p.ascent() + p.descent()) / 2f;
          int rowCenter = headerHeight + rowHeight / 2;
  
          for (int day = 1, col = findDayOffset(); day <= mDaysInMonth; day++) {
              final int colCenter = colWidth * col + colWidth / 2;
              final int colCenterRtl;
              if (isLayoutRtl()) {
                  colCenterRtl = mPaddedWidth - colCenter;
              } else {
                  colCenterRtl = colCenter;
              }
  
              int stateMask = 0;
  
              final boolean isDayEnabled = isDayEnabled(day);
              if (isDayEnabled) {
                  stateMask |= StateSet.VIEW_STATE_ENABLED;
              }
  
              final boolean isDayActivated = mActivatedDay == day;
              final boolean isDayHighlighted = mHighlightedDay == day;
              if (isDayActivated) {
                  stateMask |= StateSet.VIEW_STATE_ACTIVATED;
  
                  // Adjust the circle to be centered on the row.
                  final Paint paint = isDayHighlighted ? mDayHighlightSelectorPaint :
                          mDaySelectorPaint;
                  canvas.drawCircle(colCenterRtl, rowCenter, mDaySelectorRadius, paint);
              } else if (isDayHighlighted) {
                  stateMask |= StateSet.VIEW_STATE_PRESSED;
  
                  if (isDayEnabled) {
                      // Adjust the circle to be centered on the row.
                      canvas.drawCircle(colCenterRtl, rowCenter,
                              mDaySelectorRadius, mDayHighlightPaint);
                  }
              }
  
              final boolean isDayToday = mToday == day;
              final int dayTextColor;
              if (isDayToday && !isDayActivated) {
                  dayTextColor = mDaySelectorPaint.getColor();
              } else {
                  final int[] stateSet = StateSet.get(stateMask);
                  dayTextColor = mDayTextColor.getColorForState(stateSet, 0);
              }
              p.setColor(dayTextColor);
  
              canvas.drawText(mDayFormatter.format(day), colCenterRtl, rowCenter - halfLineHeight, p);
  
              col++;
  
              if (col == DAYS_IN_WEEK) {
                  col = 0;
                  rowCenter += rowHeight;
              }
          }
      }
  
      private boolean isDayEnabled(int day) {
          return day >= mEnabledDayStart && day <= mEnabledDayEnd;
      }
  
      private boolean isValidDayOfMonth(int day) {
          return day >= 1 && day <= mDaysInMonth;
      }
  
      private static boolean isValidDayOfWeek(int day) {
          return day >= Calendar.SUNDAY && day <= Calendar.SATURDAY;
      }
  
      private static boolean isValidMonth(int month) {
          return month >= Calendar.JANUARY && month <= Calendar.DECEMBER;
      }
  
      /**
       * Sets the selected day.
       *
       * @param dayOfMonth the selected day of the month, or {@code -1} to clear
       *                   the selection
       */
      public void setSelectedDay(int dayOfMonth) {
          mActivatedDay = dayOfMonth;
  
          // Invalidate cached accessibility information.
          mTouchHelper.invalidateRoot();
          invalidate();
      }
  
      /**
       * Sets the first day of the week.
       *
       * @param weekStart which day the week should start on, valid values are
       *                  {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY}
       */
      public void setFirstDayOfWeek(int weekStart) {
          if (isValidDayOfWeek(weekStart)) {
              mWeekStart = weekStart;
          } else {
              mWeekStart = mCalendar.getFirstDayOfWeek();
          }
  
          updateDayOfWeekLabels();
  
          // Invalidate cached accessibility information.
          mTouchHelper.invalidateRoot();
          invalidate();
      }
  
      /**
       * Sets all the parameters for displaying this week.
       * <p>
       * Parameters have a default value and will only update if a new value is
       * included, except for focus month, which will always default to no focus
       * month if no value is passed in. The only required parameter is the week
       * start.
       *
       * @param selectedDay the selected day of the month, or -1 for no selection
       * @param month the month
       * @param year the year
       * @param weekStart which day the week should start on, valid values are
       *                  {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY}
       * @param enabledDayStart the first enabled day
       * @param enabledDayEnd the last enabled day
       */
      void setMonthParams(int selectedDay, int month, int year, int weekStart, int enabledDayStart,
              int enabledDayEnd) {
          mActivatedDay = selectedDay;
  
          if (isValidMonth(month)) {
              mMonth = month;
          }
          mYear = year;
  
          mCalendar.set(Calendar.MONTH, mMonth);
          mCalendar.set(Calendar.YEAR, mYear);
          mCalendar.set(Calendar.DAY_OF_MONTH, 1);
          mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK);
  
          if (isValidDayOfWeek(weekStart)) {
              mWeekStart = weekStart;
          } else {
              mWeekStart = mCalendar.getFirstDayOfWeek();
          }
  
          // Figure out what day today is.
          final Calendar today = Calendar.getInstance();
          mToday = -1;
          mDaysInMonth = getDaysInMonth(mMonth, mYear);
          for (int i = 0; i < mDaysInMonth; i++) {
              final int day = i + 1;
              if (sameDay(day, today)) {
                  mToday = day;
              }
          }
  
          mEnabledDayStart = MathUtils.constrain(enabledDayStart, 1, mDaysInMonth);
          mEnabledDayEnd = MathUtils.constrain(enabledDayEnd, mEnabledDayStart, mDaysInMonth);
  
          updateMonthYearLabel();
          updateDayOfWeekLabels();
  
          // Invalidate cached accessibility information.
          mTouchHelper.invalidateRoot();
          invalidate();
      }
  
      private 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 boolean sameDay(int day, Calendar today) {
          return mYear == today.get(Calendar.YEAR) && mMonth == today.get(Calendar.MONTH)
                  && day == today.get(Calendar.DAY_OF_MONTH);
      }
  
      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
          final int preferredHeight = mDesiredDayHeight * MAX_WEEKS_IN_MONTH
                  + mDesiredDayOfWeekHeight + mDesiredMonthHeight
                  + getPaddingTop() + getPaddingBottom();
          final int preferredWidth = mDesiredCellWidth * DAYS_IN_WEEK
                  + getPaddingStart() + getPaddingEnd();
          final int resolvedWidth = resolveSize(preferredWidth, widthMeasureSpec);
          final int resolvedHeight = resolveSize(preferredHeight, heightMeasureSpec);
          setMeasuredDimension(resolvedWidth, resolvedHeight);
      }
  
      @Override
      public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) {
          super.onRtlPropertiesChanged(layoutDirection);
  
          requestLayout();
      }
  
      @Override
      protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
          if (!changed) {
              return;
          }
  
          // Let's initialize a completely reasonable number of variables.
          final int w = right - left;
          final int h = bottom - top;
          final int paddingLeft = getPaddingLeft();
          final int paddingTop = getPaddingTop();
          final int paddingRight = getPaddingRight();
          final int paddingBottom = getPaddingBottom();
          final int paddedRight = w - paddingRight;
          final int paddedBottom = h - paddingBottom;
          final int paddedWidth = paddedRight - paddingLeft;
          final int paddedHeight = paddedBottom - paddingTop;
          if (paddedWidth == mPaddedWidth || paddedHeight == mPaddedHeight) {
              return;
          }
  
          mPaddedWidth = paddedWidth;
          mPaddedHeight = paddedHeight;
  
          // We may have been laid out smaller than our preferred size. If so,
          // scale all dimensions to fit.
          final int measuredPaddedHeight = getMeasuredHeight() - paddingTop - paddingBottom;
          final float scaleH = paddedHeight / (float) measuredPaddedHeight;
          final int monthHeight = (int) (mDesiredMonthHeight * scaleH);
          final int cellWidth = mPaddedWidth / DAYS_IN_WEEK;
          mMonthHeight = monthHeight;
          mDayOfWeekHeight = (int) (mDesiredDayOfWeekHeight * scaleH);
          mDayHeight = (int) (mDesiredDayHeight * scaleH);
          mCellWidth = cellWidth;
  
          // Compute the largest day selector radius that's still within the clip
          // bounds and desired selector radius.
          final int maxSelectorWidth = cellWidth / 2 + Math.min(paddingLeft, paddingRight);
          final int maxSelectorHeight = mDayHeight / 2 + paddingBottom;
          mDaySelectorRadius = Math.min(mDesiredDaySelectorRadius,
                  Math.min(maxSelectorWidth, maxSelectorHeight));
  
          // Invalidate cached accessibility information.
          mTouchHelper.invalidateRoot();
      }
  
      private int findDayOffset() {
          final int offset = mDayOfWeekStart - mWeekStart;
          if (mDayOfWeekStart < mWeekStart) {
              return offset + DAYS_IN_WEEK;
          }
          return offset;
      }
  
      /**
       * Calculates the day of the month at the specified touch position. Returns
       * the day of the month or -1 if the position wasn't in a valid day.
       *
       * @param x the x position of the touch event
       * @param y the y position of the touch event
       * @return the day of the month at (x, y), or -1 if the position wasn't in
       *         a valid day
       */
      private int getDayAtLocation(int x, int y) {
          final int paddedX = x - getPaddingLeft();
          if (paddedX < 0 || paddedX >= mPaddedWidth) {
              return -1;
          }
  
          final int headerHeight = mMonthHeight + mDayOfWeekHeight;
          final int paddedY = y - getPaddingTop();
          if (paddedY < headerHeight || paddedY >= mPaddedHeight) {
              return -1;
          }
  
          // Adjust for RTL after applying padding.
          final int paddedXRtl;
          if (isLayoutRtl()) {
              paddedXRtl = mPaddedWidth - paddedX;
          } else {
              paddedXRtl = paddedX;
          }
  
          final int row = (paddedY - headerHeight) / mDayHeight;
          final int col = (paddedXRtl * DAYS_IN_WEEK) / mPaddedWidth;
          final int index = col + row * DAYS_IN_WEEK;
          final int day = index + 1 - findDayOffset();
          if (!isValidDayOfMonth(day)) {
              return -1;
          }
  
          return day;
      }
  
      /**
       * Calculates the bounds of the specified day.
       *
       * @param id the day of the month
       * @param outBounds the rect to populate with bounds
       */
      public boolean getBoundsForDay(int id, Rect outBounds) {
          if (!isValidDayOfMonth(id)) {
              return false;
          }
  
          final int index = id - 1 + findDayOffset();
  
          // Compute left edge, taking into account RTL.
          final int col = index % DAYS_IN_WEEK;
          final int colWidth = mCellWidth;
          final int left;
          if (isLayoutRtl()) {
              left = getWidth() - getPaddingRight() - (col + 1) * colWidth;
          } else {
              left = getPaddingLeft() + col * colWidth;
          }
  
          // Compute top edge.
          final int row = index / DAYS_IN_WEEK;
          final int rowHeight = mDayHeight;
          final int headerHeight = mMonthHeight + mDayOfWeekHeight;
          final int top = getPaddingTop() + headerHeight + row * rowHeight;
  
          outBounds.set(left, top, left + colWidth, top + rowHeight);
  
          return true;
      }
  
      /**
       * Called when the user clicks on a day. Handles callbacks to the
       * {@link OnDayClickListener} if one is set.
       *
       * @param day the day that was clicked
       */
      private boolean onDayClicked(int day) {
          if (!isValidDayOfMonth(day) || !isDayEnabled(day)) {
              return false;
          }
  
          if (mOnDayClickListener != null) {
              final Calendar date = Calendar.getInstance();
              date.set(mYear, mMonth, day);
              mOnDayClickListener.onDayClick(this, date);
          }
  
          // This is a no-op if accessibility is turned off.
          mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED);
          return true;
      }
  
      @Override
      public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
          if (!isEnabled()) {
              return null;
          }
          // Add 0.5f to event coordinates to match the logic in onTouchEvent.
          final int x = (int) (event.getX() + 0.5f);
          final int y = (int) (event.getY() + 0.5f);
          final int dayUnderPointer = getDayAtLocation(x, y);
          if (dayUnderPointer >= 0) {
              return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
          }
          return super.onResolvePointerIcon(event, pointerIndex);
      }
  
      /**
       * Provides a virtual view hierarchy for interfacing with an accessibility
       * service.
       */
      private class MonthViewTouchHelper extends ExploreByTouchHelper {
          private static final String DATE_FORMAT = "dd MMMM yyyy";
  
          private final Rect mTempRect = new Rect();
          private final Calendar mTempCalendar = Calendar.getInstance();
  
          public MonthViewTouchHelper(View host) {
              super(host);
          }
  
          @Override
          protected int getVirtualViewAt(float x, float y) {
              final int day = getDayAtLocation((int) (x + 0.5f), (int) (y + 0.5f));
              if (day != -1) {
                  return day;
              }
              return ExploreByTouchHelper.INVALID_ID;
          }
  
          @Override
          protected void getVisibleVirtualViews(IntArray virtualViewIds) {
              for (int day = 1; day <= mDaysInMonth; day++) {
                  virtualViewIds.add(day);
              }
          }
  
          @Override
          protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
              event.setContentDescription(getDayDescription(virtualViewId));
          }
  
          @Override
          protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) {
              final boolean hasBounds = getBoundsForDay(virtualViewId, mTempRect);
  
              if (!hasBounds) {
                  // The day is invalid, kill the node.
                  mTempRect.setEmpty();
                  node.setContentDescription("");
                  node.setBoundsInParent(mTempRect);
                  node.setVisibleToUser(false);
                  return;
              }
  
              node.setText(getDayText(virtualViewId));
              node.setContentDescription(getDayDescription(virtualViewId));
              node.setBoundsInParent(mTempRect);
  
              final boolean isDayEnabled = isDayEnabled(virtualViewId);
              if (isDayEnabled) {
                  node.addAction(AccessibilityAction.ACTION_CLICK);
              }
  
              node.setEnabled(isDayEnabled);
  
              if (virtualViewId == mActivatedDay) {
                  // TODO: This should use activated once that's supported.
                  node.setChecked(true);
              }
  
          }
  
          @Override
          protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
                  Bundle arguments) {
              switch (action) {
                  case AccessibilityNodeInfo.ACTION_CLICK:
                      return onDayClicked(virtualViewId);
              }
  
              return false;
          }
  
          /**
           * Generates a description for a given virtual view.
           *
           * @param id the day to generate a description for
           * @return a description of the virtual view
           */
          private CharSequence getDayDescription(int id) {
              if (isValidDayOfMonth(id)) {
                  mTempCalendar.set(mYear, mMonth, id);
                  return DateFormat.format(DATE_FORMAT, mTempCalendar.getTimeInMillis());
              }
  
              return "";
          }
  
          /**
           * Generates displayed text for a given virtual view.
           *
           * @param id the day to generate text for
           * @return the visible text of the virtual view
           */
          private CharSequence getDayText(int id) {
              if (isValidDayOfMonth(id)) {
                  return mDayFormatter.format(id);
              }
  
              return null;
          }
      }
  
      /**
       * Handles callbacks when the user clicks on a time object.
       */
      public interface OnDayClickListener {
          void onDayClick(SimpleMonthView view, Calendar day);
      }
  }
  