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