• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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