1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.datetimepicker.date;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.graphics.Canvas;
22import android.graphics.Paint;
23import android.graphics.Paint.Align;
24import android.graphics.Paint.Style;
25import android.graphics.Rect;
26import android.graphics.Typeface;
27import android.os.Bundle;
28import android.support.v4.view.ViewCompat;
29import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
30import android.support.v4.widget.ExploreByTouchHelper;
31import android.text.format.DateFormat;
32import android.text.format.DateUtils;
33import android.text.format.Time;
34import android.util.AttributeSet;
35import android.view.MotionEvent;
36import android.view.View;
37import android.view.accessibility.AccessibilityEvent;
38import android.view.accessibility.AccessibilityNodeInfo;
39
40import com.android.datetimepicker.R;
41import com.android.datetimepicker.Utils;
42import com.android.datetimepicker.date.MonthAdapter.CalendarDay;
43
44import java.security.InvalidParameterException;
45import java.util.Calendar;
46import java.util.Formatter;
47import java.util.HashMap;
48import java.util.List;
49import java.util.Locale;
50
51/**
52 * A calendar-like view displaying a specified month and the appropriate selectable day numbers
53 * within the specified month.
54 */
55public abstract class MonthView extends View {
56    private static final String TAG = "MonthView";
57
58    /**
59     * These params can be passed into the view to control how it appears.
60     * {@link #VIEW_PARAMS_WEEK} is the only required field, though the default
61     * values are unlikely to fit most layouts correctly.
62     */
63    /**
64     * This sets the height of this week in pixels
65     */
66    public static final String VIEW_PARAMS_HEIGHT = "height";
67    /**
68     * This specifies the position (or weeks since the epoch) of this week,
69     * calculated using {@link Utils#getWeeksSinceEpochFromJulianDay}
70     */
71    public static final String VIEW_PARAMS_MONTH = "month";
72    /**
73     * This specifies the position (or weeks since the epoch) of this week,
74     * calculated using {@link Utils#getWeeksSinceEpochFromJulianDay}
75     */
76    public static final String VIEW_PARAMS_YEAR = "year";
77    /**
78     * This sets one of the days in this view as selected {@link Time#SUNDAY}
79     * through {@link Time#SATURDAY}.
80     */
81    public static final String VIEW_PARAMS_SELECTED_DAY = "selected_day";
82    /**
83     * Which day the week should start on. {@link Time#SUNDAY} through
84     * {@link Time#SATURDAY}.
85     */
86    public static final String VIEW_PARAMS_WEEK_START = "week_start";
87    /**
88     * How many days to display at a time. Days will be displayed starting with
89     * {@link #mWeekStart}.
90     */
91    public static final String VIEW_PARAMS_NUM_DAYS = "num_days";
92    /**
93     * Which month is currently in focus, as defined by {@link Time#month}
94     * [0-11].
95     */
96    public static final String VIEW_PARAMS_FOCUS_MONTH = "focus_month";
97    /**
98     * If this month should display week numbers. false if 0, true otherwise.
99     */
100    public static final String VIEW_PARAMS_SHOW_WK_NUM = "show_wk_num";
101
102    protected static int DEFAULT_HEIGHT = 32;
103    protected static int MIN_HEIGHT = 10;
104    protected static final int DEFAULT_SELECTED_DAY = -1;
105    protected static final int DEFAULT_WEEK_START = Calendar.SUNDAY;
106    protected static final int DEFAULT_NUM_DAYS = 7;
107    protected static final int DEFAULT_SHOW_WK_NUM = 0;
108    protected static final int DEFAULT_FOCUS_MONTH = -1;
109    protected static final int DEFAULT_NUM_ROWS = 6;
110    protected static final int MAX_NUM_ROWS = 6;
111
112    private static final int SELECTED_CIRCLE_ALPHA = 60;
113
114    protected static int DAY_SEPARATOR_WIDTH = 1;
115    protected static int MINI_DAY_NUMBER_TEXT_SIZE;
116    protected static int MONTH_LABEL_TEXT_SIZE;
117    protected static int MONTH_DAY_LABEL_TEXT_SIZE;
118    protected static int MONTH_HEADER_SIZE;
119    protected static int DAY_SELECTED_CIRCLE_SIZE;
120
121    // used for scaling to the device density
122    protected static float mScale = 0;
123
124    protected DatePickerController mController;
125
126    // affects the padding on the sides of this view
127    protected int mEdgePadding = 0;
128
129    private String mDayOfWeekTypeface;
130    private String mMonthTitleTypeface;
131
132    protected Paint mMonthNumPaint;
133    protected Paint mMonthTitlePaint;
134    protected Paint mMonthTitleBGPaint;
135    protected Paint mSelectedCirclePaint;
136    protected Paint mMonthDayLabelPaint;
137
138    private final Formatter mFormatter;
139    private final StringBuilder mStringBuilder;
140
141    // The Julian day of the first day displayed by this item
142    protected int mFirstJulianDay = -1;
143    // The month of the first day in this week
144    protected int mFirstMonth = -1;
145    // The month of the last day in this week
146    protected int mLastMonth = -1;
147
148    protected int mMonth;
149
150    protected int mYear;
151    // Quick reference to the width of this view, matches parent
152    protected int mWidth;
153    // The height this view should draw at in pixels, set by height param
154    protected int mRowHeight = DEFAULT_HEIGHT;
155    // If this view contains the today
156    protected boolean mHasToday = false;
157    // Which day is selected [0-6] or -1 if no day is selected
158    protected int mSelectedDay = -1;
159    // Which day is today [0-6] or -1 if no day is today
160    protected int mToday = DEFAULT_SELECTED_DAY;
161    // Which day of the week to start on [0-6]
162    protected int mWeekStart = DEFAULT_WEEK_START;
163    // How many days to display
164    protected int mNumDays = DEFAULT_NUM_DAYS;
165    // The number of days + a spot for week number if it is displayed
166    protected int mNumCells = mNumDays;
167    // The left edge of the selected day
168    protected int mSelectedLeft = -1;
169    // The right edge of the selected day
170    protected int mSelectedRight = -1;
171
172    private final Calendar mCalendar;
173    protected final Calendar mDayLabelCalendar;
174    private final MonthViewTouchHelper mTouchHelper;
175
176    protected int mNumRows = DEFAULT_NUM_ROWS;
177
178    // Optional listener for handling day click actions
179    protected OnDayClickListener mOnDayClickListener;
180
181    // Whether to prevent setting the accessibility delegate
182    private boolean mLockAccessibilityDelegate;
183
184    protected int mDayTextColor;
185    protected int mTodayNumberColor;
186    protected int mDisabledDayTextColor;
187    protected int mMonthTitleColor;
188    protected int mMonthTitleBGColor;
189
190    public MonthView(Context context) {
191        this(context, null);
192    }
193
194    public MonthView(Context context, AttributeSet attr) {
195        super(context, attr);
196        Resources res = context.getResources();
197
198        mDayLabelCalendar = Calendar.getInstance();
199        mCalendar = Calendar.getInstance();
200
201        mDayOfWeekTypeface = res.getString(R.string.day_of_week_label_typeface);
202        mMonthTitleTypeface = res.getString(R.string.sans_serif);
203
204        mDayTextColor = res.getColor(R.color.date_picker_text_normal);
205        mTodayNumberColor = res.getColor(R.color.blue);
206        mDisabledDayTextColor = res.getColor(R.color.date_picker_text_disabled);
207        mMonthTitleColor = res.getColor(android.R.color.white);
208        mMonthTitleBGColor = res.getColor(R.color.circle_background);
209
210        mStringBuilder = new StringBuilder(50);
211        mFormatter = new Formatter(mStringBuilder, Locale.getDefault());
212
213        MINI_DAY_NUMBER_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.day_number_size);
214        MONTH_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.month_label_size);
215        MONTH_DAY_LABEL_TEXT_SIZE = res.getDimensionPixelSize(R.dimen.month_day_label_text_size);
216        MONTH_HEADER_SIZE = res.getDimensionPixelOffset(R.dimen.month_list_item_header_height);
217        DAY_SELECTED_CIRCLE_SIZE = res
218                .getDimensionPixelSize(R.dimen.day_number_select_circle_radius);
219
220        mRowHeight = (res.getDimensionPixelOffset(R.dimen.date_picker_view_animator_height)
221                - getMonthHeaderSize()) / MAX_NUM_ROWS;
222
223        // Set up accessibility components.
224        mTouchHelper = getMonthViewTouchHelper();
225        ViewCompat.setAccessibilityDelegate(this, mTouchHelper);
226        ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
227        mLockAccessibilityDelegate = true;
228
229        // Sets up any standard paints that will be used
230        initView();
231    }
232
233    public void setDatePickerController(DatePickerController controller) {
234        mController = controller;
235    }
236
237    protected MonthViewTouchHelper getMonthViewTouchHelper() {
238        return new MonthViewTouchHelper(this);
239    }
240
241    @Override
242    public void setAccessibilityDelegate(AccessibilityDelegate delegate) {
243        // Workaround for a JB MR1 issue where accessibility delegates on
244        // top-level ListView items are overwritten.
245        if (!mLockAccessibilityDelegate) {
246            super.setAccessibilityDelegate(delegate);
247        }
248    }
249
250    public void setOnDayClickListener(OnDayClickListener listener) {
251        mOnDayClickListener = listener;
252    }
253
254    @Override
255    public boolean dispatchHoverEvent(MotionEvent event) {
256        // First right-of-refusal goes the touch exploration helper.
257        if (mTouchHelper.dispatchHoverEvent(event)) {
258            return true;
259        }
260        return super.dispatchHoverEvent(event);
261    }
262
263    @Override
264    public boolean onTouchEvent(MotionEvent event) {
265        switch (event.getAction()) {
266            case MotionEvent.ACTION_UP:
267                final int day = getDayFromLocation(event.getX(), event.getY());
268                if (day >= 0) {
269                    onDayClick(day);
270                }
271                break;
272        }
273        return true;
274    }
275
276    /**
277     * Sets up the text and style properties for painting. Override this if you
278     * want to use a different paint.
279     */
280    protected void initView() {
281        mMonthTitlePaint = new Paint();
282        mMonthTitlePaint.setFakeBoldText(true);
283        mMonthTitlePaint.setAntiAlias(true);
284        mMonthTitlePaint.setTextSize(MONTH_LABEL_TEXT_SIZE);
285        mMonthTitlePaint.setTypeface(Typeface.create(mMonthTitleTypeface, Typeface.BOLD));
286        mMonthTitlePaint.setColor(mDayTextColor);
287        mMonthTitlePaint.setTextAlign(Align.CENTER);
288        mMonthTitlePaint.setStyle(Style.FILL);
289
290        mMonthTitleBGPaint = new Paint();
291        mMonthTitleBGPaint.setFakeBoldText(true);
292        mMonthTitleBGPaint.setAntiAlias(true);
293        mMonthTitleBGPaint.setColor(mMonthTitleBGColor);
294        mMonthTitleBGPaint.setTextAlign(Align.CENTER);
295        mMonthTitleBGPaint.setStyle(Style.FILL);
296
297        mSelectedCirclePaint = new Paint();
298        mSelectedCirclePaint.setFakeBoldText(true);
299        mSelectedCirclePaint.setAntiAlias(true);
300        mSelectedCirclePaint.setColor(mTodayNumberColor);
301        mSelectedCirclePaint.setTextAlign(Align.CENTER);
302        mSelectedCirclePaint.setStyle(Style.FILL);
303        mSelectedCirclePaint.setAlpha(SELECTED_CIRCLE_ALPHA);
304
305        mMonthDayLabelPaint = new Paint();
306        mMonthDayLabelPaint.setAntiAlias(true);
307        mMonthDayLabelPaint.setTextSize(MONTH_DAY_LABEL_TEXT_SIZE);
308        mMonthDayLabelPaint.setColor(mDayTextColor);
309        mMonthDayLabelPaint.setTypeface(Typeface.create(mDayOfWeekTypeface, Typeface.NORMAL));
310        mMonthDayLabelPaint.setStyle(Style.FILL);
311        mMonthDayLabelPaint.setTextAlign(Align.CENTER);
312        mMonthDayLabelPaint.setFakeBoldText(true);
313
314        mMonthNumPaint = new Paint();
315        mMonthNumPaint.setAntiAlias(true);
316        mMonthNumPaint.setTextSize(MINI_DAY_NUMBER_TEXT_SIZE);
317        mMonthNumPaint.setStyle(Style.FILL);
318        mMonthNumPaint.setTextAlign(Align.CENTER);
319        mMonthNumPaint.setFakeBoldText(false);
320    }
321
322    @Override
323    protected void onDraw(Canvas canvas) {
324        drawMonthTitle(canvas);
325        drawMonthDayLabels(canvas);
326        drawMonthNums(canvas);
327    }
328
329    private int mDayOfWeekStart = 0;
330
331    /**
332     * Sets all the parameters for displaying this week. The only required
333     * parameter is the week number. Other parameters have a default value and
334     * will only update if a new value is included, except for focus month,
335     * which will always default to no focus month if no value is passed in. See
336     * {@link #VIEW_PARAMS_HEIGHT} for more info on parameters.
337     *
338     * @param params A map of the new parameters, see
339     *            {@link #VIEW_PARAMS_HEIGHT}
340     */
341    public void setMonthParams(HashMap<String, Integer> params) {
342        if (!params.containsKey(VIEW_PARAMS_MONTH) && !params.containsKey(VIEW_PARAMS_YEAR)) {
343            throw new InvalidParameterException("You must specify month and year for this view");
344        }
345        setTag(params);
346        // We keep the current value for any params not present
347        if (params.containsKey(VIEW_PARAMS_HEIGHT)) {
348            mRowHeight = params.get(VIEW_PARAMS_HEIGHT);
349            if (mRowHeight < MIN_HEIGHT) {
350                mRowHeight = MIN_HEIGHT;
351            }
352        }
353        if (params.containsKey(VIEW_PARAMS_SELECTED_DAY)) {
354            mSelectedDay = params.get(VIEW_PARAMS_SELECTED_DAY);
355        }
356
357        // Allocate space for caching the day numbers and focus values
358        mMonth = params.get(VIEW_PARAMS_MONTH);
359        mYear = params.get(VIEW_PARAMS_YEAR);
360
361        // Figure out what day today is
362        final Time today = new Time(Time.getCurrentTimezone());
363        today.setToNow();
364        mHasToday = false;
365        mToday = -1;
366
367        mCalendar.set(Calendar.MONTH, mMonth);
368        mCalendar.set(Calendar.YEAR, mYear);
369        mCalendar.set(Calendar.DAY_OF_MONTH, 1);
370        mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK);
371
372        if (params.containsKey(VIEW_PARAMS_WEEK_START)) {
373            mWeekStart = params.get(VIEW_PARAMS_WEEK_START);
374        } else {
375            mWeekStart = mCalendar.getFirstDayOfWeek();
376        }
377
378        mNumCells = Utils.getDaysInMonth(mMonth, mYear);
379        for (int i = 0; i < mNumCells; i++) {
380            final int day = i + 1;
381            if (sameDay(day, today)) {
382                mHasToday = true;
383                mToday = day;
384            }
385        }
386        mNumRows = calculateNumRows();
387
388        // Invalidate cached accessibility information.
389        mTouchHelper.invalidateRoot();
390    }
391
392    public void setSelectedDay(int day) {
393        mSelectedDay = day;
394    }
395
396    public void reuse() {
397        mNumRows = DEFAULT_NUM_ROWS;
398        requestLayout();
399    }
400
401    private int calculateNumRows() {
402        int offset = findDayOffset();
403        int dividend = (offset + mNumCells) / mNumDays;
404        int remainder = (offset + mNumCells) % mNumDays;
405        return (dividend + (remainder > 0 ? 1 : 0));
406    }
407
408    private boolean sameDay(int day, Time today) {
409        return mYear == today.year &&
410                mMonth == today.month &&
411                day == today.monthDay;
412    }
413
414    @Override
415    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
416        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mRowHeight * mNumRows
417                + getMonthHeaderSize());
418    }
419
420    @Override
421    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
422        mWidth = w;
423
424        // Invalidate cached accessibility information.
425        mTouchHelper.invalidateRoot();
426    }
427
428    public int getMonth() {
429        return mMonth;
430    }
431
432    public int getYear() {
433        return mYear;
434    }
435
436    /**
437     * A wrapper to the MonthHeaderSize to allow override it in children
438     */
439    protected int getMonthHeaderSize() {
440        return MONTH_HEADER_SIZE;
441    }
442
443    private String getMonthAndYearString() {
444        int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR
445                | DateUtils.FORMAT_NO_MONTH_DAY;
446        mStringBuilder.setLength(0);
447        long millis = mCalendar.getTimeInMillis();
448        return DateUtils.formatDateRange(getContext(), mFormatter, millis, millis, flags,
449                Time.getCurrentTimezone()).toString();
450    }
451
452    protected void drawMonthTitle(Canvas canvas) {
453        int x = (mWidth + 2 * mEdgePadding) / 2;
454        int y = (getMonthHeaderSize() - MONTH_DAY_LABEL_TEXT_SIZE) / 2 + (MONTH_LABEL_TEXT_SIZE / 3);
455        canvas.drawText(getMonthAndYearString(), x, y, mMonthTitlePaint);
456    }
457
458    protected void drawMonthDayLabels(Canvas canvas) {
459        int y = getMonthHeaderSize() - (MONTH_DAY_LABEL_TEXT_SIZE / 2);
460        int dayWidthHalf = (mWidth - mEdgePadding * 2) / (mNumDays * 2);
461
462        for (int i = 0; i < mNumDays; i++) {
463            int calendarDay = (i + mWeekStart) % mNumDays;
464            int x = (2 * i + 1) * dayWidthHalf + mEdgePadding;
465            mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay);
466            canvas.drawText(mDayLabelCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT,
467                    Locale.getDefault()).toUpperCase(Locale.getDefault()), x, y,
468                    mMonthDayLabelPaint);
469        }
470    }
471
472    /**
473     * Draws the week and month day numbers for this week. Override this method
474     * if you need different placement.
475     *
476     * @param canvas The canvas to draw on
477     */
478    protected void drawMonthNums(Canvas canvas) {
479        int y = (((mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2) - DAY_SEPARATOR_WIDTH)
480                + getMonthHeaderSize();
481        final float dayWidthHalf = (mWidth - mEdgePadding * 2) / (mNumDays * 2.0f);
482        int j = findDayOffset();
483        for (int dayNumber = 1; dayNumber <= mNumCells; dayNumber++) {
484            final int x = (int)((2 * j + 1) * dayWidthHalf + mEdgePadding);
485
486            int yRelativeToDay = (mRowHeight + MINI_DAY_NUMBER_TEXT_SIZE) / 2 - DAY_SEPARATOR_WIDTH;
487
488            final int startX = (int)(x - dayWidthHalf);
489            final int stopX = (int)(x + dayWidthHalf);
490            final int startY = (int)(y - yRelativeToDay);
491            final int stopY = (int)(startY + mRowHeight);
492
493            drawMonthDay(canvas, mYear, mMonth, dayNumber, x, y, startX, stopX, startY, stopY);
494
495            j++;
496            if (j == mNumDays) {
497                j = 0;
498                y += mRowHeight;
499            }
500        }
501    }
502
503    /**
504     * This method should draw the month day.  Implemented by sub-classes to allow customization.
505     *
506     * @param canvas  The canvas to draw on
507     * @param year  The year of this month day
508     * @param month  The month of this month day
509     * @param day  The day number of this month day
510     * @param x  The default x position to draw the day number
511     * @param y  The default y position to draw the day number
512     * @param startX  The left boundary of the day number rect
513     * @param stopX  The right boundary of the day number rect
514     * @param startY  The top boundary of the day number rect
515     * @param stopY  The bottom boundary of the day number rect
516     */
517    public abstract void drawMonthDay(Canvas canvas, int year, int month, int day,
518            int x, int y, int startX, int stopX, int startY, int stopY);
519
520    protected int findDayOffset() {
521        return (mDayOfWeekStart < mWeekStart ? (mDayOfWeekStart + mNumDays) : mDayOfWeekStart)
522                - mWeekStart;
523    }
524
525
526    /**
527     * Calculates the day that the given x position is in, accounting for week
528     * number. Returns the day or -1 if the position wasn't in a day.
529     *
530     * @param x The x position of the touch event
531     * @return The day number, or -1 if the position wasn't in a day
532     */
533    public int getDayFromLocation(float x, float y) {
534        final int day = getInternalDayFromLocation(x, y);
535        if (day < 1 || day > mNumCells) {
536            return -1;
537        }
538        return day;
539    }
540
541    /**
542     * Calculates the day that the given x position is in, accounting for week
543     * number.
544     *
545     * @param x The x position of the touch event
546     * @return The day number
547     */
548    protected int getInternalDayFromLocation(float x, float y) {
549        int dayStart = mEdgePadding;
550        if (x < dayStart || x > mWidth - mEdgePadding) {
551            return -1;
552        }
553        // Selection is (x - start) / (pixels/day) == (x -s) * day / pixels
554        int row = (int) (y - getMonthHeaderSize()) / mRowHeight;
555        int column = (int) ((x - dayStart) * mNumDays / (mWidth - dayStart - mEdgePadding));
556
557        int day = column - findDayOffset() + 1;
558        day += row * mNumDays;
559        return day;
560    }
561
562    /**
563     * Called when the user clicks on a day. Handles callbacks to the
564     * {@link OnDayClickListener} if one is set.
565     * <p/>
566     * If the day is out of the range set by minDate and/or maxDate, this is a no-op.
567     *
568     * @param day The day that was clicked
569     */
570    private void onDayClick(int day) {
571        // If the min / max date are set, only process the click if it's a valid selection.
572        if (isOutOfRange(mYear, mMonth, day)) {
573            return;
574        }
575
576
577        if (mOnDayClickListener != null) {
578            mOnDayClickListener.onDayClick(this, new CalendarDay(mYear, mMonth, day));
579        }
580
581        // This is a no-op if accessibility is turned off.
582        mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED);
583    }
584
585    /**
586     * @return true if the specified year/month/day are within the range set by minDate and maxDate.
587     * If one or either have not been set, they are considered as Integer.MIN_VALUE and
588     * Integer.MAX_VALUE.
589     */
590    protected boolean isOutOfRange(int year, int month, int day) {
591        if (isBeforeMin(year, month, day)) {
592            return true;
593        } else if (isAfterMax(year, month, day)) {
594            return true;
595        }
596
597        return false;
598    }
599
600    private boolean isBeforeMin(int year, int month, int day) {
601        if (mController == null) {
602            return false;
603        }
604        Calendar minDate = mController.getMinDate();
605        if (minDate == null) {
606            return false;
607        }
608
609        if (year < minDate.get(Calendar.YEAR)) {
610            return true;
611        } else if (year > minDate.get(Calendar.YEAR)) {
612            return false;
613        }
614
615        if (month < minDate.get(Calendar.MONTH)) {
616            return true;
617        } else if (month > minDate.get(Calendar.MONTH)) {
618            return false;
619        }
620
621        if (day < minDate.get(Calendar.DAY_OF_MONTH)) {
622            return true;
623        } else {
624            return false;
625        }
626    }
627
628    private boolean isAfterMax(int year, int month, int day) {
629        if (mController == null) {
630            return false;
631        }
632        Calendar maxDate = mController.getMaxDate();
633        if (maxDate == null) {
634            return false;
635        }
636
637        if (year > maxDate.get(Calendar.YEAR)) {
638            return true;
639        } else if (year < maxDate.get(Calendar.YEAR)) {
640            return false;
641        }
642
643        if (month > maxDate.get(Calendar.MONTH)) {
644            return true;
645        } else if (month < maxDate.get(Calendar.MONTH)) {
646            return false;
647        }
648
649        if (day > maxDate.get(Calendar.DAY_OF_MONTH)) {
650            return true;
651        } else {
652            return false;
653        }
654    }
655
656    /**
657     * @return The date that has accessibility focus, or {@code null} if no date
658     *         has focus
659     */
660    public CalendarDay getAccessibilityFocus() {
661        final int day = mTouchHelper.getFocusedVirtualView();
662        if (day >= 0) {
663            return new CalendarDay(mYear, mMonth, day);
664        }
665        return null;
666    }
667
668    /**
669     * Clears accessibility focus within the view. No-op if the view does not
670     * contain accessibility focus.
671     */
672    public void clearAccessibilityFocus() {
673        mTouchHelper.clearFocusedVirtualView();
674    }
675
676    /**
677     * Attempts to restore accessibility focus to the specified date.
678     *
679     * @param day The date which should receive focus
680     * @return {@code false} if the date is not valid for this month view, or
681     *         {@code true} if the date received focus
682     */
683    public boolean restoreAccessibilityFocus(CalendarDay day) {
684        if ((day.year != mYear) || (day.month != mMonth) || (day.day > mNumCells)) {
685            return false;
686        }
687        mTouchHelper.setFocusedVirtualView(day.day);
688        return true;
689    }
690
691    /**
692     * Provides a virtual view hierarchy for interfacing with an accessibility
693     * service.
694     */
695    protected class MonthViewTouchHelper extends ExploreByTouchHelper {
696        private static final String DATE_FORMAT = "dd MMMM yyyy";
697
698        private final Rect mTempRect = new Rect();
699        private final Calendar mTempCalendar = Calendar.getInstance();
700
701        public MonthViewTouchHelper(View host) {
702            super(host);
703        }
704
705        public void setFocusedVirtualView(int virtualViewId) {
706            getAccessibilityNodeProvider(MonthView.this).performAction(
707                    virtualViewId, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null);
708        }
709
710        public void clearFocusedVirtualView() {
711            final int focusedVirtualView = getFocusedVirtualView();
712            if (focusedVirtualView != ExploreByTouchHelper.INVALID_ID) {
713                getAccessibilityNodeProvider(MonthView.this).performAction(
714                        focusedVirtualView,
715                        AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS,
716                        null);
717            }
718        }
719
720        @Override
721        protected int getVirtualViewAt(float x, float y) {
722            final int day = getDayFromLocation(x, y);
723            if (day >= 0) {
724                return day;
725            }
726            return ExploreByTouchHelper.INVALID_ID;
727        }
728
729        @Override
730        protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
731            for (int day = 1; day <= mNumCells; day++) {
732                virtualViewIds.add(day);
733            }
734        }
735
736        @Override
737        protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
738            event.setContentDescription(getItemDescription(virtualViewId));
739        }
740
741        @Override
742        protected void onPopulateNodeForVirtualView(int virtualViewId,
743                AccessibilityNodeInfoCompat node) {
744            getItemBounds(virtualViewId, mTempRect);
745
746            node.setContentDescription(getItemDescription(virtualViewId));
747            node.setBoundsInParent(mTempRect);
748            node.addAction(AccessibilityNodeInfo.ACTION_CLICK);
749
750            if (virtualViewId == mSelectedDay) {
751                node.setSelected(true);
752            }
753
754        }
755
756        @Override
757        protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
758                Bundle arguments) {
759            switch (action) {
760                case AccessibilityNodeInfo.ACTION_CLICK:
761                    onDayClick(virtualViewId);
762                    return true;
763            }
764
765            return false;
766        }
767
768        /**
769         * Calculates the bounding rectangle of a given time object.
770         *
771         * @param day The day to calculate bounds for
772         * @param rect The rectangle in which to store the bounds
773         */
774        protected void getItemBounds(int day, Rect rect) {
775            final int offsetX = mEdgePadding;
776            final int offsetY = getMonthHeaderSize();
777            final int cellHeight = mRowHeight;
778            final int cellWidth = ((mWidth - (2 * mEdgePadding)) / mNumDays);
779            final int index = ((day - 1) + findDayOffset());
780            final int row = (index / mNumDays);
781            final int column = (index % mNumDays);
782            final int x = (offsetX + (column * cellWidth));
783            final int y = (offsetY + (row * cellHeight));
784
785            rect.set(x, y, (x + cellWidth), (y + cellHeight));
786        }
787
788        /**
789         * Generates a description for a given time object. Since this
790         * description will be spoken, the components are ordered by descending
791         * specificity as DAY MONTH YEAR.
792         *
793         * @param day The day to generate a description for
794         * @return A description of the time object
795         */
796        protected CharSequence getItemDescription(int day) {
797            mTempCalendar.set(mYear, mMonth, day);
798            final CharSequence date = DateFormat.format(DATE_FORMAT,
799                    mTempCalendar.getTimeInMillis());
800
801            if (day == mSelectedDay) {
802                return getContext().getString(R.string.item_is_selected, date);
803            }
804
805            return date;
806        }
807    }
808
809    /**
810     * Handles callbacks when the user clicks on a time object.
811     */
812    public interface OnDayClickListener {
813        public void onDayClick(MonthView view, CalendarDay day);
814    }
815}
816