1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package android.platform.test.helpers;
18 
19 import android.app.Instrumentation;
20 import android.platform.test.helpers.exceptions.UnknownUiException;
21 import android.support.test.launcherhelper.ILeanbackLauncherStrategy;
22 import android.support.test.launcherhelper.LauncherStrategyFactory;
23 import android.support.test.uiautomator.By;
24 import android.support.test.uiautomator.BySelector;
25 import android.support.test.uiautomator.Direction;
26 import android.support.test.uiautomator.UiObject2;
27 import android.support.test.uiautomator.Until;
28 import android.util.Log;
29 
30 /**
31  *  This app helper handles the following important widgets for TV apps:
32  *  BrowseFragment, DetailsFragment, SearchFragment and PlaybackOverlayFragment
33  */
34 public abstract class AbstractLeanbackAppHelper extends AbstractStandardAppHelper {
35 
36     private static final String TAG = AbstractLeanbackAppHelper.class.getSimpleName();
37     private static final long OPEN_SECTION_WAIT_TIME_MS = 5000;
38     private static final long OPEN_SIDE_PANEL_WAIT_TIME_MS = 5000;
39     private static final int OPEN_SIDE_PANEL_MAX_ATTEMPTS = 5;
40     private static final long MAIN_ACTIVITY_WAIT_TIME_MS = 250;
41 
42     protected DPadHelper mDPadHelper;
43     public ILeanbackLauncherStrategy mLauncherStrategy;
44 
45 
AbstractLeanbackAppHelper(Instrumentation instr)46     public AbstractLeanbackAppHelper(Instrumentation instr) {
47         super(instr);
48         mDPadHelper = DPadHelper.getInstance(instr);
49         mLauncherStrategy = LauncherStrategyFactory.getInstance(
50                 mDevice).getLeanbackLauncherStrategy();
51     }
52 
getAppSelector()53     protected abstract BySelector getAppSelector();
54 
getSidePanelSelector()55     protected abstract BySelector getSidePanelSelector();
56 
getSidePanelResultSelector(String sectionName)57     protected abstract BySelector getSidePanelResultSelector(String sectionName);
58 
59     /**
60      * Selector to identify main activity for getMainActivitySelector().
61      * Not every application has its main activity, so the override is optional.
62      */
getMainActivitySelector()63     protected BySelector getMainActivitySelector() {
64         return null;
65     }
66 
67     /**
68      * Setup expectation: Side panel is selected on browse fragment
69      *
70      * Best effort attempt to go to the side panel, and open the selected section.
71      */
openSection(String sectionName)72     public void openSection(String sectionName) {
73         openSidePanel();
74         // Section header is focused; it should not be after pressing the DPad
75         selectSection(sectionName);
76         mDevice.pressDPadCenter();
77 
78         // Test for focus change and selection result
79         BySelector sectionResult = getSidePanelResultSelector(sectionName);
80         if (!mDevice.wait(Until.hasObject(sectionResult), OPEN_SECTION_WAIT_TIME_MS)) {
81             throw new UnknownUiException(
82                     String.format("Failed to find result opening section %s", sectionName));
83         }
84         Log.v(TAG, "Successfully opened section");
85     }
86 
87     /**
88      * Setup expectation: On navigation screen on browse fragment
89      *
90      * Best effort attempt to open the side panel.
91      * @param onMainActivity True if it opens the side panel on app's main activity.
92      */
openSidePanel(boolean onMainActivity)93     public void openSidePanel(boolean onMainActivity) {
94         if (onMainActivity) {
95             returnToMainActivity();
96         }
97         int attempts = 0;
98         while (!isSidePanelSelected(OPEN_SIDE_PANEL_WAIT_TIME_MS)
99                 && attempts++ < OPEN_SIDE_PANEL_MAX_ATTEMPTS) {
100             mDevice.pressDPadLeft();
101         }
102         if (attempts == OPEN_SIDE_PANEL_MAX_ATTEMPTS) {
103             throw new UnknownUiException("Failed to open side panel");
104         }
105     }
106 
openSidePanel()107     public void openSidePanel() {
108         openSidePanel(false);
109     }
110 
111     /**
112      * Select target item through the container in the given direction.
113      * @param container
114      * @param target
115      * @param direction
116      * @return the focused object
117      */
select(UiObject2 container, BySelector target, Direction direction)118     public UiObject2 select(UiObject2 container, BySelector target, Direction direction) {
119         if (container == null) {
120             throw new IllegalArgumentException("The container should not be null.");
121         }
122         UiObject2 focus = container.findObject(By.focused(true));
123         if (focus == null) {
124             throw new UnknownUiException("The container should have a focus.");
125         }
126         while (!focus.hasObject(target)) {
127             UiObject2 prev = focus;
128             mDPadHelper.pressDPad(direction);
129             focus = container.findObject(By.focused(true));
130             if (focus == null) {
131                 mDPadHelper.pressDPad(Direction.reverse(direction));
132                 focus = container.findObject(By.focused(true));
133             }
134             if (focus.equals(prev)) {
135                 // It reached at the end, but no target is found.
136                 return null;
137             }
138         }
139         return focus;
140     }
141 
142     /**
143      * Attempts to return to main activity with getMainActivitySelector()
144      * by pressing the back button repeatedly and sleeping briefly to allow for UI slowness.
145      */
returnToMainActivity()146     public void returnToMainActivity() {
147         int maxBackAttempts = 10;
148         BySelector selector = getMainActivitySelector();
149         if (selector == null) {
150             throw new IllegalStateException("getMainActivitySelector() should be overridden.");
151         }
152         while (!mDevice.wait(Until.hasObject(selector), MAIN_ACTIVITY_WAIT_TIME_MS)
153                 && maxBackAttempts-- > 0) {
154             mDevice.pressBack();
155         }
156     }
157 
158     @Override
dismissInitialDialogs()159     public void dismissInitialDialogs() {
160         return;
161     }
162 
isSidePanelSelected(long timeout)163     protected boolean isSidePanelSelected(long timeout) {
164         UiObject2 sidePanel = mDevice.wait(Until.findObject(getSidePanelSelector()), timeout);
165         if (sidePanel == null) {
166             return false;
167         }
168         return sidePanel.hasObject(By.focused(true).minDepth(1));
169     }
170 
selectSection(String sectionName)171     protected UiObject2 selectSection(String sectionName) {
172         UiObject2 container = mDevice.wait(
173                 Until.findObject(getSidePanelSelector()), OPEN_SIDE_PANEL_WAIT_TIME_MS);
174         BySelector section = By.clazz(".TextView").text(sectionName);
175 
176         // Wait until the section text appears at runtime. This needs to be long enough to run under
177         // low bandwidth environments in the test lab.
178         mDevice.wait(Until.findObject(section), 60 * 1000);
179 
180         // Search up, then down
181         UiObject2 focused = select(container, section, Direction.UP);
182         if (focused != null) {
183             return focused;
184         }
185         focused = select(container, section, Direction.DOWN);
186         if (focused != null) {
187             return focused;
188         }
189         throw new UnknownUiException("Failed to select section");
190     }
191 
192 }
193