/*
 * Copyright (C) 2010 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.Widget;
import android.app.Service;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.database.DataSetObserver;
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.drawable.Drawable;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.AbsListView.OnScrollListener;

import com.android.internal.R;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;
import java.util.TimeZone;

import libcore.icu.LocaleData;

/**
 * This class is a calendar widget for displaying and selecting dates. The range
 * of dates supported by this calendar is configurable. A user can select a date
 * by taping on it and can scroll and fling the calendar to a desired date.
 *
 * @attr ref android.R.styleable#CalendarView_showWeekNumber
 * @attr ref android.R.styleable#CalendarView_firstDayOfWeek
 * @attr ref android.R.styleable#CalendarView_minDate
 * @attr ref android.R.styleable#CalendarView_maxDate
 * @attr ref android.R.styleable#CalendarView_shownWeekCount
 * @attr ref android.R.styleable#CalendarView_selectedWeekBackgroundColor
 * @attr ref android.R.styleable#CalendarView_focusedMonthDateColor
 * @attr ref android.R.styleable#CalendarView_unfocusedMonthDateColor
 * @attr ref android.R.styleable#CalendarView_weekNumberColor
 * @attr ref android.R.styleable#CalendarView_weekSeparatorLineColor
 * @attr ref android.R.styleable#CalendarView_selectedDateVerticalBar
 * @attr ref android.R.styleable#CalendarView_weekDayTextAppearance
 * @attr ref android.R.styleable#CalendarView_dateTextAppearance
 */
@Widget
public class CalendarView extends FrameLayout {

    /**
     * Tag for logging.
     */
    private static final String LOG_TAG = CalendarView.class.getSimpleName();

    private CalendarViewDelegate mDelegate;

    /**
     * The callback used to indicate the user changes the date.
     */
    public interface OnDateChangeListener {

        /**
         * Called upon change of the selected day.
         *
         * @param view The view associated with this listener.
         * @param year The year that was set.
         * @param month The month that was set [0-11].
         * @param dayOfMonth The day of the month that was set.
         */
        public void onSelectedDayChange(CalendarView view, int year, int month, int dayOfMonth);
    }

    public CalendarView(Context context) {
        this(context, null);
    }

    public CalendarView(Context context, AttributeSet attrs) {
        this(context, attrs, R.attr.calendarViewStyle);
    }

    public CalendarView(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public CalendarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

        mDelegate = new LegacyCalendarViewDelegate(this, context, attrs, defStyleAttr, defStyleRes);
    }

    /**
     * Sets the number of weeks to be shown.
     *
     * @param count The shown week count.
     *
     * @attr ref android.R.styleable#CalendarView_shownWeekCount
     */
    public void setShownWeekCount(int count) {
        mDelegate.setShownWeekCount(count);
    }

    /**
     * Gets the number of weeks to be shown.
     *
     * @return The shown week count.
     *
     * @attr ref android.R.styleable#CalendarView_shownWeekCount
     */
    public int getShownWeekCount() {
        return mDelegate.getShownWeekCount();
    }

    /**
     * Sets the background color for the selected week.
     *
     * @param color The week background color.
     *
     * @attr ref android.R.styleable#CalendarView_selectedWeekBackgroundColor
     */
    public void setSelectedWeekBackgroundColor(int color) {
        mDelegate.setSelectedWeekBackgroundColor(color);
    }

    /**
     * Gets the background color for the selected week.
     *
     * @return The week background color.
     *
     * @attr ref android.R.styleable#CalendarView_selectedWeekBackgroundColor
     */
    public int getSelectedWeekBackgroundColor() {
        return mDelegate.getSelectedWeekBackgroundColor();
    }

    /**
     * Sets the color for the dates of the focused month.
     *
     * @param color The focused month date color.
     *
     * @attr ref android.R.styleable#CalendarView_focusedMonthDateColor
     */
    public void setFocusedMonthDateColor(int color) {
        mDelegate.setFocusedMonthDateColor(color);
    }

    /**
     * Gets the color for the dates in the focused month.
     *
     * @return The focused month date color.
     *
     * @attr ref android.R.styleable#CalendarView_focusedMonthDateColor
     */
    public int getFocusedMonthDateColor() {
        return mDelegate.getFocusedMonthDateColor();
    }

    /**
     * Sets the color for the dates of a not focused month.
     *
     * @param color A not focused month date color.
     *
     * @attr ref android.R.styleable#CalendarView_unfocusedMonthDateColor
     */
    public void setUnfocusedMonthDateColor(int color) {
        mDelegate.setUnfocusedMonthDateColor(color);
    }

    /**
     * Gets the color for the dates in a not focused month.
     *
     * @return A not focused month date color.
     *
     * @attr ref android.R.styleable#CalendarView_unfocusedMonthDateColor
     */
    public int getUnfocusedMonthDateColor() {
        return mDelegate.getUnfocusedMonthDateColor();
    }

    /**
     * Sets the color for the week numbers.
     *
     * @param color The week number color.
     *
     * @attr ref android.R.styleable#CalendarView_weekNumberColor
     */
    public void setWeekNumberColor(int color) {
        mDelegate.setWeekNumberColor(color);
    }

    /**
     * Gets the color for the week numbers.
     *
     * @return The week number color.
     *
     * @attr ref android.R.styleable#CalendarView_weekNumberColor
     */
    public int getWeekNumberColor() {
        return mDelegate.getWeekNumberColor();
    }

    /**
     * Sets the color for the separator line between weeks.
     *
     * @param color The week separator color.
     *
     * @attr ref android.R.styleable#CalendarView_weekSeparatorLineColor
     */
    public void setWeekSeparatorLineColor(int color) {
        mDelegate.setWeekSeparatorLineColor(color);
    }

    /**
     * Gets the color for the separator line between weeks.
     *
     * @return The week separator color.
     *
     * @attr ref android.R.styleable#CalendarView_weekSeparatorLineColor
     */
    public int getWeekSeparatorLineColor() {
        return mDelegate.getWeekSeparatorLineColor();
    }

    /**
     * Sets the drawable for the vertical bar shown at the beginning and at
     * the end of the selected date.
     *
     * @param resourceId The vertical bar drawable resource id.
     *
     * @attr ref android.R.styleable#CalendarView_selectedDateVerticalBar
     */
    public void setSelectedDateVerticalBar(int resourceId) {
        mDelegate.setSelectedDateVerticalBar(resourceId);
    }

    /**
     * Sets the drawable for the vertical bar shown at the beginning and at
     * the end of the selected date.
     *
     * @param drawable The vertical bar drawable.
     *
     * @attr ref android.R.styleable#CalendarView_selectedDateVerticalBar
     */
    public void setSelectedDateVerticalBar(Drawable drawable) {
        mDelegate.setSelectedDateVerticalBar(drawable);
    }

    /**
     * Gets the drawable for the vertical bar shown at the beginning and at
     * the end of the selected date.
     *
     * @return The vertical bar drawable.
     */
    public Drawable getSelectedDateVerticalBar() {
        return mDelegate.getSelectedDateVerticalBar();
    }

    /**
     * Sets the text appearance for the week day abbreviation of the calendar header.
     *
     * @param resourceId The text appearance resource id.
     *
     * @attr ref android.R.styleable#CalendarView_weekDayTextAppearance
     */
    public void setWeekDayTextAppearance(int resourceId) {
        mDelegate.setWeekDayTextAppearance(resourceId);
    }

    /**
     * Gets the text appearance for the week day abbreviation of the calendar header.
     *
     * @return The text appearance resource id.
     *
     * @attr ref android.R.styleable#CalendarView_weekDayTextAppearance
     */
    public int getWeekDayTextAppearance() {
        return mDelegate.getWeekDayTextAppearance();
    }

    /**
     * Sets the text appearance for the calendar dates.
     *
     * @param resourceId The text appearance resource id.
     *
     * @attr ref android.R.styleable#CalendarView_dateTextAppearance
     */
    public void setDateTextAppearance(int resourceId) {
        mDelegate.setDateTextAppearance(resourceId);
    }

    /**
     * Gets the text appearance for the calendar dates.
     *
     * @return The text appearance resource id.
     *
     * @attr ref android.R.styleable#CalendarView_dateTextAppearance
     */
    public int getDateTextAppearance() {
        return mDelegate.getDateTextAppearance();
    }

    @Override
    public void setEnabled(boolean enabled) {
        mDelegate.setEnabled(enabled);
    }

    @Override
    public boolean isEnabled() {
        return mDelegate.isEnabled();
    }

    /**
     * Gets the minimal date supported by this {@link CalendarView} in milliseconds
     * since January 1, 1970 00:00:00 in {@link TimeZone#getDefault()} time
     * zone.
     * <p>
     * Note: The default minimal date is 01/01/1900.
     * <p>
     *
     * @return The minimal supported date.
     *
     * @attr ref android.R.styleable#CalendarView_minDate
     */
    public long getMinDate() {
        return mDelegate.getMinDate();
    }

    /**
     * Sets the minimal date supported by this {@link CalendarView} in milliseconds
     * since January 1, 1970 00:00:00 in {@link TimeZone#getDefault()} time
     * zone.
     *
     * @param minDate The minimal supported date.
     *
     * @attr ref android.R.styleable#CalendarView_minDate
     */
    public void setMinDate(long minDate) {
        mDelegate.setMinDate(minDate);
    }

    /**
     * Gets the maximal date supported by this {@link CalendarView} in milliseconds
     * since January 1, 1970 00:00:00 in {@link TimeZone#getDefault()} time
     * zone.
     * <p>
     * Note: The default maximal date is 01/01/2100.
     * <p>
     *
     * @return The maximal supported date.
     *
     * @attr ref android.R.styleable#CalendarView_maxDate
     */
    public long getMaxDate() {
        return mDelegate.getMaxDate();
    }

    /**
     * Sets the maximal date supported by this {@link CalendarView} in milliseconds
     * since January 1, 1970 00:00:00 in {@link TimeZone#getDefault()} time
     * zone.
     *
     * @param maxDate The maximal supported date.
     *
     * @attr ref android.R.styleable#CalendarView_maxDate
     */
    public void setMaxDate(long maxDate) {
        mDelegate.setMaxDate(maxDate);
    }

    /**
     * Sets whether to show the week number.
     *
     * @param showWeekNumber True to show the week number.
     *
     * @attr ref android.R.styleable#CalendarView_showWeekNumber
     */
    public void setShowWeekNumber(boolean showWeekNumber) {
        mDelegate.setShowWeekNumber(showWeekNumber);
    }

    /**
     * Gets whether to show the week number.
     *
     * @return True if showing the week number.
     *
     * @attr ref android.R.styleable#CalendarView_showWeekNumber
     */
    public boolean getShowWeekNumber() {
        return mDelegate.getShowWeekNumber();
    }

    /**
     * Gets the first day of week.
     *
     * @return The first day of the week conforming to the {@link CalendarView}
     *         APIs.
     * @see Calendar#MONDAY
     * @see Calendar#TUESDAY
     * @see Calendar#WEDNESDAY
     * @see Calendar#THURSDAY
     * @see Calendar#FRIDAY
     * @see Calendar#SATURDAY
     * @see Calendar#SUNDAY
     *
     * @attr ref android.R.styleable#CalendarView_firstDayOfWeek
     */
    public int getFirstDayOfWeek() {
        return mDelegate.getFirstDayOfWeek();
    }

    /**
     * Sets the first day of week.
     *
     * @param firstDayOfWeek The first day of the week conforming to the
     *            {@link CalendarView} APIs.
     * @see Calendar#MONDAY
     * @see Calendar#TUESDAY
     * @see Calendar#WEDNESDAY
     * @see Calendar#THURSDAY
     * @see Calendar#FRIDAY
     * @see Calendar#SATURDAY
     * @see Calendar#SUNDAY
     *
     * @attr ref android.R.styleable#CalendarView_firstDayOfWeek
     */
    public void setFirstDayOfWeek(int firstDayOfWeek) {
        mDelegate.setFirstDayOfWeek(firstDayOfWeek);
    }

    /**
     * Sets the listener to be notified upon selected date change.
     *
     * @param listener The listener to be notified.
     */
    public void setOnDateChangeListener(OnDateChangeListener listener) {
        mDelegate.setOnDateChangeListener(listener);
    }

    /**
     * Gets the selected date in milliseconds since January 1, 1970 00:00:00 in
     * {@link TimeZone#getDefault()} time zone.
     *
     * @return The selected date.
     */
    public long getDate() {
        return mDelegate.getDate();
    }

    /**
     * Sets the selected date in milliseconds since January 1, 1970 00:00:00 in
     * {@link TimeZone#getDefault()} time zone.
     *
     * @param date The selected date.
     *
     * @throws IllegalArgumentException of the provided date is before the
     *        minimal or after the maximal date.
     *
     * @see #setDate(long, boolean, boolean)
     * @see #setMinDate(long)
     * @see #setMaxDate(long)
     */
    public void setDate(long date) {
        mDelegate.setDate(date);
    }

    /**
     * Sets the selected date in milliseconds since January 1, 1970 00:00:00 in
     * {@link TimeZone#getDefault()} time zone.
     *
     * @param date The date.
     * @param animate Whether to animate the scroll to the current date.
     * @param center Whether to center the current date even if it is already visible.
     *
     * @throws IllegalArgumentException of the provided date is before the
     *        minimal or after the maximal date.
     *
     * @see #setMinDate(long)
     * @see #setMaxDate(long)
     */
    public void setDate(long date, boolean animate, boolean center) {
        mDelegate.setDate(date, animate, center);
    }

    @Override
    protected void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        mDelegate.onConfigurationChanged(newConfig);
    }

    @Override
    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
        super.onInitializeAccessibilityEvent(event);
        mDelegate.onInitializeAccessibilityEvent(event);
    }

    @Override
    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
        super.onInitializeAccessibilityNodeInfo(info);
        mDelegate.onInitializeAccessibilityNodeInfo(info);
    }

    /**
     * A delegate interface that defined the public API of the CalendarView. Allows different
     * CalendarView implementations. This would need to be implemented by the CalendarView delegates
     * for the real behavior.
     */
    private interface CalendarViewDelegate {
        void setShownWeekCount(int count);
        int getShownWeekCount();

        void setSelectedWeekBackgroundColor(int color);
        int getSelectedWeekBackgroundColor();

        void setFocusedMonthDateColor(int color);
        int getFocusedMonthDateColor();

        void setUnfocusedMonthDateColor(int color);
        int getUnfocusedMonthDateColor();

        void setWeekNumberColor(int color);
        int getWeekNumberColor();

        void setWeekSeparatorLineColor(int color);
        int getWeekSeparatorLineColor();

        void setSelectedDateVerticalBar(int resourceId);
        void setSelectedDateVerticalBar(Drawable drawable);
        Drawable getSelectedDateVerticalBar();

        void setWeekDayTextAppearance(int resourceId);
        int getWeekDayTextAppearance();

        void setDateTextAppearance(int resourceId);
        int getDateTextAppearance();

        void setEnabled(boolean enabled);
        boolean isEnabled();

        void setMinDate(long minDate);
        long getMinDate();

        void setMaxDate(long maxDate);
        long getMaxDate();

        void setShowWeekNumber(boolean showWeekNumber);
        boolean getShowWeekNumber();

        void setFirstDayOfWeek(int firstDayOfWeek);
        int getFirstDayOfWeek();

        void setDate(long date);
        void setDate(long date, boolean animate, boolean center);
        long getDate();

        void setOnDateChangeListener(OnDateChangeListener listener);

        void onConfigurationChanged(Configuration newConfig);
        void onInitializeAccessibilityEvent(AccessibilityEvent event);
        void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info);
    }

    /**
     * An abstract class which can be used as a start for CalendarView implementations
     */
    abstract static class AbstractCalendarViewDelegate implements CalendarViewDelegate {
        // The delegator
        protected CalendarView mDelegator;

        // The context
        protected Context mContext;

        // The current locale
        protected Locale mCurrentLocale;

        AbstractCalendarViewDelegate(CalendarView delegator, Context context) {
            mDelegator = delegator;
            mContext = context;

            // Initialization based on locale
            setCurrentLocale(Locale.getDefault());
        }

        protected void setCurrentLocale(Locale locale) {
            if (locale.equals(mCurrentLocale)) {
                return;
            }
            mCurrentLocale = locale;
        }
    }

    /**
     * A delegate implementing the legacy CalendarView
     */
    private static class LegacyCalendarViewDelegate extends AbstractCalendarViewDelegate {

        /**
         * Default value whether to show week number.
         */
        private static final boolean DEFAULT_SHOW_WEEK_NUMBER = true;

        /**
         * The number of milliseconds in a day.e
         */
        private static final long MILLIS_IN_DAY = 86400000L;

        /**
         * The number of day in a week.
         */
        private static final int DAYS_PER_WEEK = 7;

        /**
         * The number of milliseconds in a week.
         */
        private static final long MILLIS_IN_WEEK = DAYS_PER_WEEK * MILLIS_IN_DAY;

        /**
         * Affects when the month selection will change while scrolling upe
         */
        private static final int SCROLL_HYST_WEEKS = 2;

        /**
         * How long the GoTo fling animation should last.
         */
        private static final int GOTO_SCROLL_DURATION = 1000;

        /**
         * The duration of the adjustment upon a user scroll in milliseconds.
         */
        private static final int ADJUSTMENT_SCROLL_DURATION = 500;

        /**
         * How long to wait after receiving an onScrollStateChanged notification
         * before acting on it.
         */
        private static final int SCROLL_CHANGE_DELAY = 40;

        /**
         * String for parsing dates.
         */
        private static final String DATE_FORMAT = "MM/dd/yyyy";

        /**
         * The default minimal date.
         */
        private static final String DEFAULT_MIN_DATE = "01/01/1900";

        /**
         * The default maximal date.
         */
        private static final String DEFAULT_MAX_DATE = "01/01/2100";

        private static final int DEFAULT_SHOWN_WEEK_COUNT = 6;

        private static final int DEFAULT_DATE_TEXT_SIZE = 14;

        private static final int UNSCALED_SELECTED_DATE_VERTICAL_BAR_WIDTH = 6;

        private static final int UNSCALED_WEEK_MIN_VISIBLE_HEIGHT = 12;

        private static final int UNSCALED_LIST_SCROLL_TOP_OFFSET = 2;

        private static final int UNSCALED_BOTTOM_BUFFER = 20;

        private static final int UNSCALED_WEEK_SEPARATOR_LINE_WIDTH = 1;

        private static final int DEFAULT_WEEK_DAY_TEXT_APPEARANCE_RES_ID = -1;

        private final int mWeekSeperatorLineWidth;

        private int mDateTextSize;

        private Drawable mSelectedDateVerticalBar;

        private final int mSelectedDateVerticalBarWidth;

        private int mSelectedWeekBackgroundColor;

        private int mFocusedMonthDateColor;

        private int mUnfocusedMonthDateColor;

        private int mWeekSeparatorLineColor;

        private int mWeekNumberColor;

        private int mWeekDayTextAppearanceResId;

        private int mDateTextAppearanceResId;

        /**
         * The top offset of the weeks list.
         */
        private int mListScrollTopOffset = 2;

        /**
         * The visible height of a week view.
         */
        private int mWeekMinVisibleHeight = 12;

        /**
         * The visible height of a week view.
         */
        private int mBottomBuffer = 20;

        /**
         * The number of shown weeks.
         */
        private int mShownWeekCount;

        /**
         * Flag whether to show the week number.
         */
        private boolean mShowWeekNumber;

        /**
         * The number of day per week to be shown.
         */
        private int mDaysPerWeek = 7;

        /**
         * The friction of the week list while flinging.
         */
        private float mFriction = .05f;

        /**
         * Scale for adjusting velocity of the week list while flinging.
         */
        private float mVelocityScale = 0.333f;

        /**
         * The adapter for the weeks list.
         */
        private WeeksAdapter mAdapter;

        /**
         * The weeks list.
         */
        private ListView mListView;

        /**
         * The name of the month to display.
         */
        private TextView mMonthName;

        /**
         * The header with week day names.
         */
        private ViewGroup mDayNamesHeader;

        /**
         * Cached labels for the week names header.
         */
        private String[] mDayLabels;

        /**
         * The first day of the week.
         */
        private int mFirstDayOfWeek;

        /**
         * Which month should be displayed/highlighted [0-11].
         */
        private int mCurrentMonthDisplayed = -1;

        /**
         * Used for tracking during a scroll.
         */
        private long mPreviousScrollPosition;

        /**
         * Used for tracking which direction the view is scrolling.
         */
        private boolean mIsScrollingUp = false;

        /**
         * The previous scroll state of the weeks ListView.
         */
        private int mPreviousScrollState = OnScrollListener.SCROLL_STATE_IDLE;

        /**
         * The current scroll state of the weeks ListView.
         */
        private int mCurrentScrollState = OnScrollListener.SCROLL_STATE_IDLE;

        /**
         * Listener for changes in the selected day.
         */
        private OnDateChangeListener mOnDateChangeListener;

        /**
         * Command for adjusting the position after a scroll/fling.
         */
        private ScrollStateRunnable mScrollStateChangedRunnable = new ScrollStateRunnable();

        /**
         * Temporary instance to avoid multiple instantiations.
         */
        private Calendar mTempDate;

        /**
         * The first day of the focused month.
         */
        private Calendar mFirstDayOfMonth;

        /**
         * The start date of the range supported by this picker.
         */
        private Calendar mMinDate;

        /**
         * The end date of the range supported by this picker.
         */
        private Calendar mMaxDate;

        /**
         * Date format for parsing dates.
         */
        private final java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT);

        LegacyCalendarViewDelegate(CalendarView delegator, Context context, AttributeSet attrs,
                                   int defStyleAttr, int defStyleRes) {
            super(delegator, context);

            // initialization based on locale
            setCurrentLocale(Locale.getDefault());

            TypedArray attributesArray = context.obtainStyledAttributes(attrs,
                    R.styleable.CalendarView, defStyleAttr, defStyleRes);
            mShowWeekNumber = attributesArray.getBoolean(R.styleable.CalendarView_showWeekNumber,
                    DEFAULT_SHOW_WEEK_NUMBER);
            mFirstDayOfWeek = attributesArray.getInt(R.styleable.CalendarView_firstDayOfWeek,
                    LocaleData.get(Locale.getDefault()).firstDayOfWeek);
            String minDate = attributesArray.getString(R.styleable.CalendarView_minDate);
            if (TextUtils.isEmpty(minDate) || !parseDate(minDate, mMinDate)) {
                parseDate(DEFAULT_MIN_DATE, mMinDate);
            }
            String maxDate = attributesArray.getString(R.styleable.CalendarView_maxDate);
            if (TextUtils.isEmpty(maxDate) || !parseDate(maxDate, mMaxDate)) {
                parseDate(DEFAULT_MAX_DATE, mMaxDate);
            }
            if (mMaxDate.before(mMinDate)) {
                throw new IllegalArgumentException("Max date cannot be before min date.");
            }
            mShownWeekCount = attributesArray.getInt(R.styleable.CalendarView_shownWeekCount,
                    DEFAULT_SHOWN_WEEK_COUNT);
            mSelectedWeekBackgroundColor = attributesArray.getColor(
                    R.styleable.CalendarView_selectedWeekBackgroundColor, 0);
            mFocusedMonthDateColor = attributesArray.getColor(
                    R.styleable.CalendarView_focusedMonthDateColor, 0);
            mUnfocusedMonthDateColor = attributesArray.getColor(
                    R.styleable.CalendarView_unfocusedMonthDateColor, 0);
            mWeekSeparatorLineColor = attributesArray.getColor(
                    R.styleable.CalendarView_weekSeparatorLineColor, 0);
            mWeekNumberColor = attributesArray.getColor(R.styleable.CalendarView_weekNumberColor, 0);
            mSelectedDateVerticalBar = attributesArray.getDrawable(
                    R.styleable.CalendarView_selectedDateVerticalBar);

            mDateTextAppearanceResId = attributesArray.getResourceId(
                    R.styleable.CalendarView_dateTextAppearance, R.style.TextAppearance_Small);
            updateDateTextSize();

            mWeekDayTextAppearanceResId = attributesArray.getResourceId(
                    R.styleable.CalendarView_weekDayTextAppearance,
                    DEFAULT_WEEK_DAY_TEXT_APPEARANCE_RES_ID);
            attributesArray.recycle();

            DisplayMetrics displayMetrics = mDelegator.getResources().getDisplayMetrics();
            mWeekMinVisibleHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                    UNSCALED_WEEK_MIN_VISIBLE_HEIGHT, displayMetrics);
            mListScrollTopOffset = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                    UNSCALED_LIST_SCROLL_TOP_OFFSET, displayMetrics);
            mBottomBuffer = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                    UNSCALED_BOTTOM_BUFFER, displayMetrics);
            mSelectedDateVerticalBarWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                    UNSCALED_SELECTED_DATE_VERTICAL_BAR_WIDTH, displayMetrics);
            mWeekSeperatorLineWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                    UNSCALED_WEEK_SEPARATOR_LINE_WIDTH, displayMetrics);

            LayoutInflater layoutInflater = (LayoutInflater) mContext
                    .getSystemService(Service.LAYOUT_INFLATER_SERVICE);
            View content = layoutInflater.inflate(R.layout.calendar_view, null, false);
            mDelegator.addView(content);

            mListView = (ListView) mDelegator.findViewById(R.id.list);
            mDayNamesHeader = (ViewGroup) content.findViewById(com.android.internal.R.id.day_names);
            mMonthName = (TextView) content.findViewById(com.android.internal.R.id.month_name);

            setUpHeader();
            setUpListView();
            setUpAdapter();

            // go to today or whichever is close to today min or max date
            mTempDate.setTimeInMillis(System.currentTimeMillis());
            if (mTempDate.before(mMinDate)) {
                goTo(mMinDate, false, true, true);
            } else if (mMaxDate.before(mTempDate)) {
                goTo(mMaxDate, false, true, true);
            } else {
                goTo(mTempDate, false, true, true);
            }

            mDelegator.invalidate();
        }

        @Override
        public void setShownWeekCount(int count) {
            if (mShownWeekCount != count) {
                mShownWeekCount = count;
                mDelegator.invalidate();
            }
        }

        @Override
        public int getShownWeekCount() {
            return mShownWeekCount;
        }

        @Override
        public void setSelectedWeekBackgroundColor(int color) {
            if (mSelectedWeekBackgroundColor != color) {
                mSelectedWeekBackgroundColor = color;
                final int childCount = mListView.getChildCount();
                for (int i = 0; i < childCount; i++) {
                    WeekView weekView = (WeekView) mListView.getChildAt(i);
                    if (weekView.mHasSelectedDay) {
                        weekView.invalidate();
                    }
                }
            }
        }

        @Override
        public int getSelectedWeekBackgroundColor() {
            return mSelectedWeekBackgroundColor;
        }

        @Override
        public void setFocusedMonthDateColor(int color) {
            if (mFocusedMonthDateColor != color) {
                mFocusedMonthDateColor = color;
                final int childCount = mListView.getChildCount();
                for (int i = 0; i < childCount; i++) {
                    WeekView weekView = (WeekView) mListView.getChildAt(i);
                    if (weekView.mHasFocusedDay) {
                        weekView.invalidate();
                    }
                }
            }
        }

        @Override
        public int getFocusedMonthDateColor() {
            return mFocusedMonthDateColor;
        }

        @Override
        public void setUnfocusedMonthDateColor(int color) {
            if (mUnfocusedMonthDateColor != color) {
                mUnfocusedMonthDateColor = color;
                final int childCount = mListView.getChildCount();
                for (int i = 0; i < childCount; i++) {
                    WeekView weekView = (WeekView) mListView.getChildAt(i);
                    if (weekView.mHasUnfocusedDay) {
                        weekView.invalidate();
                    }
                }
            }
        }

        @Override
        public int getUnfocusedMonthDateColor() {
            return mFocusedMonthDateColor;
        }

        @Override
        public void setWeekNumberColor(int color) {
            if (mWeekNumberColor != color) {
                mWeekNumberColor = color;
                if (mShowWeekNumber) {
                    invalidateAllWeekViews();
                }
            }
        }

        @Override
        public int getWeekNumberColor() {
            return mWeekNumberColor;
        }

        @Override
        public void setWeekSeparatorLineColor(int color) {
            if (mWeekSeparatorLineColor != color) {
                mWeekSeparatorLineColor = color;
                invalidateAllWeekViews();
            }
        }

        @Override
        public int getWeekSeparatorLineColor() {
            return mWeekSeparatorLineColor;
        }

        @Override
        public void setSelectedDateVerticalBar(int resourceId) {
            Drawable drawable = mDelegator.getContext().getDrawable(resourceId);
            setSelectedDateVerticalBar(drawable);
        }

        @Override
        public void setSelectedDateVerticalBar(Drawable drawable) {
            if (mSelectedDateVerticalBar != drawable) {
                mSelectedDateVerticalBar = drawable;
                final int childCount = mListView.getChildCount();
                for (int i = 0; i < childCount; i++) {
                    WeekView weekView = (WeekView) mListView.getChildAt(i);
                    if (weekView.mHasSelectedDay) {
                        weekView.invalidate();
                    }
                }
            }
        }

        @Override
        public Drawable getSelectedDateVerticalBar() {
            return mSelectedDateVerticalBar;
        }

        @Override
        public void setWeekDayTextAppearance(int resourceId) {
            if (mWeekDayTextAppearanceResId != resourceId) {
                mWeekDayTextAppearanceResId = resourceId;
                setUpHeader();
            }
        }

        @Override
        public int getWeekDayTextAppearance() {
            return mWeekDayTextAppearanceResId;
        }

        @Override
        public void setDateTextAppearance(int resourceId) {
            if (mDateTextAppearanceResId != resourceId) {
                mDateTextAppearanceResId = resourceId;
                updateDateTextSize();
                invalidateAllWeekViews();
            }
        }

        @Override
        public int getDateTextAppearance() {
            return mDateTextAppearanceResId;
        }

        @Override
        public void setEnabled(boolean enabled) {
            mListView.setEnabled(enabled);
        }

        @Override
        public boolean isEnabled() {
            return mListView.isEnabled();
        }

        @Override
        public void setMinDate(long minDate) {
            mTempDate.setTimeInMillis(minDate);
            if (isSameDate(mTempDate, mMinDate)) {
                return;
            }
            mMinDate.setTimeInMillis(minDate);
            // make sure the current date is not earlier than
            // the new min date since the latter is used for
            // calculating the indices in the adapter thus
            // avoiding out of bounds error
            Calendar date = mAdapter.mSelectedDate;
            if (date.before(mMinDate)) {
                mAdapter.setSelectedDay(mMinDate);
            }
            // reinitialize the adapter since its range depends on min date
            mAdapter.init();
            if (date.before(mMinDate)) {
                setDate(mTempDate.getTimeInMillis());
            } else {
                // we go to the current date to force the ListView to query its
                // adapter for the shown views since we have changed the adapter
                // range and the base from which the later calculates item indices
                // note that calling setDate will not work since the date is the same
                goTo(date, false, true, false);
            }
        }

        @Override
        public long getMinDate() {
            return mMinDate.getTimeInMillis();
        }

        @Override
        public void setMaxDate(long maxDate) {
            mTempDate.setTimeInMillis(maxDate);
            if (isSameDate(mTempDate, mMaxDate)) {
                return;
            }
            mMaxDate.setTimeInMillis(maxDate);
            // reinitialize the adapter since its range depends on max date
            mAdapter.init();
            Calendar date = mAdapter.mSelectedDate;
            if (date.after(mMaxDate)) {
                setDate(mMaxDate.getTimeInMillis());
            } else {
                // we go to the current date to force the ListView to query its
                // adapter for the shown views since we have changed the adapter
                // range and the base from which the later calculates item indices
                // note that calling setDate will not work since the date is the same
                goTo(date, false, true, false);
            }
        }

        @Override
        public long getMaxDate() {
            return mMaxDate.getTimeInMillis();
        }

        @Override
        public void setShowWeekNumber(boolean showWeekNumber) {
            if (mShowWeekNumber == showWeekNumber) {
                return;
            }
            mShowWeekNumber = showWeekNumber;
            mAdapter.notifyDataSetChanged();
            setUpHeader();
        }

        @Override
        public boolean getShowWeekNumber() {
            return mShowWeekNumber;
        }

        @Override
        public void setFirstDayOfWeek(int firstDayOfWeek) {
            if (mFirstDayOfWeek == firstDayOfWeek) {
                return;
            }
            mFirstDayOfWeek = firstDayOfWeek;
            mAdapter.init();
            mAdapter.notifyDataSetChanged();
            setUpHeader();
        }

        @Override
        public int getFirstDayOfWeek() {
            return mFirstDayOfWeek;
        }

        @Override
        public void setDate(long date) {
            setDate(date, false, false);
        }

        @Override
        public void setDate(long date, boolean animate, boolean center) {
            mTempDate.setTimeInMillis(date);
            if (isSameDate(mTempDate, mAdapter.mSelectedDate)) {
                return;
            }
            goTo(mTempDate, animate, true, center);
        }

        @Override
        public long getDate() {
            return mAdapter.mSelectedDate.getTimeInMillis();
        }

        @Override
        public void setOnDateChangeListener(OnDateChangeListener listener) {
            mOnDateChangeListener = listener;
        }

        @Override
        public void onConfigurationChanged(Configuration newConfig) {
            setCurrentLocale(newConfig.locale);
        }

        @Override
        public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
            event.setClassName(CalendarView.class.getName());
        }

        @Override
        public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
            info.setClassName(CalendarView.class.getName());
        }

        /**
         * Sets the current locale.
         *
         * @param locale The current locale.
         */
        @Override
        protected void setCurrentLocale(Locale locale) {
            super.setCurrentLocale(locale);

            mTempDate = getCalendarForLocale(mTempDate, locale);
            mFirstDayOfMonth = getCalendarForLocale(mFirstDayOfMonth, locale);
            mMinDate = getCalendarForLocale(mMinDate, locale);
            mMaxDate = getCalendarForLocale(mMaxDate, locale);
        }
        private void updateDateTextSize() {
            TypedArray dateTextAppearance = mDelegator.getContext().obtainStyledAttributes(
                    mDateTextAppearanceResId, R.styleable.TextAppearance);
            mDateTextSize = dateTextAppearance.getDimensionPixelSize(
                    R.styleable.TextAppearance_textSize, DEFAULT_DATE_TEXT_SIZE);
            dateTextAppearance.recycle();
        }

        /**
         * Invalidates all week views.
         */
        private void invalidateAllWeekViews() {
            final int childCount = mListView.getChildCount();
            for (int i = 0; i < childCount; i++) {
                View view = mListView.getChildAt(i);
                view.invalidate();
            }
        }

        /**
         * Gets a calendar for locale bootstrapped with the value of a given calendar.
         *
         * @param oldCalendar The old calendar.
         * @param locale The locale.
         */
        private static Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
            if (oldCalendar == null) {
                return Calendar.getInstance(locale);
            } else {
                final long currentTimeMillis = oldCalendar.getTimeInMillis();
                Calendar newCalendar = Calendar.getInstance(locale);
                newCalendar.setTimeInMillis(currentTimeMillis);
                return newCalendar;
            }
        }

        /**
         * @return True if the <code>firstDate</code> is the same as the <code>
         * secondDate</code>.
         */
        private static boolean isSameDate(Calendar firstDate, Calendar secondDate) {
            return (firstDate.get(Calendar.DAY_OF_YEAR) == secondDate.get(Calendar.DAY_OF_YEAR)
                    && firstDate.get(Calendar.YEAR) == secondDate.get(Calendar.YEAR));
        }

        /**
         * Creates a new adapter if necessary and sets up its parameters.
         */
        private void setUpAdapter() {
            if (mAdapter == null) {
                mAdapter = new WeeksAdapter(mContext);
                mAdapter.registerDataSetObserver(new DataSetObserver() {
                    @Override
                    public void onChanged() {
                        if (mOnDateChangeListener != null) {
                            Calendar selectedDay = mAdapter.getSelectedDay();
                            mOnDateChangeListener.onSelectedDayChange(mDelegator,
                                    selectedDay.get(Calendar.YEAR),
                                    selectedDay.get(Calendar.MONTH),
                                    selectedDay.get(Calendar.DAY_OF_MONTH));
                        }
                    }
                });
                mListView.setAdapter(mAdapter);
            }

            // refresh the view with the new parameters
            mAdapter.notifyDataSetChanged();
        }

        /**
         * Sets up the strings to be used by the header.
         */
        private void setUpHeader() {
            mDayLabels = new String[mDaysPerWeek];
            for (int i = mFirstDayOfWeek, count = mFirstDayOfWeek + mDaysPerWeek; i < count; i++) {
                int calendarDay = (i > Calendar.SATURDAY) ? i - Calendar.SATURDAY : i;
                mDayLabels[i - mFirstDayOfWeek] = DateUtils.getDayOfWeekString(calendarDay,
                        DateUtils.LENGTH_SHORTEST);
            }

            TextView label = (TextView) mDayNamesHeader.getChildAt(0);
            if (mShowWeekNumber) {
                label.setVisibility(View.VISIBLE);
            } else {
                label.setVisibility(View.GONE);
            }
            for (int i = 1, count = mDayNamesHeader.getChildCount(); i < count; i++) {
                label = (TextView) mDayNamesHeader.getChildAt(i);
                if (mWeekDayTextAppearanceResId > -1) {
                    label.setTextAppearance(mContext, mWeekDayTextAppearanceResId);
                }
                if (i < mDaysPerWeek + 1) {
                    label.setText(mDayLabels[i - 1]);
                    label.setVisibility(View.VISIBLE);
                } else {
                    label.setVisibility(View.GONE);
                }
            }
            mDayNamesHeader.invalidate();
        }

        /**
         * Sets all the required fields for the list view.
         */
        private void setUpListView() {
            // Configure the listview
            mListView.setDivider(null);
            mListView.setItemsCanFocus(true);
            mListView.setVerticalScrollBarEnabled(false);
            mListView.setOnScrollListener(new OnScrollListener() {
                public void onScrollStateChanged(AbsListView view, int scrollState) {
                    LegacyCalendarViewDelegate.this.onScrollStateChanged(view, scrollState);
                }

                public void onScroll(
                        AbsListView view, int firstVisibleItem, int visibleItemCount,
                        int totalItemCount) {
                    LegacyCalendarViewDelegate.this.onScroll(view, firstVisibleItem,
                            visibleItemCount, totalItemCount);
                }
            });
            // Make the scrolling behavior nicer
            mListView.setFriction(mFriction);
            mListView.setVelocityScale(mVelocityScale);
        }

        /**
         * This moves to the specified time in the view. If the time is not already
         * in range it will move the list so that the first of the month containing
         * the time is at the top of the view. If the new time is already in view
         * the list will not be scrolled unless forceScroll is true. This time may
         * optionally be highlighted as selected as well.
         *
         * @param date The time to move to.
         * @param animate Whether to scroll to the given time or just redraw at the
         *            new location.
         * @param setSelected Whether to set the given time as selected.
         * @param forceScroll Whether to recenter even if the time is already
         *            visible.
         *
         * @throws IllegalArgumentException of the provided date is before the
         *        range start of after the range end.
         */
        private void goTo(Calendar date, boolean animate, boolean setSelected,
                boolean forceScroll) {
            if (date.before(mMinDate) || date.after(mMaxDate)) {
                throw new IllegalArgumentException("Time not between " + mMinDate.getTime()
                        + " and " + mMaxDate.getTime());
            }
            // Find the first and last entirely visible weeks
            int firstFullyVisiblePosition = mListView.getFirstVisiblePosition();
            View firstChild = mListView.getChildAt(0);
            if (firstChild != null && firstChild.getTop() < 0) {
                firstFullyVisiblePosition++;
            }
            int lastFullyVisiblePosition = firstFullyVisiblePosition + mShownWeekCount - 1;
            if (firstChild != null && firstChild.getTop() > mBottomBuffer) {
                lastFullyVisiblePosition--;
            }
            if (setSelected) {
                mAdapter.setSelectedDay(date);
            }
            // Get the week we're going to
            int position = getWeeksSinceMinDate(date);

            // Check if the selected day is now outside of our visible range
            // and if so scroll to the month that contains it
            if (position < firstFullyVisiblePosition || position > lastFullyVisiblePosition
                    || forceScroll) {
                mFirstDayOfMonth.setTimeInMillis(date.getTimeInMillis());
                mFirstDayOfMonth.set(Calendar.DAY_OF_MONTH, 1);

                setMonthDisplayed(mFirstDayOfMonth);

                // the earliest time we can scroll to is the min date
                if (mFirstDayOfMonth.before(mMinDate)) {
                    position = 0;
                } else {
                    position = getWeeksSinceMinDate(mFirstDayOfMonth);
                }

                mPreviousScrollState = OnScrollListener.SCROLL_STATE_FLING;
                if (animate) {
                    mListView.smoothScrollToPositionFromTop(position, mListScrollTopOffset,
                            GOTO_SCROLL_DURATION);
                } else {
                    mListView.setSelectionFromTop(position, mListScrollTopOffset);
                    // Perform any after scroll operations that are needed
                    onScrollStateChanged(mListView, OnScrollListener.SCROLL_STATE_IDLE);
                }
            } else if (setSelected) {
                // Otherwise just set the selection
                setMonthDisplayed(date);
            }
        }

        /**
         * Parses the given <code>date</code> and in case of success sets
         * the result to the <code>outDate</code>.
         *
         * @return True if the date was parsed.
         */
        private boolean parseDate(String date, Calendar outDate) {
            try {
                outDate.setTime(mDateFormat.parse(date));
                return true;
            } catch (ParseException e) {
                Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT);
                return false;
            }
        }

        /**
         * Called when a <code>view</code> transitions to a new <code>scrollState
         * </code>.
         */
        private void onScrollStateChanged(AbsListView view, int scrollState) {
            mScrollStateChangedRunnable.doScrollStateChange(view, scrollState);
        }

        /**
         * Updates the title and selected month if the <code>view</code> has moved to a new
         * month.
         */
        private void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
                              int totalItemCount) {
            WeekView child = (WeekView) view.getChildAt(0);
            if (child == null) {
                return;
            }

            // Figure out where we are
            long currScroll =
                    view.getFirstVisiblePosition() * child.getHeight() - child.getBottom();

            // If we have moved since our last call update the direction
            if (currScroll < mPreviousScrollPosition) {
                mIsScrollingUp = true;
            } else if (currScroll > mPreviousScrollPosition) {
                mIsScrollingUp = false;
            } else {
                return;
            }

            // Use some hysteresis for checking which month to highlight. This
            // causes the month to transition when two full weeks of a month are
            // visible when scrolling up, and when the first day in a month reaches
            // the top of the screen when scrolling down.
            int offset = child.getBottom() < mWeekMinVisibleHeight ? 1 : 0;
            if (mIsScrollingUp) {
                child = (WeekView) view.getChildAt(SCROLL_HYST_WEEKS + offset);
            } else if (offset != 0) {
                child = (WeekView) view.getChildAt(offset);
            }

            if (child != null) {
                // Find out which month we're moving into
                int month;
                if (mIsScrollingUp) {
                    month = child.getMonthOfFirstWeekDay();
                } else {
                    month = child.getMonthOfLastWeekDay();
                }

                // And how it relates to our current highlighted month
                int monthDiff;
                if (mCurrentMonthDisplayed == 11 && month == 0) {
                    monthDiff = 1;
                } else if (mCurrentMonthDisplayed == 0 && month == 11) {
                    monthDiff = -1;
                } else {
                    monthDiff = month - mCurrentMonthDisplayed;
                }

                // Only switch months if we're scrolling away from the currently
                // selected month
                if ((!mIsScrollingUp && monthDiff > 0) || (mIsScrollingUp && monthDiff < 0)) {
                    Calendar firstDay = child.getFirstDay();
                    if (mIsScrollingUp) {
                        firstDay.add(Calendar.DAY_OF_MONTH, -DAYS_PER_WEEK);
                    } else {
                        firstDay.add(Calendar.DAY_OF_MONTH, DAYS_PER_WEEK);
                    }
                    setMonthDisplayed(firstDay);
                }
            }
            mPreviousScrollPosition = currScroll;
            mPreviousScrollState = mCurrentScrollState;
        }

        /**
         * Sets the month displayed at the top of this view based on time. Override
         * to add custom events when the title is changed.
         *
         * @param calendar A day in the new focus month.
         */
        private void setMonthDisplayed(Calendar calendar) {
            mCurrentMonthDisplayed = calendar.get(Calendar.MONTH);
            mAdapter.setFocusMonth(mCurrentMonthDisplayed);
            final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_MONTH_DAY
                    | DateUtils.FORMAT_SHOW_YEAR;
            final long millis = calendar.getTimeInMillis();
            String newMonthName = DateUtils.formatDateRange(mContext, millis, millis, flags);
            mMonthName.setText(newMonthName);
            mMonthName.invalidate();
        }

        /**
         * @return Returns the number of weeks between the current <code>date</code>
         *         and the <code>mMinDate</code>.
         */
        private int getWeeksSinceMinDate(Calendar date) {
            if (date.before(mMinDate)) {
                throw new IllegalArgumentException("fromDate: " + mMinDate.getTime()
                        + " does not precede toDate: " + date.getTime());
            }
            long endTimeMillis = date.getTimeInMillis()
                    + date.getTimeZone().getOffset(date.getTimeInMillis());
            long startTimeMillis = mMinDate.getTimeInMillis()
                    + mMinDate.getTimeZone().getOffset(mMinDate.getTimeInMillis());
            long dayOffsetMillis = (mMinDate.get(Calendar.DAY_OF_WEEK) - mFirstDayOfWeek)
                    * MILLIS_IN_DAY;
            return (int) ((endTimeMillis - startTimeMillis + dayOffsetMillis) / MILLIS_IN_WEEK);
        }

        /**
         * Command responsible for acting upon scroll state changes.
         */
        private class ScrollStateRunnable implements Runnable {
            private AbsListView mView;

            private int mNewState;

            /**
             * Sets up the runnable with a short delay in case the scroll state
             * immediately changes again.
             *
             * @param view The list view that changed state
             * @param scrollState The new state it changed to
             */
            public void doScrollStateChange(AbsListView view, int scrollState) {
                mView = view;
                mNewState = scrollState;
                mDelegator.removeCallbacks(this);
                mDelegator.postDelayed(this, SCROLL_CHANGE_DELAY);
            }

            public void run() {
                mCurrentScrollState = mNewState;
                // Fix the position after a scroll or a fling ends
                if (mNewState == OnScrollListener.SCROLL_STATE_IDLE
                        && mPreviousScrollState != OnScrollListener.SCROLL_STATE_IDLE) {
                    View child = mView.getChildAt(0);
                    if (child == null) {
                        // The view is no longer visible, just return
                        return;
                    }
                    int dist = child.getBottom() - mListScrollTopOffset;
                    if (dist > mListScrollTopOffset) {
                        if (mIsScrollingUp) {
                            mView.smoothScrollBy(dist - child.getHeight(),
                                    ADJUSTMENT_SCROLL_DURATION);
                        } else {
                            mView.smoothScrollBy(dist, ADJUSTMENT_SCROLL_DURATION);
                        }
                    }
                }
                mPreviousScrollState = mNewState;
            }
        }

        /**
         * <p>
         * This is a specialized adapter for creating a list of weeks with
         * selectable days. It can be configured to display the week number, start
         * the week on a given day, show a reduced number of days, or display an
         * arbitrary number of weeks at a time.
         * </p>
         */
        private class WeeksAdapter extends BaseAdapter implements OnTouchListener {

            private int mSelectedWeek;

            private GestureDetector mGestureDetector;

            private int mFocusedMonth;

            private final Calendar mSelectedDate = Calendar.getInstance();

            private int mTotalWeekCount;

            public WeeksAdapter(Context context) {
                mContext = context;
                mGestureDetector = new GestureDetector(mContext, new CalendarGestureListener());
                init();
            }

            /**
             * Set up the gesture detector and selected time
             */
            private void init() {
                mSelectedWeek = getWeeksSinceMinDate(mSelectedDate);
                mTotalWeekCount = getWeeksSinceMinDate(mMaxDate);
                if (mMinDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek
                        || mMaxDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek) {
                    mTotalWeekCount++;
                }
                notifyDataSetChanged();
            }

            /**
             * Updates the selected day and related parameters.
             *
             * @param selectedDay The time to highlight
             */
            public void setSelectedDay(Calendar selectedDay) {
                if (selectedDay.get(Calendar.DAY_OF_YEAR) == mSelectedDate.get(Calendar.DAY_OF_YEAR)
                        && selectedDay.get(Calendar.YEAR) == mSelectedDate.get(Calendar.YEAR)) {
                    return;
                }
                mSelectedDate.setTimeInMillis(selectedDay.getTimeInMillis());
                mSelectedWeek = getWeeksSinceMinDate(mSelectedDate);
                mFocusedMonth = mSelectedDate.get(Calendar.MONTH);
                notifyDataSetChanged();
            }

            /**
             * @return The selected day of month.
             */
            public Calendar getSelectedDay() {
                return mSelectedDate;
            }

            @Override
            public int getCount() {
                return mTotalWeekCount;
            }

            @Override
            public Object getItem(int position) {
                return null;
            }

            @Override
            public long getItemId(int position) {
                return position;
            }

            @Override
            public View getView(int position, View convertView, ViewGroup parent) {
                WeekView weekView = null;
                if (convertView != null) {
                    weekView = (WeekView) convertView;
                } else {
                    weekView = new WeekView(mContext);
                    android.widget.AbsListView.LayoutParams params =
                            new android.widget.AbsListView.LayoutParams(LayoutParams.WRAP_CONTENT,
                                    LayoutParams.WRAP_CONTENT);
                    weekView.setLayoutParams(params);
                    weekView.setClickable(true);
                    weekView.setOnTouchListener(this);
                }

                int selectedWeekDay = (mSelectedWeek == position) ? mSelectedDate.get(
                        Calendar.DAY_OF_WEEK) : -1;
                weekView.init(position, selectedWeekDay, mFocusedMonth);

                return weekView;
            }

            /**
             * Changes which month is in focus and updates the view.
             *
             * @param month The month to show as in focus [0-11]
             */
            public void setFocusMonth(int month) {
                if (mFocusedMonth == month) {
                    return;
                }
                mFocusedMonth = month;
                notifyDataSetChanged();
            }

            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if (mListView.isEnabled() && mGestureDetector.onTouchEvent(event)) {
                    WeekView weekView = (WeekView) v;
                    // if we cannot find a day for the given location we are done
                    if (!weekView.getDayFromLocation(event.getX(), mTempDate)) {
                        return true;
                    }
                    // it is possible that the touched day is outside the valid range
                    // we draw whole weeks but range end can fall not on the week end
                    if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) {
                        return true;
                    }
                    onDateTapped(mTempDate);
                    return true;
                }
                return false;
            }

            /**
             * Maintains the same hour/min/sec but moves the day to the tapped day.
             *
             * @param day The day that was tapped
             */
            private void onDateTapped(Calendar day) {
                setSelectedDay(day);
                setMonthDisplayed(day);
            }

            /**
             * This is here so we can identify single tap events and set the
             * selected day correctly
             */
            class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener {
                @Override
                public boolean onSingleTapUp(MotionEvent e) {
                    return true;
                }
            }
        }

        /**
         * <p>
         * This is a dynamic view for drawing a single week. It can be configured to
         * display the week number, start the week on a given day, or show a reduced
         * number of days. It is intended for use as a single view within a
         * ListView. See {@link WeeksAdapter} for usage.
         * </p>
         */
        private class WeekView extends View {

            private final Rect mTempRect = new Rect();

            private final Paint mDrawPaint = new Paint();

            private final Paint mMonthNumDrawPaint = new Paint();

            // Cache the number strings so we don't have to recompute them each time
            private String[] mDayNumbers;

            // Quick lookup for checking which days are in the focus month
            private boolean[] mFocusDay;

            // Whether this view has a focused day.
            private boolean mHasFocusedDay;

            // Whether this view has only focused days.
            private boolean mHasUnfocusedDay;

            // The first day displayed by this item
            private Calendar mFirstDay;

            // The month of the first day in this week
            private int mMonthOfFirstWeekDay = -1;

            // The month of the last day in this week
            private int mLastWeekDayMonth = -1;

            // The position of this week, equivalent to weeks since the week of Jan
            // 1st, 1900
            private int mWeek = -1;

            // Quick reference to the width of this view, matches parent
            private int mWidth;

            // The height this view should draw at in pixels, set by height param
            private int mHeight;

            // If this view contains the selected day
            private boolean mHasSelectedDay = false;

            // Which day is selected [0-6] or -1 if no day is selected
            private int mSelectedDay = -1;

            // The number of days + a spot for week number if it is displayed
            private int mNumCells;

            // The left edge of the selected day
            private int mSelectedLeft = -1;

            // The right edge of the selected day
            private int mSelectedRight = -1;

            public WeekView(Context context) {
                super(context);

                // Sets up any standard paints that will be used
                initilaizePaints();
            }

            /**
             * Initializes this week view.
             *
             * @param weekNumber The number of the week this view represents. The
             *            week number is a zero based index of the weeks since
             *            {@link CalendarView#getMinDate()}.
             * @param selectedWeekDay The selected day of the week from 0 to 6, -1 if no
             *            selected day.
             * @param focusedMonth The month that is currently in focus i.e.
             *            highlighted.
             */
            public void init(int weekNumber, int selectedWeekDay, int focusedMonth) {
                mSelectedDay = selectedWeekDay;
                mHasSelectedDay = mSelectedDay != -1;
                mNumCells = mShowWeekNumber ? mDaysPerWeek + 1 : mDaysPerWeek;
                mWeek = weekNumber;
                mTempDate.setTimeInMillis(mMinDate.getTimeInMillis());

                mTempDate.add(Calendar.WEEK_OF_YEAR, mWeek);
                mTempDate.setFirstDayOfWeek(mFirstDayOfWeek);

                // Allocate space for caching the day numbers and focus values
                mDayNumbers = new String[mNumCells];
                mFocusDay = new boolean[mNumCells];

                // If we're showing the week number calculate it based on Monday
                int i = 0;
                if (mShowWeekNumber) {
                    mDayNumbers[0] = String.format(Locale.getDefault(), "%d",
                            mTempDate.get(Calendar.WEEK_OF_YEAR));
                    i++;
                }

                // Now adjust our starting day based on the start day of the week
                int diff = mFirstDayOfWeek - mTempDate.get(Calendar.DAY_OF_WEEK);
                mTempDate.add(Calendar.DAY_OF_MONTH, diff);

                mFirstDay = (Calendar) mTempDate.clone();
                mMonthOfFirstWeekDay = mTempDate.get(Calendar.MONTH);

                mHasUnfocusedDay = true;
                for (; i < mNumCells; i++) {
                    final boolean isFocusedDay = (mTempDate.get(Calendar.MONTH) == focusedMonth);
                    mFocusDay[i] = isFocusedDay;
                    mHasFocusedDay |= isFocusedDay;
                    mHasUnfocusedDay &= !isFocusedDay;
                    // do not draw dates outside the valid range to avoid user confusion
                    if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) {
                        mDayNumbers[i] = "";
                    } else {
                        mDayNumbers[i] = String.format(Locale.getDefault(), "%d",
                                mTempDate.get(Calendar.DAY_OF_MONTH));
                    }
                    mTempDate.add(Calendar.DAY_OF_MONTH, 1);
                }
                // We do one extra add at the end of the loop, if that pushed us to
                // new month undo it
                if (mTempDate.get(Calendar.DAY_OF_MONTH) == 1) {
                    mTempDate.add(Calendar.DAY_OF_MONTH, -1);
                }
                mLastWeekDayMonth = mTempDate.get(Calendar.MONTH);

                updateSelectionPositions();
            }

            /**
             * Initialize the paint instances.
             */
            private void initilaizePaints() {
                mDrawPaint.setFakeBoldText(false);
                mDrawPaint.setAntiAlias(true);
                mDrawPaint.setStyle(Style.FILL);

                mMonthNumDrawPaint.setFakeBoldText(true);
                mMonthNumDrawPaint.setAntiAlias(true);
                mMonthNumDrawPaint.setStyle(Style.FILL);
                mMonthNumDrawPaint.setTextAlign(Align.CENTER);
                mMonthNumDrawPaint.setTextSize(mDateTextSize);
            }

            /**
             * Returns the month of the first day in this week.
             *
             * @return The month the first day of this view is in.
             */
            public int getMonthOfFirstWeekDay() {
                return mMonthOfFirstWeekDay;
            }

            /**
             * Returns the month of the last day in this week
             *
             * @return The month the last day of this view is in
             */
            public int getMonthOfLastWeekDay() {
                return mLastWeekDayMonth;
            }

            /**
             * Returns the first day in this view.
             *
             * @return The first day in the view.
             */
            public Calendar getFirstDay() {
                return mFirstDay;
            }

            /**
             * Calculates the day that the given x position is in, accounting for
             * week number.
             *
             * @param x The x position of the touch event.
             * @return True if a day was found for the given location.
             */
            public boolean getDayFromLocation(float x, Calendar outCalendar) {
                final boolean isLayoutRtl = isLayoutRtl();

                int start;
                int end;

                if (isLayoutRtl) {
                    start = 0;
                    end = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth;
                } else {
                    start = mShowWeekNumber ? mWidth / mNumCells : 0;
                    end = mWidth;
                }

                if (x < start || x > end) {
                    outCalendar.clear();
                    return false;
                }

                // Selection is (x - start) / (pixels/day) which is (x - start) * day / pixels
                int dayPosition = (int) ((x - start) * mDaysPerWeek / (end - start));

                if (isLayoutRtl) {
                    dayPosition = mDaysPerWeek - 1 - dayPosition;
                }

                outCalendar.setTimeInMillis(mFirstDay.getTimeInMillis());
                outCalendar.add(Calendar.DAY_OF_MONTH, dayPosition);

                return true;
            }

            @Override
            protected void onDraw(Canvas canvas) {
                drawBackground(canvas);
                drawWeekNumbersAndDates(canvas);
                drawWeekSeparators(canvas);
                drawSelectedDateVerticalBars(canvas);
            }

            /**
             * This draws the selection highlight if a day is selected in this week.
             *
             * @param canvas The canvas to draw on
             */
            private void drawBackground(Canvas canvas) {
                if (!mHasSelectedDay) {
                    return;
                }
                mDrawPaint.setColor(mSelectedWeekBackgroundColor);

                mTempRect.top = mWeekSeperatorLineWidth;
                mTempRect.bottom = mHeight;

                final boolean isLayoutRtl = isLayoutRtl();

                if (isLayoutRtl) {
                    mTempRect.left = 0;
                    mTempRect.right = mSelectedLeft - 2;
                } else {
                    mTempRect.left = mShowWeekNumber ? mWidth / mNumCells : 0;
                    mTempRect.right = mSelectedLeft - 2;
                }
                canvas.drawRect(mTempRect, mDrawPaint);

                if (isLayoutRtl) {
                    mTempRect.left = mSelectedRight + 3;
                    mTempRect.right = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth;
                } else {
                    mTempRect.left = mSelectedRight + 3;
                    mTempRect.right = mWidth;
                }
                canvas.drawRect(mTempRect, mDrawPaint);
            }

            /**
             * Draws the week and month day numbers for this week.
             *
             * @param canvas The canvas to draw on
             */
            private void drawWeekNumbersAndDates(Canvas canvas) {
                final float textHeight = mDrawPaint.getTextSize();
                final int y = (int) ((mHeight + textHeight) / 2) - mWeekSeperatorLineWidth;
                final int nDays = mNumCells;
                final int divisor = 2 * nDays;

                mDrawPaint.setTextAlign(Align.CENTER);
                mDrawPaint.setTextSize(mDateTextSize);

                int i = 0;

                if (isLayoutRtl()) {
                    for (; i < nDays - 1; i++) {
                        mMonthNumDrawPaint.setColor(mFocusDay[i] ? mFocusedMonthDateColor
                                : mUnfocusedMonthDateColor);
                        int x = (2 * i + 1) * mWidth / divisor;
                        canvas.drawText(mDayNumbers[nDays - 1 - i], x, y, mMonthNumDrawPaint);
                    }
                    if (mShowWeekNumber) {
                        mDrawPaint.setColor(mWeekNumberColor);
                        int x = mWidth - mWidth / divisor;
                        canvas.drawText(mDayNumbers[0], x, y, mDrawPaint);
                    }
                } else {
                    if (mShowWeekNumber) {
                        mDrawPaint.setColor(mWeekNumberColor);
                        int x = mWidth / divisor;
                        canvas.drawText(mDayNumbers[0], x, y, mDrawPaint);
                        i++;
                    }
                    for (; i < nDays; i++) {
                        mMonthNumDrawPaint.setColor(mFocusDay[i] ? mFocusedMonthDateColor
                                : mUnfocusedMonthDateColor);
                        int x = (2 * i + 1) * mWidth / divisor;
                        canvas.drawText(mDayNumbers[i], x, y, mMonthNumDrawPaint);
                    }
                }
            }

            /**
             * Draws a horizontal line for separating the weeks.
             *
             * @param canvas The canvas to draw on.
             */
            private void drawWeekSeparators(Canvas canvas) {
                // If it is the topmost fully visible child do not draw separator line
                int firstFullyVisiblePosition = mListView.getFirstVisiblePosition();
                if (mListView.getChildAt(0).getTop() < 0) {
                    firstFullyVisiblePosition++;
                }
                if (firstFullyVisiblePosition == mWeek) {
                    return;
                }
                mDrawPaint.setColor(mWeekSeparatorLineColor);
                mDrawPaint.setStrokeWidth(mWeekSeperatorLineWidth);
                float startX;
                float stopX;
                if (isLayoutRtl()) {
                    startX = 0;
                    stopX = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth;
                } else {
                    startX = mShowWeekNumber ? mWidth / mNumCells : 0;
                    stopX = mWidth;
                }
                canvas.drawLine(startX, 0, stopX, 0, mDrawPaint);
            }

            /**
             * Draws the selected date bars if this week has a selected day.
             *
             * @param canvas The canvas to draw on
             */
            private void drawSelectedDateVerticalBars(Canvas canvas) {
                if (!mHasSelectedDay) {
                    return;
                }
                mSelectedDateVerticalBar.setBounds(
                        mSelectedLeft - mSelectedDateVerticalBarWidth / 2,
                        mWeekSeperatorLineWidth,
                        mSelectedLeft + mSelectedDateVerticalBarWidth / 2,
                        mHeight);
                mSelectedDateVerticalBar.draw(canvas);
                mSelectedDateVerticalBar.setBounds(
                        mSelectedRight - mSelectedDateVerticalBarWidth / 2,
                        mWeekSeperatorLineWidth,
                        mSelectedRight + mSelectedDateVerticalBarWidth / 2,
                        mHeight);
                mSelectedDateVerticalBar.draw(canvas);
            }

            @Override
            protected void onSizeChanged(int w, int h, int oldw, int oldh) {
                mWidth = w;
                updateSelectionPositions();
            }

            /**
             * This calculates the positions for the selected day lines.
             */
            private void updateSelectionPositions() {
                if (mHasSelectedDay) {
                    final boolean isLayoutRtl = isLayoutRtl();
                    int selectedPosition = mSelectedDay - mFirstDayOfWeek;
                    if (selectedPosition < 0) {
                        selectedPosition += 7;
                    }
                    if (mShowWeekNumber && !isLayoutRtl) {
                        selectedPosition++;
                    }
                    if (isLayoutRtl) {
                        mSelectedLeft = (mDaysPerWeek - 1 - selectedPosition) * mWidth / mNumCells;

                    } else {
                        mSelectedLeft = selectedPosition * mWidth / mNumCells;
                    }
                    mSelectedRight = mSelectedLeft + mWidth / mNumCells;
                }
            }

            @Override
            protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
                mHeight = (mListView.getHeight() - mListView.getPaddingTop() - mListView
                        .getPaddingBottom()) / mShownWeekCount;
                setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mHeight);
            }
        }

    }

}
