summaryrefslogtreecommitdiffstats
path: root/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
blob: 466bc5ed0280d8b6851afb13a67648ce8afa69fa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
/*
 * Copyright (C) 2018 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 com.android.launcher3.tapl;

import static com.android.systemui.shared.system.SettingsCompat.SWIPE_UP_SETTING_NAME;

import android.app.ActivityManager;
import android.app.Instrumentation;
import android.app.UiAutomation;
import android.graphics.Point;
import android.os.Bundle;
import android.os.Parcelable;
import android.os.SystemClock;
import android.provider.Settings;
import android.util.Log;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.accessibility.AccessibilityEvent;

import androidx.annotation.NonNull;
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.BySelector;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject2;
import androidx.test.uiautomator.Until;

import com.android.launcher3.TestProtocol;
import com.android.quickstep.SwipeUpSetting;

import org.junit.Assert;

import java.lang.ref.WeakReference;
import java.util.List;
import java.util.concurrent.TimeoutException;

/**
 * The main tapl object. The only object that can be explicitly constructed by the using code. It
 * produces all other objects.
 */
public final class LauncherInstrumentation {

    private static final String TAG = "Tapl";

    // Types for launcher containers that the user is interacting with. "Background" is a
    // pseudo-container corresponding to inactive launcher covered by another app.
    enum ContainerType {
        WORKSPACE, ALL_APPS, OVERVIEW, WIDGETS, BACKGROUND, BASE_OVERVIEW
    }

    // Base class for launcher containers.
    static abstract class VisibleContainer {
        protected final LauncherInstrumentation mLauncher;

        protected VisibleContainer(LauncherInstrumentation launcher) {
            mLauncher = launcher;
            launcher.setActiveContainer(this);
        }

        protected abstract ContainerType getContainerType();

        /**
         * Asserts that the launcher is in the mode matching 'this' object.
         *
         * @return UI object for the container.
         */
        final UiObject2 verifyActiveContainer() {
            assertTrue("Attempt to use a stale container", this == sActiveContainer.get());
            return mLauncher.verifyContainerType(getContainerType());
        }
    }

    private static final String WORKSPACE_RES_ID = "workspace";
    private static final String APPS_RES_ID = "apps_view";
    private static final String OVERVIEW_RES_ID = "overview_panel";
    private static final String WIDGETS_RES_ID = "widgets_list_view";
    public static final int WAIT_TIME_MS = 60000;
    private static final String SYSTEMUI_PACKAGE = "com.android.systemui";

    private static WeakReference<VisibleContainer> sActiveContainer = new WeakReference<>(null);

    private final UiDevice mDevice;
    private final boolean mSwipeUpEnabled;
    private Boolean mSwipeUpEnabledOverride = null;
    private final Instrumentation mInstrumentation;
    private int mExpectedRotation = Surface.ROTATION_0;

    /**
     * Constructs the root of TAPL hierarchy. You get all other objects from it.
     */
    public LauncherInstrumentation(Instrumentation instrumentation) {
        mInstrumentation = instrumentation;
        mDevice = UiDevice.getInstance(instrumentation);
        final boolean swipeUpEnabledDefault =
                !SwipeUpSetting.isSwipeUpSettingAvailable() ||
                        SwipeUpSetting.isSwipeUpEnabledDefaultValue();
        mSwipeUpEnabled = Settings.Secure.getInt(
                instrumentation.getTargetContext().getContentResolver(),
                SWIPE_UP_SETTING_NAME,
                swipeUpEnabledDefault ? 1 : 0) == 1;

        // Launcher should run in test harness so that custom accessibility protocol between
        // Launcher and TAPL is enabled. In-process tests enable this protocol with a direct call
        // into Launcher.
        assertTrue("Device must run in a test harness",
                TestHelpers.isInLauncherProcess() || ActivityManager.isRunningInTestHarness());
    }

    // Used only by TaplTests.
    public void overrideSwipeUpEnabled(Boolean swipeUpEnabledOverride) {
        mSwipeUpEnabledOverride = swipeUpEnabledOverride;
    }

    void setActiveContainer(VisibleContainer container) {
        sActiveContainer = new WeakReference<>(container);
    }

    public boolean isSwipeUpEnabled() {
        return mSwipeUpEnabledOverride != null ? mSwipeUpEnabledOverride : mSwipeUpEnabled;
    }

    static void log(String message) {
        Log.d(TAG, message);
    }

    private static void fail(String message) {
        Assert.fail("http://go/tapl : " + message);
    }

    static void assertTrue(String message, boolean condition) {
        if (!condition) {
            fail(message);
        }
    }

    static void assertNotNull(String message, Object object) {
        assertTrue(message, object != null);
    }

    static private void failEquals(String message, Object actual) {
        fail(message + ". " + "Actual: " + actual);
    }

    static public void assertEquals(String message, int expected, int actual) {
        if (expected != actual) {
            fail(message + " expected: " + expected + " but was: " + actual);
        }
    }

    static void assertNotEquals(String message, int unexpected, int actual) {
        if (unexpected == actual) {
            failEquals(message, actual);
        }
    }

    public void setExpectedRotation(int expectedRotation) {
        mExpectedRotation = expectedRotation;
    }

    private UiObject2 verifyContainerType(ContainerType containerType) {
        assertEquals("Unexpected display rotation",
                mExpectedRotation, mDevice.getDisplayRotation());
        assertTrue("Presence of recents button doesn't match isSwipeUpEnabled()",
                isSwipeUpEnabled() ==
                        (mDevice.findObject(By.res(SYSTEMUI_PACKAGE, "recent_apps")) == null));
        log("verifyContainerType: " + containerType);

        switch (containerType) {
            case WORKSPACE: {
                waitForLauncherObject(APPS_RES_ID);
                waitUntilGone(OVERVIEW_RES_ID);
                waitUntilGone(WIDGETS_RES_ID);
                return waitForLauncherObject(WORKSPACE_RES_ID);
            }
            case WIDGETS: {
                waitUntilGone(WORKSPACE_RES_ID);
                waitUntilGone(APPS_RES_ID);
                waitUntilGone(OVERVIEW_RES_ID);
                return waitForLauncherObject(WIDGETS_RES_ID);
            }
            case ALL_APPS: {
                waitUntilGone(WORKSPACE_RES_ID);
                waitUntilGone(OVERVIEW_RES_ID);
                waitUntilGone(WIDGETS_RES_ID);
                return waitForLauncherObject(APPS_RES_ID);
            }
            case OVERVIEW: {
                if (mDevice.isNaturalOrientation()) {
                    waitForLauncherObject(APPS_RES_ID);
                } else {
                    waitUntilGone(APPS_RES_ID);
                }
                // Fall through
            }
            case BASE_OVERVIEW: {
                waitUntilGone(WORKSPACE_RES_ID);
                waitUntilGone(WIDGETS_RES_ID);

                return waitForLauncherObject(OVERVIEW_RES_ID);
            }
            case BACKGROUND: {
                waitUntilGone(WORKSPACE_RES_ID);
                waitUntilGone(APPS_RES_ID);
                waitUntilGone(OVERVIEW_RES_ID);
                waitUntilGone(WIDGETS_RES_ID);
                return null;
            }
            default:
                fail("Invalid state: " + containerType);
                return null;
        }
    }

    private Parcelable executeAndWaitForEvent(Runnable command,
            UiAutomation.AccessibilityEventFilter eventFilter, String message) {
        try {
            final AccessibilityEvent event =
                    mInstrumentation.getUiAutomation().executeAndWaitForEvent(
                            command, eventFilter, WAIT_TIME_MS);
            assertNotNull("executeAndWaitForEvent returned null (this can't happen)", event);
            return event.getParcelableData();
        } catch (TimeoutException e) {
            fail(message);
            return null;
        }
    }

    Bundle getAnswerFromLauncher(UiObject2 view, String requestTag) {
        // Send a fake set-text request to Launcher to initiate a response with requested data.
        final String responseTag = requestTag + TestProtocol.RESPONSE_MESSAGE_POSTFIX;
        return (Bundle) executeAndWaitForEvent(
                () -> view.setText(requestTag),
                event -> responseTag.equals(event.getClassName()),
                "Launcher didn't respond to request: " + requestTag);
    }

    /**
     * Presses nav bar home button.
     *
     * @return the Workspace object.
     */
    public Workspace pressHome() {
        // Click home, then wait for any accessibility event, then wait until accessibility events
        // stop.
        // We need waiting for any accessibility event generated after pressing Home because
        // otherwise waitForIdle may return immediately in case when there was a big enough pause in
        // accessibility events prior to pressing Home.
        executeAndWaitForEvent(
                () -> {
                    log("LauncherInstrumentation.pressHome before clicking");
                    getSystemUiObject("home").click();
                },
                event -> true,
                "Pressing Home didn't produce any events");
        mDevice.waitForIdle();
        return getWorkspace();
    }

    /**
     * Gets the Workspace object if the current state is "active home", i.e. workspace. Fails if the
     * launcher is not in that state.
     *
     * @return Workspace object.
     */
    @NonNull
    public Workspace getWorkspace() {
        return new Workspace(this);
    }

    /**
     * Gets the Workspace object if the current state is "background home", i.e. some other app is
     * active. Fails if the launcher is not in that state.
     *
     * @return Background object.
     */
    @NonNull
    public Background getBackground() {
        return new Background(this);
    }

    /**
     * Gets the Widgets object if the current state is showing all widgets. Fails if the launcher is
     * not in that state.
     *
     * @return Widgets object.
     */
    @NonNull
    public Widgets getAllWidgets() {
        return new Widgets(this);
    }

    /**
     * Gets the Overview object if the current state is showing the overview panel. Fails if the
     * launcher is not in that state.
     *
     * @return Overview object.
     */
    @NonNull
    public Overview getOverview() {
        return new Overview(this);
    }

    /**
     * Gets the Base overview object if either Launcher is in overview state or the fallback
     * overview activity is showing. Fails otherwise.
     *
     * @return BaseOverview object.
     */
    @NonNull
    public BaseOverview getBaseOverview() {
        return new BaseOverview(this);
    }

    /**
     * Gets the All Apps object if the current state is showing the all apps panel opened by swiping
     * from workspace. Fails if the launcher is not in that state. Please don't call this method if
     * App Apps was opened by swiping up from Overview, as it won't fail and will return an
     * incorrect object.
     *
     * @return All Aps object.
     */
    @NonNull
    public AllApps getAllApps() {
        return new AllApps(this);
    }

    /**
     * Gets the All Apps object if the current state is showing the all apps panel opened by swiping
     * from overview. Fails if the launcher is not in that state. Please don't call this method if
     * App Apps was opened by swiping up from home, as it won't fail and will return an
     * incorrect object.
     *
     * @return All Aps object.
     */
    @NonNull
    public AllAppsFromOverview getAllAppsFromOverview() {
        return new AllAppsFromOverview(this);
    }

    void waitUntilGone(String resId) {
        assertTrue("Unexpected launcher object visible: " + resId,
                mDevice.wait(Until.gone(getLauncherObjectSelector(resId)),
                        WAIT_TIME_MS));
    }

    @NonNull
    UiObject2 getSystemUiObject(String resId) {
        final UiObject2 object = mDevice.findObject(By.res(SYSTEMUI_PACKAGE, resId));
        assertNotNull("Can't find a systemui object with id: " + resId, object);
        return object;
    }

    @NonNull
    UiObject2 getObjectInContainer(UiObject2 container, BySelector selector) {
        final UiObject2 object = container.findObject(selector);
        assertNotNull("Can't find an object with selector: " + selector, object);
        return object;
    }

    @NonNull
    List<UiObject2> getObjectsInContainer(UiObject2 container, String resName) {
        return container.findObjects(getLauncherObjectSelector(resName));
    }

    @NonNull
    UiObject2 waitForObjectInContainer(UiObject2 container, String resName) {
        final UiObject2 object = container.wait(
                Until.findObject(getLauncherObjectSelector(resName)),
                WAIT_TIME_MS);
        assertNotNull("Can't find a launcher object id: " + resName + " in container: " +
                container.getResourceName(), object);
        return object;
    }

    @NonNull
    UiObject2 waitForLauncherObject(String resName) {
        final BySelector selector = getLauncherObjectSelector(resName);
        final UiObject2 object = mDevice.wait(Until.findObject(selector), WAIT_TIME_MS);
        assertNotNull("Can't find a launcher object; selector: " + selector, object);
        return object;
    }

    BySelector getLauncherObjectSelector(String resName) {
        return By.res(getLauncherPackageName(), resName);
    }

    String getLauncherPackageName() {
        return mDevice.getLauncherPackageName();
    }

    @NonNull
    UiDevice getDevice() {
        return mDevice;
    }

    void swipe(int startX, int startY, int endX, int endY) {
        executeAndWaitForEvent(
                () -> mDevice.swipe(startX, startY, endX, endY, 60),
                event -> TestProtocol.SWITCHED_TO_STATE_MESSAGE.equals(event.getClassName()),
                "Swipe failed to receive an event for the swipe end: " + startX + ", " + startY
                        + ", " + endX + ", " + endY);
    }

    void waitForIdle() {
        mDevice.waitForIdle();
    }

    void sendPointer(int action, Point point) {
        final MotionEvent event = MotionEvent.obtain(SystemClock.uptimeMillis(),
                SystemClock.uptimeMillis(), action, point.x, point.y, 0);
        mInstrumentation.sendPointerSync(event);
        event.recycle();
    }

    float getDisplayDensity() {
        return mInstrumentation.getTargetContext().getResources().getDisplayMetrics().density;
    }
}