1 /*
2  * Copyright (C) 2020 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 com.android.quickstep.util;
18 
19 import static android.util.DisplayMetrics.DENSITY_DEVICE_STABLE;
20 import static android.view.OrientationEventListener.ORIENTATION_UNKNOWN;
21 import static android.view.Surface.ROTATION_0;
22 import static android.view.Surface.ROTATION_180;
23 import static android.view.Surface.ROTATION_270;
24 import static android.view.Surface.ROTATION_90;
25 
26 import static com.android.launcher3.logging.LoggerUtils.extractObjectNameAndAddress;
27 import static com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY;
28 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
29 import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
30 
31 import static java.lang.annotation.RetentionPolicy.SOURCE;
32 
33 import android.content.ContentResolver;
34 import android.content.Context;
35 import android.content.SharedPreferences;
36 import android.content.res.Configuration;
37 import android.content.res.Resources;
38 import android.database.ContentObserver;
39 import android.graphics.Matrix;
40 import android.graphics.PointF;
41 import android.graphics.Rect;
42 import android.os.Handler;
43 import android.provider.Settings;
44 import android.util.Log;
45 import android.view.MotionEvent;
46 import android.view.OrientationEventListener;
47 import android.view.Surface;
48 
49 import androidx.annotation.IntDef;
50 import androidx.annotation.NonNull;
51 import androidx.annotation.Nullable;
52 
53 import com.android.launcher3.DeviceProfile;
54 import com.android.launcher3.InvariantDeviceProfile;
55 import com.android.launcher3.Utilities;
56 import com.android.launcher3.testing.TestProtocol;
57 import com.android.launcher3.touch.PagedOrientationHandler;
58 import com.android.launcher3.util.WindowBounds;
59 import com.android.quickstep.BaseActivityInterface;
60 import com.android.quickstep.SysUINavigationMode;
61 import com.android.systemui.shared.system.ConfigurationCompat;
62 
63 import java.lang.annotation.Retention;
64 import java.util.function.IntConsumer;
65 
66 /**
67  * Container to hold orientation/rotation related information for Launcher.
68  * This is not meant to be an abstraction layer for applying different functionality between
69  * the different orientation/rotations. For that see {@link PagedOrientationHandler}
70  *
71  * This class has initial default state assuming the device and foreground app have
72  * no ({@link Surface#ROTATION_0} rotation.
73  */
74 public final class RecentsOrientedState implements SharedPreferences.OnSharedPreferenceChangeListener {
75 
76     private static final String TAG = "RecentsOrientedState";
77     private static final boolean DEBUG = true;
78 
79     private ContentObserver mSystemAutoRotateObserver = new ContentObserver(new Handler()) {
80         @Override
81         public void onChange(boolean selfChange) {
82             updateAutoRotateSetting();
83         }
84     };
85     @Retention(SOURCE)
86     @IntDef({ROTATION_0, ROTATION_90, ROTATION_180, ROTATION_270})
87     public @interface SurfaceRotation {}
88 
89     private PagedOrientationHandler mOrientationHandler = PagedOrientationHandler.PORTRAIT;
90 
91     private @SurfaceRotation int mTouchRotation = ROTATION_0;
92     private @SurfaceRotation int mDisplayRotation = ROTATION_0;
93     private @SurfaceRotation int mRecentsActivityRotation = ROTATION_0;
94 
95     // Launcher activity supports multiple orientation, but fallback activity does not
96     private static final int FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_ACTIVITY = 1 << 0;
97     // Multiple orientation is only supported if density is < 600
98     private static final int FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_DENSITY = 1 << 1;
99     // Shared prefs for rotation, only if activity supports it
100     private static final int FLAG_HOME_ROTATION_ALLOWED_IN_PREFS = 1 << 2;
101     // If the user has enabled system rotation
102     private static final int FLAG_SYSTEM_ROTATION_ALLOWED = 1 << 3;
103     // Multiple orientation is not supported in multiwindow mode
104     private static final int FLAG_MULTIWINDOW_ROTATION_ALLOWED = 1 << 4;
105     // Whether to rotation sensor is supported on the device
106     private static final int FLAG_ROTATION_WATCHER_SUPPORTED = 1 << 5;
107     // Whether to enable rotation watcher when multi-rotation is supported
108     private static final int FLAG_ROTATION_WATCHER_ENABLED = 1 << 6;
109     // Enable home rotation for UI tests, ignoring home rotation value from prefs
110     private static final int FLAG_HOME_ROTATION_FORCE_ENABLED_FOR_TESTING = 1 << 7;
111     // Whether the swipe gesture is running, so the recents would stay locked in the
112     // current orientation
113     private static final int FLAG_SWIPE_UP_NOT_RUNNING = 1 << 8;
114 
115     private static final int MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE =
116             FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_ACTIVITY
117             | FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_DENSITY;
118 
119     // State for which rotation watcher will be enabled. We skip it when home rotation or
120     // multi-window is enabled as in that case, activity itself rotates.
121     private static final int VALUE_ROTATION_WATCHER_ENABLED =
122             MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE | FLAG_SYSTEM_ROTATION_ALLOWED
123                     | FLAG_ROTATION_WATCHER_SUPPORTED | FLAG_ROTATION_WATCHER_ENABLED
124                     | FLAG_SWIPE_UP_NOT_RUNNING;
125 
126     private final Context mContext;
127     private final ContentResolver mContentResolver;
128     private final SharedPreferences mSharedPrefs;
129     private final OrientationEventListener mOrientationListener;
130 
131     private final Matrix mTmpMatrix = new Matrix();
132 
133     private int mFlags;
134     private int mPreviousRotation = ROTATION_0;
135 
136     @Nullable private Configuration mActivityConfiguration;
137 
138     /**
139      * @param rotationChangeListener Callback for receiving rotation events when rotation watcher
140      *                              is enabled
141      * @see #setRotationWatcherEnabled(boolean)
142      */
RecentsOrientedState(Context context, BaseActivityInterface sizeStrategy, IntConsumer rotationChangeListener)143     public RecentsOrientedState(Context context, BaseActivityInterface sizeStrategy,
144             IntConsumer rotationChangeListener) {
145         mContext = context;
146         mContentResolver = context.getContentResolver();
147         mSharedPrefs = Utilities.getPrefs(context);
148         mOrientationListener = new OrientationEventListener(context) {
149             @Override
150             public void onOrientationChanged(int degrees) {
151                 int newRotation = getRotationForUserDegreesRotated(degrees, mPreviousRotation);
152                 if (newRotation != mPreviousRotation) {
153                     mPreviousRotation = newRotation;
154                     rotationChangeListener.accept(newRotation);
155                 }
156             }
157         };
158 
159         mFlags = sizeStrategy.rotationSupportedByActivity
160                 ? FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_ACTIVITY : 0;
161 
162         Resources res = context.getResources();
163         int originalSmallestWidth = res.getConfiguration().smallestScreenWidthDp
164                 * res.getDisplayMetrics().densityDpi / DENSITY_DEVICE_STABLE;
165         if (originalSmallestWidth < 600) {
166             mFlags |= FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_DENSITY;
167         }
168         mFlags |= FLAG_SWIPE_UP_NOT_RUNNING;
169         initFlags();
170     }
171 
172     /**
173      * Sets the configuration for the recents activity, which could affect the activity's rotation
174      * @see #update(int, int)
175      */
setActivityConfiguration(Configuration activityConfiguration)176     public boolean setActivityConfiguration(Configuration activityConfiguration) {
177         mActivityConfiguration = activityConfiguration;
178         return update(mTouchRotation, mDisplayRotation);
179     }
180 
181     /**
182      * Sets if the host is in multi-window mode
183      */
setMultiWindowMode(boolean isMultiWindow)184     public void setMultiWindowMode(boolean isMultiWindow) {
185         setFlag(FLAG_MULTIWINDOW_ROTATION_ALLOWED, isMultiWindow);
186     }
187 
188     /**
189      * Sets if the swipe up gesture is currently running or not
190      */
setGestureActive(boolean isGestureActive)191     public boolean setGestureActive(boolean isGestureActive) {
192         setFlag(FLAG_SWIPE_UP_NOT_RUNNING, !isGestureActive);
193         return update(mTouchRotation, mDisplayRotation);
194     }
195 
196     /**
197      * Sets the appropriate {@link PagedOrientationHandler} for {@link #mOrientationHandler}
198      * @param touchRotation The rotation the nav bar region that is touched is in
199      * @param displayRotation Rotation of the display/device
200      *
201      * @return true if there was any change in the internal state as a result of this call,
202      *         false otherwise
203      */
update( @urfaceRotation int touchRotation, @SurfaceRotation int displayRotation)204     public boolean update(
205             @SurfaceRotation int touchRotation, @SurfaceRotation int displayRotation) {
206         mRecentsActivityRotation = inferRecentsActivityRotation(displayRotation);
207         mDisplayRotation = displayRotation;
208         mTouchRotation = touchRotation;
209         mPreviousRotation = touchRotation;
210 
211         PagedOrientationHandler oldHandler = mOrientationHandler;
212         if (mRecentsActivityRotation == mTouchRotation
213                 || (canRecentsActivityRotate() && (mFlags & FLAG_SWIPE_UP_NOT_RUNNING) != 0)) {
214             mOrientationHandler = PagedOrientationHandler.PORTRAIT;
215             if (DEBUG) {
216                 Log.d(TAG, "current RecentsOrientedState: " + this);
217             }
218         } else if (mTouchRotation == ROTATION_90) {
219             mOrientationHandler = PagedOrientationHandler.LANDSCAPE;
220         } else if (mTouchRotation == ROTATION_270) {
221             mOrientationHandler = PagedOrientationHandler.SEASCAPE;
222         } else {
223             mOrientationHandler = PagedOrientationHandler.PORTRAIT;
224         }
225         if (DEBUG) {
226             Log.d(TAG, "current RecentsOrientedState: " + this);
227         }
228         return oldHandler != mOrientationHandler;
229     }
230 
231     @SurfaceRotation
inferRecentsActivityRotation(@urfaceRotation int displayRotation)232     private int inferRecentsActivityRotation(@SurfaceRotation int displayRotation) {
233         if (isRecentsActivityRotationAllowed()) {
234             return mActivityConfiguration == null
235                     ? displayRotation
236                     : ConfigurationCompat.getWindowConfigurationRotation(mActivityConfiguration);
237         } else {
238             return ROTATION_0;
239         }
240     }
241 
setFlag(int mask, boolean enabled)242     private void setFlag(int mask, boolean enabled) {
243         boolean wasRotationEnabled = !TestProtocol.sDisableSensorRotation
244                 && (mFlags & VALUE_ROTATION_WATCHER_ENABLED) == VALUE_ROTATION_WATCHER_ENABLED
245                 && !canRecentsActivityRotate();
246         if (enabled) {
247             mFlags |= mask;
248         } else {
249             mFlags &= ~mask;
250         }
251 
252         boolean isRotationEnabled = !TestProtocol.sDisableSensorRotation
253                 && (mFlags & VALUE_ROTATION_WATCHER_ENABLED) == VALUE_ROTATION_WATCHER_ENABLED
254                 && !canRecentsActivityRotate();
255         if (wasRotationEnabled != isRotationEnabled) {
256             UI_HELPER_EXECUTOR.execute(() -> {
257                 if (isRotationEnabled) {
258                     mOrientationListener.enable();
259                 } else {
260                     mOrientationListener.disable();
261                 }
262             });
263         }
264     }
265 
266     @Override
onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s)267     public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
268         if (ALLOW_ROTATION_PREFERENCE_KEY.equals(s)) {
269             updateHomeRotationSetting();
270         }
271     }
272 
updateAutoRotateSetting()273     private void updateAutoRotateSetting() {
274         setFlag(FLAG_SYSTEM_ROTATION_ALLOWED, Settings.System.getInt(mContentResolver,
275                 Settings.System.ACCELEROMETER_ROTATION, 1) == 1);
276     }
277 
updateHomeRotationSetting()278     private void updateHomeRotationSetting() {
279         setFlag(FLAG_HOME_ROTATION_ALLOWED_IN_PREFS,
280                 mSharedPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY, false));
281     }
282 
initFlags()283     private void initFlags() {
284         SysUINavigationMode.Mode currentMode = SysUINavigationMode.getMode(mContext);
285         boolean rotationWatcherSupported = mOrientationListener.canDetectOrientation() &&
286                 currentMode != TWO_BUTTONS;
287         setFlag(FLAG_ROTATION_WATCHER_SUPPORTED, rotationWatcherSupported);
288 
289         // initialize external flags
290         updateAutoRotateSetting();
291         updateHomeRotationSetting();
292     }
293 
294     /**
295      * Initializes any system values and registers corresponding change listeners. It must be
296      * paired with {@link #destroyListeners()} call
297      */
initListeners()298     public void initListeners() {
299         if (isMultipleOrientationSupportedByDevice()) {
300             mSharedPrefs.registerOnSharedPreferenceChangeListener(this);
301             mContentResolver.registerContentObserver(
302                     Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION),
303                     false, mSystemAutoRotateObserver);
304         }
305         initFlags();
306     }
307 
308     /**
309      * Unregisters any previously registered listeners.
310      */
destroyListeners()311     public void destroyListeners() {
312         if (isMultipleOrientationSupportedByDevice()) {
313             mSharedPrefs.unregisterOnSharedPreferenceChangeListener(this);
314             mContentResolver.unregisterContentObserver(mSystemAutoRotateObserver);
315         }
316         setRotationWatcherEnabled(false);
317     }
318 
forceAllowRotationForTesting(boolean forceAllow)319     public void forceAllowRotationForTesting(boolean forceAllow) {
320         setFlag(FLAG_HOME_ROTATION_FORCE_ENABLED_FOR_TESTING, forceAllow);
321     }
322 
323     @SurfaceRotation
getDisplayRotation()324     public int getDisplayRotation() {
325         return mDisplayRotation;
326     }
327 
328     @SurfaceRotation
getTouchRotation()329     public int getTouchRotation() {
330         return mTouchRotation;
331     }
332 
333     @SurfaceRotation
getRecentsActivityRotation()334     public int getRecentsActivityRotation() {
335         return mRecentsActivityRotation;
336     }
337 
isMultipleOrientationSupportedByDevice()338     public boolean isMultipleOrientationSupportedByDevice() {
339         return (mFlags & MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE)
340                 == MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE;
341     }
342 
isRecentsActivityRotationAllowed()343     public boolean isRecentsActivityRotationAllowed() {
344         // Activity rotation is allowed if the multi-simulated-rotation is not supported
345         // (fallback recents or tablets) or activity rotation is enabled by various settings.
346         return ((mFlags & MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE)
347                 != MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE)
348                 || (mFlags & (FLAG_HOME_ROTATION_ALLOWED_IN_PREFS
349                         | FLAG_MULTIWINDOW_ROTATION_ALLOWED
350                         | FLAG_HOME_ROTATION_FORCE_ENABLED_FOR_TESTING)) != 0;
351     }
352 
353     /**
354      * Returns true if the activity can rotate, if allowed by system rotation settings
355      */
canRecentsActivityRotate()356     public boolean canRecentsActivityRotate() {
357         return (mFlags & FLAG_SYSTEM_ROTATION_ALLOWED) != 0 && isRecentsActivityRotationAllowed();
358     }
359 
360     /**
361      * Enables or disables the rotation watcher for listening to rotation callbacks
362      */
setRotationWatcherEnabled(boolean isEnabled)363     public void setRotationWatcherEnabled(boolean isEnabled) {
364         setFlag(FLAG_ROTATION_WATCHER_ENABLED, isEnabled);
365     }
366 
367     /**
368      * Returns the scale and pivot so that the provided taskRect can fit the provided full size
369      */
getFullScreenScaleAndPivot(Rect taskView, DeviceProfile dp, PointF outPivot)370     public float getFullScreenScaleAndPivot(Rect taskView, DeviceProfile dp, PointF outPivot) {
371         Rect insets = dp.getInsets();
372         float fullWidth = dp.widthPx - insets.left - insets.right;
373         float fullHeight = dp.heightPx - insets.top - insets.bottom;
374 
375         if (dp.isMultiWindowMode) {
376             WindowBounds bounds = SplitScreenBounds.INSTANCE.getSecondaryWindowBounds(mContext);
377             outPivot.set(bounds.availableSize.x, bounds.availableSize.y);
378         } else {
379             outPivot.set(fullWidth, fullHeight);
380         }
381         float scale = Math.min(outPivot.x / taskView.width(), outPivot.y / taskView.height());
382         // We also scale the preview as part of fullScreenParams, so account for that as well.
383         if (fullWidth > 0) {
384             scale = scale * dp.widthPx / fullWidth;
385         }
386 
387         if (scale == 1) {
388             outPivot.set(fullWidth / 2, fullHeight / 2);
389         } else if (dp.isMultiWindowMode) {
390             float denominator = 1 / (scale - 1);
391             // Ensure that the task aligns to right bottom for the root view
392             float y = (scale * taskView.bottom - fullHeight) * denominator;
393             float x = (scale * taskView.right - fullWidth) * denominator;
394             outPivot.set(x, y);
395         } else {
396             float factor = scale / (scale - 1);
397             outPivot.set(taskView.left * factor, taskView.top * factor);
398         }
399         return scale;
400     }
401 
getOrientationHandler()402     public PagedOrientationHandler getOrientationHandler() {
403         return mOrientationHandler;
404     }
405 
406     /**
407      * For landscape, since the navbar is already in a vertical position, we don't have to do any
408      * rotations as the change in Y coordinate is what is read. We only flip the sign of the
409      * y coordinate to make it match existing behavior of swipe to the top to go previous
410      */
flipVertical(MotionEvent ev)411     public void flipVertical(MotionEvent ev) {
412         mTmpMatrix.setScale(1, -1);
413         ev.transform(mTmpMatrix);
414     }
415 
416     /**
417      * Creates a matrix to transform the given motion event specified by degrees.
418      * If inverse is {@code true}, the inverse of that matrix will be applied
419      */
transformEvent(float degrees, MotionEvent ev, boolean inverse)420     public void transformEvent(float degrees, MotionEvent ev, boolean inverse) {
421         mTmpMatrix.setRotate(inverse ? -degrees : degrees);
422         ev.transform(mTmpMatrix);
423 
424         // TODO: Add scaling back in based on degrees
425         /*
426         if (getWidth() > 0 && getHeight() > 0) {
427             float scale = ((float) getWidth()) / getHeight();
428             transform.postScale(scale, 1 / scale);
429         }
430         */
431     }
432 
433     @SurfaceRotation
getRotationForUserDegreesRotated(float degrees, int currentRotation)434     public static int getRotationForUserDegreesRotated(float degrees, int currentRotation) {
435         if (degrees == ORIENTATION_UNKNOWN) {
436             return currentRotation;
437         }
438 
439         int threshold = 70;
440         switch (currentRotation) {
441             case ROTATION_0:
442                 if (degrees > 180 && degrees < (360 - threshold)) {
443                     return ROTATION_90;
444                 }
445                 if (degrees < 180 && degrees > threshold) {
446                     return ROTATION_270;
447                 }
448                 break;
449             case ROTATION_270:
450                 if (degrees < (90 - threshold) ||
451                         (degrees > (270 + threshold) && degrees < 360)) {
452                     return ROTATION_0;
453                 }
454                 if (degrees > (90 + threshold) && degrees < 180) {
455                     return ROTATION_180;
456                 }
457                 // flip from seascape to landscape
458                 if (degrees > (180 + threshold) && degrees < 360) {
459                     return ROTATION_90;
460                 }
461                 break;
462             case ROTATION_180:
463                 if (degrees < (180 - threshold)) {
464                     return ROTATION_270;
465                 }
466                 if (degrees > (180 + threshold)) {
467                     return ROTATION_90;
468                 }
469                 break;
470             case ROTATION_90:
471                 if (degrees < (270 - threshold) && degrees > 90) {
472                     return ROTATION_180;
473                 }
474                 if (degrees > (270 + threshold) && degrees < 360
475                         || (degrees >= 0 && degrees < threshold)) {
476                     return ROTATION_0;
477                 }
478                 // flip from landscape to seascape
479                 if (degrees > threshold && degrees < 180) {
480                     return ROTATION_270;
481                 }
482                 break;
483         }
484 
485         return currentRotation;
486     }
487 
isDisplayPhoneNatural()488     public boolean isDisplayPhoneNatural() {
489         return mDisplayRotation == Surface.ROTATION_0 || mDisplayRotation == Surface.ROTATION_180;
490     }
491 
492     /**
493      * Posts the transformation on the matrix representing the provided display rotation
494      */
postDisplayRotation(@urfaceRotation int displayRotation, float screenWidth, float screenHeight, Matrix out)495     public static void postDisplayRotation(@SurfaceRotation int displayRotation,
496             float screenWidth, float screenHeight, Matrix out) {
497         switch (displayRotation) {
498             case ROTATION_0:
499                 return;
500             case ROTATION_90:
501                 out.postRotate(270);
502                 out.postTranslate(0, screenWidth);
503                 break;
504             case ROTATION_180:
505                 out.postRotate(180);
506                 out.postTranslate(screenHeight, screenWidth);
507                 break;
508             case ROTATION_270:
509                 out.postRotate(90);
510                 out.postTranslate(screenHeight, 0);
511                 break;
512         }
513     }
514 
515     @NonNull
516     @Override
toString()517     public String toString() {
518         boolean systemRotationOn = (mFlags & FLAG_SYSTEM_ROTATION_ALLOWED) != 0;
519         return "["
520                 + "this=" + extractObjectNameAndAddress(super.toString())
521                 + " mOrientationHandler=" +
522                     extractObjectNameAndAddress(mOrientationHandler.toString())
523                 + " mDisplayRotation=" + mDisplayRotation
524                 + " mTouchRotation=" + mTouchRotation
525                 + " mRecentsActivityRotation=" + mRecentsActivityRotation
526                 + " isRecentsActivityRotationAllowed=" + isRecentsActivityRotationAllowed()
527                 + " mSystemRotation=" + systemRotationOn
528                 + " mFlags=" + mFlags
529                 + "]";
530     }
531 
532     /**
533      * Returns the device profile based on expected launcher rotation
534      */
getLauncherDeviceProfile()535     public DeviceProfile getLauncherDeviceProfile() {
536         InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(mContext);
537         // TODO also check the natural orientation is landscape or portrait
538         return  (mRecentsActivityRotation == ROTATION_90
539                 || mRecentsActivityRotation == ROTATION_270)
540                 ? idp.landscapeProfile
541                 : idp.portraitProfile;
542     }
543 }
544