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