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