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