chromium/content/public/android/java/src/org/chromium/content/browser/picker/TwoFieldDatePicker.java

// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.content.browser.picker;

import android.content.Context;
import android.os.Build;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.accessibility.AccessibilityEvent;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.NumberPicker;
import android.widget.NumberPicker.OnValueChangeListener;

import org.chromium.content.R;

import java.util.Calendar;
import java.util.Locale;
import java.util.TimeZone;

/** This class is heavily based on android.widget.DatePicker. */
public abstract class TwoFieldDatePicker extends FrameLayout {

    private final NumberPicker mPositionInYearSpinner;

    private final NumberPicker mYearSpinner;

    private OnMonthOrWeekChangedListener mMonthOrWeekChangedListener;

    // It'd be nice to use android.text.Time like in other Dialogs but
    // it suffers from the 2038 effect so it would prevent us from
    // having dates over 2038.
    private Calendar mMinDate;

    private Calendar mMaxDate;

    private Calendar mCurrentDate;

    /** The callback used to indicate the user changes\d the date. */
    public interface OnMonthOrWeekChangedListener {

        /**
         * Called upon a date change.
         *
         * @param view The view associated with this listener.
         * @param year The year that was set.
         * @param positionInYear The month or week in year.
         */
        void onMonthOrWeekChanged(TwoFieldDatePicker view, int year, int positionInYear);
    }

    public TwoFieldDatePicker(Context context, double minValue, double maxValue) {
        super(context, null, android.R.attr.datePickerStyle);

        LayoutInflater inflater =
                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        inflater.inflate(R.layout.two_field_date_picker, this, true);

        OnValueChangeListener onChangeListener =
                new OnValueChangeListener() {
                    @Override
                    public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
                        int year = getYear();
                        int positionInYear = getPositionInYear();
                        // take care of wrapping of days and months to update greater fields
                        if (picker == mPositionInYearSpinner) {
                            positionInYear = newVal;
                            if (oldVal == picker.getMaxValue() && newVal == picker.getMinValue()) {
                                year += 1;
                                positionInYear = getMinPositionInYear(year);
                            } else if (oldVal == picker.getMinValue()
                                    && newVal == picker.getMaxValue()) {
                                year -= 1;
                                positionInYear = getMaxPositionInYear(year);
                            }
                        } else if (picker == mYearSpinner) {
                            year = newVal;
                        } else {
                            throw new IllegalArgumentException();
                        }

                        // now set the date to the adjusted one
                        setCurrentDate(year, positionInYear);
                        updateSpinners();
                        notifyDateChanged();
                    }
                };

        mCurrentDate = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
        if (minValue >= maxValue) {
            mMinDate = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
            mMinDate.set(0, 0, 1);
            mMaxDate = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
            mMaxDate.set(9999, 0, 1);
        } else {
            mMinDate = getDateForValue(minValue);
            mMaxDate = getDateForValue(maxValue);
        }

        // month
        mPositionInYearSpinner = (NumberPicker) findViewById(R.id.position_in_year);
        mPositionInYearSpinner.setOnLongPressUpdateInterval(200);
        mPositionInYearSpinner.setOnValueChangedListener(onChangeListener);

        // year
        mYearSpinner = (NumberPicker) findViewById(R.id.year);
        mYearSpinner.setOnLongPressUpdateInterval(100);
        mYearSpinner.setOnValueChangedListener(onChangeListener);

        reorderSpinners();
    }

    /**
     * Reorder the date picker spinners to match the order suggested by the locale.
     * Assumes that the order of month and year in the locale is also the right order
     * for the spinner columns.
     */
    private void reorderSpinners() {
        boolean posInserted = false;
        boolean yearInserted = false;

        LinearLayout pickers = (LinearLayout) findViewById(R.id.pickers);

        pickers.removeView(mPositionInYearSpinner);
        pickers.removeView(mYearSpinner);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            // logic duplicated from android.widget.DatePicker
            String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), "yyyyMMMdd");

            for (int i = 0; i < pattern.length(); ++i) {
                char ch = pattern.charAt(i);
                if (ch == '\'') {
                    i = pattern.indexOf('\'', i + 1);
                    if (i == -1) {
                        throw new IllegalArgumentException("Bad quoting in " + pattern);
                    }
                } else if ((ch == 'M' || ch == 'L') && !posInserted) {
                    pickers.addView(mPositionInYearSpinner);
                    posInserted = true;
                } else if (ch == 'y' && !yearInserted) {
                    pickers.addView(mYearSpinner);
                    yearInserted = true;
                }
            }
        } else {
            // This method was used to order android.widget.DatePicker
            // fields in JB prior to the availability of getBestDateTimePattern.
            char[] order = DateFormat.getDateFormatOrder(getContext());
            for (int i = 0; i < order.length; ++i) {
                if (order[i] == 'M') {
                    pickers.addView(mPositionInYearSpinner);
                    posInserted = true;
                } else if (order[i] == 'y') {
                    pickers.addView(mYearSpinner);
                    yearInserted = true;
                }
            }
        }

        if (!posInserted) pickers.addView(mPositionInYearSpinner);
        if (!yearInserted) pickers.addView(mYearSpinner);
    }

    /**
     * Initialize the state. If the provided values designate an inconsistent
     * date the values are normalized before updating the spinners.
     *
     * @param year The initial year.
     * @param positionInYear The initial month <strong>starting from zero</strong> or week in year.
     * @param onMonthOrWeekChangedListener How user is notified date is changed by
     *            user, can be null.
     */
    public void init(
            int year,
            int positionInYear,
            OnMonthOrWeekChangedListener onMonthOrWeekChangedListener) {
        setCurrentDate(year, positionInYear);
        updateSpinners();
        mMonthOrWeekChangedListener = onMonthOrWeekChangedListener;
    }

    public boolean isNewDate(int year, int positionInYear) {
        return (getYear() != year || getPositionInYear() != positionInYear);
    }

    /**
     * Subclasses know the semantics of @value, and need to return
     * a Calendar corresponding to it.
     */
    protected abstract Calendar getDateForValue(double value);

    /**
     * Updates the current date.
     *
     * @param year The year.
     * @param positionInYear The month or week in year.
     */
    public void updateDate(int year, int positionInYear) {
        if (!isNewDate(year, positionInYear)) {
            return;
        }
        setCurrentDate(year, positionInYear);
        updateSpinners();
        notifyDateChanged();
    }

    /**
     * Subclasses know the semantics of @positionInYear, and need to update @mCurrentDate to the
     * appropriate date.
     */
    protected abstract void setCurrentDate(int year, int positionInYear);

    protected void setCurrentDate(Calendar date) {
        mCurrentDate = date;
    }

    @Override
    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
        onPopulateAccessibilityEvent(event);
        return true;
    }

    @Override
    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
        super.onPopulateAccessibilityEvent(event);

        final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR;
        String selectedDateUtterance =
                DateUtils.formatDateTime(getContext(), mCurrentDate.getTimeInMillis(), flags);
        event.getText().add(selectedDateUtterance);
    }

    /**
     * @return The selected year.
     */
    public int getYear() {
        return mCurrentDate.get(Calendar.YEAR);
    }

    /**
     * @return The selected month or week.
     */
    public abstract int getPositionInYear();

    protected abstract int getMaxYear();

    protected abstract int getMinYear();

    protected abstract int getMaxPositionInYear(int year);

    protected abstract int getMinPositionInYear(int year);

    protected Calendar getMaxDate() {
        return mMaxDate;
    }

    protected Calendar getMinDate() {
        return mMinDate;
    }

    protected Calendar getCurrentDate() {
        return mCurrentDate;
    }

    protected NumberPicker getPositionInYearSpinner() {
        return mPositionInYearSpinner;
    }

    protected NumberPicker getYearSpinner() {
        return mYearSpinner;
    }

    /** This method should be subclassed to update the spinners based on mCurrentDate. */
    protected void updateSpinners() {
        mPositionInYearSpinner.setDisplayedValues(null);

        // set the spinner ranges respecting the min and max dates
        mPositionInYearSpinner.setMinValue(getMinPositionInYear(getYear()));
        mPositionInYearSpinner.setMaxValue(getMaxPositionInYear(getYear()));
        mPositionInYearSpinner.setWrapSelectorWheel(
                !mCurrentDate.equals(mMinDate) && !mCurrentDate.equals(mMaxDate));

        // year spinner range does not change based on the current date
        mYearSpinner.setMinValue(getMinYear());
        mYearSpinner.setMaxValue(getMaxYear());
        mYearSpinner.setWrapSelectorWheel(false);

        // set the spinner values
        mYearSpinner.setValue(getYear());
        mPositionInYearSpinner.setValue(getPositionInYear());
    }

    /** Notifies the listener, if such, for a change in the selected date. */
    protected void notifyDateChanged() {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
        if (mMonthOrWeekChangedListener != null) {
            mMonthOrWeekChangedListener.onMonthOrWeekChanged(this, getYear(), getPositionInYear());
        }
    }
}