| 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 | |
| 5 | package org.chromium.content.browser.input; |
| 6 | |
| 7 | import android.app.AlertDialog; |
| 8 | import android.content.Context; |
| 9 | import android.content.DialogInterface; |
| 10 | import android.content.DialogInterface.OnClickListener; |
| 11 | import android.os.Build; |
| 12 | import android.view.LayoutInflater; |
| 13 | import android.view.View; |
| 14 | import android.widget.NumberPicker; |
| 15 | |
| 16 | import org.chromium.content.R; |
| 17 | |
| 18 | import java.util.ArrayList; |
| 19 | |
| 20 | /** |
| 21 | * A time picker dialog with upto 5 number pickers left to right: |
| 22 | * hour, minute, second, milli, AM/PM. |
| 23 | * |
| 24 | * If is24hourFormat is true then AM/PM picker is not displayed and |
| 25 | * hour range is 0..23. Otherwise hour range is 1..12. |
| 26 | * The milli picker is not displayed if step >= SECOND_IN_MILLIS |
| 27 | * The second picker is not displayed if step >= MINUTE_IN_MILLIS. |
| 28 | */ |
| 29 | public class MultiFieldTimePickerDialog |
| 30 | extends AlertDialog implements OnClickListener { |
| 31 | |
| 32 | private final NumberPicker mHourSpinner; |
| 33 | private final NumberPicker mMinuteSpinner; |
| 34 | private final NumberPicker mSecSpinner; |
| 35 | private final NumberPicker mMilliSpinner; |
| 36 | private final NumberPicker mAmPmSpinner; |
| 37 | private final OnMultiFieldTimeSetListener mListener; |
| 38 | private final int mStep; |
| 39 | private final int mBaseMilli; |
| 40 | private final boolean mIs24hourFormat; |
| 41 | |
| 42 | public interface OnMultiFieldTimeSetListener { |
| 43 | void onTimeSet(int hourOfDay, int minute, int second, int milli); |
| 44 | } |
| 45 | |
| 46 | private final static int SECOND_IN_MILLIS = 1000; |
| 47 | private final static int MINUTE_IN_MILLIS = 60 * SECOND_IN_MILLIS; |
| 48 | private final static int HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS; |
| 49 | |
| 50 | public MultiFieldTimePickerDialog( |
| 51 | Context context, |
| 52 | int theme, |
| 53 | int hour, int minute, int second, int milli, |
| 54 | int min, int max, int step, boolean is24hourFormat, |
| 55 | OnMultiFieldTimeSetListener listener) { |
| 56 | super(context, theme); |
| 57 | mListener = listener; |
| 58 | mStep = step; |
| 59 | mIs24hourFormat = is24hourFormat; |
| 60 | |
| 61 | if (min >= max) { |
| 62 | min = 0; |
| 63 | max = 24 * HOUR_IN_MILLIS - 1; |
| 64 | } |
| 65 | if (step < 0 || step >= 24 * HOUR_IN_MILLIS) { |
| 66 | step = MINUTE_IN_MILLIS; |
| 67 | } |
| 68 | |
| 69 | LayoutInflater inflater = |
| 70 | (LayoutInflater) context.getSystemService( |
| 71 | Context.LAYOUT_INFLATER_SERVICE); |
| 72 | View view = inflater.inflate(R.layout.multi_field_time_picker_dialog, null); |
| 73 | setView(view); |
| 74 | |
| 75 | mHourSpinner = (NumberPicker) view.findViewById(R.id.hour); |
| 76 | mMinuteSpinner = (NumberPicker) view.findViewById(R.id.minute); |
| 77 | mSecSpinner = (NumberPicker) view.findViewById(R.id.second); |
| 78 | mMilliSpinner = (NumberPicker) view.findViewById(R.id.milli); |
| 79 | mAmPmSpinner = (NumberPicker) view.findViewById(R.id.ampm); |
| 80 | |
| 81 | int minHour = min / HOUR_IN_MILLIS; |
| 82 | int maxHour = max / HOUR_IN_MILLIS; |
| 83 | min -= minHour * HOUR_IN_MILLIS; |
| 84 | max -= maxHour * HOUR_IN_MILLIS; |
| 85 | |
| 86 | if (minHour == maxHour) { |
| 87 | mHourSpinner.setEnabled(false); |
| 88 | hour = minHour; |
| 89 | } |
| 90 | |
| 91 | if (is24hourFormat) { |
| 92 | mAmPmSpinner.setVisibility(View.GONE); |
| 93 | } else { |
| 94 | int minAmPm = minHour / 12; |
| 95 | int maxAmPm = maxHour / 12; |
| 96 | int amPm = hour / 12; |
| 97 | mAmPmSpinner.setMinValue(minAmPm); |
| 98 | mAmPmSpinner.setMaxValue(maxAmPm); |
| 99 | mAmPmSpinner.setDisplayedValues(new String[] { |
| 100 | context.getString(R.string.time_picker_dialog_am), |
| 101 | context.getString(R.string.time_picker_dialog_pm) |
| 102 | }); |
| 103 | |
| 104 | hour %= 12; |
| 105 | if (hour == 0) { |
| 106 | hour = 12; |
| 107 | } |
| 108 | if (minAmPm == maxAmPm) { |
| 109 | mAmPmSpinner.setEnabled(false); |
| 110 | amPm = minAmPm; |
| 111 | |
| 112 | minHour %= 12; |
| 113 | maxHour %= 12; |
| 114 | if (minHour == 0 && maxHour == 0) { |
| 115 | minHour = 12; |
| 116 | maxHour = 12; |
| 117 | } else if (minHour == 0) { |
| 118 | minHour = maxHour; |
| 119 | maxHour = 12; |
| 120 | } else if (maxHour == 0) { |
| 121 | maxHour = 12; |
| 122 | } |
| 123 | } else { |
| 124 | minHour = 1; |
| 125 | maxHour = 12; |
| 126 | } |
| 127 | mAmPmSpinner.setValue(amPm); |
| 128 | } |
| 129 | |
| 130 | if (minHour == maxHour) { |
| 131 | mHourSpinner.setEnabled(false); |
| 132 | } |
| 133 | mHourSpinner.setMinValue(minHour); |
| 134 | mHourSpinner.setMaxValue(maxHour); |
| 135 | mHourSpinner.setValue(hour); |
| 136 | |
| 137 | NumberFormatter twoDigitPaddingFormatter = new NumberFormatter("%02d"); |
| 138 | |
| 139 | int minMinute = min / MINUTE_IN_MILLIS; |
| 140 | int maxMinute = max / MINUTE_IN_MILLIS; |
| 141 | min -= minMinute * MINUTE_IN_MILLIS; |
| 142 | max -= maxMinute * MINUTE_IN_MILLIS; |
| 143 | |
| 144 | if (minHour == maxHour) { |
| 145 | mMinuteSpinner.setMinValue(minMinute); |
| 146 | mMinuteSpinner.setMaxValue(maxMinute); |
| 147 | if (minMinute == maxMinute) { |
| 148 | // Set this otherwise the box is empty until you stroke it. |
| 149 | mMinuteSpinner.setDisplayedValues( |
| 150 | new String[] { twoDigitPaddingFormatter.format(minMinute) }); |
| 151 | mMinuteSpinner.setEnabled(false); |
| 152 | minute = minMinute; |
| 153 | } |
| 154 | } else { |
| 155 | mMinuteSpinner.setMinValue(0); |
| 156 | mMinuteSpinner.setMaxValue(59); |
| 157 | } |
| 158 | |
| 159 | if (step >= HOUR_IN_MILLIS) { |
| 160 | mMinuteSpinner.setEnabled(false); |
| 161 | } |
| 162 | |
| 163 | mMinuteSpinner.setValue(minute); |
| 164 | mMinuteSpinner.setFormatter(twoDigitPaddingFormatter); |
| 165 | |
| 166 | if (step >= MINUTE_IN_MILLIS) { |
| 167 | // Remove the ':' in front of the second spinner as well. |
| 168 | view.findViewById(R.id.second_colon).setVisibility(View.GONE); |
| 169 | mSecSpinner.setVisibility(View.GONE); |
| 170 | } |
| 171 | |
| 172 | int minSecond = min / SECOND_IN_MILLIS; |
| 173 | int maxSecond = max / SECOND_IN_MILLIS; |
| 174 | min -= minSecond * SECOND_IN_MILLIS; |
| 175 | max -= maxSecond * SECOND_IN_MILLIS; |
| 176 | |
| 177 | if (minHour == maxHour && minMinute == maxMinute) { |
| 178 | mSecSpinner.setMinValue(minSecond); |
| 179 | mSecSpinner.setMaxValue(maxSecond); |
| 180 | if (minSecond == maxSecond) { |
| 181 | // Set this otherwise the box is empty until you stroke it. |
| 182 | mSecSpinner.setDisplayedValues( |
| 183 | new String[] { twoDigitPaddingFormatter.format(minSecond) }); |
| 184 | mSecSpinner.setEnabled(false); |
| 185 | second = minSecond; |
| 186 | } |
| 187 | } else { |
| 188 | mSecSpinner.setMinValue(0); |
| 189 | mSecSpinner.setMaxValue(59); |
| 190 | } |
| 191 | |
| 192 | mSecSpinner.setValue(second); |
| 193 | mSecSpinner.setFormatter(twoDigitPaddingFormatter); |
| 194 | |
| 195 | if (step >= SECOND_IN_MILLIS) { |
| 196 | // Remove the '.' in front of the milli spinner as well. |
| 197 | view.findViewById(R.id.second_dot).setVisibility(View.GONE); |
| 198 | mMilliSpinner.setVisibility(View.GONE); |
| 199 | } |
| 200 | |
| 201 | // Round to the nearest step. |
| 202 | milli = ((milli + step / 2) / step) * step; |
| 203 | if (step == 1 || step == 10 || step == 100) { |
| 204 | if (minHour == maxHour && minMinute == maxMinute && |
| 205 | minSecond == maxSecond) { |
| 206 | mMilliSpinner.setMinValue(min / step); |
| 207 | mMilliSpinner.setMaxValue(max / step); |
| 208 | |
| 209 | if (min == max) { |
| 210 | mMilliSpinner.setEnabled(false); |
| 211 | milli = min; |
| 212 | } |
| 213 | } else { |
| 214 | mMilliSpinner.setMinValue(0); |
| 215 | mMilliSpinner.setMaxValue(999 / step); |
| 216 | } |
| 217 | |
| 218 | if (step == 1) { |
| 219 | mMilliSpinner.setFormatter(new NumberFormatter("%03d")); |
| 220 | } else if (step == 10) { |
| 221 | mMilliSpinner.setFormatter(new NumberFormatter("%02d")); |
| 222 | } else if (step == 100) { |
| 223 | mMilliSpinner.setFormatter(new NumberFormatter("%d")); |
| 224 | } |
| 225 | mMilliSpinner.setValue(milli / step); |
| 226 | mBaseMilli = 0; |
| 227 | } else if (step < SECOND_IN_MILLIS) { |
| 228 | // Non-decimal step value. |
| 229 | ArrayList<String> strValue = new ArrayList<String>(); |
| 230 | for (int i = min; i < max; i += step) { |
| 231 | strValue.add(String.format("%03d", i)); |
| 232 | } |
| 233 | mMilliSpinner.setMinValue(0); |
| 234 | mMilliSpinner.setMaxValue(strValue.size() - 1); |
| 235 | mMilliSpinner.setValue( (milli - min) / step); |
| 236 | mMilliSpinner.setDisplayedValues( |
| 237 | strValue.toArray(new String[strValue.size()])); |
| 238 | mBaseMilli = min; |
| 239 | } else { |
| 240 | mBaseMilli = 0; |
| 241 | } |
| 242 | } |
| 243 | |
| 244 | @Override |
| 245 | public void onClick(DialogInterface dialog, int which) { |
| 246 | notifyDateSet(); |
| 247 | } |
| 248 | |
| 249 | private void notifyDateSet() { |
| 250 | int hour = mHourSpinner.getValue(); |
| 251 | int minute = mMinuteSpinner.getValue(); |
| 252 | int sec = mSecSpinner.getValue(); |
| 253 | int milli = mMilliSpinner.getValue() * mStep + mBaseMilli; |
| 254 | if (!mIs24hourFormat) { |
| 255 | int ampm = mAmPmSpinner.getValue(); |
| 256 | if (hour == 12) { |
| 257 | hour = 0; |
| 258 | } |
| 259 | hour += ampm * 12; |
| 260 | } |
| 261 | mListener.onTimeSet(hour, minute, sec, milli); |
| 262 | } |
| 263 | |
| 264 | @Override |
| 265 | protected void onStop() { |
| 266 | if (Build.VERSION.SDK_INT >= 16) { |
| 267 | // The default behavior of dialogs changed in JellyBean and onwards. |
| 268 | // Dismissing a dialog (by pressing back for example) |
| 269 | // applies the chosen date. This code is added here so that the custom |
| 270 | // pickers behave the same as the internal DatePickerDialog. |
| 271 | notifyDateSet(); |
| 272 | } |
| 273 | super.onStop(); |
| 274 | } |
| 275 | |
| 276 | private static class NumberFormatter implements NumberPicker.Formatter { |
| 277 | private final String mFormat; |
| 278 | |
| 279 | NumberFormatter(String format) { |
| 280 | mFormat = format; |
| 281 | } |
| 282 | |
| 283 | @Override |
| 284 | public String format(int value) { |
| 285 | return String.format(mFormat, value); |
| 286 | } |
| 287 | } |
| 288 | } |