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