| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.leanback.widget.picker; |
| |
| import static org.hamcrest.CoreMatchers.is; |
| import static org.hamcrest.MatcherAssert.assertThat; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertTrue; |
| |
| import android.content.Context; |
| import android.content.Intent; |
| import android.support.test.InstrumentationRegistry; |
| import android.support.test.filters.LargeTest; |
| import android.support.test.rule.ActivityTestRule; |
| import android.support.test.runner.AndroidJUnit4; |
| import android.util.Log; |
| import android.view.KeyEvent; |
| import android.view.View; |
| import android.view.ViewGroup; |
| |
| import androidx.leanback.test.R; |
| |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import java.util.Arrays; |
| import java.util.List; |
| |
| @LargeTest |
| @RunWith(AndroidJUnit4.class) |
| public class DatePickerTest { |
| |
| private static final String TAG = "DatePickerTest"; |
| private static final long TRANSITION_LENGTH = 1000; |
| |
| Context mContext; |
| View mViewAbove; |
| DatePicker mDatePickerView; |
| ViewGroup mDatePickerInnerView; |
| View mViewBelow; |
| |
| @Rule |
| public ActivityTestRule<DatePickerActivity> mActivityTestRule = |
| new ActivityTestRule<>(DatePickerActivity.class, false, false); |
| private DatePickerActivity mActivity; |
| |
| @Before |
| public void setUp() throws Exception { |
| mContext = InstrumentationRegistry.getTargetContext(); |
| } |
| |
| public void initActivity(Intent intent) throws Throwable { |
| mActivity = mActivityTestRule.launchActivity(intent); |
| mDatePickerView = (DatePicker) mActivity.findViewById(R.id.date_picker); |
| mDatePickerInnerView = (ViewGroup) mDatePickerView.findViewById(R.id.picker); |
| mDatePickerView.setActivatedVisibleItemCount(3); |
| mDatePickerView.setOnClickListener(new View.OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| mDatePickerView.setActivated(!mDatePickerView.isActivated()); |
| } |
| }); |
| if (intent.getIntExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID, |
| R.layout.datepicker_with_other_widgets) == R.layout.datepicker_with_other_widgets) { |
| mViewAbove = mActivity.findViewById(R.id.above_picker); |
| mViewBelow = mActivity.findViewById(R.id.below_picker); |
| } else if (intent.getIntExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID, |
| R.layout.datepicker_with_other_widgets) == R.layout.datepicker_alone) { |
| // A layout with only a DatePicker widget that is initially activated. |
| mActivityTestRule.runOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| mDatePickerView.setActivated(true); |
| } |
| }); |
| Thread.sleep(500); |
| } |
| } |
| |
| @Test |
| public void testFocusTravel() throws Throwable { |
| Intent intent = new Intent(); |
| intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID, |
| R.layout.datepicker_with_other_widgets); |
| initActivity(intent); |
| |
| assertThat("TextView above should have focus initially", mViewAbove.hasFocus(), is(true)); |
| |
| sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); |
| Thread.sleep(TRANSITION_LENGTH); |
| assertThat("DatePicker should have focus now", mDatePickerView.isFocused(), is(true)); |
| |
| sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); |
| Thread.sleep(TRANSITION_LENGTH); |
| assertThat("The first column of DatePicker should hold focus", |
| mDatePickerInnerView.getChildAt(0).hasFocus(), is(true)); |
| |
| // skipping the separator in the child indices |
| sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT); |
| Thread.sleep(TRANSITION_LENGTH); |
| assertThat("The second column of DatePicker should hold focus", |
| mDatePickerInnerView.getChildAt(2).hasFocus(), is(true)); |
| |
| sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT); |
| Thread.sleep(TRANSITION_LENGTH); |
| assertThat("The third column of DatePicker should hold focus", |
| mDatePickerInnerView.getChildAt(4).hasFocus(), is(true)); |
| } |
| |
| @Test |
| public void testFocusRetainedForASelectedColumn() |
| throws Throwable { |
| Intent intent = new Intent(); |
| intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID, |
| R.layout.datepicker_with_other_widgets); |
| initActivity(intent); |
| mDatePickerView.setFocusable(true); |
| sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); |
| Thread.sleep(TRANSITION_LENGTH); |
| |
| assertThat("DatePicker should have focus when it's focusable", |
| mDatePickerView.isFocused(), is(true)); |
| |
| |
| sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); |
| Thread.sleep(TRANSITION_LENGTH); |
| assertThat("After the first activation, the first column of DatePicker should hold focus", |
| mDatePickerInnerView.getChildAt(0).hasFocus(), is(true)); |
| |
| sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT); |
| Thread.sleep(TRANSITION_LENGTH); |
| |
| sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT); |
| Thread.sleep(TRANSITION_LENGTH); |
| assertThat("The third column of DatePicker should hold focus", |
| mDatePickerInnerView.getChildAt(4).hasFocus(), is(true)); |
| |
| sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); |
| Thread.sleep(TRANSITION_LENGTH); |
| assertThat("After the first deactivation, the DatePicker itself should hold focus", |
| mDatePickerView.isFocused(), is(true)); |
| |
| sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); |
| Thread.sleep(TRANSITION_LENGTH); |
| assertThat("After the second activation, the last selected column (3rd) should hold focus", |
| mDatePickerInnerView.getChildAt(4).hasFocus(), is(true)); |
| } |
| |
| @Test |
| public void testFocusSkippedWhenDatePickerUnFocusable() |
| throws Throwable { |
| Intent intent = new Intent(); |
| intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID, |
| R.layout.datepicker_with_other_widgets); |
| initActivity(intent); |
| |
| mDatePickerView.setFocusable(false); |
| assertThat("TextView above should have focus initially.", mViewAbove.hasFocus(), is(true)); |
| |
| sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); |
| Thread.sleep(TRANSITION_LENGTH); |
| |
| assertThat("DatePicker should be skipped and TextView below should have focus.", |
| mViewBelow.hasFocus(), is(true)); |
| } |
| |
| @Test |
| public void testTemporaryFocusLossWhenDeactivated() |
| throws Throwable { |
| Intent intent = new Intent(); |
| intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID, |
| R.layout.datepicker_with_other_widgets); |
| initActivity(intent); |
| |
| final int[] currentFocusChangeCountForViewAbove = {0}; |
| mDatePickerView.setFocusable(true); |
| Log.d(TAG, "view above: " + mViewAbove); |
| mViewAbove.setOnFocusChangeListener(new View.OnFocusChangeListener(){ |
| @Override |
| public void onFocusChange(View v, boolean hasFocus) { |
| currentFocusChangeCountForViewAbove[0]++; |
| } |
| }); |
| assertThat("TextView above should have focus initially.", mViewAbove.hasFocus(), is(true)); |
| |
| // Traverse to the third column of date picker |
| sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); |
| Thread.sleep(TRANSITION_LENGTH); |
| // Click once to activate |
| sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); |
| Thread.sleep(TRANSITION_LENGTH); |
| // Traverse to the third column |
| sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT); |
| Thread.sleep(TRANSITION_LENGTH); |
| sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT); |
| Thread.sleep(TRANSITION_LENGTH); |
| // Click to deactivate. Before that we remember the focus change count for the view above. |
| // This view should NOT receive temporary focus when DatePicker is deactivated, and |
| // DatePicker itself should capture the focus. |
| int[] lastFocusChangeCountForViewAbove = {currentFocusChangeCountForViewAbove[0]}; |
| sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); |
| Thread.sleep(TRANSITION_LENGTH); |
| assertThat("DatePicker should have focus now since it's focusable", |
| mDatePickerView.isFocused(), is(true)); |
| assertThat("Focus change count of view above should not be changed after last click.", |
| currentFocusChangeCountForViewAbove[0], is(lastFocusChangeCountForViewAbove[0])); |
| } |
| |
| @Test |
| public void testTemporaryFocusLossWhenActivated() throws Throwable { |
| Intent intent = new Intent(); |
| intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID, |
| R.layout.datepicker_with_other_widgets); |
| initActivity(intent); |
| final int[] currentFocusChangeCountForColumns = {0, 0, 0}; |
| mDatePickerView.setFocusable(true); |
| mDatePickerInnerView.getChildAt(0).setOnFocusChangeListener( |
| new View.OnFocusChangeListener() { |
| @Override |
| public void onFocusChange(View v, boolean hasFocus) { |
| currentFocusChangeCountForColumns[0]++; |
| } |
| }); |
| |
| mDatePickerInnerView.getChildAt(2).setOnFocusChangeListener( |
| new View.OnFocusChangeListener() { |
| @Override |
| public void onFocusChange(View v, boolean hasFocus) { |
| currentFocusChangeCountForColumns[1]++; |
| } |
| }); |
| |
| mDatePickerInnerView.getChildAt(4).setOnFocusChangeListener( |
| new View.OnFocusChangeListener() { |
| @Override |
| public void onFocusChange(View v, boolean hasFocus) { |
| currentFocusChangeCountForColumns[2]++; |
| } |
| }); |
| |
| // Traverse to the third column of date picker |
| sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); |
| Thread.sleep(TRANSITION_LENGTH); |
| // Click once to activate |
| sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); |
| Thread.sleep(TRANSITION_LENGTH); |
| // Traverse to the third column |
| sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT); |
| Thread.sleep(TRANSITION_LENGTH); |
| sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT); |
| Thread.sleep(TRANSITION_LENGTH); |
| // Click to deactivate |
| sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); |
| Thread.sleep(TRANSITION_LENGTH); |
| // Click again. The focus should NOT be temporarily moved to the other columns and the third |
| // column should receive focus. |
| // Before that we will remember the last focus change count to compare it against after the |
| // click. |
| int[] lastFocusChangeCountForColumns = {currentFocusChangeCountForColumns[0], |
| currentFocusChangeCountForColumns[1], currentFocusChangeCountForColumns[2]}; |
| sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); |
| Thread.sleep(TRANSITION_LENGTH); |
| assertThat("Focus change count of column 0 should not be changed after last click.", |
| currentFocusChangeCountForColumns[0], is(lastFocusChangeCountForColumns[0])); |
| assertThat("Focus change count of column 1 should not be changed after last click.", |
| currentFocusChangeCountForColumns[1], is(lastFocusChangeCountForColumns[1])); |
| assertThat("Focus change count of column 2 should not be changed after last click.", |
| currentFocusChangeCountForColumns[2], is(lastFocusChangeCountForColumns[2])); |
| } |
| |
| @Test |
| public void testInitiallyActiveDatePicker() |
| throws Throwable { |
| Intent intent = new Intent(); |
| intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID, |
| R.layout.datepicker_alone); |
| initActivity(intent); |
| |
| assertThat("The first column of DatePicker should initially hold focus", |
| mDatePickerInnerView.getChildAt(0).hasFocus(), is(true)); |
| |
| // focus on first column |
| sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); |
| Thread.sleep(TRANSITION_LENGTH); |
| assertThat("The first column of DatePicker should still hold focus after scrolling down", |
| mDatePickerInnerView.getChildAt(0).hasFocus(), is(true)); |
| |
| // focus on second column |
| sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT); |
| Thread.sleep(TRANSITION_LENGTH); |
| assertThat("The second column of DatePicker should hold focus after scrolling right", |
| mDatePickerInnerView.getChildAt(2).hasFocus(), is(true)); |
| |
| sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); |
| Thread.sleep(TRANSITION_LENGTH); |
| assertThat("The second column of DatePicker should still hold focus after scrolling down", |
| mDatePickerInnerView.getChildAt(2).hasFocus(), is(true)); |
| |
| // focus on third column |
| sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT); |
| Thread.sleep(TRANSITION_LENGTH); |
| assertThat("The third column of DatePicker should hold focus after scrolling right", |
| mDatePickerInnerView.getChildAt(4).hasFocus(), is(true)); |
| |
| sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); |
| Thread.sleep(TRANSITION_LENGTH); |
| assertThat("The third column of DatePicker should still hold focus after scrolling down", |
| mDatePickerInnerView.getChildAt(4).hasFocus(), is(true)); |
| } |
| |
| @Test |
| public void testInvisibleColumnsAlpha() throws Throwable { |
| Intent intent = new Intent(); |
| intent.putExtra(DatePickerActivity.EXTRA_LAYOUT_RESOURCE_ID, |
| R.layout.datepicker_with_other_widgets); |
| initActivity(intent); |
| |
| mActivityTestRule.runOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| mDatePickerView.updateDate(2017, 2, 21, false); |
| } |
| }); |
| |
| Thread.sleep(TRANSITION_LENGTH); |
| mActivityTestRule.runOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| mDatePickerView.updateDate(2017, 2, 20, false); |
| } |
| }); |
| Thread.sleep(TRANSITION_LENGTH); |
| |
| sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); |
| Thread.sleep(TRANSITION_LENGTH); |
| // Click once to activate |
| sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); |
| Thread.sleep(TRANSITION_LENGTH); |
| |
| int activeColumn = 0; |
| // For the inactive columns: the alpha for all the rows except the selected row should be |
| // zero: Picker#mInvisibleColumnAlpha (they should all be invisible). |
| for (int i = 0; i < 3; i++) { |
| ViewGroup gridView = (ViewGroup) mDatePickerInnerView.getChildAt(2 * i); |
| int childCount = gridView.getChildCount(); |
| int alpha1RowsCount = 0; |
| int alphaNonZeroRowsCount = 0; |
| for (int j = 0; j < childCount; j++) { |
| View pickerItem = gridView.getChildAt(j); |
| if (pickerItem.getAlpha() > 0) { |
| alphaNonZeroRowsCount++; |
| } |
| if (pickerItem.getAlpha() == 1) { |
| alpha1RowsCount++; |
| } |
| } |
| if (i == activeColumn) { |
| assertThat("The active column " + i + " should have only one row with an alpha of " |
| + "1", alpha1RowsCount, is(1)); |
| assertTrue("The active column " + i + " should have more than one view with alpha " |
| + "greater than 1", alphaNonZeroRowsCount > 1); |
| } else { |
| assertThat("The inactive column " + i + " should have only one row with an alpha of" |
| + " 1", alpha1RowsCount, is(1)); |
| assertThat("The inactive column " + i + " should have only one row with a non-zero" |
| + " alpha", alphaNonZeroRowsCount, is(1)); |
| } |
| } |
| } |
| |
| @Test |
| public void testExtractSeparatorsForDifferentLocales() throws Throwable { |
| // date pattern for en_US (English) |
| DatePicker datePicker = new DatePicker(mContext, null) { |
| @Override |
| String getBestYearMonthDayPattern(String datePickerFormat) { |
| return "M/d/y"; |
| } |
| }; |
| List<CharSequence> actualSeparators = datePicker.extractSeparators(); |
| List<String> expectedSeparators = Arrays.asList(new String[]{"", "/", "/", ""}); |
| assertEquals(expectedSeparators, actualSeparators); |
| |
| // date pattern for fa_IR (Farsi) |
| datePicker = new DatePicker(mContext, null) { |
| @Override |
| String getBestYearMonthDayPattern(String datePickerFormat) { |
| return "y/M/d"; |
| } |
| }; |
| actualSeparators = datePicker.extractSeparators(); |
| expectedSeparators = Arrays.asList(new String[]{"", "/", "/", ""}); |
| assertEquals(expectedSeparators, actualSeparators); |
| |
| // date pattern for ar_EG (Arabic) |
| datePicker = new DatePicker(mContext, null) { |
| @Override |
| String getBestYearMonthDayPattern(String datePickerFormat) { |
| return "d/M/y"; |
| } |
| }; |
| actualSeparators = datePicker.extractSeparators(); |
| expectedSeparators = Arrays.asList(new String[]{"", "/", "/", ""}); |
| assertEquals(expectedSeparators, actualSeparators); |
| |
| // date pattern for cs_CZ (Czech) |
| datePicker = new DatePicker(mContext, null) { |
| @Override |
| String getBestYearMonthDayPattern(String datePickerFormat) { |
| return "d. M. y"; |
| } |
| }; |
| actualSeparators = datePicker.extractSeparators(); |
| expectedSeparators = Arrays.asList(new String[]{"", ".", ".", ""}); |
| assertEquals(expectedSeparators, actualSeparators); |
| |
| // date pattern for hr_HR (Croatian) |
| datePicker = new DatePicker(mContext, null) { |
| @Override |
| String getBestYearMonthDayPattern(String datePickerFormat) { |
| return "dd. MM. y."; |
| } |
| }; |
| actualSeparators = datePicker.extractSeparators(); |
| expectedSeparators = Arrays.asList(new String[]{"", ".", ".", "."}); |
| assertEquals(expectedSeparators, actualSeparators); |
| |
| // date pattern for hr_HR (Bulgarian) |
| datePicker = new DatePicker(mContext, null) { |
| @Override |
| String getBestYearMonthDayPattern(String datePickerFormat) { |
| return "d.MM.y 'r'."; |
| } |
| }; |
| actualSeparators = datePicker.extractSeparators(); |
| expectedSeparators = Arrays.asList(new String[]{"", ".", ".", "r."}); |
| assertEquals(expectedSeparators, actualSeparators); |
| |
| // date pattern for en_XA (English pseudo-locale) |
| datePicker = new DatePicker(mContext, null) { |
| @Override |
| String getBestYearMonthDayPattern(String datePickerFormat) { |
| return "[M/d/y]"; |
| } |
| }; |
| actualSeparators = datePicker.extractSeparators(); |
| expectedSeparators = Arrays.asList(new String[]{"[", "/", "/", "]"}); |
| assertEquals(expectedSeparators, actualSeparators); |
| } |
| |
| private void sendKeys(int ...keys) { |
| for (int i = 0; i < keys.length; i++) { |
| InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]); |
| } |
| } |
| } |