1 /* 2 * Copyright (C) 2017 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 androidx.leanback.widget.picker; 18 19 import static org.hamcrest.CoreMatchers.is; 20 import static org.hamcrest.MatcherAssert.assertThat; 21 import static org.junit.Assert.assertEquals; 22 import static org.junit.Assert.assertTrue; 23 24 import android.content.Context; 25 import android.content.Intent; 26 import android.support.test.InstrumentationRegistry; 27 import android.support.test.filters.LargeTest; 28 import android.support.test.rule.ActivityTestRule; 29 import android.support.test.runner.AndroidJUnit4; 30 import android.util.Log; 31 import android.view.KeyEvent; 32 import android.view.View; 33 import android.view.ViewGroup; 34 35 import androidx.leanback.test.R; 36 37 import org.junit.Before; 38 import org.junit.Rule; 39 import org.junit.Test; 40 import org.junit.runner.RunWith; 41 42 import java.util.Arrays; 43 import java.util.List; 44 45 @LargeTest 46 @RunWith(AndroidJUnit4.class) 47 public class DatePickerTest { 48 49 private static final String TAG = "DatePickerTest"; 50 private static final long TRANSITION_LENGTH = 1000; 51 52 Context mContext; 53 View mViewAbove; 54 DatePicker mDatePickerView; 55 ViewGroup mDatePickerInnerView; 56 View mViewBelow; 57 58 @Rule 59 public ActivityTestRule<DatePickerActivity> mActivityTestRule = 60 new ActivityTestRule<>(DatePickerActivity.class, false, false); 61 private DatePickerActivity mActivity; 62 63 @Before setUp()64 public void setUp() throws Exception { 65 mContext = InstrumentationRegistry.getTargetContext(); 66 } 67 initActivity(Intent intent)68 public void initActivity(Intent intent) throws Throwable { 69 mActivity = mActivityTestRule.launchActivity(intent); 70 mDatePickerView = (DatePicker) mActivity.findViewById(R.id.date_picker); 71 mDatePickerInnerView = (ViewGroup) mDatePickerView.findViewById(R.id.picker); 72 mDatePickerView.setActivatedVisibleItemCount(3); 73 mDatePickerView.setOnClickListener(new View.OnClickListener() { 74 @Override 75 public void onClick(View v) { 76 mDatePickerView.setActivated(!mDatePickerView.isActivated()); 77 } 78 }); 79 if (intent.getIntExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID, 80 R.layout.datepicker_with_other_widgets) == R.layout.datepicker_with_other_widgets) { 81 mViewAbove = mActivity.findViewById(R.id.above_picker); 82 mViewBelow = mActivity.findViewById(R.id.below_picker); 83 } else if (intent.getIntExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID, 84 R.layout.datepicker_with_other_widgets) == R.layout.datepicker_alone) { 85 // A layout with only a DatePicker widget that is initially activated. 86 mActivityTestRule.runOnUiThread(new Runnable() { 87 @Override 88 public void run() { 89 mDatePickerView.setActivated(true); 90 } 91 }); 92 Thread.sleep(500); 93 } 94 } 95 96 @Test testFocusTravel()97 public void testFocusTravel() throws Throwable { 98 Intent intent = new Intent(); 99 intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID, 100 R.layout.datepicker_with_other_widgets); 101 initActivity(intent); 102 103 assertThat("TextView above should have focus initially", mViewAbove.hasFocus(), is(true)); 104 105 sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); 106 Thread.sleep(TRANSITION_LENGTH); 107 assertThat("DatePicker should have focus now", mDatePickerView.isFocused(), is(true)); 108 109 sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); 110 Thread.sleep(TRANSITION_LENGTH); 111 assertThat("The first column of DatePicker should hold focus", 112 mDatePickerInnerView.getChildAt(0).hasFocus(), is(true)); 113 114 // skipping the separator in the child indices 115 sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT); 116 Thread.sleep(TRANSITION_LENGTH); 117 assertThat("The second column of DatePicker should hold focus", 118 mDatePickerInnerView.getChildAt(2).hasFocus(), is(true)); 119 120 sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT); 121 Thread.sleep(TRANSITION_LENGTH); 122 assertThat("The third column of DatePicker should hold focus", 123 mDatePickerInnerView.getChildAt(4).hasFocus(), is(true)); 124 } 125 126 @Test testFocusRetainedForASelectedColumn()127 public void testFocusRetainedForASelectedColumn() 128 throws Throwable { 129 Intent intent = new Intent(); 130 intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID, 131 R.layout.datepicker_with_other_widgets); 132 initActivity(intent); 133 mDatePickerView.setFocusable(true); 134 sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); 135 Thread.sleep(TRANSITION_LENGTH); 136 137 assertThat("DatePicker should have focus when it's focusable", 138 mDatePickerView.isFocused(), is(true)); 139 140 141 sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); 142 Thread.sleep(TRANSITION_LENGTH); 143 assertThat("After the first activation, the first column of DatePicker should hold focus", 144 mDatePickerInnerView.getChildAt(0).hasFocus(), is(true)); 145 146 sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT); 147 Thread.sleep(TRANSITION_LENGTH); 148 149 sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT); 150 Thread.sleep(TRANSITION_LENGTH); 151 assertThat("The third column of DatePicker should hold focus", 152 mDatePickerInnerView.getChildAt(4).hasFocus(), is(true)); 153 154 sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); 155 Thread.sleep(TRANSITION_LENGTH); 156 assertThat("After the first deactivation, the DatePicker itself should hold focus", 157 mDatePickerView.isFocused(), is(true)); 158 159 sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); 160 Thread.sleep(TRANSITION_LENGTH); 161 assertThat("After the second activation, the last selected column (3rd) should hold focus", 162 mDatePickerInnerView.getChildAt(4).hasFocus(), is(true)); 163 } 164 165 @Test testFocusSkippedWhenDatePickerUnFocusable()166 public void testFocusSkippedWhenDatePickerUnFocusable() 167 throws Throwable { 168 Intent intent = new Intent(); 169 intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID, 170 R.layout.datepicker_with_other_widgets); 171 initActivity(intent); 172 173 mDatePickerView.setFocusable(false); 174 assertThat("TextView above should have focus initially.", mViewAbove.hasFocus(), is(true)); 175 176 sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); 177 Thread.sleep(TRANSITION_LENGTH); 178 179 assertThat("DatePicker should be skipped and TextView below should have focus.", 180 mViewBelow.hasFocus(), is(true)); 181 } 182 183 @Test testTemporaryFocusLossWhenDeactivated()184 public void testTemporaryFocusLossWhenDeactivated() 185 throws Throwable { 186 Intent intent = new Intent(); 187 intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID, 188 R.layout.datepicker_with_other_widgets); 189 initActivity(intent); 190 191 final int[] currentFocusChangeCountForViewAbove = {0}; 192 mDatePickerView.setFocusable(true); 193 Log.d(TAG, "view above: " + mViewAbove); 194 mViewAbove.setOnFocusChangeListener(new View.OnFocusChangeListener(){ 195 @Override 196 public void onFocusChange(View v, boolean hasFocus) { 197 currentFocusChangeCountForViewAbove[0]++; 198 } 199 }); 200 assertThat("TextView above should have focus initially.", mViewAbove.hasFocus(), is(true)); 201 202 // Traverse to the third column of date picker 203 sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); 204 Thread.sleep(TRANSITION_LENGTH); 205 // Click once to activate 206 sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); 207 Thread.sleep(TRANSITION_LENGTH); 208 // Traverse to the third column 209 sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT); 210 Thread.sleep(TRANSITION_LENGTH); 211 sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT); 212 Thread.sleep(TRANSITION_LENGTH); 213 // Click to deactivate. Before that we remember the focus change count for the view above. 214 // This view should NOT receive temporary focus when DatePicker is deactivated, and 215 // DatePicker itself should capture the focus. 216 int[] lastFocusChangeCountForViewAbove = {currentFocusChangeCountForViewAbove[0]}; 217 sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); 218 Thread.sleep(TRANSITION_LENGTH); 219 assertThat("DatePicker should have focus now since it's focusable", 220 mDatePickerView.isFocused(), is(true)); 221 assertThat("Focus change count of view above should not be changed after last click.", 222 currentFocusChangeCountForViewAbove[0], is(lastFocusChangeCountForViewAbove[0])); 223 } 224 225 @Test testTemporaryFocusLossWhenActivated()226 public void testTemporaryFocusLossWhenActivated() throws Throwable { 227 Intent intent = new Intent(); 228 intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID, 229 R.layout.datepicker_with_other_widgets); 230 initActivity(intent); 231 final int[] currentFocusChangeCountForColumns = {0, 0, 0}; 232 mDatePickerView.setFocusable(true); 233 mDatePickerInnerView.getChildAt(0).setOnFocusChangeListener( 234 new View.OnFocusChangeListener() { 235 @Override 236 public void onFocusChange(View v, boolean hasFocus) { 237 currentFocusChangeCountForColumns[0]++; 238 } 239 }); 240 241 mDatePickerInnerView.getChildAt(2).setOnFocusChangeListener( 242 new View.OnFocusChangeListener() { 243 @Override 244 public void onFocusChange(View v, boolean hasFocus) { 245 currentFocusChangeCountForColumns[1]++; 246 } 247 }); 248 249 mDatePickerInnerView.getChildAt(4).setOnFocusChangeListener( 250 new View.OnFocusChangeListener() { 251 @Override 252 public void onFocusChange(View v, boolean hasFocus) { 253 currentFocusChangeCountForColumns[2]++; 254 } 255 }); 256 257 // Traverse to the third column of date picker 258 sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); 259 Thread.sleep(TRANSITION_LENGTH); 260 // Click once to activate 261 sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); 262 Thread.sleep(TRANSITION_LENGTH); 263 // Traverse to the third column 264 sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT); 265 Thread.sleep(TRANSITION_LENGTH); 266 sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT); 267 Thread.sleep(TRANSITION_LENGTH); 268 // Click to deactivate 269 sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); 270 Thread.sleep(TRANSITION_LENGTH); 271 // Click again. The focus should NOT be temporarily moved to the other columns and the third 272 // column should receive focus. 273 // Before that we will remember the last focus change count to compare it against after the 274 // click. 275 int[] lastFocusChangeCountForColumns = {currentFocusChangeCountForColumns[0], 276 currentFocusChangeCountForColumns[1], currentFocusChangeCountForColumns[2]}; 277 sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); 278 Thread.sleep(TRANSITION_LENGTH); 279 assertThat("Focus change count of column 0 should not be changed after last click.", 280 currentFocusChangeCountForColumns[0], is(lastFocusChangeCountForColumns[0])); 281 assertThat("Focus change count of column 1 should not be changed after last click.", 282 currentFocusChangeCountForColumns[1], is(lastFocusChangeCountForColumns[1])); 283 assertThat("Focus change count of column 2 should not be changed after last click.", 284 currentFocusChangeCountForColumns[2], is(lastFocusChangeCountForColumns[2])); 285 } 286 287 @Test testInitiallyActiveDatePicker()288 public void testInitiallyActiveDatePicker() 289 throws Throwable { 290 Intent intent = new Intent(); 291 intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID, 292 R.layout.datepicker_alone); 293 initActivity(intent); 294 295 assertThat("The first column of DatePicker should initially hold focus", 296 mDatePickerInnerView.getChildAt(0).hasFocus(), is(true)); 297 298 // focus on first column 299 sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); 300 Thread.sleep(TRANSITION_LENGTH); 301 assertThat("The first column of DatePicker should still hold focus after scrolling down", 302 mDatePickerInnerView.getChildAt(0).hasFocus(), is(true)); 303 304 // focus on second column 305 sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT); 306 Thread.sleep(TRANSITION_LENGTH); 307 assertThat("The second column of DatePicker should hold focus after scrolling right", 308 mDatePickerInnerView.getChildAt(2).hasFocus(), is(true)); 309 310 sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); 311 Thread.sleep(TRANSITION_LENGTH); 312 assertThat("The second column of DatePicker should still hold focus after scrolling down", 313 mDatePickerInnerView.getChildAt(2).hasFocus(), is(true)); 314 315 // focus on third column 316 sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT); 317 Thread.sleep(TRANSITION_LENGTH); 318 assertThat("The third column of DatePicker should hold focus after scrolling right", 319 mDatePickerInnerView.getChildAt(4).hasFocus(), is(true)); 320 321 sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); 322 Thread.sleep(TRANSITION_LENGTH); 323 assertThat("The third column of DatePicker should still hold focus after scrolling down", 324 mDatePickerInnerView.getChildAt(4).hasFocus(), is(true)); 325 } 326 327 @Test testInvisibleColumnsAlpha()328 public void testInvisibleColumnsAlpha() throws Throwable { 329 Intent intent = new Intent(); 330 intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID, 331 R.layout.datepicker_with_other_widgets); 332 initActivity(intent); 333 334 mActivityTestRule.runOnUiThread(new Runnable() { 335 @Override 336 public void run() { 337 mDatePickerView.updateDate(2017, 2, 21, false); 338 } 339 }); 340 341 Thread.sleep(TRANSITION_LENGTH); 342 mActivityTestRule.runOnUiThread(new Runnable() { 343 @Override 344 public void run() { 345 mDatePickerView.updateDate(2017, 2, 20, false); 346 } 347 }); 348 Thread.sleep(TRANSITION_LENGTH); 349 350 sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); 351 Thread.sleep(TRANSITION_LENGTH); 352 // Click once to activate 353 sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); 354 Thread.sleep(TRANSITION_LENGTH); 355 356 int activeColumn = 0; 357 // For the inactive columns: the alpha for all the rows except the selected row should be 358 // zero: Picker#mInvisibleColumnAlpha (they should all be invisible). 359 for (int i = 0; i < 3; i++) { 360 ViewGroup gridView = (ViewGroup) mDatePickerInnerView.getChildAt(2 * i); 361 int childCount = gridView.getChildCount(); 362 int alpha1RowsCount = 0; 363 int alphaNonZeroRowsCount = 0; 364 for (int j = 0; j < childCount; j++) { 365 View pickerItem = gridView.getChildAt(j); 366 if (pickerItem.getAlpha() > 0) { 367 alphaNonZeroRowsCount++; 368 } 369 if (pickerItem.getAlpha() == 1) { 370 alpha1RowsCount++; 371 } 372 } 373 if (i == activeColumn) { 374 assertThat("The active column " + i + " should have only one row with an alpha of " 375 + "1", alpha1RowsCount, is(1)); 376 assertTrue("The active column " + i + " should have more than one view with alpha " 377 + "greater than 1", alphaNonZeroRowsCount > 1); 378 } else { 379 assertThat("The inactive column " + i + " should have only one row with an alpha of" 380 + " 1", alpha1RowsCount, is(1)); 381 assertThat("The inactive column " + i + " should have only one row with a non-zero" 382 + " alpha", alphaNonZeroRowsCount, is(1)); 383 } 384 } 385 } 386 387 @Test testExtractSeparatorsForDifferentLocales()388 public void testExtractSeparatorsForDifferentLocales() throws Throwable { 389 // date pattern for en_US (English) 390 DatePicker datePicker = new DatePicker(mContext, null) { 391 @Override 392 String getBestYearMonthDayPattern(String datePickerFormat) { 393 return "M/d/y"; 394 } 395 }; 396 List<CharSequence> actualSeparators = datePicker.extractSeparators(); 397 List<String> expectedSeparators = Arrays.asList(new String[]{"", "/", "/", ""}); 398 assertEquals(expectedSeparators, actualSeparators); 399 400 // date pattern for fa_IR (Farsi) 401 datePicker = new DatePicker(mContext, null) { 402 @Override 403 String getBestYearMonthDayPattern(String datePickerFormat) { 404 return "y/M/d"; 405 } 406 }; 407 actualSeparators = datePicker.extractSeparators(); 408 expectedSeparators = Arrays.asList(new String[]{"", "/", "/", ""}); 409 assertEquals(expectedSeparators, actualSeparators); 410 411 // date pattern for ar_EG (Arabic) 412 datePicker = new DatePicker(mContext, null) { 413 @Override 414 String getBestYearMonthDayPattern(String datePickerFormat) { 415 return "d/M/y"; 416 } 417 }; 418 actualSeparators = datePicker.extractSeparators(); 419 expectedSeparators = Arrays.asList(new String[]{"", "/", "/", ""}); 420 assertEquals(expectedSeparators, actualSeparators); 421 422 // date pattern for cs_CZ (Czech) 423 datePicker = new DatePicker(mContext, null) { 424 @Override 425 String getBestYearMonthDayPattern(String datePickerFormat) { 426 return "d. M. y"; 427 } 428 }; 429 actualSeparators = datePicker.extractSeparators(); 430 expectedSeparators = Arrays.asList(new String[]{"", ".", ".", ""}); 431 assertEquals(expectedSeparators, actualSeparators); 432 433 // date pattern for hr_HR (Croatian) 434 datePicker = new DatePicker(mContext, null) { 435 @Override 436 String getBestYearMonthDayPattern(String datePickerFormat) { 437 return "dd. MM. y."; 438 } 439 }; 440 actualSeparators = datePicker.extractSeparators(); 441 expectedSeparators = Arrays.asList(new String[]{"", ".", ".", "."}); 442 assertEquals(expectedSeparators, actualSeparators); 443 444 // date pattern for hr_HR (Bulgarian) 445 datePicker = new DatePicker(mContext, null) { 446 @Override 447 String getBestYearMonthDayPattern(String datePickerFormat) { 448 return "d.MM.y 'r'."; 449 } 450 }; 451 actualSeparators = datePicker.extractSeparators(); 452 expectedSeparators = Arrays.asList(new String[]{"", ".", ".", "r."}); 453 assertEquals(expectedSeparators, actualSeparators); 454 455 // date pattern for en_XA (English pseudo-locale) 456 datePicker = new DatePicker(mContext, null) { 457 @Override 458 String getBestYearMonthDayPattern(String datePickerFormat) { 459 return "[M/d/y]"; 460 } 461 }; 462 actualSeparators = datePicker.extractSeparators(); 463 expectedSeparators = Arrays.asList(new String[]{"[", "/", "/", "]"}); 464 assertEquals(expectedSeparators, actualSeparators); 465 } 466 sendKeys(int ...keys)467 private void sendKeys(int ...keys) { 468 for (int i = 0; i < keys.length; i++) { 469 InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]); 470 } 471 } 472 } 473