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