1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.datetimepicker.date;
18
19import android.animation.ObjectAnimator;
20import android.app.Activity;
21import android.app.DialogFragment;
22import android.content.res.Resources;
23import android.os.Bundle;
24import android.text.format.DateUtils;
25import android.util.Log;
26import android.view.LayoutInflater;
27import android.view.View;
28import android.view.View.OnClickListener;
29import android.view.ViewGroup;
30import android.view.Window;
31import android.view.WindowManager;
32import android.view.animation.AlphaAnimation;
33import android.view.animation.Animation;
34import android.widget.Button;
35import android.widget.LinearLayout;
36import android.widget.TextView;
37
38import com.android.datetimepicker.HapticFeedbackController;
39import com.android.datetimepicker.R;
40import com.android.datetimepicker.Utils;
41import com.android.datetimepicker.date.SimpleMonthAdapter.CalendarDay;
42
43import java.text.SimpleDateFormat;
44import java.util.Calendar;
45import java.util.HashSet;
46import java.util.Iterator;
47import java.util.Locale;
48
49/**
50 * Dialog allowing users to select a date.
51 */
52public class DatePickerDialog extends DialogFragment implements
53        OnClickListener, DatePickerController {
54
55    private static final String TAG = "DatePickerDialog";
56
57    private static final int UNINITIALIZED = -1;
58    private static final int MONTH_AND_DAY_VIEW = 0;
59    private static final int YEAR_VIEW = 1;
60
61    private static final String KEY_SELECTED_YEAR = "year";
62    private static final String KEY_SELECTED_MONTH = "month";
63    private static final String KEY_SELECTED_DAY = "day";
64    private static final String KEY_LIST_POSITION = "list_position";
65    private static final String KEY_WEEK_START = "week_start";
66    private static final String KEY_YEAR_START = "year_start";
67    private static final String KEY_YEAR_END = "year_end";
68    private static final String KEY_CURRENT_VIEW = "current_view";
69    private static final String KEY_LIST_POSITION_OFFSET = "list_position_offset";
70
71    private static final int DEFAULT_START_YEAR = 1900;
72    private static final int DEFAULT_END_YEAR = 2100;
73
74    private static final int ANIMATION_DURATION = 300;
75    private static final int ANIMATION_DELAY = 500;
76
77    private static SimpleDateFormat YEAR_FORMAT = new SimpleDateFormat("yyyy", Locale.getDefault());
78    private static SimpleDateFormat DAY_FORMAT = new SimpleDateFormat("dd", Locale.getDefault());
79
80    private final Calendar mCalendar = Calendar.getInstance();
81    private OnDateSetListener mCallBack;
82    private HashSet<OnDateChangedListener> mListeners = new HashSet<OnDateChangedListener>();
83
84    private AccessibleDateAnimator mAnimator;
85
86    private TextView mDayOfWeekView;
87    private LinearLayout mMonthAndDayView;
88    private TextView mSelectedMonthTextView;
89    private TextView mSelectedDayTextView;
90    private TextView mYearView;
91    private DayPickerView mDayPickerView;
92    private YearPickerView mYearPickerView;
93    private Button mDoneButton;
94
95    private int mCurrentView = UNINITIALIZED;
96
97    private int mWeekStart = mCalendar.getFirstDayOfWeek();
98    private int mMinYear = DEFAULT_START_YEAR;
99    private int mMaxYear = DEFAULT_END_YEAR;
100
101    private HapticFeedbackController mHapticFeedbackController;
102
103    private boolean mDelayAnimation = true;
104
105    // Accessibility strings.
106    private String mDayPickerDescription;
107    private String mSelectDay;
108    private String mYearPickerDescription;
109    private String mSelectYear;
110
111    /**
112     * The callback used to indicate the user is done filling in the date.
113     */
114    public interface OnDateSetListener {
115
116        /**
117         * @param view The view associated with this listener.
118         * @param year The year that was set.
119         * @param monthOfYear The month that was set (0-11) for compatibility
120         *            with {@link java.util.Calendar}.
121         * @param dayOfMonth The day of the month that was set.
122         */
123        void onDateSet(DatePickerDialog dialog, int year, int monthOfYear, int dayOfMonth);
124    }
125
126    /**
127     * The callback used to notify other date picker components of a change in selected date.
128     */
129    public interface OnDateChangedListener {
130
131        public void onDateChanged();
132    }
133
134
135    public DatePickerDialog() {
136        // Empty constructor required for dialog fragment.
137    }
138
139    /**
140     * @param callBack How the parent is notified that the date is set.
141     * @param year The initial year of the dialog.
142     * @param monthOfYear The initial month of the dialog.
143     * @param dayOfMonth The initial day of the dialog.
144     */
145    public static DatePickerDialog newInstance(OnDateSetListener callBack, int year,
146            int monthOfYear,
147            int dayOfMonth) {
148        DatePickerDialog ret = new DatePickerDialog();
149        ret.initialize(callBack, year, monthOfYear, dayOfMonth);
150        return ret;
151    }
152
153    public void initialize(OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth) {
154        mCallBack = callBack;
155        mCalendar.set(Calendar.YEAR, year);
156        mCalendar.set(Calendar.MONTH, monthOfYear);
157        mCalendar.set(Calendar.DAY_OF_MONTH, dayOfMonth);
158    }
159
160    @Override
161    public void onCreate(Bundle savedInstanceState) {
162        super.onCreate(savedInstanceState);
163        final Activity activity = getActivity();
164        activity.getWindow().setSoftInputMode(
165                WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
166        if (savedInstanceState != null) {
167            mCalendar.set(Calendar.YEAR, savedInstanceState.getInt(KEY_SELECTED_YEAR));
168            mCalendar.set(Calendar.MONTH, savedInstanceState.getInt(KEY_SELECTED_MONTH));
169            mCalendar.set(Calendar.DAY_OF_MONTH, savedInstanceState.getInt(KEY_SELECTED_DAY));
170        }
171    }
172
173    @Override
174    public void onSaveInstanceState(Bundle outState) {
175        super.onSaveInstanceState(outState);
176        outState.putInt(KEY_SELECTED_YEAR, mCalendar.get(Calendar.YEAR));
177        outState.putInt(KEY_SELECTED_MONTH, mCalendar.get(Calendar.MONTH));
178        outState.putInt(KEY_SELECTED_DAY, mCalendar.get(Calendar.DAY_OF_MONTH));
179        outState.putInt(KEY_WEEK_START, mWeekStart);
180        outState.putInt(KEY_YEAR_START, mMinYear);
181        outState.putInt(KEY_YEAR_END, mMaxYear);
182        outState.putInt(KEY_CURRENT_VIEW, mCurrentView);
183        int listPosition = -1;
184        if (mCurrentView == MONTH_AND_DAY_VIEW) {
185            listPosition = mDayPickerView.getMostVisiblePosition();
186        } else if (mCurrentView == YEAR_VIEW) {
187            listPosition = mYearPickerView.getFirstVisiblePosition();
188            outState.putInt(KEY_LIST_POSITION_OFFSET, mYearPickerView.getFirstPositionOffset());
189        }
190        outState.putInt(KEY_LIST_POSITION, listPosition);
191    }
192
193    @Override
194    public View onCreateView(LayoutInflater inflater, ViewGroup container,
195            Bundle savedInstanceState) {
196        Log.d(TAG, "onCreateView: ");
197        getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE);
198
199        View view = inflater.inflate(R.layout.date_picker_dialog, null);
200
201        mDayOfWeekView = (TextView) view.findViewById(R.id.date_picker_header);
202        mMonthAndDayView = (LinearLayout) view.findViewById(R.id.date_picker_month_and_day);
203        mMonthAndDayView.setOnClickListener(this);
204        mSelectedMonthTextView = (TextView) view.findViewById(R.id.date_picker_month);
205        mSelectedDayTextView = (TextView) view.findViewById(R.id.date_picker_day);
206        mYearView = (TextView) view.findViewById(R.id.date_picker_year);
207        mYearView.setOnClickListener(this);
208
209        int listPosition = -1;
210        int listPositionOffset = 0;
211        int currentView = MONTH_AND_DAY_VIEW;
212        if (savedInstanceState != null) {
213            mWeekStart = savedInstanceState.getInt(KEY_WEEK_START);
214            mMinYear = savedInstanceState.getInt(KEY_YEAR_START);
215            mMaxYear = savedInstanceState.getInt(KEY_YEAR_END);
216            currentView = savedInstanceState.getInt(KEY_CURRENT_VIEW);
217            listPosition = savedInstanceState.getInt(KEY_LIST_POSITION);
218            listPositionOffset = savedInstanceState.getInt(KEY_LIST_POSITION_OFFSET);
219        }
220
221        final Activity activity = getActivity();
222        mDayPickerView = new DayPickerView(activity, this);
223        mYearPickerView = new YearPickerView(activity, this);
224
225        Resources res = getResources();
226        mDayPickerDescription = res.getString(R.string.day_picker_description);
227        mSelectDay = res.getString(R.string.select_day);
228        mYearPickerDescription = res.getString(R.string.year_picker_description);
229        mSelectYear = res.getString(R.string.select_year);
230
231        mAnimator = (AccessibleDateAnimator) view.findViewById(R.id.animator);
232        mAnimator.addView(mDayPickerView);
233        mAnimator.addView(mYearPickerView);
234        mAnimator.setDateMillis(mCalendar.getTimeInMillis());
235        // TODO: Replace with animation decided upon by the design team.
236        Animation animation = new AlphaAnimation(0.0f, 1.0f);
237        animation.setDuration(ANIMATION_DURATION);
238        mAnimator.setInAnimation(animation);
239        // TODO: Replace with animation decided upon by the design team.
240        Animation animation2 = new AlphaAnimation(1.0f, 0.0f);
241        animation2.setDuration(ANIMATION_DURATION);
242        mAnimator.setOutAnimation(animation2);
243
244        mDoneButton = (Button) view.findViewById(R.id.done);
245        mDoneButton.setOnClickListener(new OnClickListener() {
246
247            @Override
248            public void onClick(View v) {
249                tryVibrate();
250                if (mCallBack != null) {
251                    mCallBack.onDateSet(DatePickerDialog.this, mCalendar.get(Calendar.YEAR),
252                            mCalendar.get(Calendar.MONTH), mCalendar.get(Calendar.DAY_OF_MONTH));
253                }
254                dismiss();
255            }
256        });
257
258        updateDisplay(false);
259        setCurrentView(currentView);
260
261        if (listPosition != -1) {
262            if (currentView == MONTH_AND_DAY_VIEW) {
263                mDayPickerView.postSetSelection(listPosition);
264            } else if (currentView == YEAR_VIEW) {
265                mYearPickerView.postSetSelectionFromTop(listPosition, listPositionOffset);
266            }
267        }
268
269        mHapticFeedbackController = new HapticFeedbackController(activity);
270        return view;
271    }
272
273    @Override
274    public void onResume() {
275        super.onResume();
276        mHapticFeedbackController.start();
277    }
278
279    @Override
280    public void onPause() {
281        super.onPause();
282        mHapticFeedbackController.stop();
283    }
284
285    private void setCurrentView(final int viewIndex) {
286        long millis = mCalendar.getTimeInMillis();
287
288        switch (viewIndex) {
289            case MONTH_AND_DAY_VIEW:
290                ObjectAnimator pulseAnimator = Utils.getPulseAnimator(mMonthAndDayView, 0.9f,
291                        1.05f);
292                if (mDelayAnimation) {
293                    pulseAnimator.setStartDelay(ANIMATION_DELAY);
294                    mDelayAnimation = false;
295                }
296                mDayPickerView.onDateChanged();
297                if (mCurrentView != viewIndex) {
298                    mMonthAndDayView.setSelected(true);
299                    mYearView.setSelected(false);
300                    mAnimator.setDisplayedChild(MONTH_AND_DAY_VIEW);
301                    mCurrentView = viewIndex;
302                }
303                pulseAnimator.start();
304
305                int flags = DateUtils.FORMAT_SHOW_DATE;
306                String dayString = DateUtils.formatDateTime(getActivity(), millis, flags);
307                mAnimator.setContentDescription(mDayPickerDescription+": "+dayString);
308                Utils.tryAccessibilityAnnounce(mAnimator, mSelectDay);
309                break;
310            case YEAR_VIEW:
311                pulseAnimator = Utils.getPulseAnimator(mYearView, 0.85f, 1.1f);
312                if (mDelayAnimation) {
313                    pulseAnimator.setStartDelay(ANIMATION_DELAY);
314                    mDelayAnimation = false;
315                }
316                mYearPickerView.onDateChanged();
317                if (mCurrentView != viewIndex) {
318                    mMonthAndDayView.setSelected(false);
319                    mYearView.setSelected(true);
320                    mAnimator.setDisplayedChild(YEAR_VIEW);
321                    mCurrentView = viewIndex;
322                }
323                pulseAnimator.start();
324
325                CharSequence yearString = YEAR_FORMAT.format(millis);
326                mAnimator.setContentDescription(mYearPickerDescription+": "+yearString);
327                Utils.tryAccessibilityAnnounce(mAnimator, mSelectYear);
328                break;
329        }
330    }
331
332    private void updateDisplay(boolean announce) {
333        if (mDayOfWeekView != null) {
334            mDayOfWeekView.setText(mCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.LONG,
335                    Locale.getDefault()).toUpperCase(Locale.getDefault()));
336        }
337
338        mSelectedMonthTextView.setText(mCalendar.getDisplayName(Calendar.MONTH, Calendar.SHORT,
339                Locale.getDefault()).toUpperCase(Locale.getDefault()));
340        mSelectedDayTextView.setText(DAY_FORMAT.format(mCalendar.getTime()));
341        mYearView.setText(YEAR_FORMAT.format(mCalendar.getTime()));
342
343        // Accessibility.
344        long millis = mCalendar.getTimeInMillis();
345        mAnimator.setDateMillis(millis);
346        int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR;
347        String monthAndDayText = DateUtils.formatDateTime(getActivity(), millis, flags);
348        mMonthAndDayView.setContentDescription(monthAndDayText);
349
350        if (announce) {
351            flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR;
352            String fullDateText = DateUtils.formatDateTime(getActivity(), millis, flags);
353            Utils.tryAccessibilityAnnounce(mAnimator, fullDateText);
354        }
355    }
356
357    public void setFirstDayOfWeek(int startOfWeek) {
358        if (startOfWeek < Calendar.SUNDAY || startOfWeek > Calendar.SATURDAY) {
359            throw new IllegalArgumentException("Value must be between Calendar.SUNDAY and " +
360                    "Calendar.SATURDAY");
361        }
362        mWeekStart = startOfWeek;
363        if (mDayPickerView != null) {
364            mDayPickerView.onChange();
365        }
366    }
367
368    public void setYearRange(int startYear, int endYear) {
369        if (endYear <= startYear) {
370            throw new IllegalArgumentException("Year end must be larger than year start");
371        }
372        mMinYear = startYear;
373        mMaxYear = endYear;
374        if (mDayPickerView != null) {
375            mDayPickerView.onChange();
376        }
377    }
378
379    public void setOnDateSetListener(OnDateSetListener listener) {
380        mCallBack = listener;
381    }
382
383    // If the newly selected month / year does not contain the currently selected day number,
384    // change the selected day number to the last day of the selected month or year.
385    //      e.g. Switching from Mar to Apr when Mar 31 is selected -> Apr 30
386    //      e.g. Switching from 2012 to 2013 when Feb 29, 2012 is selected -> Feb 28, 2013
387    private void adjustDayInMonthIfNeeded(int month, int year) {
388        int day = mCalendar.get(Calendar.DAY_OF_MONTH);
389        int daysInMonth = Utils.getDaysInMonth(month, year);
390        if (day > daysInMonth) {
391            mCalendar.set(Calendar.DAY_OF_MONTH, daysInMonth);
392        }
393    }
394
395    @Override
396    public void onClick(View v) {
397        tryVibrate();
398        if (v.getId() == R.id.date_picker_year) {
399            setCurrentView(YEAR_VIEW);
400        } else if (v.getId() == R.id.date_picker_month_and_day) {
401            setCurrentView(MONTH_AND_DAY_VIEW);
402        }
403    }
404
405    @Override
406    public void onYearSelected(int year) {
407        adjustDayInMonthIfNeeded(mCalendar.get(Calendar.MONTH), year);
408        mCalendar.set(Calendar.YEAR, year);
409        updatePickers();
410        setCurrentView(MONTH_AND_DAY_VIEW);
411        updateDisplay(true);
412    }
413
414    @Override
415    public void onDayOfMonthSelected(int year, int month, int day) {
416        mCalendar.set(Calendar.YEAR, year);
417        mCalendar.set(Calendar.MONTH, month);
418        mCalendar.set(Calendar.DAY_OF_MONTH, day);
419        updatePickers();
420        updateDisplay(true);
421    }
422
423    private void updatePickers() {
424        Iterator<OnDateChangedListener> iterator = mListeners.iterator();
425        while (iterator.hasNext()) {
426            iterator.next().onDateChanged();
427        }
428    }
429
430
431    @Override
432    public CalendarDay getSelectedDay() {
433        return new CalendarDay(mCalendar);
434    }
435
436    @Override
437    public int getMinYear() {
438        return mMinYear;
439    }
440
441    @Override
442    public int getMaxYear() {
443        return mMaxYear;
444    }
445
446    @Override
447    public int getFirstDayOfWeek() {
448        return mWeekStart;
449    }
450
451    @Override
452    public void registerOnDateChangedListener(OnDateChangedListener listener) {
453        mListeners.add(listener);
454    }
455
456    @Override
457    public void unregisterOnDateChangedListener(OnDateChangedListener listener) {
458        mListeners.remove(listener);
459    }
460
461    @Override
462    public void tryVibrate() {
463        mHapticFeedbackController.tryVibrate();
464    }
465}
466