1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.content.browser.input;
6
7import android.content.Context;
8import android.text.format.DateUtils;
9import android.view.LayoutInflater;
10import android.view.accessibility.AccessibilityEvent;
11import android.widget.FrameLayout;
12import android.widget.NumberPicker;
13import android.widget.NumberPicker.OnValueChangeListener;
14
15import org.chromium.content.R;
16
17import java.util.Calendar;
18import java.util.TimeZone;
19
20/**
21 * This class is heavily based on android.widget.DatePicker.
22 */
23public abstract class TwoFieldDatePicker extends FrameLayout {
24
25    private final NumberPicker mPositionInYearSpinner;
26
27    private final NumberPicker mYearSpinner;
28
29    private OnMonthOrWeekChangedListener mMonthOrWeekChangedListener;
30
31    // It'd be nice to use android.text.Time like in other Dialogs but
32    // it suffers from the 2038 effect so it would prevent us from
33    // having dates over 2038.
34    private Calendar mMinDate;
35
36    private Calendar mMaxDate;
37
38    private Calendar mCurrentDate;
39
40    /**
41     * The callback used to indicate the user changes\d the date.
42     */
43    public interface OnMonthOrWeekChangedListener {
44
45        /**
46         * Called upon a date change.
47         *
48         * @param view The view associated with this listener.
49         * @param year The year that was set.
50         * @param positionInYear The month or week in year.
51         */
52        void onMonthOrWeekChanged(TwoFieldDatePicker view, int year, int positionInYear);
53    }
54
55    public TwoFieldDatePicker(Context context, double minValue, double maxValue) {
56        super(context, null, android.R.attr.datePickerStyle);
57
58        LayoutInflater inflater = (LayoutInflater) context
59                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
60        inflater.inflate(R.layout.two_field_date_picker, this, true);
61
62        OnValueChangeListener onChangeListener = new OnValueChangeListener() {
63            @Override
64            public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
65                int year = getYear();
66                int positionInYear = getPositionInYear();
67                // take care of wrapping of days and months to update greater fields
68                if (picker == mPositionInYearSpinner) {
69                    positionInYear = newVal;
70                    if (oldVal == picker.getMaxValue() && newVal == picker.getMinValue()) {
71                        year += 1;
72                        positionInYear = getMinPositionInYear(year);
73                    } else if (oldVal == picker.getMinValue() && newVal == picker.getMaxValue()) {
74                        year -= 1;
75                        positionInYear = getMaxPositionInYear(year);
76                    }
77                } else if (picker == mYearSpinner) {
78                    year = newVal;
79                } else {
80                    throw new IllegalArgumentException();
81                }
82
83                // now set the date to the adjusted one
84                setCurrentDate(year, positionInYear);
85                updateSpinners();
86                notifyDateChanged();
87            }
88        };
89
90        mCurrentDate = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
91        if (minValue >= maxValue) {
92            mMinDate = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
93            mMinDate.set(0, 0, 1);
94            mMaxDate = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
95            mMaxDate.set(9999, 0, 1);
96        } else {
97            mMinDate = getDateForValue(minValue);
98            mMaxDate = getDateForValue(maxValue);
99        }
100
101        // month
102        mPositionInYearSpinner = (NumberPicker) findViewById(R.id.position_in_year);
103        mPositionInYearSpinner.setOnLongPressUpdateInterval(200);
104        mPositionInYearSpinner.setOnValueChangedListener(onChangeListener);
105
106        // year
107        mYearSpinner = (NumberPicker) findViewById(R.id.year);
108        mYearSpinner.setOnLongPressUpdateInterval(100);
109        mYearSpinner.setOnValueChangedListener(onChangeListener);
110    }
111
112    /**
113     * Initialize the state. If the provided values designate an inconsistent
114     * date the values are normalized before updating the spinners.
115     *
116     * @param year The initial year.
117     * @param positionInYear The initial month <strong>starting from zero</strong> or week in year.
118     * @param onMonthOrWeekChangedListener How user is notified date is changed by
119     *            user, can be null.
120     */
121    public void init(int year, int positionInYear,
122            OnMonthOrWeekChangedListener onMonthOrWeekChangedListener) {
123        setCurrentDate(year, positionInYear);
124        updateSpinners();
125        mMonthOrWeekChangedListener = onMonthOrWeekChangedListener;
126    }
127
128    public boolean isNewDate(int year, int positionInYear) {
129        return (getYear() != year || getPositionInYear() != positionInYear);
130    }
131
132    /**
133     * Subclasses know the semantics of @value, and need to return
134     * a Calendar corresponding to it.
135     */
136    protected abstract Calendar getDateForValue(double value);
137
138    /**
139     * Updates the current date.
140     *
141     * @param year The year.
142     * @param positionInYear The month or week in year.
143     */
144    public void updateDate(int year, int positionInYear) {
145        if (!isNewDate(year, positionInYear)) {
146            return;
147        }
148        setCurrentDate(year, positionInYear);
149        updateSpinners();
150        notifyDateChanged();
151    }
152
153    /**
154     * Subclasses know the semantics of @positionInYear, and need to update @mCurrentDate to the
155     * appropriate date.
156     */
157    protected abstract void setCurrentDate(int year, int positionInYear);
158
159    protected void setCurrentDate(Calendar date) {
160        mCurrentDate = date;
161    }
162
163    @Override
164    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
165        onPopulateAccessibilityEvent(event);
166        return true;
167    }
168
169    @Override
170    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
171        super.onPopulateAccessibilityEvent(event);
172
173        final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR;
174        String selectedDateUtterance = DateUtils.formatDateTime(getContext(),
175                mCurrentDate.getTimeInMillis(), flags);
176        event.getText().add(selectedDateUtterance);
177    }
178
179    /**
180     * @return The selected year.
181     */
182    public int getYear() {
183        return mCurrentDate.get(Calendar.YEAR);
184    }
185
186    /**
187     * @return The selected month or week.
188     */
189    public abstract int getPositionInYear();
190
191    protected abstract int getMaxYear();
192
193    protected abstract int getMinYear();
194
195    protected abstract int getMaxPositionInYear(int year);
196
197    protected abstract int getMinPositionInYear(int year);
198
199    protected Calendar getMaxDate() {
200        return mMaxDate;
201    }
202
203    protected Calendar getMinDate() {
204        return mMinDate;
205    }
206
207    protected Calendar getCurrentDate() {
208        return mCurrentDate;
209    }
210
211    protected NumberPicker getPositionInYearSpinner() {
212        return mPositionInYearSpinner;
213    }
214
215    protected NumberPicker getYearSpinner() {
216        return mYearSpinner;
217    }
218
219    /**
220     * This method should be subclassed to update the spinners based on mCurrentDate.
221     */
222    protected void updateSpinners() {
223        mPositionInYearSpinner.setDisplayedValues(null);
224
225        // set the spinner ranges respecting the min and max dates
226        mPositionInYearSpinner.setMinValue(getMinPositionInYear(getYear()));
227        mPositionInYearSpinner.setMaxValue(getMaxPositionInYear(getYear()));
228        mPositionInYearSpinner.setWrapSelectorWheel(
229                !mCurrentDate.equals(mMinDate) && !mCurrentDate.equals(mMaxDate));
230
231        // year spinner range does not change based on the current date
232        mYearSpinner.setMinValue(getMinYear());
233        mYearSpinner.setMaxValue(getMaxYear());
234        mYearSpinner.setWrapSelectorWheel(false);
235
236        // set the spinner values
237        mYearSpinner.setValue(getYear());
238        mPositionInYearSpinner.setValue(getPositionInYear());
239    }
240
241    /**
242     * Notifies the listener, if such, for a change in the selected date.
243     */
244    protected void notifyDateChanged() {
245        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
246        if (mMonthOrWeekChangedListener != null) {
247            mMonthOrWeekChangedListener.onMonthOrWeekChanged(this, getYear(), getPositionInYear());
248        }
249    }
250}
251