1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License
10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 * or implied. See the License for the specific language governing permissions and limitations under
12 * the License.
13 */
14
15package androidx.leanback.app.wizard;
16
17import static org.junit.Assert.assertTrue;
18
19import android.content.Context;
20import android.content.Intent;
21import android.content.res.Resources;
22import android.support.test.InstrumentationRegistry;
23import android.support.test.filters.LargeTest;
24import android.support.test.rule.ActivityTestRule;
25import android.support.test.runner.AndroidJUnit4;
26import android.view.KeyEvent;
27
28import androidx.leanback.app.GuidedStepFragment;
29import androidx.leanback.test.R;
30import androidx.leanback.widget.GuidanceStylist;
31import androidx.leanback.widget.GuidedAction;
32
33import org.junit.Before;
34import org.junit.Rule;
35import org.junit.Test;
36import org.junit.runner.RunWith;
37
38import java.util.ArrayList;
39import java.util.Arrays;
40import java.util.Collections;
41import java.util.List;
42
43@LargeTest
44@RunWith(AndroidJUnit4.class)
45public class GuidedStepAttributesTest {
46    static final long TRANSITION_LENGTH = 1000;
47
48    static final String TAG = "GuidedStepAttributesTest";
49
50    @Rule
51    public ActivityTestRule<GuidedStepAttributesTestActivity> activityTestRule =
52            new ActivityTestRule<>(GuidedStepAttributesTestActivity.class, false, false);
53
54    GuidedStepAttributesTestActivity mActivity;
55
56    private void initActivity(Intent intent) {
57        mActivity = activityTestRule.launchActivity(intent);
58        try {
59            Thread.sleep(2000);
60        } catch(InterruptedException e) {
61            e.printStackTrace();
62        }
63    }
64
65    Context mContext;
66    @Before
67    public void setUp() {
68        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();;
69    }
70
71    public static void sendKey(int keyCode) {
72        InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keyCode);
73    }
74
75    @Test
76    public void testFocusDisabledOnActions() throws Throwable {
77
78        Intent intent = new Intent();
79        Resources res = mContext.getResources();
80
81        final int NUM_SEARCH_ACTIONS = 10;
82        final List<Integer> ACTIONS_WITH_DISABLED_FOCUS = new ArrayList<>(
83                Arrays.asList(1, 3, 4, 5, 8));
84        final int ACTION_ID_SEARCH = 1;
85        List<Integer> EXPECTED_ACTIONS_ID_AFTER_EACH_SELECT = new ArrayList<>();
86
87        // we will traverse actions from top to bottom and then back to the top
88        for(int i = 0; i < NUM_SEARCH_ACTIONS; i++) {
89            if (!ACTIONS_WITH_DISABLED_FOCUS.contains(i))
90                EXPECTED_ACTIONS_ID_AFTER_EACH_SELECT.add(i);
91        }
92        for(int i = EXPECTED_ACTIONS_ID_AFTER_EACH_SELECT.size(); i-- != 0;) {
93            EXPECTED_ACTIONS_ID_AFTER_EACH_SELECT.add(EXPECTED_ACTIONS_ID_AFTER_EACH_SELECT.get(i));
94        }
95
96
97        String title = "Guided Actions Focusable Test";
98        String breadcrumb = "Focusable Test Demo";
99        String description = "";
100        GuidanceStylist.Guidance guidance = new GuidanceStylist.Guidance(title, description,
101                breadcrumb, null);
102
103        List<GuidedAction> actionList = new ArrayList<>();
104        for (int i = 0; i < NUM_SEARCH_ACTIONS; i++ ) {
105            actionList.add(new GuidedAction.Builder(mContext)
106                    .id(ACTION_ID_SEARCH)
107                    .title(res.getString(R.string.search) + "" + i)
108                    .description(res.getString(R.string.search_description) + i)
109                    .build()
110            );
111        }
112        for(int action_id : ACTIONS_WITH_DISABLED_FOCUS )
113            actionList.get(action_id).setFocusable(false);
114
115        GuidedStepAttributesTestFragment.GUIDANCE = guidance;
116        GuidedStepAttributesTestFragment.ACTION_LIST = actionList;
117
118        initActivity(intent);
119
120        int lastSelectedActionId = -1;
121        int selectIndex = 0;
122        GuidedStepFragment mFragment = (GuidedStepFragment) mActivity.getGuidedStepTestFragment();
123        int prevSelectedActionPosition = -1;
124        int nextSelectedActionPosition = mFragment.getSelectedActionPosition();
125        while ( nextSelectedActionPosition != prevSelectedActionPosition ) {
126            lastSelectedActionId = mFragment.getSelectedActionPosition();
127            assertTrue(res.getString(R.string.focusable_test_error_message,
128                    actionList.get(lastSelectedActionId).getTitle()),
129                    lastSelectedActionId == EXPECTED_ACTIONS_ID_AFTER_EACH_SELECT.get(selectIndex));
130            selectIndex++;
131            sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
132            prevSelectedActionPosition = nextSelectedActionPosition;
133            nextSelectedActionPosition = mFragment.getSelectedActionPosition();
134            Thread.sleep(TRANSITION_LENGTH);
135        }
136
137        prevSelectedActionPosition = -1;
138        while ( nextSelectedActionPosition != prevSelectedActionPosition ) {
139            lastSelectedActionId = mFragment.getSelectedActionPosition();
140            assertTrue(res.getString(R.string.focusable_test_error_message,
141                    actionList.get(lastSelectedActionId).getTitle()),
142                    lastSelectedActionId == EXPECTED_ACTIONS_ID_AFTER_EACH_SELECT.get(selectIndex));
143            selectIndex++;
144            sendKey(KeyEvent.KEYCODE_DPAD_UP);
145            prevSelectedActionPosition = nextSelectedActionPosition;
146            nextSelectedActionPosition = mFragment.getSelectedActionPosition();
147            Thread.sleep(TRANSITION_LENGTH);
148        }
149
150    }
151
152    // Note: do not remove final or sRevertCallback gets null from 2nd test on!
153     static final GuidedStepAttributesTestFragment.Callback sRevertCallback = new
154            GuidedStepAttributesTestFragment.Callback() {
155        @Override
156        public void onActionClicked(GuidedStepFragment fragment, long id) {
157            List<GuidedAction> allActions = fragment.getActions();
158            for(int i = 1; i < allActions.size(); i++) {
159                GuidedAction action = allActions.get(i);
160                action.setEnabled(!action.isEnabled());
161                fragment.notifyActionChanged(fragment.findActionPositionById(action.getId()));
162            }
163        }
164    };
165
166    /**
167     * Creates a number of enabled and disable actions and tests whether the flag is correctly set
168     * by clicking on each individual action and checking whether the click event is triggered.
169     * @throws Throwable
170     */
171    @Test
172    public void testDisabledActions() throws Throwable {
173
174        Intent intent = new Intent();
175        Resources res = mContext.getResources();
176
177        final int NUM_SEARCH_ACTIONS = 10;
178        final List<Integer> DISABLED_ACTIONS = new ArrayList<>(
179                Arrays.asList(1, 3, 5, 7));
180        final int ACTION_ID_REVERT_BUTTON = 0;
181        final int ACTION_ID_SEARCH_BEGIN = ACTION_ID_REVERT_BUTTON + 1;
182        int ACTION_ID_SEARCH_END = ACTION_ID_SEARCH_BEGIN;
183
184        // sequence of clicked actions simulated in the test
185        List<Integer> CLICK_SEQUENCE = new ArrayList<>();
186
187        // Expected Clicked sequence can be different from focused ones since some of the actions
188        // are disabled hence not clickable
189        List<Integer> EXPECTED_FOCUSED_SEQUENCE = new ArrayList<>();
190        List<Integer> EXPECTED_CLICKED_SEQUENCE = new ArrayList<>();
191        // Expected actions state according to list of DISABLED_ACTIONS: false for disabled actions
192        List<Boolean> EXPECTED_ACTIONS_STATE = new ArrayList<>(
193                Arrays.asList(new Boolean[NUM_SEARCH_ACTIONS])
194        );
195        Collections.fill(EXPECTED_ACTIONS_STATE, Boolean.TRUE);
196
197        for(int i = 0; i < NUM_SEARCH_ACTIONS; i++) {
198            CLICK_SEQUENCE.add(i + 1);
199        }
200        for(int clickedActionId : CLICK_SEQUENCE) {
201            EXPECTED_FOCUSED_SEQUENCE.add(clickedActionId);
202            if (!DISABLED_ACTIONS.contains(clickedActionId - 1))
203                EXPECTED_CLICKED_SEQUENCE.add(clickedActionId);
204            else
205                EXPECTED_CLICKED_SEQUENCE.add(-1);
206        }
207
208        String title = "Guided Actions Enabled Test";
209        String breadcrumb = "Enabled Test Demo";
210        String description = "";
211        GuidanceStylist.Guidance guidance = new GuidanceStylist.Guidance(title, description,
212                breadcrumb, null);
213
214        List<GuidedAction> actionList = new ArrayList<>();
215        actionList.add(new GuidedAction.Builder(mContext)
216                .id(ACTION_ID_REVERT_BUTTON)
217                .title(res.getString(R.string.invert_title))
218                .description(res.getString(R.string.revert_description))
219                .build()
220        );
221
222        for (int i = 0; i < NUM_SEARCH_ACTIONS; i++ ) {
223            actionList.add(new GuidedAction.Builder(mContext)
224                    .id(ACTION_ID_SEARCH_END++)
225                    .title(res.getString(R.string.search) + "" + i)
226                    .description(res.getString(R.string.search_description) + i)
227                    .build()
228            );
229        }
230        for(int action_id : DISABLED_ACTIONS ) {
231            if ( action_id >= 0 && action_id < NUM_SEARCH_ACTIONS ) {
232                actionList.get(action_id + 1).setEnabled(false);
233                EXPECTED_ACTIONS_STATE.set(action_id, Boolean.FALSE);
234            }
235        }
236
237        GuidedStepAttributesTestFragment.clear();
238        GuidedStepAttributesTestFragment.GUIDANCE = guidance;
239        GuidedStepAttributesTestFragment.ACTION_LIST = actionList;
240        GuidedStepAttributesTestFragment.setActionClickCallback(ACTION_ID_REVERT_BUTTON,
241                sRevertCallback);
242
243        initActivity(intent);
244
245        examineEnabledAndDisabledActions(actionList, CLICK_SEQUENCE, EXPECTED_FOCUSED_SEQUENCE,
246                EXPECTED_CLICKED_SEQUENCE);
247    }
248
249    /**
250     * Toggles Enabled flags in oll the actions of the prior test, and tests whether they are
251     * correctly reverted.
252     */
253    @Test
254    public void testToggleEnabledFlags() throws Throwable {
255
256        Intent intent = new Intent();
257        Resources res = mContext.getResources();
258
259        final int NUM_SEARCH_ACTIONS = 10;
260        final List<Integer> DISABLED_ACTIONS = new ArrayList<>(
261                Arrays.asList(1, 3, 5, 7));
262        final int ACTION_ID_REVERT_BUTTON = 0;
263        final int ACTION_ID_SEARCH_BEGIN = ACTION_ID_REVERT_BUTTON + 1;
264        int ACTION_ID_SEARCH_END = ACTION_ID_SEARCH_BEGIN;
265
266        // sequence of clicked actions simulated in the test
267        List<Integer> CLICK_SEQUENCE = new ArrayList<>();
268
269        // Expected Clicked sequence can be different from focused ones since some of the actions
270        // are disabled hence not clickable
271        List<Integer> EXPECTED_FOCUSED_SEQUENCE = new ArrayList<>();
272        List<Integer> EXPECTED_CLICKED_SEQUENCE = new ArrayList<>();
273        // Expected actions state according to list of DISABLED_ACTIONS: false for disabled actions
274        List<Boolean> EXPECTED_ACTIONS_STATE = new ArrayList<>(
275                Arrays.asList(new Boolean[NUM_SEARCH_ACTIONS])
276        );
277        Collections.fill(EXPECTED_ACTIONS_STATE, Boolean.FALSE);
278
279        for(int i = 0; i < NUM_SEARCH_ACTIONS; i++) {
280            CLICK_SEQUENCE.add(i + 1);
281        }
282        for(int clickedActionId : CLICK_SEQUENCE) {
283            EXPECTED_FOCUSED_SEQUENCE.add(clickedActionId);
284            if (DISABLED_ACTIONS.contains(clickedActionId - 1))
285                EXPECTED_CLICKED_SEQUENCE.add(clickedActionId);
286            else
287                EXPECTED_CLICKED_SEQUENCE.add(-1);
288        }
289
290        String title = "Guided Actions Enabled Test";
291        String breadcrumb = "Toggle Enabled Flag Test Demo";
292        String description = "";
293        GuidanceStylist.Guidance guidance = new GuidanceStylist.Guidance(title, description,
294                breadcrumb, null);
295
296        List<GuidedAction> actionList = new ArrayList<>();
297        actionList.add(new GuidedAction.Builder(mContext)
298                .id(ACTION_ID_REVERT_BUTTON)
299                .title(res.getString(R.string.invert_title))
300                .description(res.getString(R.string.revert_description))
301                .build()
302        );
303
304        for (int i = 0; i < NUM_SEARCH_ACTIONS; i++ ) {
305            actionList.add(new GuidedAction.Builder(mContext)
306                    .id(ACTION_ID_SEARCH_END++)
307                    .title(res.getString(R.string.search) + "" + i)
308                    .description(res.getString(R.string.search_description) + i)
309                    .build()
310            );
311        }
312        for(int action_id : DISABLED_ACTIONS ) {
313            if ( action_id >= 0 && action_id < NUM_SEARCH_ACTIONS ) {
314                actionList.get(action_id + 1).setEnabled(false);
315                EXPECTED_ACTIONS_STATE.set(action_id, Boolean.TRUE);
316            }
317        }
318
319        GuidedStepAttributesTestFragment.clear();
320        GuidedStepAttributesTestFragment.GUIDANCE = guidance;
321        GuidedStepAttributesTestFragment.ACTION_LIST = actionList;
322        GuidedStepAttributesTestFragment.setActionClickCallback(ACTION_ID_REVERT_BUTTON,
323                sRevertCallback);
324
325        initActivity(intent);
326
327        final GuidedStepFragment mFragment = (GuidedStepFragment)
328                mActivity.getGuidedStepTestFragment();
329
330        mActivity.runOnUiThread(new Runnable() {
331            @Override
332            public void run() {
333                mFragment.setSelectedActionPosition(0);
334            }
335        });
336        Thread.sleep(TRANSITION_LENGTH);
337        sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
338        Thread.sleep(TRANSITION_LENGTH);
339        examineEnabledAndDisabledActions(actionList, CLICK_SEQUENCE, EXPECTED_FOCUSED_SEQUENCE,
340                EXPECTED_CLICKED_SEQUENCE);
341    }
342
343    private void examineEnabledAndDisabledActions(
344            List<GuidedAction> actionList, List<Integer> CLICK_SEQUENCE,
345                                List<Integer> EXPECTED_FOCUSED_SEQUENCE,
346                                List<Integer> EXPECTED_CLICKED_SEQUENCE)
347            throws Throwable {
348
349        final GuidedStepFragment mFragment = (GuidedStepFragment)
350                mActivity.getGuidedStepTestFragment();
351
352        for(int i = 0; i < CLICK_SEQUENCE.size(); i++) {
353            GuidedStepAttributesTestFragment.LAST_SELECTED_ACTION_ID =
354                    GuidedStepAttributesTestFragment.LAST_CLICKED_ACTION_ID = -1;
355            final int id = CLICK_SEQUENCE.get(i);
356            mActivity.runOnUiThread(new Runnable() {
357                @Override
358                public void run() {
359                    mFragment.setSelectedActionPosition(id);
360                }
361            });
362            Thread.sleep(TRANSITION_LENGTH);
363
364            sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
365            Thread.sleep(TRANSITION_LENGTH);
366
367            assertTrue(mContext.getResources().getString(
368                    R.string.enabled_test_wrong_focus_error_message),
369                    GuidedStepAttributesTestFragment.LAST_SELECTED_ACTION_ID
370                            == EXPECTED_FOCUSED_SEQUENCE.get(i)
371            );
372            assertTrue(mContext.getResources().getString(
373                    R.string.enabled_test_wrong_click_error_message),
374                    GuidedStepAttributesTestFragment.LAST_CLICKED_ACTION_ID
375                            == EXPECTED_CLICKED_SEQUENCE.get(i)
376            );
377            assertTrue(mContext.getResources().getString(
378                    R.string.enabled_test_wrong_flag_error_message),
379                    (GuidedStepAttributesTestFragment.LAST_CLICKED_ACTION_ID == -1)
380                            ? !actionList.get(id).isEnabled()
381                            : actionList.get(id).isEnabled()
382            );
383        }
384    }
385
386    @Test
387    public void testCheckedActions() throws Throwable {
388
389        Intent intent = new Intent();
390        Resources res = mContext.getResources();
391
392        final int NUM_RADIO_ACTIONS = 3;
393        final int NUM_CHECK_BOX_ACTIONS = 3;
394        final int INITIALLY_CHECKED_RADIO_ACTION = 0;
395        final List<Integer> INITIALLY_CHECKED_CHECKBOX_ACTIONS = new ArrayList<>(
396                Arrays.asList(1, 2)
397        );
398
399        List<Integer> CLICK_SEQUENCE = new ArrayList<>();
400        for(int i = 0; i < NUM_RADIO_ACTIONS + NUM_CHECK_BOX_ACTIONS; i++) {
401            CLICK_SEQUENCE.add(i);
402        }
403
404        List<Boolean> EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK = new ArrayList<>(
405                Arrays.asList(new Boolean[CLICK_SEQUENCE.size()])
406        );
407        Collections.fill(EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK, Boolean.FALSE);
408
409        // initial state of actions before any clicks happen
410        EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK.set(INITIALLY_CHECKED_RADIO_ACTION, true);
411        for(int checkedCheckBox : INITIALLY_CHECKED_CHECKBOX_ACTIONS) {
412            EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK.set(NUM_RADIO_ACTIONS + checkedCheckBox, true);
413        }
414
415        String title = "Guided Actions Checked Test";
416        String breadcrumb = "Checked Test Demo";
417        String description = "";
418        GuidanceStylist.Guidance guidance = new GuidanceStylist.Guidance(title, description,
419                breadcrumb, null);
420
421        List<GuidedAction> actionList = new ArrayList<>();
422        actionList.add(new GuidedAction.Builder(mContext)
423                .title(res.getString(R.string.radio_actions_info_title))
424                .description(res.getString(R.string.radio_actions_info_desc))
425                .infoOnly(true)
426                .enabled(true)
427                .focusable(false)
428                .build()
429        );
430
431        int firstRadioActionIndex = actionList.size();
432        for(int i = 0; i < NUM_RADIO_ACTIONS; i++) {
433            actionList.add(new GuidedAction.Builder(mContext)
434                    .title(res.getString(R.string.checkbox_title) + i)
435                    .description(res.getString(R.string.checkbox_desc) + i)
436                    .checkSetId(GuidedAction.DEFAULT_CHECK_SET_ID)
437                    .build()
438            );
439            if (i == INITIALLY_CHECKED_RADIO_ACTION)
440                actionList.get(firstRadioActionIndex + i).setChecked(true);
441        }
442
443        actionList.add(new GuidedAction.Builder(mContext)
444                .title(res.getString(R.string.checkbox_actions_info_title))
445                .description(res.getString(R.string.checkbox_actions_info_desc))
446                .infoOnly(true)
447                .enabled(true)
448                .focusable(false)
449                .build()
450        );
451        int firstCheckBoxActionIndex = actionList.size();
452        for(int i = 0; i < NUM_CHECK_BOX_ACTIONS; i++) {
453            actionList.add(new GuidedAction.Builder(mContext)
454                    .title(res.getString(R.string.checkbox_title) + i)
455                    .description(res.getString(R.string.checkbox_desc) + i)
456                    .checkSetId(GuidedAction.CHECKBOX_CHECK_SET_ID)
457                    .build()
458            );
459        }
460        for(int i = 0; i < INITIALLY_CHECKED_CHECKBOX_ACTIONS.size(); i++ ) {
461            actionList.get(firstCheckBoxActionIndex + INITIALLY_CHECKED_CHECKBOX_ACTIONS.get(i))
462                    .setChecked(true);
463        }
464
465        GuidedStepAttributesTestFragment.GUIDANCE = guidance;
466        GuidedStepAttributesTestFragment.ACTION_LIST = actionList;
467        initActivity(intent);
468
469        examineCheckedAndUncheckedActions(actionList, EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK,
470                NUM_RADIO_ACTIONS, NUM_CHECK_BOX_ACTIONS);
471    }
472
473    private void updateExpectedActionsStateAfterClick(
474            List<Boolean> EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK, int NUM_RADIO_ACTIONS,
475            int NUM_CHECK_BOX_ACTIONS, int clickedActionIndex) {
476
477        if (clickedActionIndex < NUM_RADIO_ACTIONS) {
478            for(int i = 0; i < NUM_RADIO_ACTIONS; i++)
479                EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK.set(i, false);
480            EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK.set(clickedActionIndex, true);
481        }
482        else if (clickedActionIndex < NUM_RADIO_ACTIONS + NUM_CHECK_BOX_ACTIONS) {
483            EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK.set(clickedActionIndex,
484                    !EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK.get(clickedActionIndex));
485        }
486    }
487
488    private void verifyIfActionsStateIsCorrect(List<GuidedAction> actionList,
489            List<Boolean> EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK) {
490
491        int actionIndex = 0;
492        for(GuidedAction checkAction : actionList) {
493            if (checkAction.infoOnly())
494                continue;
495            assertTrue("Action " + actionIndex + " is " + (!checkAction.isChecked() ? "un-" : "")
496                    + "checked while it shouldn't be!", checkAction.isChecked()
497                            == EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK.get(actionIndex));
498            actionIndex++;
499        }
500    }
501
502    private void examineCheckedAndUncheckedActions(List<GuidedAction> actionList,
503                                List<Boolean> EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK,
504                                int NUM_RADIO_ACTIONS,
505                                int NUM_CHECK_BOX_ACTIONS) throws Throwable {
506
507        final GuidedStepFragment guidedStepCheckedFragment = (GuidedStepFragment)
508                mActivity.getGuidedStepTestFragment();
509        final int firstRadioActionIndex = 1;
510        final int firstCheckBoxActionIndex = firstRadioActionIndex + NUM_RADIO_ACTIONS + 1;
511        for(int actionId = 0; actionId < NUM_RADIO_ACTIONS; actionId++) {
512            final int id = actionId;
513            mActivity.runOnUiThread(new Runnable() {
514                @Override
515                public void run() {
516                    guidedStepCheckedFragment
517                            .setSelectedActionPosition(firstRadioActionIndex + id);
518                }
519            });
520            Thread.sleep(TRANSITION_LENGTH);
521
522            sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
523            Thread.sleep(TRANSITION_LENGTH);
524            updateExpectedActionsStateAfterClick(EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK,
525                    NUM_RADIO_ACTIONS, NUM_CHECK_BOX_ACTIONS, actionId);
526            verifyIfActionsStateIsCorrect(actionList, EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK);
527        }
528
529        for(int actionId = 0; actionId < NUM_CHECK_BOX_ACTIONS; actionId++) {
530            final int id = actionId;
531            mActivity.runOnUiThread(new Runnable() {
532                @Override
533                public void run() {
534                    guidedStepCheckedFragment
535                            .setSelectedActionPosition(firstCheckBoxActionIndex + id);
536                }
537            });
538            Thread.sleep(TRANSITION_LENGTH);
539
540            sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
541            Thread.sleep(TRANSITION_LENGTH);
542            updateExpectedActionsStateAfterClick(EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK,
543                    NUM_RADIO_ACTIONS, NUM_CHECK_BOX_ACTIONS, NUM_RADIO_ACTIONS + actionId);
544            verifyIfActionsStateIsCorrect(actionList, EXPECTED_ACTIONS_STATE_AFTER_EACH_CLICK);
545        }
546    }
547
548    @Test
549    public void testActionWithTwoSubActions() throws Throwable {
550        ExpectedSubActionResult result = setUpActionsForSubActionsTest();
551
552        final int actionPos = 0;
553        final GuidedAction selectedAction = result.actionList.get(actionPos);
554        List<Integer> expectedFocusedSeq = result.expectedFocusedSeq.get(actionPos);
555        List<Integer> expectedClickedSeq = result.expectedClickedSeq.get(actionPos);
556
557        traverseSubActionsAndVerifyFocusAndClickEvents(selectedAction, actionPos, expectedFocusedSeq,
558                expectedClickedSeq);
559    }
560
561    @Test
562    public void testActionWithOneSubAction() throws Throwable {
563        ExpectedSubActionResult result = setUpActionsForSubActionsTest();
564
565        final int actionPos = 1;
566        final GuidedAction selectedAction = result.actionList.get(actionPos);
567        List<Integer> expectedFocusedSeq = result.expectedFocusedSeq.get(actionPos);
568        List<Integer> expectedClickedSeq = result.expectedClickedSeq.get(actionPos);
569
570        traverseSubActionsAndVerifyFocusAndClickEvents(selectedAction, actionPos, expectedFocusedSeq,
571                expectedClickedSeq);
572    }
573
574    @Test
575    public void testActionWithZeroSubActions() throws Throwable {
576        ExpectedSubActionResult result = setUpActionsForSubActionsTest();
577
578        final int actionPos = 2;
579        final GuidedAction selectedAction = result.actionList.get(actionPos);
580        List<Integer> expectedFocusedSeq = result.expectedFocusedSeq.get(actionPos);
581        List<Integer> expectedClickedSeq = result.expectedClickedSeq.get(actionPos);
582
583        traverseSubActionsAndVerifyFocusAndClickEvents(selectedAction, actionPos, expectedFocusedSeq,
584                expectedClickedSeq);
585    }
586
587    @Test
588    public void testActionWithThreeSubActions() throws Throwable {
589        ExpectedSubActionResult result = setUpActionsForSubActionsTest();
590
591        final int actionPos = 3;
592        final GuidedAction selectedAction = result.actionList.get(actionPos);
593        List<Integer> expectedFocusedSeq = result.expectedFocusedSeq.get(actionPos);
594        List<Integer> expectedClickedSeq = result.expectedClickedSeq.get(actionPos);
595
596        traverseSubActionsAndVerifyFocusAndClickEvents(selectedAction, actionPos, expectedFocusedSeq,
597                expectedClickedSeq);
598    }
599
600    /**
601     * Traverses the list of sub actions of a gudied action. It also verifies the correct action
602     * or sub action is focused or clicked as the traversal is performed.
603     * @param selectedAction The action of interest
604     * @param actionPos The position of selectedAction within the array of guidedactions
605     * @param expectedFocusedSeq The actual actions IDs used as a reference to verify focused actions
606     * @param expectedClickedSeq The actual action IDs used as a reference to verify clicked actions
607     * @throws Throwable
608     */
609    private void traverseSubActionsAndVerifyFocusAndClickEvents(GuidedAction selectedAction,
610                                                                int actionPos,
611                                                                List<Integer> expectedFocusedSeq,
612                                                                List<Integer> expectedClickedSeq)
613            throws Throwable{
614
615        final GuidedStepFragment mFragment =
616                (GuidedStepFragment) mActivity.getGuidedStepTestFragment();
617        int focusStep = 0, clickStep = 0;
618        GuidedStepAttributesTestFragment.LAST_SELECTED_ACTION_ID =
619                GuidedStepAttributesTestFragment.LAST_CLICKED_ACTION_ID = -1;
620
621
622        final int pos = actionPos;
623        mActivity.runOnUiThread(new Runnable() {
624            @Override
625            public void run() {
626                mFragment.setSelectedActionPosition(pos);
627            }
628        });
629        Thread.sleep(TRANSITION_LENGTH);
630
631        if (mFragment.getSelectedActionPosition() != actionPos) {
632            assertTrue(mContext.getResources().getString(
633                    R.string.subaction_test_wrong_focus_error_message),
634                    GuidedStepAttributesTestFragment.LAST_SELECTED_ACTION_ID
635                            == expectedFocusedSeq.get(focusStep++)
636            );
637        } else {
638            // If the currently focused position is the same as the position of the action of interest,
639            // then GuidedStepFragment won't received onGuidedActionFocused callback. Since the first
640            // element in the expectedFocusSeq is always the id of this action, we need to move focusStep
641            // one step forward.
642            focusStep++;
643        }
644        if (selectedAction.hasSubActions()) {
645            // Following for loop clicks on a specific action and scrolls & clicks through
646            // all its subactions
647            for (int j = 0; j < selectedAction.getSubActions().size(); j++) {
648                sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
649                Thread.sleep(TRANSITION_LENGTH);
650                assertTrue(mContext.getResources().getString(
651                        R.string.subaction_test_wrong_focus_error_message),
652                        GuidedStepAttributesTestFragment.LAST_SELECTED_ACTION_ID
653                                == expectedFocusedSeq.get(focusStep++)
654                );
655                assertTrue(mContext.getResources().getString(
656                        R.string.subaction_test_wrong_click_error_message),
657                        GuidedStepAttributesTestFragment.LAST_CLICKED_ACTION_ID
658                                == expectedClickedSeq.get(clickStep++)
659                );
660
661                for (int k = 0; k < j; k++) {
662                    sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
663                    Thread.sleep(TRANSITION_LENGTH);
664                    assertTrue(mContext.getResources().getString(
665                            R.string.subaction_test_wrong_focus_error_message),
666                            GuidedStepAttributesTestFragment.LAST_SELECTED_ACTION_ID
667                                    == expectedFocusedSeq.get(focusStep++)
668                    );
669                }
670                sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
671                Thread.sleep(TRANSITION_LENGTH);
672
673                assertTrue(mContext.getResources().getString(
674                        R.string.subaction_test_wrong_focus_error_message),
675                        GuidedStepAttributesTestFragment.LAST_SELECTED_ACTION_ID
676                                == expectedFocusedSeq.get(focusStep++)
677                );
678                assertTrue(mContext.getResources().getString(
679                        R.string.subaction_test_wrong_click_error_message),
680                        GuidedStepAttributesTestFragment.LAST_CLICKED_ACTION_ID
681                                == expectedClickedSeq.get(clickStep++)
682                );
683            }
684        } else {
685            sendKey(KeyEvent.KEYCODE_DPAD_CENTER);
686            Thread.sleep(TRANSITION_LENGTH);
687            assertTrue(mContext.getResources().getString(
688                    R.string.subaction_test_wrong_focus_error_message),
689                    GuidedStepAttributesTestFragment.LAST_SELECTED_ACTION_ID
690                            == expectedFocusedSeq.get(focusStep++)
691            );
692            assertTrue(mContext.getResources().getString(
693                    R.string.subaction_test_wrong_click_error_message),
694                    GuidedStepAttributesTestFragment.LAST_CLICKED_ACTION_ID
695                            == expectedClickedSeq.get(clickStep++)
696            );
697        }
698    }
699
700    static class ExpectedSubActionResult {
701        List<List<Integer>> expectedFocusedSeq; // Expected sequence of action (or subaction) ids to receive focus events;
702        // Each entry corresponds to an action item in the guidedactions pane
703        List<List<Integer>> expectedClickedSeq; // Expected sequence of action (or subaction) ids to receive click events;
704        // Each entry corresponds to an action item in the guidedactions pane
705        List<GuidedAction> actionList;          // List of GuidedActions in the guidedactions pane
706    }
707
708    /**
709     * Populates a sample list of actions and subactions in the guidedactions pane.
710     * @return  An object holding the expected sequence of action and subactions IDs that receive
711     * focus and click events as well as the list of GuidedActions.
712     */
713    private ExpectedSubActionResult setUpActionsForSubActionsTest() {
714        Intent intent = new Intent();
715        Resources res = mContext.getResources();
716
717        ExpectedSubActionResult result = new ExpectedSubActionResult();
718        result.expectedFocusedSeq = new ArrayList<>();
719        result.expectedClickedSeq = new ArrayList<>();
720
721        final int NUM_REGULAR_ACTIONS = 4;
722        final int[] NUM_SUBACTIONS_PER_ACTION = {2, 1, 0, 3};
723        final int[] REGULAR_ACTIONS_INDEX =  new int[NUM_REGULAR_ACTIONS];
724        final int[] BEGIN_SUBACTION_INDEX_PER_ACTION = new int[NUM_REGULAR_ACTIONS];
725        final int[] END_SUBACTION_INDEX_PER_ACTION = new int[NUM_REGULAR_ACTIONS];
726        // Actions and SubActions are assigned unique sequential IDs
727        int lastIndex = 0;
728        for(int i = 0; i < NUM_REGULAR_ACTIONS; i++) {
729            REGULAR_ACTIONS_INDEX[i] = lastIndex;
730            lastIndex++;
731            BEGIN_SUBACTION_INDEX_PER_ACTION[i] = lastIndex;
732            END_SUBACTION_INDEX_PER_ACTION[i] = (lastIndex += NUM_SUBACTIONS_PER_ACTION[i]);
733        }
734
735        for (int i = 0; i < NUM_REGULAR_ACTIONS; i++) {
736            List<Integer> expectedFocusSeqForEachAction = new ArrayList<>();
737            List<Integer> expectedClickedSeqForEachAction = new ArrayList<>();
738            expectedFocusSeqForEachAction.add(REGULAR_ACTIONS_INDEX[i]);
739
740            if (NUM_SUBACTIONS_PER_ACTION[i] > 0) {
741                for (int j = BEGIN_SUBACTION_INDEX_PER_ACTION[i];
742                        j < END_SUBACTION_INDEX_PER_ACTION[i]; j++) {
743                    expectedClickedSeqForEachAction.add(REGULAR_ACTIONS_INDEX[i]);
744                    for (int k = BEGIN_SUBACTION_INDEX_PER_ACTION[i]; k <= j; k++) {
745                        expectedFocusSeqForEachAction.add(k);
746                    }
747                    expectedClickedSeqForEachAction.add(j);
748                    expectedFocusSeqForEachAction.add(REGULAR_ACTIONS_INDEX[i]);
749                }
750            } else {
751                expectedClickedSeqForEachAction.add(REGULAR_ACTIONS_INDEX[i]);
752                expectedFocusSeqForEachAction.add(REGULAR_ACTIONS_INDEX[i]);
753            }
754            result.expectedFocusedSeq.add(expectedFocusSeqForEachAction);
755            result.expectedClickedSeq.add(expectedClickedSeqForEachAction);
756        }
757
758        String title = "Guided SubActions Test";
759        String breadcrumb = "SubActions Test Demo";
760        String description = "";
761        GuidanceStylist.Guidance guidance = new GuidanceStylist.Guidance(title, description,
762                breadcrumb, null);
763
764        List<GuidedAction> actionList = new ArrayList<>();
765
766        lastIndex = 0;
767        for (int i = 0; i < NUM_REGULAR_ACTIONS; i++ ) {
768            GuidedAction action = new GuidedAction.Builder(mContext)
769                    .id(lastIndex++)
770                    .title(res.getString(R.string.dropdown_action_title, i))
771                    .description(res.getString(R.string.dropdown_action_desc, i))
772                    .build();
773            if (NUM_SUBACTIONS_PER_ACTION[i] > 0) {
774                List<GuidedAction> subActions = new ArrayList<>();
775                action.setSubActions(subActions);
776                for(int j = 0; j < NUM_SUBACTIONS_PER_ACTION[i]; j++) {
777                    subActions.add(new GuidedAction.Builder(mContext)
778                            .id(lastIndex++)
779                            .title(res.getString(R.string.subaction_title, j))
780                            .description("")
781                            .build()
782                    );
783                }
784            }
785            actionList.add(action);
786        }
787        result.actionList = actionList;
788
789        GuidedStepAttributesTestFragment.clear();
790        GuidedStepAttributesTestFragment.GUIDANCE = guidance;
791        GuidedStepAttributesTestFragment.ACTION_LIST = actionList;
792
793        initActivity(intent);
794        return result;
795    }
796}
797