1 /*
2  * Copyright (C) 2008 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 
17 package android.widget.cts;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertFalse;
21 import static org.junit.Assert.assertNotNull;
22 import static org.junit.Assert.assertNull;
23 import static org.junit.Assert.assertTrue;
24 import static org.mockito.Mockito.mock;
25 import static org.mockito.Mockito.reset;
26 import static org.mockito.Mockito.times;
27 import static org.mockito.Mockito.verify;
28 import static org.mockito.Mockito.verifyZeroInteractions;
29 
30 import android.app.Activity;
31 import android.app.Instrumentation;
32 import android.content.Context;
33 import android.content.res.Configuration;
34 import android.os.Parcelable;
35 import android.util.AttributeSet;
36 import android.view.KeyEvent;
37 import android.view.View;
38 import android.view.autofill.AutofillValue;
39 import android.widget.TimePicker;
40 
41 import androidx.test.annotation.UiThreadTest;
42 import androidx.test.ext.junit.runners.AndroidJUnit4;
43 import androidx.test.filters.MediumTest;
44 import androidx.test.platform.app.InstrumentationRegistry;
45 import androidx.test.rule.ActivityTestRule;
46 
47 import com.android.compatibility.common.util.CtsKeyEventUtil;
48 import com.android.compatibility.common.util.CtsTouchUtils;
49 import com.android.compatibility.common.util.PollingCheck;
50 
51 import org.junit.Before;
52 import org.junit.Rule;
53 import org.junit.Test;
54 import org.junit.runner.RunWith;
55 
56 import java.util.ArrayList;
57 import java.util.Calendar;
58 import java.util.Collections;
59 import java.util.GregorianCalendar;
60 import java.util.concurrent.atomic.AtomicInteger;
61 
62 /**
63  * Test {@link TimePicker}.
64  */
65 @MediumTest
66 @RunWith(AndroidJUnit4.class)
67 public class TimePickerTest {
68     private Instrumentation mInstrumentation;
69     private Activity mActivity;
70     private TimePicker mTimePicker;
71 
72     @Rule
73     public ActivityTestRule<TimePickerCtsActivity> mActivityRule =
74             new ActivityTestRule<>(TimePickerCtsActivity.class);
75 
76     @Before
setup()77     public void setup() {
78         mInstrumentation = InstrumentationRegistry.getInstrumentation();
79         mActivity = mActivityRule.getActivity();
80         mTimePicker = (TimePicker) mActivity.findViewById(R.id.timepicker_clock);
81         PollingCheck.waitFor(mActivity::hasWindowFocus);
82     }
83 
84     @Test
testConstructors()85     public void testConstructors() {
86         AttributeSet attrs = mActivity.getResources().getLayout(R.layout.timepicker);
87         assertNotNull(attrs);
88 
89         new TimePicker(mActivity);
90 
91         new TimePicker(mActivity, attrs);
92         new TimePicker(mActivity, null);
93 
94         new TimePicker(mActivity, attrs, 0);
95         new TimePicker(mActivity, null, 0);
96         new TimePicker(mActivity, attrs, 0);
97         new TimePicker(mActivity, null, android.R.attr.timePickerStyle);
98         new TimePicker(mActivity, null, 0, android.R.style.Widget_Material_TimePicker);
99         new TimePicker(mActivity, null, 0, android.R.style.Widget_Material_Light_TimePicker);
100     }
101 
102     @Test(expected=NullPointerException.class)
testConstructorNullContext1()103     public void testConstructorNullContext1() {
104         new TimePicker(null);
105     }
106 
107     @Test(expected=NullPointerException.class)
testConstructorNullContext2()108     public void testConstructorNullContext2() {
109         AttributeSet attrs = mActivity.getResources().getLayout(R.layout.timepicker);
110         new TimePicker(null, attrs);
111     }
112 
113     @Test(expected=NullPointerException.class)
testConstructorNullContext3()114     public void testConstructorNullContext3() {
115         AttributeSet attrs = mActivity.getResources().getLayout(R.layout.timepicker);
116         new TimePicker(null, attrs, 0);
117     }
118 
119     @UiThreadTest
120     @Test
testSetEnabled()121     public void testSetEnabled() {
122         assertTrue(mTimePicker.isEnabled());
123 
124         mTimePicker.setEnabled(false);
125         assertFalse(mTimePicker.isEnabled());
126         assertNull(mTimePicker.getAutofillValue());
127         assertEquals(View.AUTOFILL_TYPE_NONE, mTimePicker.getAutofillType());
128 
129         mTimePicker.setEnabled(true);
130         assertTrue(mTimePicker.isEnabled());
131         assertNotNull(mTimePicker.getAutofillValue());
132         assertEquals(View.AUTOFILL_TYPE_DATE, mTimePicker.getAutofillType());
133     }
134 
135     @UiThreadTest
136     @Test
testAutofill()137     public void testAutofill() {
138         mTimePicker.setEnabled(true);
139 
140         final AtomicInteger numberOfListenerCalls = new AtomicInteger();
141         mTimePicker.setOnTimeChangedListener((v, h, m) -> numberOfListenerCalls.incrementAndGet());
142 
143         final Calendar calendar = new GregorianCalendar();
144         calendar.set(Calendar.HOUR_OF_DAY, 4);
145         calendar.set(Calendar.MINUTE, 20);
146 
147         final AutofillValue autofilledValue = AutofillValue.forDate(calendar.getTimeInMillis());
148         mTimePicker.autofill(autofilledValue);
149         assertEquals(autofilledValue, mTimePicker.getAutofillValue());
150         assertEquals(4, mTimePicker.getHour());
151         assertEquals(20, mTimePicker.getMinute());
152         assertEquals(1, numberOfListenerCalls.get());
153 
154         // Make sure autofill() is ignored when value is null.
155         numberOfListenerCalls.set(0);
156         mTimePicker.autofill((AutofillValue) null);
157         assertEquals(autofilledValue, mTimePicker.getAutofillValue());
158         assertEquals(4, mTimePicker.getHour());
159         assertEquals(20, mTimePicker.getMinute());
160         assertEquals(0, numberOfListenerCalls.get());
161 
162         // Make sure autofill() is ignored when value is not a date.
163         numberOfListenerCalls.set(0);
164         mTimePicker.autofill(AutofillValue.forText("Y U NO IGNORE ME?"));
165         assertEquals(autofilledValue, mTimePicker.getAutofillValue());
166         assertEquals(4, mTimePicker.getHour());
167         assertEquals(20, mTimePicker.getMinute());
168         assertEquals(0, numberOfListenerCalls.get());
169 
170         // Make sure getAutofillValue() is reset when value is manually filled.
171         mTimePicker.autofill(autofilledValue); // 04:20
172         mTimePicker.setHour(10);
173         calendar.setTimeInMillis(mTimePicker.getAutofillValue().getDateValue());
174         assertEquals(10, calendar.get(Calendar.HOUR));
175         mTimePicker.autofill(autofilledValue); // 04:20
176         mTimePicker.setMinute(8);
177         calendar.setTimeInMillis(mTimePicker.getAutofillValue().getDateValue());
178         assertEquals(8, calendar.get(Calendar.MINUTE));
179     }
180 
181     @UiThreadTest
182     @Test
testSetOnTimeChangedListener()183     public void testSetOnTimeChangedListener() {
184         // On time change listener is notified on every call to setCurrentHour / setCurrentMinute.
185         // We want to make sure that before we register our listener, we initialize the time picker
186         // to the time that is explicitly different from the values we'll be testing for in both
187         // hour and minute. Otherwise if the test happens to run at the time that ends in
188         // "minuteForTesting" minutes, we'll get two onTimeChanged callbacks with identical values.
189         final int initialHour = 10;
190         final int initialMinute = 38;
191         final int hourForTesting = 13;
192         final int minuteForTesting = 50;
193 
194         mTimePicker.setHour(initialHour);
195         mTimePicker.setMinute(initialMinute);
196 
197         // Now register the listener
198         TimePicker.OnTimeChangedListener mockOnTimeChangeListener =
199                 mock(TimePicker.OnTimeChangedListener.class);
200         mTimePicker.setOnTimeChangedListener(mockOnTimeChangeListener);
201         mTimePicker.setCurrentHour(Integer.valueOf(hourForTesting));
202         mTimePicker.setCurrentMinute(Integer.valueOf(minuteForTesting));
203         // We're expecting two onTimeChanged callbacks, one with new hour and one with new
204         // hour+minute
205         verify(mockOnTimeChangeListener, times(1)).onTimeChanged(
206                 mTimePicker, hourForTesting, initialMinute);
207         verify(mockOnTimeChangeListener, times(1)).onTimeChanged(
208                 mTimePicker, hourForTesting, minuteForTesting);
209 
210         // set the same hour as current
211         reset(mockOnTimeChangeListener);
212         mTimePicker.setCurrentHour(Integer.valueOf(hourForTesting));
213         verifyZeroInteractions(mockOnTimeChangeListener);
214 
215         mTimePicker.setCurrentHour(Integer.valueOf(hourForTesting + 1));
216         verify(mockOnTimeChangeListener, times(1)).onTimeChanged(
217                 mTimePicker, hourForTesting + 1, minuteForTesting);
218 
219         // set the same minute as current
220         reset(mockOnTimeChangeListener);
221         mTimePicker.setCurrentMinute(minuteForTesting);
222         verifyZeroInteractions(mockOnTimeChangeListener);
223 
224         reset(mockOnTimeChangeListener);
225         mTimePicker.setCurrentMinute(minuteForTesting + 1);
226         verify(mockOnTimeChangeListener, times(1)).onTimeChanged(
227                 mTimePicker, hourForTesting + 1, minuteForTesting + 1);
228 
229         // change time picker mode
230         reset(mockOnTimeChangeListener);
231         mTimePicker.setIs24HourView(!mTimePicker.is24HourView());
232         verifyZeroInteractions(mockOnTimeChangeListener);
233     }
234 
235     @UiThreadTest
236     @Test
testAccessCurrentHour()237     public void testAccessCurrentHour() {
238         // AM/PM mode
239         mTimePicker.setIs24HourView(false);
240 
241         mTimePicker.setCurrentHour(0);
242         assertEquals(Integer.valueOf(0), mTimePicker.getCurrentHour());
243 
244         mTimePicker.setCurrentHour(12);
245         assertEquals(Integer.valueOf(12), mTimePicker.getCurrentHour());
246 
247         mTimePicker.setCurrentHour(13);
248         assertEquals(Integer.valueOf(13), mTimePicker.getCurrentHour());
249 
250         mTimePicker.setCurrentHour(23);
251         assertEquals(Integer.valueOf(23), mTimePicker.getCurrentHour());
252 
253         // for 24 hour mode
254         mTimePicker.setIs24HourView(true);
255 
256         mTimePicker.setCurrentHour(0);
257         assertEquals(Integer.valueOf(0), mTimePicker.getCurrentHour());
258 
259         mTimePicker.setCurrentHour(13);
260         assertEquals(Integer.valueOf(13), mTimePicker.getCurrentHour());
261 
262         mTimePicker.setCurrentHour(23);
263         assertEquals(Integer.valueOf(23), mTimePicker.getCurrentHour());
264     }
265 
266     @UiThreadTest
267     @Test
testAccessHour()268     public void testAccessHour() {
269         // AM/PM mode
270         mTimePicker.setIs24HourView(false);
271 
272         mTimePicker.setHour(0);
273         assertEquals(0, mTimePicker.getHour());
274 
275         mTimePicker.setHour(12);
276         assertEquals(12, mTimePicker.getHour());
277 
278         mTimePicker.setHour(13);
279         assertEquals(13, mTimePicker.getHour());
280 
281         mTimePicker.setHour(23);
282         assertEquals(23, mTimePicker.getHour());
283 
284         // for 24 hour mode
285         mTimePicker.setIs24HourView(true);
286 
287         mTimePicker.setHour(0);
288         assertEquals(0, mTimePicker.getHour());
289 
290         mTimePicker.setHour(13);
291         assertEquals(13, mTimePicker.getHour());
292 
293         mTimePicker.setHour(23);
294         assertEquals(23, mTimePicker.getHour());
295     }
296 
297     @UiThreadTest
298     @Test
testAccessIs24HourView()299     public void testAccessIs24HourView() {
300         assertFalse(mTimePicker.is24HourView());
301 
302         mTimePicker.setIs24HourView(true);
303         assertTrue(mTimePicker.is24HourView());
304 
305         mTimePicker.setIs24HourView(false);
306         assertFalse(mTimePicker.is24HourView());
307     }
308 
309     @UiThreadTest
310     @Test
testAccessCurrentMinute()311     public void testAccessCurrentMinute() {
312         mTimePicker.setCurrentMinute(0);
313         assertEquals(Integer.valueOf(0), mTimePicker.getCurrentMinute());
314 
315         mTimePicker.setCurrentMinute(12);
316         assertEquals(Integer.valueOf(12), mTimePicker.getCurrentMinute());
317 
318         mTimePicker.setCurrentMinute(33);
319         assertEquals(Integer.valueOf(33), mTimePicker.getCurrentMinute());
320 
321         mTimePicker.setCurrentMinute(59);
322         assertEquals(Integer.valueOf(59), mTimePicker.getCurrentMinute());
323     }
324 
325     @UiThreadTest
326     @Test
testAccessMinute()327     public void testAccessMinute() {
328         mTimePicker.setMinute(0);
329         assertEquals(0, mTimePicker.getMinute());
330 
331         mTimePicker.setMinute(12);
332         assertEquals(12, mTimePicker.getMinute());
333 
334         mTimePicker.setMinute(33);
335         assertEquals(33, mTimePicker.getMinute());
336 
337         mTimePicker.setMinute(59);
338         assertEquals(59, mTimePicker.getMinute());
339     }
340 
341     @Test
testGetBaseline()342     public void testGetBaseline() {
343         assertEquals(-1, mTimePicker.getBaseline());
344     }
345 
346     @Test
testOnSaveInstanceStateAndOnRestoreInstanceState()347     public void testOnSaveInstanceStateAndOnRestoreInstanceState() {
348         MyTimePicker source = new MyTimePicker(mActivity);
349         MyTimePicker dest = new MyTimePicker(mActivity);
350         int expectHour = (dest.getCurrentHour() + 10) % 24;
351         int expectMinute = (dest.getCurrentMinute() + 10) % 60;
352         source.setCurrentHour(expectHour);
353         source.setCurrentMinute(expectMinute);
354 
355         Parcelable p = source.onSaveInstanceState();
356         dest.onRestoreInstanceState(p);
357 
358         assertEquals(Integer.valueOf(expectHour), dest.getCurrentHour());
359         assertEquals(Integer.valueOf(expectMinute), dest.getCurrentMinute());
360     }
361 
isWatch()362     private boolean isWatch() {
363         return (mActivity.getResources().getConfiguration().uiMode
364                 & Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_WATCH;
365     }
366 
367     @Test
testKeyboardTabTraversalModeClock()368     public void testKeyboardTabTraversalModeClock() throws Throwable {
369         if (isWatch()) {
370             return;
371         }
372         mTimePicker = (TimePicker) mActivity.findViewById(R.id.timepicker_clock);
373 
374         mActivityRule.runOnUiThread(() -> mTimePicker.setIs24HourView(false));
375         mInstrumentation.waitForIdleSync();
376         verifyTimePickerKeyboardTraversal(
377                 true /* goForward */,
378                 false /* is24HourView */);
379         verifyTimePickerKeyboardTraversal(
380                 false /* goForward */,
381                 false /* is24HourView */);
382 
383         mActivityRule.runOnUiThread(() -> mTimePicker.setIs24HourView(true));
384         mInstrumentation.waitForIdleSync();
385         verifyTimePickerKeyboardTraversal(
386                 true /* goForward */,
387                 true /* is24HourView */);
388         verifyTimePickerKeyboardTraversal(
389                 false /* goForward */,
390                 true /* is24HourView */);
391     }
392 
393     @Test
testKeyboardTabTraversalModeSpinner()394     public void testKeyboardTabTraversalModeSpinner() throws Throwable {
395         if (isWatch()) {
396             return;
397         }
398         // Hide timepicker_clock so that timepicker_spinner would be visible.
399         mActivityRule.runOnUiThread(() ->
400                 mActivity.findViewById(R.id.timepicker_clock).setVisibility(View.GONE));
401         mTimePicker = (TimePicker) mActivity.findViewById(R.id.timepicker_spinner);
402 
403         mActivityRule.runOnUiThread(() -> mTimePicker.setIs24HourView(false));
404         mInstrumentation.waitForIdleSync();
405 
406         // Spinner time-picker doesn't explicitly define a focus order. Just make sure inputs
407         // are able to be traversed (added to focusables).
408         ArrayList<View> focusables = new ArrayList<>();
409         mTimePicker.addFocusables(focusables, View.FOCUS_FORWARD);
410         assertTrue(focusables.contains(mTimePicker.getHourView()));
411         assertTrue(focusables.contains(mTimePicker.getMinuteView()));
412         assertTrue(focusables.contains(mTimePicker.getAmView()));
413         focusables.clear();
414 
415         mActivityRule.runOnUiThread(() -> mTimePicker.setIs24HourView(true));
416         mInstrumentation.waitForIdleSync();
417         mTimePicker.addFocusables(focusables, View.FOCUS_FORWARD);
418         assertTrue(focusables.contains(mTimePicker.getHourView()));
419         assertTrue(focusables.contains(mTimePicker.getMinuteView()));
420     }
421 
422     @Test
testKeyboardInputModeClockAmPm()423     public void testKeyboardInputModeClockAmPm() throws Throwable {
424         if (isWatch()) {
425             return;
426         }
427         final int initialHour = 6;
428         final int initialMinute = 59;
429         prepareForKeyboardInput(initialHour, initialMinute, false /* is24hFormat */,
430                 true /* isClockMode */);
431 
432         // Input valid hour.
433         assertEquals(initialHour, mTimePicker.getHour());
434         CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule,
435                 mTimePicker.getHourView());
436         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_1);
437         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_0);
438         assertEquals(10, mTimePicker.getHour());
439         assertTrue(mTimePicker.getMinuteView().hasFocus());
440 
441         // Input valid minute.
442         assertEquals(initialMinute, mTimePicker.getMinute());
443         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_4);
444         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_3);
445         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_TAB);
446         assertEquals(43, mTimePicker.getMinute());
447         assertTrue(mTimePicker.getAmView().hasFocus());
448 
449         // Accepting AM changes nothing.
450         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_ENTER);
451         assertEquals(10, mTimePicker.getHour());
452         assertEquals(43, mTimePicker.getMinute());
453 
454         // Focus PM radio.
455         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_TAB);
456         assertTrue(mTimePicker.getPmView().hasFocus());
457         // Still nothing has changed.
458         assertEquals(10, mTimePicker.getHour());
459         assertEquals(43, mTimePicker.getMinute());
460         // Select PM and verify the hour has changed.
461         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_ENTER);
462         assertEquals(22, mTimePicker.getHour());
463         assertEquals(43, mTimePicker.getMinute());
464         // Set AM again.
465         CtsKeyEventUtil.sendKeyWhileHoldingModifier(mInstrumentation, mTimePicker,
466                 KeyEvent.KEYCODE_TAB, KeyEvent.KEYCODE_SHIFT_LEFT);
467         assertTrue(mTimePicker.getAmView().hasFocus());
468         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_ENTER);
469         assertEquals(10, mTimePicker.getHour());
470 
471         // Re-focus the hour view.
472         CtsKeyEventUtil.sendKeyWhileHoldingModifier(mInstrumentation, mTimePicker,
473                 KeyEvent.KEYCODE_TAB, KeyEvent.KEYCODE_SHIFT_LEFT);
474         CtsKeyEventUtil.sendKeyWhileHoldingModifier(mInstrumentation, mTimePicker,
475                 KeyEvent.KEYCODE_TAB, KeyEvent.KEYCODE_SHIFT_LEFT);
476         assertTrue(mTimePicker.getHourView().hasFocus());
477 
478         // Input an invalid value (larger than 12).
479         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_1);
480         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_3);
481         // Force setting the hour by moving to minute.
482         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_TAB);
483         // After sending 1 and 3 only 1 is accepted.
484         assertEquals(1, mTimePicker.getHour());
485         assertEquals(43, mTimePicker.getMinute());
486         CtsKeyEventUtil.sendKeyWhileHoldingModifier(mInstrumentation, mTimePicker,
487                 KeyEvent.KEYCODE_TAB, KeyEvent.KEYCODE_SHIFT_LEFT);
488         // The hour view still has focus.
489         assertTrue(mTimePicker.getHourView().hasFocus());
490 
491         // This time send a valid hour (11).
492         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_1);
493         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_1);
494         // The value is valid.
495         assertEquals(11, mTimePicker.getHour());
496         assertEquals(43, mTimePicker.getMinute());
497 
498         verifyModeClockMinuteInput();
499     }
500 
501     @Test
testKeyboardInputModeClock24H()502     public void testKeyboardInputModeClock24H() throws Throwable {
503         if (isWatch()) {
504             return;
505         }
506         final int initialHour = 6;
507         final int initialMinute = 59;
508         prepareForKeyboardInput(initialHour, initialMinute, true /* is24hFormat */,
509                 true /* isClockMode */);
510 
511         // Input valid hour.
512         assertEquals(initialHour, mTimePicker.getHour());
513         CtsTouchUtils.emulateTapOnViewCenter(mInstrumentation, mActivityRule,
514                 mTimePicker.getHourView());
515         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_1);
516         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_0);
517         assertEquals(10, mTimePicker.getHour());
518         assertTrue(mTimePicker.getMinuteView().hasFocus());
519 
520         // Input valid minute.
521         assertEquals(initialMinute, mTimePicker.getMinute());
522         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_4);
523         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_3);
524         assertEquals(43, mTimePicker.getMinute());
525 
526         // Re-focus the hour view.
527         CtsKeyEventUtil.sendKeyWhileHoldingModifier(mInstrumentation, mTimePicker,
528                 KeyEvent.KEYCODE_TAB, KeyEvent.KEYCODE_SHIFT_LEFT);
529         assertTrue(mTimePicker.getHourView().hasFocus());
530 
531         // Input an invalid value (larger than 24).
532         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_2);
533         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_5);
534         // Force setting the hour by moving to minute.
535         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_TAB);
536         // After sending 2 and 5 only 2 is accepted.
537         assertEquals(2, mTimePicker.getHour());
538         assertEquals(43, mTimePicker.getMinute());
539         CtsKeyEventUtil.sendKeyWhileHoldingModifier(mInstrumentation, mTimePicker,
540                 KeyEvent.KEYCODE_TAB, KeyEvent.KEYCODE_SHIFT_LEFT);
541         // The hour view still has focus.
542         assertTrue(mTimePicker.getHourView().hasFocus());
543 
544         // This time send a valid hour.
545         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_2);
546         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_3);
547         // The value is valid.
548         assertEquals(23, mTimePicker.getHour());
549         assertEquals(43, mTimePicker.getMinute());
550 
551         verifyModeClockMinuteInput();
552     }
553 
554     @Test
testKeyboardInputModeSpinnerAmPm()555     public void testKeyboardInputModeSpinnerAmPm() throws Throwable {
556         if (isWatch()) {
557             return;
558         }
559         final int initialHour = 6;
560         final int initialMinute = 59;
561         prepareForKeyboardInput(initialHour, initialMinute, false /* is24hFormat */,
562                 false /* isClockMode */);
563 
564         // when testing on device with lower resolution, the Spinner mode time picker may not show
565         // completely, which will cause case fail, so in this case remove the clock time picker to
566         // focus on the test of Spinner mode
567         final TimePicker clock = mActivity.findViewById(R.id.timepicker_clock);
568         mActivityRule.runOnUiThread(() -> clock.setVisibility(View.GONE));
569 
570         assertEquals(initialHour, mTimePicker.getHour());
571         mActivityRule.runOnUiThread(() -> mTimePicker.getHourView().requestFocus());
572         mInstrumentation.waitForIdleSync();
573 
574         // Input invalid hour.
575         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_1);
576         // None of the keys below should be accepted after 1 was pressed.
577         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_3);
578         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_4);
579         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_5);
580         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_TAB);
581         // Since only 0, 1 or 2 are accepted for AM/PM hour mode after pressing 1, we expect the
582         // hour value to be 1.
583         assertEquals(1, mTimePicker.getHour());
584         assertFalse(mTimePicker.getHourView().hasFocus());
585 
586         //  Go back to hour view and input valid hour.
587         mActivityRule.runOnUiThread(() -> mTimePicker.getHourView().requestFocus());
588         mInstrumentation.waitForIdleSync();
589         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_1);
590         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_1);
591         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_TAB);
592         assertEquals(11, mTimePicker.getHour());
593         assertFalse(mTimePicker.getHourView().hasFocus());
594 
595         // Go back to hour view and exercise UP and DOWN keys.
596         mActivityRule.runOnUiThread(() -> mTimePicker.getHourView().requestFocus());
597         mInstrumentation.waitForIdleSync();
598         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_DPAD_DOWN);
599         assertEquals(12, mTimePicker.getHour());
600         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_DPAD_UP);
601         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_DPAD_UP);
602         assertEquals(10, mTimePicker.getHour());
603 
604         // Minute input testing.
605         assertEquals(initialMinute, mTimePicker.getMinute());
606         verifyModeSpinnerMinuteInput();
607 
608         // Reset to values preparing to test the AM/PM picker.
609         mActivityRule.runOnUiThread(() -> {
610             mTimePicker.setHour(6);
611             mTimePicker.setMinute(initialMinute);
612         });
613         mInstrumentation.waitForIdleSync();
614         // In spinner mode the AM view and PM view are the same.
615         assertEquals(mTimePicker.getAmView(), mTimePicker.getPmView());
616         mActivityRule.runOnUiThread(() -> mTimePicker.getAmView().requestFocus());
617         mInstrumentation.waitForIdleSync();
618         assertTrue(mTimePicker.getAmView().hasFocus());
619         assertEquals(6, mTimePicker.getHour());
620         // Pressing A changes nothing.
621         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_A);
622         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_TAB);
623         assertEquals(6, mTimePicker.getHour());
624         assertEquals(initialMinute, mTimePicker.getMinute());
625         // Pressing P switches to PM.
626         CtsKeyEventUtil.sendKeyWhileHoldingModifier(mInstrumentation, mTimePicker,
627                 KeyEvent.KEYCODE_TAB, KeyEvent.KEYCODE_SHIFT_LEFT);
628         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_P);
629         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_TAB);
630         assertEquals(18, mTimePicker.getHour());
631         assertEquals(initialMinute, mTimePicker.getMinute());
632         // Pressing P again changes nothing.
633         CtsKeyEventUtil.sendKeyWhileHoldingModifier(mInstrumentation, mTimePicker,
634                 KeyEvent.KEYCODE_TAB, KeyEvent.KEYCODE_SHIFT_LEFT);
635         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_P);
636         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_TAB);
637         assertEquals(18, mTimePicker.getHour());
638         assertEquals(initialMinute, mTimePicker.getMinute());
639         // Pressing A switches to AM.
640         CtsKeyEventUtil.sendKeyWhileHoldingModifier(mInstrumentation, mTimePicker,
641                 KeyEvent.KEYCODE_TAB, KeyEvent.KEYCODE_SHIFT_LEFT);
642         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_A);
643         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_TAB);
644         assertEquals(6, mTimePicker.getHour());
645         assertEquals(initialMinute, mTimePicker.getMinute());
646         // Given that we are already set to AM, pressing UP changes nothing.
647         mActivityRule.runOnUiThread(() -> mTimePicker.getAmView().requestFocus());
648         mInstrumentation.waitForIdleSync();
649         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_DPAD_UP);
650         assertEquals(6, mTimePicker.getHour());
651         assertEquals(initialMinute, mTimePicker.getMinute());
652         mActivityRule.runOnUiThread(() -> mTimePicker.getAmView().requestFocus());
653         mInstrumentation.waitForIdleSync();
654         // Pressing down switches to PM.
655         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_DPAD_DOWN);
656         assertEquals(18, mTimePicker.getHour());
657         assertEquals(initialMinute, mTimePicker.getMinute());
658         mActivityRule.runOnUiThread(() -> mTimePicker.getAmView().requestFocus());
659         mInstrumentation.waitForIdleSync();
660         // Given that we are set to PM, pressing DOWN again changes nothing.
661         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_DPAD_DOWN);
662         assertEquals(18, mTimePicker.getHour());
663         assertEquals(initialMinute, mTimePicker.getMinute());
664         mActivityRule.runOnUiThread(() -> mTimePicker.getAmView().requestFocus());
665         mInstrumentation.waitForIdleSync();
666         // Pressing UP switches to AM.
667         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_DPAD_UP);
668         assertEquals(6, mTimePicker.getHour());
669         assertEquals(initialMinute, mTimePicker.getMinute());
670     }
671 
672     @Test
testKeyboardInputModeSpinner24H()673     public void testKeyboardInputModeSpinner24H() throws Throwable {
674         if (isWatch()) {
675             return;
676         }
677         final int initialHour = 6;
678         final int initialMinute = 59;
679         prepareForKeyboardInput(initialHour, initialMinute, true /* is24hFormat */,
680                 false /* isClockMode */);
681 
682         // when testing on device with lower resolution, the Spinner mode time picker may not show
683         // completely, which will cause case fail, so in this case remove the clock time picker to
684         // focus on the test of Spinner mode
685         final TimePicker clock = mActivity.findViewById(R.id.timepicker_clock);
686         mActivityRule.runOnUiThread(() -> clock.setVisibility(View.GONE));
687 
688         assertEquals(initialHour, mTimePicker.getHour());
689         mActivityRule.runOnUiThread(() -> mTimePicker.getHourView().requestFocus());
690         mInstrumentation.waitForIdleSync();
691 
692         // Input invalid hour.
693         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_2);
694         // None of the keys below should be accepted after 2 was pressed.
695         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_4);
696         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_5);
697         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_6);
698         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_TAB);
699         // Only 2 is accepted (as the only 0, 1, 2, and 3 can form valid hours after pressing 2).
700         assertEquals(2, mTimePicker.getHour());
701         assertFalse(mTimePicker.getHourView().hasFocus());
702 
703         //  Go back to hour view and input valid hour.
704         mActivityRule.runOnUiThread(() -> mTimePicker.getHourView().requestFocus());
705         mInstrumentation.waitForIdleSync();
706         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_2);
707         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_3);
708         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_TAB);
709         assertEquals(23, mTimePicker.getHour());
710         assertFalse(mTimePicker.getHourView().hasFocus());
711 
712         // Go back to hour view and exercise UP and DOWN keys.
713         mActivityRule.runOnUiThread(() -> mTimePicker.getHourView().requestFocus());
714         mInstrumentation.waitForIdleSync();
715         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_DPAD_DOWN);
716         assertEquals(0 /* 24 */, mTimePicker.getHour());
717         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_DPAD_UP);
718         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_DPAD_UP);
719         assertEquals(22, mTimePicker.getHour());
720 
721         // Minute input testing.
722         assertEquals(initialMinute, mTimePicker.getMinute());
723         verifyModeSpinnerMinuteInput();
724     }
725 
verifyModeClockMinuteInput()726     private void verifyModeClockMinuteInput() {
727         assertTrue(mTimePicker.getMinuteView().hasFocus());
728         // Send a invalid minute.
729         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_6);
730         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_7);
731         // Sent 6 and 7 but only 6 was valid.
732         assertEquals(6, mTimePicker.getMinute());
733         // No matter what other invalid values we send, the minute is unchanged and the focus is
734         // kept.
735         // 61 invalid.
736         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_1);
737         assertTrue(mTimePicker.getMinuteView().hasFocus());
738         // 62 invalid.
739         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_2);
740         assertTrue(mTimePicker.getMinuteView().hasFocus());
741         // 63 invalid.
742         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_3);
743         assertTrue(mTimePicker.getMinuteView().hasFocus());
744         assertEquals(6, mTimePicker.getMinute());
745         // Refocus.
746         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_TAB);
747         CtsKeyEventUtil.sendKeyWhileHoldingModifier(mInstrumentation, mTimePicker,
748                 KeyEvent.KEYCODE_TAB, KeyEvent.KEYCODE_SHIFT_LEFT);
749         assertTrue(mTimePicker.getMinuteView().hasFocus());
750 
751         // In the end pass a valid minute.
752         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_5);
753         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_9);
754         assertEquals(59, mTimePicker.getMinute());
755     }
756 
verifyModeSpinnerMinuteInput()757     private void verifyModeSpinnerMinuteInput() throws Throwable {
758         mActivityRule.runOnUiThread(() -> mTimePicker.getMinuteView().requestFocus());
759         mInstrumentation.waitForIdleSync();
760         assertTrue(mTimePicker.getMinuteView().hasFocus());
761 
762         // Input invalid minute.
763         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_6);
764         // None of the keys below should be accepted after 6 was pressed.
765         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_3);
766         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_4);
767         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_5);
768         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_TAB);
769         // Only 6 is accepted (as the only valid minute value that starts with 6 is 6 itself).
770         assertEquals(6, mTimePicker.getMinute());
771 
772         // Go back to minute view and input valid minute.
773         CtsKeyEventUtil.sendKeyWhileHoldingModifier(mInstrumentation, mTimePicker,
774                 KeyEvent.KEYCODE_TAB, KeyEvent.KEYCODE_SHIFT_LEFT);
775         assertTrue(mTimePicker.getMinuteView().hasFocus());
776         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_4);
777         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_8);
778         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_TAB);
779         assertEquals(48, mTimePicker.getMinute());
780 
781         // Go back to minute view and exercise UP and DOWN keys.
782         CtsKeyEventUtil.sendKeyWhileHoldingModifier(mInstrumentation, mTimePicker,
783                 KeyEvent.KEYCODE_TAB, KeyEvent.KEYCODE_SHIFT_LEFT);
784         assertTrue(mTimePicker.getMinuteView().hasFocus());
785         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_DPAD_DOWN);
786         assertEquals(49, mTimePicker.getMinute());
787         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_DPAD_UP);
788         CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, mTimePicker, KeyEvent.KEYCODE_DPAD_UP);
789         assertEquals(47, mTimePicker.getMinute());
790     }
791 
prepareForKeyboardInput(int initialHour, int initialMinute, boolean is24hFormat, boolean isClockMode)792     private void prepareForKeyboardInput(int initialHour, int initialMinute, boolean is24hFormat,
793             boolean isClockMode) throws Throwable {
794         mTimePicker = isClockMode
795                 ? (TimePicker) mActivity.findViewById(R.id.timepicker_clock)
796                 : (TimePicker) mActivity.findViewById(R.id.timepicker_spinner);
797 
798         mActivityRule.runOnUiThread(() -> {
799             /* hide one of the widgets to assure they fit onto the screen */
800             if (isClockMode) {
801                 mActivity.findViewById(R.id.timepicker_spinner).setVisibility(View.GONE);
802             } else {
803                 mActivity.findViewById(R.id.timepicker_clock).setVisibility(View.GONE);
804             }
805             mTimePicker.setIs24HourView(is24hFormat);
806             mTimePicker.setHour(initialHour);
807             mTimePicker.setMinute(initialMinute);
808             mTimePicker.requestFocus();
809         });
810         mInstrumentation.waitForIdleSync();
811     }
812 
verifyTimePickerKeyboardTraversal(boolean goForward, boolean is24HourView)813     private void verifyTimePickerKeyboardTraversal(boolean goForward, boolean is24HourView)
814             throws Throwable {
815         ArrayList<View> forwardViews = new ArrayList<>();
816         String summary = (goForward ? " forward " : " backward ")
817                 + "traversal, is24HourView=" + is24HourView;
818         assertNotNull("Unexpected NULL hour view for" + summary, mTimePicker.getHourView());
819         forwardViews.add(mTimePicker.getHourView());
820         assertNotNull("Unexpected NULL minute view for" + summary, mTimePicker.getMinuteView());
821         forwardViews.add(mTimePicker.getMinuteView());
822         if (!is24HourView) {
823             assertNotNull("Unexpected NULL AM view for" + summary, mTimePicker.getAmView());
824             forwardViews.add(mTimePicker.getAmView());
825             assertNotNull("Unexpected NULL PM view for" + summary, mTimePicker.getPmView());
826             forwardViews.add(mTimePicker.getPmView());
827         }
828 
829         if (!goForward) {
830             Collections.reverse(forwardViews);
831         }
832 
833         final int viewsSize = forwardViews.size();
834         for (int i = 0; i < viewsSize; i++) {
835             final View currentView = forwardViews.get(i);
836             String afterKeyCodeFormattedString = "";
837             int goForwardKeyCode = KeyEvent.KEYCODE_TAB;
838             int modifierKeyCodeToHold = KeyEvent.KEYCODE_SHIFT_LEFT;
839 
840             if (i == 0) {
841                 // Make sure we always start by focusing the 1st element in the list.
842                 mActivityRule.runOnUiThread(currentView::requestFocus);
843             } else {
844                 if (goForward) {
845                     afterKeyCodeFormattedString = " after pressing="
846                             + KeyEvent.keyCodeToString(goForwardKeyCode);
847                 } else {
848                     afterKeyCodeFormattedString = " after pressing="
849                             + KeyEvent.keyCodeToString(modifierKeyCodeToHold)
850                             + "+" + KeyEvent.keyCodeToString(goForwardKeyCode)  + " for" + summary;
851                 }
852             }
853 
854             assertTrue("View='" + currentView + "'" + " with index " + i + " is not enabled"
855                     + afterKeyCodeFormattedString + " for" + summary, currentView.isEnabled());
856             assertTrue("View='" + currentView + "'" + " with index " + i + " is not focused"
857                     + afterKeyCodeFormattedString + " for" + summary, currentView.isFocused());
858 
859             if (i < viewsSize - 1) {
860                 if (goForward) {
861                     CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, currentView, goForwardKeyCode);
862                 } else {
863                     CtsKeyEventUtil.sendKeyWhileHoldingModifier(mInstrumentation, currentView,
864                             goForwardKeyCode, modifierKeyCodeToHold);
865                 }
866             }
867         }
868     }
869 
870     private class MyTimePicker extends TimePicker {
MyTimePicker(Context context)871         public MyTimePicker(Context context) {
872             super(context);
873         }
874 
875         @Override
onRestoreInstanceState(Parcelable state)876         protected void onRestoreInstanceState(Parcelable state) {
877             super.onRestoreInstanceState(state);
878         }
879 
880         @Override
onSaveInstanceState()881         protected Parcelable onSaveInstanceState() {
882             return super.onSaveInstanceState();
883         }
884     }
885 }
886