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.date;
  
  import android.content.Context;
  import android.content.res.Resources;
  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.os.Bundle;
  import androidx.core.view.ViewCompat;
  import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
  import androidx.customview.widget.ExploreByTouchHelper;
  import android.text.format.DateFormat;
  import android.text.format.DateUtils;
  import android.text.format.Time;
  import android.util.AttributeSet;
  import android.view.MotionEvent;
  import android.view.View;
  import android.view.accessibility.AccessibilityEvent;
  import android.view.accessibility.AccessibilityNodeInfo;
  
  import com.android.datetimepicker.R;
  import com.android.datetimepicker.Utils;
  import com.android.datetimepicker.date.MonthAdapter.CalendarDay;
  
  import java.security.InvalidParameterException;
  import java.util.Calendar;
  import java.util.Formatter;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Locale;
  
  /**
   * A calendar-like view displaying a specified month and the appropriate selectable day numbers
   * within the specified month.
   */
  abstract class MonthView extends View {
      private static final String TAG = "MonthView";
  
      /**
       * These params can be passed into the view to control how it appears.
       * {@link #VIEW_PARAMS_WEEK} is the only required field, though the default
       * values are unlikely to fit most layouts correctly.
       */
      /**
       * This sets the height of this week in pixels
       */
      public static final String VIEW_PARAMS_HEIGHT = "height";
      /**
       * This specifies the position (or weeks since the epoch) of this week,
       * calculated using {@link Utils#getWeeksSinceEpochFromJulianDay}
       */
      public static final String VIEW_PARAMS_MONTH = "month";
      /**
       * This specifies the position (or weeks since the epoch) of this week,
       * calculated using {@link Utils#getWeeksSinceEpochFromJulianDay}
       */
      public static final String VIEW_PARAMS_YEAR = "year";
      /**
       * This sets one of the days in this view as selected {@link Time#SUNDAY}
       * through {@link Time#SATURDAY}.
       */
      public static final String VIEW_PARAMS_SELECTED_DAY = "selected_day";
      /**
       * Which day the week should start on. {@link Time#SUNDAY} through
       * {@link Time#SATURDAY}.
       */
      public static final String VIEW_PARAMS_WEEK_START = "week_start";
      /**
       * How many days to display at a time. Days will be displayed starting with
       * {@link #mWeekStart}.
       */
      public static final String VIEW_PARAMS_NUM_DAYS = "num_days";
      /**
       * Which month is currently in focus, as defined by {@link Time#month}
       * [0-11].
       */
      public static final String VIEW_PARAMS_FOCUS_MONTH = "focus_month";
      /**
       * If this month should display week numbers. false if 0, true otherwise.
       */
      public static final String VIEW_PARAMS_SHOW_WK_NUM = "show_wk_num";
  
      protected static int DEFAULT_HEIGHT = 32;
      protected static int MIN_HEIGHT = 10;
      protected static final int DEFAULT_SELECTED_DAY = -1;
      protected static final int DEFAULT_WEEK_START = Calendar.SUNDAY;
      protected static final int DEFAULT_NUM_DAYS = 7;
      protected static final int DEFAULT_SHOW_WK_NUM = 0;
      protected static final int DEFAULT_FOCUS_MONTH = -1;
      protected static final int DEFAULT_NUM_ROWS = 6;
      protected static final int MAX_NUM_ROWS = 6;
  
      private static final int SELECTED_CIRCLE_ALPHA = 60;
  
      protected static int DAY_SEPARATOR_WIDTH = 1;
      protected static int MINI_DAY_NUMBER_TEXT_SIZE;
      protected static int MONTH_LABEL_TEXT_SIZE;
      protected static int MONTH_DAY_LABEL_TEXT_SIZE;
      protected static int MONTH_HEADER_SIZE;
      protected static int DAY_SELECTED_CIRCLE_SIZE;
  
      // used for scaling to the device density
      protected static float mScale = 0;
  
      protected DatePickerController mController;
  
      // affects the padding on the sides of this view
      protected int mEdgePadding = 0;
  
      private String mDayOfWeekTypeface;
      private String mMonthTitleTypeface;
  
      protected Paint mMonthNumPaint;
      protected Paint mMonthTitlePaint;
      protected Paint mMonthTitleBGPaint;
      protected Paint mSelectedCirclePaint;
      protected Paint mMonthDayLabelPaint;
  
      private final Formatter mFormatter;
      private final StringBuilder mStringBuilder;
  
      // The Julian day of the first day displayed by this item
      protected int mFirstJulianDay = -1;
      // The month of the first day in this week
      protected int mFirstMonth = -1;
      // The month of the last day in this week
      protected int mLastMonth = -1;
  
      protected int mMonth;
  
      protected int mYear;
      // Quick reference to the width of this view, matches parent
      protected int mWidth;
      // The height this view should draw at in pixels, set by height param
      protected int mRowHeight = DEFAULT_HEIGHT;
      // If this view contains the today
      protected boolean mHasToday = false;
      // Which day is selected [0-6] or -1 if no day is selected
      protected int mSelectedDay = -1;
      // Which day is today [0-6] or -1 if no day is today
      protected int mToday = DEFAULT_SELECTED_DAY;
      // Which day of the week to start on [0-6]
      protected int mWeekStart = DEFAULT_WEEK_START;
      // How many days to display
      protected int mNumDays = DEFAULT_NUM_DAYS;
      // The number of days + a spot for week number if it is displayed
      protected int mNumCells = mNumDays;
      // The left edge of the selected day
      protected int mSelectedLeft = -1;
      // The right edge of the selected day
      protected int mSelectedRight = -1;
  
      private final Calendar mCalendar;
      protected final Calendar mDayLabelCalendar;
      private final MonthViewTouchHelper mTouchHelper;
  
      protected int mNumRows = DEFAULT_NUM_ROWS;
  
      // Optional listener for handling day click actions
      protected OnDayClickListener mOnDayClickListener;
  
      // Whether to prevent setting the accessibility delegate
      private boolean mLockAccessibilityDelegate;
  
      protected int mDayTextColor;
      protected int mTodayNumberColor;
      protected int mDisabledDayTextColor;
      protected int mMonthTitleColor;
      protected int mMonthTitleBGColor;
  
      public MonthView(Context context) {
          this(context, null);
      }
  
      public MonthView(Context context, AttributeSet attr) {
          super(context, attr);
          Resources res = context.getResources();
  
          mDayLabelCalendar = Calendar.getInstance();
          mCalendar = Calendar.getInstance();
  
          mDayOfWeekTypeface = res.getString(R.string.day_of_week_label_typeface);
          mMonthTitleTypeface = res.getString(R.string.sans_serif);
  
          mDayTextColor = res.getColor(R.color.date_picker_text_normal);
          mTodayNumberColor = res.getColor(R.color.blue);
          mDisabledDayTextColor = res.getColor(R.color.date_picker_text_disabled);
          mMonthTitleColor = res.getColor(android.R.color.white);
          mMonthTitleBGColor = res.getColor(R.color.circle_background);
  
          mStringBuilder = new StringBuilder(50);
          mFormatter = new Formatter(mStringBuilder, Locale.getDefault());
  
          MINI_DAY_NUMBER_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.day_number_size);
          MONTH_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.month_label_size);
          MONTH_DAY_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.month_day_label_text_size);
          MONTH_HEADER_SIZE = res.getDimensionPixelOffset(R.dimen.month_list_item_header_height);
          DAY_SELECTED_CIRCLE_SIZE = res
                  .getDimensionPixelSize(R.dimen.day_number_select_circle_radius);
  
          mRowHeight = (res.getDimensionPixelOffset(R.dimen.date_picker_view_animator_height)
                  - getMonthHeaderSize()) / MAX_NUM_ROWS;
  
          // Set up accessibility components.
          mTouchHelper = getMonthViewTouchHelper();
          ViewCompat.setAccessibilityDelegate(this, mTouchHelper);
          ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
          mLockAccessibilityDelegate = true;
  
          // Sets up any standard paints that will be used
          initView();
      }
  
      public void setDatePickerController(DatePickerController controller) {
          mController = controller;
      }
  
      protected MonthViewTouchHelper getMonthViewTouchHelper() {
          return new MonthViewTouchHelper(this);
      }
  
      @Override
      public void setAccessibilityDelegate(AccessibilityDelegate delegate) {
          // Workaround for a JB MR1 issue where accessibility delegates on
          // top-level ListView items are overwritten.
          if (!mLockAccessibilityDelegate) {
              super.setAccessibilityDelegate(delegate);
          }
      }
  
      public void setOnDayClickListener(OnDayClickListener listener) {
          mOnDayClickListener = listener;
      }
  
      @Override
      public boolean dispatchHoverEvent(MotionEvent event) {
          // First right-of-refusal goes the touch exploration helper.
          if (mTouchHelper.dispatchHoverEvent(event)) {
              return true;
          }
          return super.dispatchHoverEvent(event);
      }
  
      @Override
      public boolean onTouchEvent(MotionEvent event) {
          switch (event.getAction()) {
              case MotionEvent.ACTION_UP:
                  final int day = getDayFromLocation(event.getX(), event.getY());
                  if (day >= 0) {
                      onDayClick(day);
                  }
                  break;
          }
          return true;
      }
  
      /**
       * Sets up the text and style properties for painting. Override this if you
       * want to use a different paint.
       */
      protected void initView() {
          mMonthTitlePaint = new Paint();
          mMonthTitlePaint.setFakeBoldText(true);
          mMonthTitlePaint.setAntiAlias(true);
          mMonthTitlePaint.setTextSize(MONTH_LABEL_TEXT_SIZE);
          mMonthTitlePaint.setTypeface(Typeface.create(mMonthTitleTypeface, Typeface.BOLD));
          mMonthTitlePaint.setColor(mDayTextColor);
          mMonthTitlePaint.setTextAlign(Align.CENTER);
          mMonthTitlePaint.setStyle(Style.FILL);
  
          mMonthTitleBGPaint = new Paint();
          mMonthTitleBGPaint.setFakeBoldText(true);
          mMonthTitleBGPaint.setAntiAlias(true);
          mMonthTitleBGPaint.setColor(mMonthTitleBGColor);
          mMonthTitleBGPaint.setTextAlign(Align.CENTER);
          mMonthTitleBGPaint.setStyle(Style.FILL);
  
          mSelectedCirclePaint = new Paint();
          mSelectedCirclePaint.setFakeBoldText(true);
          mSelectedCirclePaint.setAntiAlias(true);
          mSelectedCirclePaint.setColor(mTodayNumberColor);
          mSelectedCirclePaint.setTextAlign(Align.CENTER);
          mSelectedCirclePaint.setStyle(Style.FILL);
          mSelectedCirclePaint.setAlpha(SELECTED_CIRCLE_ALPHA);
  
          mMonthDayLabelPaint = new Paint();
          mMonthDayLabelPaint.setAntiAlias(true);
          mMonthDayLabelPaint.setTextSize(MONTH_DAY_LABEL_TEXT_SIZE);
          mMonthDayLabelPaint.setColor(mDayTextColor);
          mMonthDayLabelPaint.setTypeface(Typeface.create(mDayOfWeekTypeface, Typeface.NORMAL));
          mMonthDayLabelPaint.setStyle(Style.FILL);
          mMonthDayLabelPaint.setTextAlign(Align.CENTER);
          mMonthDayLabelPaint.setFakeBoldText(true);
  
          mMonthNumPaint = new Paint();
          mMonthNumPaint.setAntiAlias(true);
          mMonthNumPaint.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE);
          mMonthNumPaint.setStyle(Style.FILL);
          mMonthNumPaint.setTextAlign(Align.CENTER);
          mMonthNumPaint.setFakeBoldText(false);
      }
  
      @Override
      protected void onDraw(Canvas canvas) {
          drawMonthTitle(canvas);
          drawMonthDayLabels(canvas);
          drawMonthNums(canvas);
      }
  
      private int mDayOfWeekStart = 0;
  
      /**
       * Sets all the parameters for displaying this week. The only required
       * parameter is the week number. Other 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. See
       * {@link #VIEW_PARAMS_HEIGHT} for more info on parameters.
       *
       * @param params A map of the new parameters, see
       *            {@link #VIEW_PARAMS_HEIGHT}
       */
      public void setMonthParams(HashMap<String, Integer> params) {
          if (!params.containsKey(VIEW_PARAMS_MONTH) && !params.containsKey(VIEW_PARAMS_YEAR)) {
              throw new InvalidParameterException("You must specify month and year for this view");
          }
          setTag(params);
          // We keep the current value for any params not present
          if (params.containsKey(VIEW_PARAMS_HEIGHT)) {
              mRowHeight = params.get(VIEW_PARAMS_HEIGHT);
              if (mRowHeight < MIN_HEIGHT) {
                  mRowHeight = MIN_HEIGHT;
              }
          }
          if (params.containsKey(VIEW_PARAMS_SELECTED_DAY)) {
              mSelectedDay = params.get(VIEW_PARAMS_SELECTED_DAY);
          }
  
          // Allocate space for caching the day numbers and focus values
          mMonth = params.get(VIEW_PARAMS_MONTH);
          mYear = params.get(VIEW_PARAMS_YEAR);
  
          // Figure out what day today is
          final Time today = new Time(Time.getCurrentTimezone());
          today.setToNow();
          mHasToday = false;
          mToday = -1;
  
          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 (params.containsKey(VIEW_PARAMS_WEEK_START)) {
              mWeekStart = params.get(VIEW_PARAMS_WEEK_START);
          } else {
              mWeekStart = mCalendar.getFirstDayOfWeek();
          }
  
          mNumCells = Utils.getDaysInMonth(mMonth, mYear);
          for (int i = 0; i < mNumCells; i++) {
              final int day = i + 1;
              if (sameDay(day, today)) {
                  mHasToday = true;
                  mToday = day;
              }
          }
          mNumRows = calculateNumRows();
  
          // Invalidate cached accessibility information.
          mTouchHelper.invalidateRoot();
      }
  
      public void setSelectedDay(int day) {
          mSelectedDay = day;
      }
  
      public void reuse() {
          mNumRows = DEFAULT_NUM_ROWS;
          requestLayout();
      }
  
      private int calculateNumRows() {
          int offset = findDayOffset();
          int dividend = (offset + mNumCells) / mNumDays;
          int remainder = (offset + mNumCells) % mNumDays;
          return (dividend + (remainder > 0 ? 1 : 0));
      }
  
      private boolean sameDay(int day, Time today) {
          return mYear == today.year &&
                  mMonth == today.month &&
                  day == today.monthDay;
      }
  
      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
          setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows
                  + getMonthHeaderSize());
      }
  
      @Override
      protected void onSizeChanged(int w, int h, int oldw, int oldh) {
          mWidth = w;
  
          // Invalidate cached accessibility information.
          mTouchHelper.invalidateRoot();
      }
  
      public int getMonth() {
          return mMonth;
      }
  
      public int getYear() {
          return mYear;
      }
  
      /**
       * A wrapper to the MonthHeaderSize to allow override it in children
       */
      protected int getMonthHeaderSize() {
          return MONTH_HEADER_SIZE;
      }
  
      private String getMonthAndYearString() {
          int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR
                  | DateUtils.FORMAT_NO_MONTH_DAY;
          mStringBuilder.setLength(0);
          long millis = mCalendar.getTimeInMillis();
          return DateUtils.formatDateRange(getContext(), mFormatter, millis, millis, flags,
                  Time.getCurrentTimezone()).toString();
      }
  
      protected void drawMonthTitle(Canvas canvas) {
          int x = (mWidth + 2 * mEdgePadding) / 2;
          int y = (getMonthHeaderSize() - MONTH_DAY_LABEL_TEXT_SIZE) / 2 + (MONTH_LABEL_TEXT_SIZE / 3);
          canvas.drawText(getMonthAndYearString(), x, y, mMonthTitlePaint);
      }
  
      protected void drawMonthDayLabels(Canvas canvas) {
          int y = getMonthHeaderSize() - (MONTH_DAY_LABEL_TEXT_SIZE / 2);
          int dayWidthHalf = (mWidth - mEdgePadding * 2) / (mNumDays * 2);
  
          for (int i = 0; i < mNumDays; i++) {
              int calendarDay = (i + mWeekStart) % mNumDays;
              int x = (2 * i + 1) * dayWidthHalf + mEdgePadding;
              mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay);
              canvas.drawText(mDayLabelCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT,
                      Locale.getDefault()).toUpperCase(Locale.getDefault()), x, y,
                      mMonthDayLabelPaint);
          }
      }
  
      /**
       * Draws the week and month day numbers for this week. Override this method
       * if you need different placement.
       *
       * @param canvas The canvas to draw on
       */
      protected void drawMonthNums(Canvas canvas) {
          int y = (((mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2) - DAY_SEPARATOR_WIDTH)
                  + getMonthHeaderSize();
          final float dayWidthHalf = (mWidth - mEdgePadding * 2) / (mNumDays * 2.0f);
          int j = findDayOffset();
          for (int dayNumber = 1; dayNumber <= mNumCells; dayNumber++) {
              final int x = (int)((2 * j + 1) * dayWidthHalf + mEdgePadding);
  
              int yRelativeToDay = (mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2 - DAY_SEPARATOR_WIDTH;
  
              final int startX = (int)(x - dayWidthHalf);
              final int stopX = (int)(x + dayWidthHalf);
              final int startY = (int)(y - yRelativeToDay);
              final int stopY = (int)(startY + mRowHeight);
  
              drawMonthDay(canvas, mYear, mMonth, dayNumber, x, y, startX, stopX, startY, stopY);
  
              j++;
              if (j == mNumDays) {
                  j = 0;
                  y += mRowHeight;
              }
          }
      }
  
      /**
       * This method should draw the month day.  Implemented by sub-classes to allow customization.
       *
       * @param canvas  The canvas to draw on
       * @param year  The year of this month day
       * @param month  The month of this month day
       * @param day  The day number of this month day
       * @param x  The default x position to draw the day number
       * @param y  The default y position to draw the day number
       * @param startX  The left boundary of the day number rect
       * @param stopX  The right boundary of the day number rect
       * @param startY  The top boundary of the day number rect
       * @param stopY  The bottom boundary of the day number rect
       */
      public abstract void drawMonthDay(Canvas canvas, int year, int month, int day,
              int x, int y, int startX, int stopX, int startY, int stopY);
  
      protected int findDayOffset() {
          return (mDayOfWeekStart < mWeekStart ? (mDayOfWeekStart + mNumDays) : mDayOfWeekStart)
                  - mWeekStart;
      }
  
  
      /**
       * Calculates the day that the given x position is in, accounting for week
       * number. Returns the day or -1 if the position wasn't in a day.
       *
       * @param x The x position of the touch event
       * @return The day number, or -1 if the position wasn't in a day
       */
      public int getDayFromLocation(float x, float y) {
          final int day = getInternalDayFromLocation(x, y);
          if (day < 1 || day > mNumCells) {
              return -1;
          }
          return day;
      }
  
      /**
       * Calculates the day that the given x position is in, accounting for week
       * number.
       *
       * @param x The x position of the touch event
       * @return The day number
       */
      protected int getInternalDayFromLocation(float x, float y) {
          int dayStart = mEdgePadding;
          if (x < dayStart || x > mWidth - mEdgePadding) {
              return -1;
          }
          // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels
          int row = (int) (y - getMonthHeaderSize()) / mRowHeight;
          int column = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mEdgePadding));
  
          int day = column - findDayOffset() + 1;
          day += row * mNumDays;
          return day;
      }
  
      /**
       * Called when the user clicks on a day. Handles callbacks to the
       * {@link OnDayClickListener} if one is set.
       * <p/>
       * If the day is out of the range set by minDate and/or maxDate, this is a no-op.
       *
       * @param day The day that was clicked
       */
      private void onDayClick(int day) {
          // If the min / max date are set, only process the click if it's a valid selection.
          if (isOutOfRange(mYear, mMonth, day)) {
              return;
          }
  
  
          if (mOnDayClickListener != null) {
              mOnDayClickListener.onDayClick(this, new CalendarDay(mYear, mMonth, day));
          }
  
          // This is a no-op if accessibility is turned off.
          mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED);
      }
  
      /**
       * @return true if the specified year/month/day are within the range set by minDate and maxDate.
       * If one or either have not been set, they are considered as Integer.MIN_VALUE and
       * Integer.MAX_VALUE.
       */
      protected boolean isOutOfRange(int year, int month, int day) {
          if (isBeforeMin(year, month, day)) {
              return true;
          } else if (isAfterMax(year, month, day)) {
              return true;
          }
  
          return false;
      }
  
      private boolean isBeforeMin(int year, int month, int day) {
          if (mController == null) {
              return false;
          }
          Calendar minDate = mController.getMinDate();
          if (minDate == null) {
              return false;
          }
  
          if (year < minDate.get(Calendar.YEAR)) {
              return true;
          } else if (year > minDate.get(Calendar.YEAR)) {
              return false;
          }
  
          if (month < minDate.get(Calendar.MONTH)) {
              return true;
          } else if (month > minDate.get(Calendar.MONTH)) {
              return false;
          }
  
          if (day < minDate.get(Calendar.DAY_OF_MONTH)) {
              return true;
          } else {
              return false;
          }
      }
  
      private boolean isAfterMax(int year, int month, int day) {
          if (mController == null) {
              return false;
          }
          Calendar maxDate = mController.getMaxDate();
          if (maxDate == null) {
              return false;
          }
  
          if (year > maxDate.get(Calendar.YEAR)) {
              return true;
          } else if (year < maxDate.get(Calendar.YEAR)) {
              return false;
          }
  
          if (month > maxDate.get(Calendar.MONTH)) {
              return true;
          } else if (month < maxDate.get(Calendar.MONTH)) {
              return false;
          }
  
          if (day > maxDate.get(Calendar.DAY_OF_MONTH)) {
              return true;
          } else {
              return false;
          }
      }
  
      /**
       * @return The date that has accessibility focus, or {@code null} if no date
       *         has focus
       */
      public CalendarDay getAccessibilityFocus() {
          final int day = mTouchHelper.getFocusedVirtualView();
          if (day >= 0) {
              return new CalendarDay(mYear, mMonth, day);
          }
          return null;
      }
  
      /**
       * Clears accessibility focus within the view. No-op if the view does not
       * contain accessibility focus.
       */
      public void clearAccessibilityFocus() {
          mTouchHelper.clearFocusedVirtualView();
      }
  
      /**
       * Attempts to restore accessibility focus to the specified date.
       *
       * @param day The date which should receive focus
       * @return {@code false} if the date is not valid for this month view, or
       *         {@code true} if the date received focus
       */
      public boolean restoreAccessibilityFocus(CalendarDay day) {
          if ((day.year != mYear) || (day.month != mMonth) || (day.day > mNumCells)) {
              return false;
          }
          mTouchHelper.setFocusedVirtualView(day.day);
          return true;
      }
  
      /**
       * Provides a virtual view hierarchy for interfacing with an accessibility
       * service.
       */
      protected 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);
          }
  
          public void setFocusedVirtualView(int virtualViewId) {
              getAccessibilityNodeProvider(MonthView.this).performAction(
                      virtualViewId, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null);
          }
  
          public void clearFocusedVirtualView() {
              final int focusedVirtualView = getFocusedVirtualView();
              if (focusedVirtualView != ExploreByTouchHelper.INVALID_ID) {
                  getAccessibilityNodeProvider(MonthView.this).performAction(
                          focusedVirtualView,
                          AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS,
                          null);
              }
          }
  
          @Override
          protected int getVirtualViewAt(float x, float y) {
              final int day = getDayFromLocation(x, y);
              if (day >= 0) {
                  return day;
              }
              return ExploreByTouchHelper.INVALID_ID;
          }
  
          @Override
          protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
              for (int day = 1; day <= mNumCells; day++) {
                  virtualViewIds.add(day);
              }
          }
  
          @Override
          protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
              event.setContentDescription(getItemDescription(virtualViewId));
          }
  
          @Override
          protected void onPopulateNodeForVirtualView(int virtualViewId,
                  AccessibilityNodeInfoCompat node) {
              getItemBounds(virtualViewId, mTempRect);
  
              node.setContentDescription(getItemDescription(virtualViewId));
              node.setBoundsInParent(mTempRect);
              node.addAction(AccessibilityNodeInfo.ACTION_CLICK);
  
              if (virtualViewId == mSelectedDay) {
                  node.setSelected(true);
              }
  
          }
  
          @Override
          protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
                  Bundle arguments) {
              switch (action) {
                  case AccessibilityNodeInfo.ACTION_CLICK:
                      onDayClick(virtualViewId);
                      return true;
              }
  
              return false;
          }
  
          /**
           * Calculates the bounding rectangle of a given time object.
           *
           * @param day The day to calculate bounds for
           * @param rect The rectangle in which to store the bounds
           */
          protected void getItemBounds(int day, Rect rect) {
              final int offsetX = mEdgePadding;
              final int offsetY = getMonthHeaderSize();
              final int cellHeight = mRowHeight;
              final int cellWidth = ((mWidth - (2 * mEdgePadding)) / mNumDays);
              final int index = ((day - 1) + findDayOffset());
              final int row = (index / mNumDays);
              final int column = (index % mNumDays);
              final int x = (offsetX + (column * cellWidth));
              final int y = (offsetY + (row * cellHeight));
  
              rect.set(x, y, (x + cellWidth), (y + cellHeight));
          }
  
          /**
           * Generates a description for a given time object. Since this
           * description will be spoken, the components are ordered by descending
           * specificity as DAY MONTH YEAR.
           *
           * @param day The day to generate a description for
           * @return A description of the time object
           */
          protected CharSequence getItemDescription(int day) {
              mTempCalendar.set(mYear, mMonth, day);
              final CharSequence date = DateFormat.format(DATE_FORMAT,
                      mTempCalendar.getTimeInMillis());
  
              if (day == mSelectedDay) {
                  return getContext().getString(R.string.item_is_selected, date);
              }
  
              return date;
          }
      }
  
      /**
       * Handles callbacks when the user clicks on a time object.
       */
      public interface OnDayClickListener {
          public void onDayClick(MonthView view, CalendarDay day);
      }
  }
  