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