summaryrefslogtreecommitdiff
path: root/quickstep/src/com/android/quickstep/RotationTouchHelper.java
blob: 678b17615175529cb824656add67fae76730d5ea (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
/*
 * Copyright (C) 2020 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.quickstep;

import static android.view.Surface.ROTATION_0;

import static com.android.launcher3.util.DisplayController.CHANGE_ACTIVE_SCREEN;
import static com.android.launcher3.util.DisplayController.CHANGE_ALL;
import static com.android.launcher3.util.DisplayController.CHANGE_ROTATION;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.quickstep.SysUINavigationMode.Mode.THREE_BUTTONS;

import android.content.Context;
import android.content.res.Resources;
import android.util.Log;
import android.view.MotionEvent;
import android.view.OrientationEventListener;

import com.android.launcher3.testing.TestProtocol;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener;
import com.android.launcher3.util.DisplayController.Info;
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.quickstep.util.RecentsOrientedState;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.TaskStackChangeListeners;

import java.io.PrintWriter;
import java.util.ArrayList;

public class RotationTouchHelper implements
        SysUINavigationMode.NavigationModeChangeListener,
        DisplayInfoChangeListener {

    public static final MainThreadInitializedObject<RotationTouchHelper> INSTANCE =
            new MainThreadInitializedObject<>(RotationTouchHelper::new);

    private OrientationTouchTransformer mOrientationTouchTransformer;
    private DisplayController mDisplayController;
    private SysUINavigationMode mSysUiNavMode;
    private int mDisplayId;
    private int mDisplayRotation;

    private final ArrayList<Runnable> mOnDestroyActions = new ArrayList<>();

    private SysUINavigationMode.Mode mMode = THREE_BUTTONS;

    private TaskStackChangeListener mFrozenTaskListener = new TaskStackChangeListener() {
        @Override
        public void onRecentTaskListFrozenChanged(boolean frozen) {
            mTaskListFrozen = frozen;
            if (frozen || mInOverview) {
                return;
            }
            enableMultipleRegions(false);
        }

        @Override
        public void onActivityRotation(int displayId) {
            super.onActivityRotation(displayId);
            // This always gets called before onDisplayInfoChanged() so we know how to process
            // the rotation in that method. This is done to avoid having a race condition between
            // the sensor readings and onDisplayInfoChanged() call
            if (displayId != mDisplayId) {
                return;
            }

            mPrioritizeDeviceRotation = true;
            if (mInOverview) {
                // reset, launcher must be rotating
                mExitOverviewRunnable.run();
            }
        }
    };

    private Runnable mExitOverviewRunnable = new Runnable() {
        @Override
        public void run() {
            mInOverview = false;
            enableMultipleRegions(false);
        }
    };

    /**
     * Used to listen for when the device rotates into the orientation of the current foreground
     * app. For example, if a user quickswitches from a portrait to a fixed landscape app and then
     * rotates rotates the device to match that orientation, this triggers calls to sysui to adjust
     * the navbar.
     */
    private OrientationEventListener mOrientationListener;
    private int mSensorRotation = ROTATION_0;
    /**
     * This is the configuration of the foreground app or the app that will be in the foreground
     * once a quickstep gesture finishes.
     */
    private int mCurrentAppRotation = -1;
    /**
     * This flag is set to true when the device physically changes orientations. When true, we will
     * always report the current rotation of the foreground app whenever the display changes, as it
     * would indicate the user's intention to rotate the foreground app.
     */
    private boolean mPrioritizeDeviceRotation = false;
    private Runnable mOnDestroyFrozenTaskRunnable;
    /**
     * Set to true when user swipes to recents. In recents, we ignore the state of the recents
     * task list being frozen or not to allow the user to keep interacting with nav bar rotation
     * they went into recents with as opposed to defaulting to the default display rotation.
     * TODO: (b/156984037) For when user rotates after entering overview
     */
    private boolean mInOverview;
    private boolean mTaskListFrozen;
    private final Context mContext;

    /**
     * Keeps track of whether destroy has been called for this instance. Mainly used for TAPL tests
     * where multiple instances of RotationTouchHelper are being created. b/177316094
     */
    private boolean mNeedsInit = true;

    private RotationTouchHelper(Context context) {
        mContext = context;
        if (mNeedsInit) {
            init();
        }
    }

    public void init() {
        if (!mNeedsInit) {
            return;
        }
        mDisplayController = DisplayController.INSTANCE.get(mContext);
        Resources resources = mContext.getResources();
        mSysUiNavMode = SysUINavigationMode.INSTANCE.get(mContext);
        mDisplayId = mDisplayController.getInfo().id;

        mOrientationTouchTransformer = new OrientationTouchTransformer(resources, mMode,
                () -> QuickStepContract.getWindowCornerRadius(resources));

        // Register for navigation mode changes
        SysUINavigationMode.Mode newMode = mSysUiNavMode.addModeChangeListener(this);
        onNavModeChangedInternal(newMode, newMode.hasGestures);
        runOnDestroy(() -> mSysUiNavMode.removeModeChangeListener(this));

        mOrientationListener = new OrientationEventListener(mContext) {
            @Override
            public void onOrientationChanged(int degrees) {
                int newRotation = RecentsOrientedState.getRotationForUserDegreesRotated(degrees,
                        mSensorRotation);
                if (newRotation == mSensorRotation) {
                    return;
                }

                mSensorRotation = newRotation;
                mPrioritizeDeviceRotation = true;

                if (newRotation == mCurrentAppRotation) {
                    // When user rotates device to the orientation of the foreground app after
                    // quickstepping
                    toggleSecondaryNavBarsForRotation();
                }
            }
        };
        mNeedsInit = false;
    }

    private void setupOrientationSwipeHandler() {
        TaskStackChangeListeners.getInstance().registerTaskStackListener(mFrozenTaskListener);
        mOnDestroyFrozenTaskRunnable = () -> TaskStackChangeListeners.getInstance()
                .unregisterTaskStackListener(mFrozenTaskListener);
        runOnDestroy(mOnDestroyFrozenTaskRunnable);
    }

    private void destroyOrientationSwipeHandlerCallback() {
        TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mFrozenTaskListener);
        mOnDestroyActions.remove(mOnDestroyFrozenTaskRunnable);
    }

    private void runOnDestroy(Runnable action) {
        mOnDestroyActions.add(action);
    }

    /**
     * Cleans up all the registered listeners and receivers.
     */
    public void destroy() {
        for (Runnable r : mOnDestroyActions) {
            r.run();
        }
        mNeedsInit = true;
    }

    public boolean isTaskListFrozen() {
        return mTaskListFrozen;
    }

    public boolean touchInAssistantRegion(MotionEvent ev) {
        return mOrientationTouchTransformer.touchInAssistantRegion(ev);
    }

    public boolean touchInOneHandedModeRegion(MotionEvent ev) {
        return mOrientationTouchTransformer.touchInOneHandedModeRegion(ev);
    }

    /**
     * Updates the regions for detecting the swipe up/quickswitch and assistant gestures.
     */
    public void updateGestureTouchRegions() {
        if (!mMode.hasGestures) {
            return;
        }

        mOrientationTouchTransformer.createOrAddTouchRegion(mDisplayController.getInfo());
    }

    /**
     * @return whether the coordinates of the {@param event} is in the swipe up gesture region.
     */
    public boolean isInSwipeUpTouchRegion(MotionEvent event) {
        return mOrientationTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY());
    }

    /**
     * @return whether the coordinates of the {@param event} with the given {@param pointerIndex}
     *         is in the swipe up gesture region.
     */
    public boolean isInSwipeUpTouchRegion(MotionEvent event, int pointerIndex) {
        return mOrientationTouchTransformer.touchInValidSwipeRegions(event.getX(pointerIndex),
                event.getY(pointerIndex));
    }


    @Override
    public void onNavigationModeChanged(SysUINavigationMode.Mode newMode) {
        onNavModeChangedInternal(newMode, false);
    }

    /**
     * @param forceRegister if {@code true}, this will register {@link #mFrozenTaskListener} via
     *                      {@link #setupOrientationSwipeHandler()}
     */
    private void onNavModeChangedInternal(SysUINavigationMode.Mode newMode, boolean forceRegister) {
        mDisplayController.removeChangeListener(this);
        mDisplayController.addChangeListener(this);
        onDisplayInfoChanged(mContext, mDisplayController.getInfo(), CHANGE_ALL);

        mOrientationTouchTransformer.setNavigationMode(newMode, mDisplayController.getInfo(),
                mContext.getResources());

        if (forceRegister || (!mMode.hasGestures && newMode.hasGestures)) {
            setupOrientationSwipeHandler();
        } else if (mMode.hasGestures && !newMode.hasGestures){
            destroyOrientationSwipeHandlerCallback();
        }

        mMode = newMode;
    }

    public int getDisplayRotation() {
        return mDisplayRotation;
    }

    @Override
    public void onDisplayInfoChanged(Context context, Info info, int flags) {
        if ((flags & (CHANGE_ROTATION | CHANGE_ACTIVE_SCREEN)) == 0) {
            return;
        }

        mDisplayRotation = info.rotation;

        if (!mMode.hasGestures) {
            return;
        }
        updateGestureTouchRegions();
        mOrientationTouchTransformer.createOrAddTouchRegion(info);
        mCurrentAppRotation = mDisplayRotation;

        /* Update nav bars on the following:
         * a) if this is coming from an activity rotation OR
         *   aa) we launch an app in the orientation that user is already in
         * b) We're not in overview, since overview will always be portrait (w/o home rotation)
         * c) We're actively in quickswitch mode
         */
        if ((mPrioritizeDeviceRotation
                || mCurrentAppRotation == mSensorRotation) // switch to an app of orientation user is in
                && !mInOverview
                && mTaskListFrozen) {
            toggleSecondaryNavBarsForRotation();
        }
    }

    /**
     * Sets the gestural height.
     */
    void setGesturalHeight(int newGesturalHeight) {
        mOrientationTouchTransformer.setGesturalHeight(
                newGesturalHeight, mDisplayController.getInfo(), mContext.getResources());
    }

    /**
     * *May* apply a transform on the motion event if it lies in the nav bar region for another
     * orientation that is currently being tracked as a part of quickstep
     */
    void setOrientationTransformIfNeeded(MotionEvent event) {
        // negative coordinates bug b/143901881
        if (event.getX() < 0 || event.getY() < 0) {
            event.setLocation(Math.max(0, event.getX()), Math.max(0, event.getY()));
        }
        mOrientationTouchTransformer.transform(event);
    }

    private void enableMultipleRegions(boolean enable) {
        mOrientationTouchTransformer.enableMultipleRegions(enable, mDisplayController.getInfo());
        notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getQuickStepStartingRotation());
        if (enable && !mInOverview && !TestProtocol.sDisableSensorRotation) {
            // Clear any previous state from sensor manager
            mSensorRotation = mCurrentAppRotation;
            mOrientationListener.enable();
        } else {
            mOrientationListener.disable();
        }
    }

    public void onStartGesture() {
        if (mTaskListFrozen) {
            // Prioritize whatever nav bar user touches once in quickstep
            // This case is specifically when user changes what nav bar they are using mid
            // quickswitch session before tasks list is unfrozen
            notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
        }
    }

    void onEndTargetCalculated(GestureState.GestureEndTarget endTarget,
            BaseActivityInterface activityInterface) {
        if (endTarget == GestureState.GestureEndTarget.RECENTS) {
            mInOverview = true;
            if (!mTaskListFrozen) {
                // If we're in landscape w/o ever quickswitching, show the navbar in landscape
                enableMultipleRegions(true);
            }
            activityInterface.onExitOverview(this, mExitOverviewRunnable);
        } else if (endTarget == GestureState.GestureEndTarget.HOME) {
            enableMultipleRegions(false);
        } else if (endTarget == GestureState.GestureEndTarget.NEW_TASK) {
            if (mOrientationTouchTransformer.getQuickStepStartingRotation() == -1) {
                // First gesture to start quickswitch
                enableMultipleRegions(true);
            } else {
                notifySysuiOfCurrentRotation(
                        mOrientationTouchTransformer.getCurrentActiveRotation());
            }

            // A new gesture is starting, reset the current device rotation
            // This is done under the assumption that the user won't rotate the phone and then
            // quickswitch in the old orientation.
            mPrioritizeDeviceRotation = false;
        } else if (endTarget == GestureState.GestureEndTarget.LAST_TASK) {
            if (!mTaskListFrozen) {
                // touched nav bar but didn't go anywhere and not quickswitching, do nothing
                return;
            }
            notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
        }
    }

    private void notifySysuiOfCurrentRotation(int rotation) {
        UI_HELPER_EXECUTOR.execute(() -> SystemUiProxy.INSTANCE.get(mContext)
                .notifyPrioritizedRotation(rotation));
    }

    /**
     * Disables/Enables multiple nav bars on {@link OrientationTouchTransformer} and then
     * notifies system UI of the primary rotation the user is interacting with
     */
    private void toggleSecondaryNavBarsForRotation() {
        mOrientationTouchTransformer.setSingleActiveRegion(mDisplayController.getInfo());
        notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
    }

    public int getCurrentActiveRotation() {
        if (!mMode.hasGestures) {
            // touch rotation should always match that of display for 3 button
            return mDisplayRotation;
        }
        return mOrientationTouchTransformer.getCurrentActiveRotation();
    }

    public void dump(PrintWriter pw) {
        pw.println("RotationTouchHelper:");
        pw.println("  currentActiveRotation=" + getCurrentActiveRotation());
        pw.println("  displayRotation=" + getDisplayRotation());
        mOrientationTouchTransformer.dump(pw);
    }

    public OrientationTouchTransformer getOrientationTouchTransformer() {
        return mOrientationTouchTransformer;
    }
}