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

// Copyright 2012 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.app.AlertDialog;
import android.app.DatePickerDialog.OnDateSetListener;
import android.app.TimePickerDialog;
import android.app.TimePickerDialog.OnTimeSetListener;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnDismissListener;
import android.text.format.DateFormat;
import android.view.View;
import android.widget.AdapterView;
import android.widget.DatePicker;
import android.widget.ListView;
import android.widget.TimePicker;

import org.chromium.base.Log;
import org.chromium.content.R;
import org.chromium.content.browser.picker.DateTimePickerDialog.OnDateTimeSetListener;
import org.chromium.content.browser.picker.MultiFieldTimePickerDialog.OnMultiFieldTimeSetListener;
import org.chromium.content_public.browser.util.DialogTypeRecorder;
import org.chromium.ui.base.ime.TextInputType;

import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;

/** Opens the appropriate date/time picker dialog for the given dialog type. */
public class InputDialogContainer {
    private static final String TAG = "InputDialogContainer";

    /** Delegate that implements the picker's actions. */
    public interface InputActionDelegate {
        void cancelDateTimeDialog();

        void replaceDateTime(double value);
    }

    private final Context mContext;

    // Prevents sending two notifications (from onClick and from onDismiss)
    private boolean mDialogAlreadyDismissed;

    private AlertDialog mDialog;
    private final InputActionDelegate mInputActionDelegate;

    public static boolean isDialogInputType(int type) {
        return type == TextInputType.DATE
                || type == TextInputType.TIME
                || type == TextInputType.DATE_TIME
                || type == TextInputType.DATE_TIME_LOCAL
                || type == TextInputType.MONTH
                || type == TextInputType.WEEK;
    }

    public InputDialogContainer(Context context, InputActionDelegate inputActionDelegate) {
        mContext = context;
        mInputActionDelegate = inputActionDelegate;
    }

    protected void showPickerDialog(
            final int dialogType, double dialogValue, double min, double max, double step) {
        Calendar cal;
        // |dialogValue|, |min|, |max| mean different things depending on the |dialogType|.
        // For input type=month is the number of months since 1970.
        // For input type=time it is milliseconds since midnight.
        // For other types they are just milliseconds since 1970.
        // If |dialogValue| is NaN it means an empty value. We will show the current time.
        if (Double.isNaN(dialogValue)) {
            cal = Calendar.getInstance();
            cal.set(Calendar.MILLISECOND, 0);
        } else {
            if (dialogType == TextInputType.MONTH) {
                cal = MonthPicker.createDateFromValue(dialogValue);
            } else if (dialogType == TextInputType.WEEK) {
                cal = WeekPicker.createDateFromValue(dialogValue);
            } else {
                GregorianCalendar gregorianCalendar =
                        new GregorianCalendar(TimeZone.getTimeZone("UTC"));
                // According to the HTML spec we only use the Gregorian calendar
                // so we ignore the Julian/Gregorian transition.
                gregorianCalendar.setGregorianChange(new Date(Long.MIN_VALUE));
                gregorianCalendar.setTimeInMillis((long) dialogValue);
                cal = gregorianCalendar;
            }
        }
        if (dialogType == TextInputType.DATE) {
            showPickerDialog(
                    dialogType,
                    cal.get(Calendar.YEAR),
                    cal.get(Calendar.MONTH),
                    cal.get(Calendar.DAY_OF_MONTH),
                    0,
                    0,
                    0,
                    0,
                    0,
                    min,
                    max,
                    step);
            DialogTypeRecorder.recordDialogType(DialogTypeRecorder.DialogType.DATE);
        } else if (dialogType == TextInputType.TIME) {
            showPickerDialog(
                    dialogType,
                    0,
                    0,
                    0,
                    cal.get(Calendar.HOUR_OF_DAY),
                    cal.get(Calendar.MINUTE),
                    0,
                    0,
                    0,
                    min,
                    max,
                    step);
            DialogTypeRecorder.recordDialogType(DialogTypeRecorder.DialogType.TIME);
        } else if (dialogType == TextInputType.DATE_TIME
                || dialogType == TextInputType.DATE_TIME_LOCAL) {
            showPickerDialog(
                    dialogType,
                    cal.get(Calendar.YEAR),
                    cal.get(Calendar.MONTH),
                    cal.get(Calendar.DAY_OF_MONTH),
                    cal.get(Calendar.HOUR_OF_DAY),
                    cal.get(Calendar.MINUTE),
                    cal.get(Calendar.SECOND),
                    cal.get(Calendar.MILLISECOND),
                    0,
                    min,
                    max,
                    step);
            DialogTypeRecorder.recordDialogType(DialogTypeRecorder.DialogType.DATETIME);
        } else if (dialogType == TextInputType.MONTH) {
            showPickerDialog(
                    dialogType,
                    cal.get(Calendar.YEAR),
                    cal.get(Calendar.MONTH),
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    min,
                    max,
                    step);
            DialogTypeRecorder.recordDialogType(DialogTypeRecorder.DialogType.MONTH);
        } else if (dialogType == TextInputType.WEEK) {
            int year = WeekPicker.getISOWeekYearForDate(cal);
            int week = WeekPicker.getWeekForDate(cal);
            showPickerDialog(dialogType, year, 0, 0, 0, 0, 0, 0, week, min, max, step);
            DialogTypeRecorder.recordDialogType(DialogTypeRecorder.DialogType.WEEK);
        }
    }

    private void showSuggestionDialog(
            final int dialogType,
            final double dialogValue,
            final double min,
            final double max,
            final double step,
            DateTimeSuggestion[] suggestions) {
        ListView suggestionListView = new ListView(mContext);
        final DateTimeSuggestionListAdapter adapter =
                new DateTimeSuggestionListAdapter(mContext, Arrays.asList(suggestions));
        suggestionListView.setAdapter(adapter);
        suggestionListView.setOnItemClickListener(
                new AdapterView.OnItemClickListener() {
                    @Override
                    public void onItemClick(
                            AdapterView<?> parent, View view, int position, long id) {
                        if (position == adapter.getCount() - 1) {
                            dismissDialog();
                            showPickerDialog(dialogType, dialogValue, min, max, step);
                        } else {
                            double suggestionValue = adapter.getItem(position).value();
                            mInputActionDelegate.replaceDateTime(suggestionValue);
                            dismissDialog();
                            mDialogAlreadyDismissed = true;
                        }
                    }
                });

        int dialogTitleId = R.string.date_picker_dialog_title;
        if (dialogType == TextInputType.TIME) {
            dialogTitleId = R.string.time_picker_dialog_title;
        } else if (dialogType == TextInputType.DATE_TIME
                || dialogType == TextInputType.DATE_TIME_LOCAL) {
            dialogTitleId = R.string.date_time_picker_dialog_title;
        } else if (dialogType == TextInputType.MONTH) {
            dialogTitleId = R.string.month_picker_dialog_title;
        } else if (dialogType == TextInputType.WEEK) {
            dialogTitleId = R.string.week_picker_dialog_title;
        }

        mDialog =
                new AlertDialog.Builder(mContext)
                        .setTitle(dialogTitleId)
                        .setView(suggestionListView)
                        .setNegativeButton(
                                mContext.getText(android.R.string.cancel),
                                new DialogInterface.OnClickListener() {
                                    @Override
                                    public void onClick(DialogInterface dialog, int which) {
                                        dismissDialog();
                                    }
                                })
                        .create();

        mDialog.setOnDismissListener(
                new DialogInterface.OnDismissListener() {
                    @Override
                    public void onDismiss(DialogInterface dialog) {
                        if (mDialog == dialog && !mDialogAlreadyDismissed) {
                            mDialogAlreadyDismissed = true;
                            mInputActionDelegate.cancelDateTimeDialog();
                        }
                    }
                });
        mDialogAlreadyDismissed = false;
        mDialog.show();
    }

    public void showDialog(
            final int type,
            final double value,
            double min,
            double max,
            double step,
            DateTimeSuggestion[] suggestions) {
        // When the web page asks to show a dialog while there is one already open,
        // dismiss the old one.
        dismissDialog();
        if (suggestions == null) {
            showPickerDialog(type, value, min, max, step);
        } else {
            showSuggestionDialog(type, value, min, max, step, suggestions);
        }
    }

    protected void showPickerDialog(
            final int dialogType,
            int year,
            int month,
            int monthDay,
            int hourOfDay,
            int minute,
            int second,
            int millis,
            int week,
            double min,
            double max,
            double step) {
        dismissDialog();

        int stepTime = (int) step;

        if (dialogType == TextInputType.DATE) {
            DatePickerDialogCompat dialog =
                    new DatePickerDialogCompat(
                            mContext, new DateListener(dialogType), year, month, monthDay);
            DateDialogNormalizer.normalize(
                    dialog.getDatePicker(), dialog, year, month, monthDay, (long) min, (long) max);

            dialog.setTitle(mContext.getText(R.string.date_picker_dialog_title));
            mDialog = dialog;
        } else if (dialogType == TextInputType.TIME) {
            // If user doesn't need to set seconds and milliseconds, show the default clock style
            // time picker dialog. Otherwise, show a full spinner style time picker.
            if (stepTime < 0 || stepTime >= 60000 /* milliseconds in a minute */) {
                mDialog =
                        new TimePickerDialog(
                                mContext,
                                new TimeListener(dialogType),
                                hourOfDay,
                                minute,
                                DateFormat.is24HourFormat(mContext));
            } else {
                mDialog =
                        new MultiFieldTimePickerDialog(
                                mContext,
                                /* theme= */ 0,
                                hourOfDay,
                                minute,
                                second,
                                millis,
                                (int) min,
                                (int) max,
                                stepTime,
                                DateFormat.is24HourFormat(mContext),
                                new FullTimeListener(dialogType));
            }
        } else if (dialogType == TextInputType.DATE_TIME
                || dialogType == TextInputType.DATE_TIME_LOCAL) {
            mDialog =
                    new DateTimePickerDialog(
                            mContext,
                            new DateTimeListener(dialogType),
                            year,
                            month,
                            monthDay,
                            hourOfDay,
                            minute,
                            DateFormat.is24HourFormat(mContext),
                            min,
                            max);
        } else if (dialogType == TextInputType.MONTH) {
            mDialog =
                    new MonthPickerDialog(
                            mContext, new MonthOrWeekListener(dialogType), year, month, min, max);
        } else if (dialogType == TextInputType.WEEK) {
            mDialog =
                    new WeekPickerDialog(
                            mContext, new MonthOrWeekListener(dialogType), year, week, min, max);
        }

        mDialog.setButton(
                DialogInterface.BUTTON_POSITIVE,
                mContext.getText(R.string.date_picker_dialog_set),
                (DialogInterface.OnClickListener) mDialog);

        mDialog.setButton(
                DialogInterface.BUTTON_NEGATIVE,
                mContext.getText(android.R.string.cancel),
                (DialogInterface.OnClickListener) null);

        mDialog.setButton(
                DialogInterface.BUTTON_NEUTRAL,
                mContext.getText(R.string.date_picker_dialog_clear),
                new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        mDialogAlreadyDismissed = true;
                        mInputActionDelegate.replaceDateTime(Double.NaN);
                    }
                });

        mDialog.setOnDismissListener(
                new OnDismissListener() {
                    @Override
                    public void onDismiss(final DialogInterface dialog) {
                        if (!mDialogAlreadyDismissed) {
                            mDialogAlreadyDismissed = true;
                            mInputActionDelegate.cancelDateTimeDialog();
                        }
                    }
                });

        mDialogAlreadyDismissed = false;
        mDialog.show();
    }

    private boolean isDialogShowing() {
        return mDialog != null && mDialog.isShowing();
    }

    public void dismissDialog() {
        if (!isDialogShowing()) return;
        try {
            mDialog.dismiss();
        } catch (IllegalArgumentException e) {
            Log.w(TAG, "Ignoring exception from dialog.dismiss", e);
        }
    }

    private class DateListener implements OnDateSetListener {
        private final int mDialogType;

        DateListener(int dialogType) {
            mDialogType = dialogType;
        }

        @Override
        public void onDateSet(DatePicker view, int year, int month, int monthDay) {
            setFieldDateTimeValue(mDialogType, year, month, monthDay, 0, 0, 0, 0, 0);
        }
    }

    private class TimeListener implements OnTimeSetListener {
        private final int mDialogType;

        TimeListener(int dialogType) {
            mDialogType = dialogType;
        }

        @Override
        public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
            setFieldDateTimeValue(mDialogType, 0, 0, 0, hourOfDay, minute, 0, 0, 0);
        }
    }

    private class FullTimeListener implements OnMultiFieldTimeSetListener {
        private final int mDialogType;

        FullTimeListener(int dialogType) {
            mDialogType = dialogType;
        }

        @Override
        public void onTimeSet(int hourOfDay, int minute, int second, int milli) {
            setFieldDateTimeValue(mDialogType, 0, 0, 0, hourOfDay, minute, second, milli, 0);
        }
    }

    private class DateTimeListener implements OnDateTimeSetListener {
        private final boolean mLocal;
        private final int mDialogType;

        public DateTimeListener(int dialogType) {
            mLocal = dialogType == TextInputType.DATE_TIME_LOCAL;
            mDialogType = dialogType;
        }

        @Override
        public void onDateTimeSet(
                DatePicker dateView,
                TimePicker timeView,
                int year,
                int month,
                int monthDay,
                int hourOfDay,
                int minute) {
            setFieldDateTimeValue(mDialogType, year, month, monthDay, hourOfDay, minute, 0, 0, 0);
        }
    }

    private class MonthOrWeekListener implements TwoFieldDatePickerDialog.OnValueSetListener {
        private final int mDialogType;

        MonthOrWeekListener(int dialogType) {
            mDialogType = dialogType;
        }

        @Override
        public void onValueSet(int year, int positionInYear) {
            if (mDialogType == TextInputType.MONTH) {
                setFieldDateTimeValue(mDialogType, year, positionInYear, 0, 0, 0, 0, 0, 0);
            } else {
                setFieldDateTimeValue(mDialogType, year, 0, 0, 0, 0, 0, 0, positionInYear);
            }
        }
    }

    protected void setFieldDateTimeValue(
            int dialogType,
            int year,
            int month,
            int monthDay,
            int hourOfDay,
            int minute,
            int second,
            int millis,
            int week) {
        // Prevents more than one callback being sent to the native
        // side when the dialog triggers multiple events.
        if (mDialogAlreadyDismissed) return;
        mDialogAlreadyDismissed = true;

        if (dialogType == TextInputType.MONTH) {
            mInputActionDelegate.replaceDateTime((year - 1970) * 12 + month);
        } else if (dialogType == TextInputType.WEEK) {
            mInputActionDelegate.replaceDateTime(
                    WeekPicker.createDateFromWeek(year, week).getTimeInMillis());
        } else if (dialogType == TextInputType.TIME) {
            mInputActionDelegate.replaceDateTime(
                    TimeUnit.HOURS.toMillis(hourOfDay)
                            + TimeUnit.MINUTES.toMillis(minute)
                            + TimeUnit.SECONDS.toMillis(second)
                            + millis);
        } else {
            Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
            cal.clear();
            cal.set(Calendar.YEAR, year);
            cal.set(Calendar.MONTH, month);
            cal.set(Calendar.DAY_OF_MONTH, monthDay);
            cal.set(Calendar.HOUR_OF_DAY, hourOfDay);
            cal.set(Calendar.MINUTE, minute);
            cal.set(Calendar.SECOND, second);
            cal.set(Calendar.MILLISECOND, millis);
            mInputActionDelegate.replaceDateTime(cal.getTimeInMillis());
        }
    }
}