1package com.android.launcher3.ui;
2
3import android.app.SearchManager;
4import android.appwidget.AppWidgetProviderInfo;
5import android.content.ComponentName;
6import android.content.Context;
7import android.content.Intent;
8import android.content.pm.PackageManager;
9import android.graphics.Point;
10import android.os.ParcelFileDescriptor;
11import android.os.RemoteException;
12import android.os.SystemClock;
13import android.support.test.uiautomator.By;
14import android.support.test.uiautomator.BySelector;
15import android.support.test.uiautomator.Direction;
16import android.support.test.uiautomator.UiDevice;
17import android.support.test.uiautomator.UiObject2;
18import android.support.test.uiautomator.Until;
19import android.test.InstrumentationTestCase;
20import android.view.MotionEvent;
21
22import com.android.launcher3.InvariantDeviceProfile;
23import com.android.launcher3.Launcher;
24import com.android.launcher3.LauncherAppState;
25import com.android.launcher3.LauncherAppWidgetProviderInfo;
26import com.android.launcher3.LauncherClings;
27import com.android.launcher3.LauncherSettings;
28import com.android.launcher3.R;
29import com.android.launcher3.Utilities;
30import com.android.launcher3.compat.AppWidgetManagerCompat;
31import com.android.launcher3.config.FeatureFlags;
32import com.android.launcher3.util.ManagedProfileHeuristic;
33
34import java.io.FileInputStream;
35import java.io.IOException;
36import java.util.Locale;
37import java.util.concurrent.Callable;
38import java.util.concurrent.atomic.AtomicReference;
39
40/**
41 * Base class for all instrumentation tests providing various utility methods.
42 */
43public class LauncherInstrumentationTestCase extends InstrumentationTestCase {
44
45    public static final long DEFAULT_UI_TIMEOUT = 3000;
46
47    protected UiDevice mDevice;
48    protected Context mTargetContext;
49    protected String mTargetPackage;
50
51    @Override
52    protected void setUp() throws Exception {
53        super.setUp();
54
55        mDevice = UiDevice.getInstance(getInstrumentation());
56        mTargetContext = getInstrumentation().getTargetContext();
57        mTargetPackage = mTargetContext.getPackageName();
58    }
59
60    protected void lockRotation(boolean naturalOrientation) throws RemoteException {
61        Utilities.getPrefs(mTargetContext)
62                .edit()
63                .putBoolean(Utilities.ALLOW_ROTATION_PREFERENCE_KEY, !naturalOrientation)
64                .commit();
65
66        if (naturalOrientation) {
67            mDevice.setOrientationNatural();
68        } else {
69            mDevice.setOrientationRight();
70        }
71    }
72
73    /**
74     * Starts the launcher activity in the target package and returns the Launcher instance.
75     */
76    protected Launcher startLauncher() {
77        Intent homeIntent = new Intent(Intent.ACTION_MAIN)
78                .addCategory(Intent.CATEGORY_HOME)
79                .setPackage(mTargetPackage)
80                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
81        return (Launcher) getInstrumentation().startActivitySync(homeIntent);
82    }
83
84    /**
85     * Grants the launcher permission to bind widgets.
86     */
87    protected void grantWidgetPermission() throws IOException {
88        // Check bind widget permission
89        if (mTargetContext.getPackageManager().checkPermission(
90                mTargetPackage, android.Manifest.permission.BIND_APPWIDGET)
91                != PackageManager.PERMISSION_GRANTED) {
92            ParcelFileDescriptor pfd = getInstrumentation().getUiAutomation().executeShellCommand(
93                    "appwidget grantbind --package " + mTargetPackage);
94            // Read the input stream fully.
95            FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd);
96            while (fis.read() != -1);
97            fis.close();
98        }
99    }
100
101    /**
102     * Opens all apps and returns the recycler view
103     */
104    protected UiObject2 openAllApps() {
105        if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP) {
106            // clicking on the page indicator brings up all apps tray on non tablets.
107            findViewById(R.id.page_indicator).click();
108        } else {
109            mDevice.wait(Until.findObject(
110                    By.desc(mTargetContext.getString(R.string.all_apps_button_label))),
111                    DEFAULT_UI_TIMEOUT).click();
112        }
113        return findViewById(R.id.apps_list_view);
114    }
115
116    /**
117     * Opens widget tray and returns the recycler view.
118     */
119    protected UiObject2 openWidgetsTray() {
120        mDevice.pressMenu(); // Enter overview mode.
121        mDevice.wait(Until.findObject(
122                By.text(mTargetContext.getString(R.string.widget_button_text)
123                        .toUpperCase(Locale.getDefault()))), DEFAULT_UI_TIMEOUT).click();
124        return findViewById(R.id.widgets_list_view);
125    }
126
127    /**
128     * Scrolls the {@param container} until it finds an object matching {@param condition}.
129     * @return the matching object.
130     */
131    protected UiObject2 scrollAndFind(UiObject2 container, BySelector condition) {
132        do {
133            UiObject2 widget = container.findObject(condition);
134            if (widget != null) {
135                return widget;
136            }
137        } while (container.scroll(Direction.DOWN, 1f));
138        return container.findObject(condition);
139    }
140
141    /**
142     * Drags an icon to the center of homescreen.
143     */
144    protected void dragToWorkspace(UiObject2 icon) {
145        Point center = icon.getVisibleCenter();
146
147        // Action Down
148        sendPointer(MotionEvent.ACTION_DOWN, center);
149
150        // Wait until "Remove/Delete target is visible
151        assertNotNull(findViewById(R.id.delete_target_text));
152
153        Point moveLocation = findViewById(R.id.drag_layer).getVisibleCenter();
154
155        // Move to center
156        while(!moveLocation.equals(center)) {
157            center.x = getNextMoveValue(moveLocation.x, center.x);
158            center.y = getNextMoveValue(moveLocation.y, center.y);
159            sendPointer(MotionEvent.ACTION_MOVE, center);
160        }
161        sendPointer(MotionEvent.ACTION_UP, center);
162
163        // Wait until remove target is gone.
164        mDevice.wait(Until.gone(getSelectorForId(R.id.delete_target_text)), DEFAULT_UI_TIMEOUT);
165    }
166
167    private int getNextMoveValue(int targetValue, int oldValue) {
168        if (targetValue - oldValue > 10) {
169            return oldValue + 10;
170        } else if (targetValue - oldValue < -10) {
171            return oldValue - 10;
172        } else {
173            return targetValue;
174        }
175    }
176
177    private void sendPointer(int action, Point point) {
178        MotionEvent event = MotionEvent.obtain(SystemClock.uptimeMillis(),
179                SystemClock.uptimeMillis(), action, point.x, point.y, 0);
180        getInstrumentation().sendPointerSync(event);
181        event.recycle();
182    }
183
184    /**
185     * Removes all icons from homescreen and hotseat.
186     */
187    public void clearHomescreen() throws Throwable {
188        LauncherSettings.Settings.call(mTargetContext.getContentResolver(),
189                LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
190        LauncherSettings.Settings.call(mTargetContext.getContentResolver(),
191                LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG);
192        LauncherClings.markFirstRunClingDismissed(mTargetContext);
193        ManagedProfileHeuristic.markExistingUsersForNoFolderCreation(mTargetContext);
194
195        runTestOnUiThread(new Runnable() {
196            @Override
197            public void run() {
198                // Reset the loader state
199                LauncherAppState.getInstance().getModel().resetLoadedState(true, true);
200            }
201        });
202    }
203
204    /**
205     * Runs the callback on the UI thread and returns the result.
206     */
207    protected <T> T getOnUiThread(final Callable<T> callback) {
208        final AtomicReference<T> result = new AtomicReference<>(null);
209        try {
210            runTestOnUiThread(new Runnable() {
211                @Override
212                public void run() {
213                    try {
214                        result.set(callback.call());
215                    } catch (Exception e) { }
216                }
217            });
218        } catch (Throwable t) { }
219        return result.get();
220    }
221
222    /**
223     * Finds a widget provider which can fit on the home screen.
224     * @param hasConfigureScreen if true, a provider with a config screen is returned.
225     */
226    protected LauncherAppWidgetProviderInfo findWidgetProvider(final boolean hasConfigureScreen) {
227        LauncherAppWidgetProviderInfo info = getOnUiThread(new Callable<LauncherAppWidgetProviderInfo>() {
228            @Override
229            public LauncherAppWidgetProviderInfo call() throws Exception {
230                InvariantDeviceProfile idv =
231                        LauncherAppState.getInstance().getInvariantDeviceProfile();
232
233                ComponentName searchComponent = ((SearchManager) mTargetContext
234                        .getSystemService(Context.SEARCH_SERVICE)).getGlobalSearchActivity();
235                String searchPackage = searchComponent == null
236                        ? null : searchComponent.getPackageName();
237
238                for (AppWidgetProviderInfo info :
239                        AppWidgetManagerCompat.getInstance(mTargetContext).getAllProviders()) {
240                    if ((info.configure != null) ^ hasConfigureScreen) {
241                        continue;
242                    }
243                    // Exclude the widgets in search package, as Launcher already binds them in
244                    // QSB, so they can cause conflicts.
245                    if (info.provider.getPackageName().equals(searchPackage)) {
246                        continue;
247                    }
248                    LauncherAppWidgetProviderInfo widgetInfo = LauncherAppWidgetProviderInfo
249                            .fromProviderInfo(mTargetContext, info);
250                    if (widgetInfo.minSpanX >= idv.numColumns
251                            || widgetInfo.minSpanY >= idv.numRows) {
252                        continue;
253                    }
254                    return widgetInfo;
255                }
256                return null;
257            }
258        });
259        if (info == null) {
260            throw new IllegalArgumentException("No valid widget provider");
261        }
262        return info;
263    }
264
265    protected UiObject2 findViewById(int id) {
266        return mDevice.wait(Until.findObject(getSelectorForId(id)), DEFAULT_UI_TIMEOUT);
267    }
268
269    protected BySelector getSelectorForId(int id) {
270        String name = mTargetContext.getResources().getResourceEntryName(id);
271        return By.res(mTargetPackage, name);
272    }
273}
274