1 /*
2  * Copyright (C) 2018 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 package com.android.launcher3.states;
17 
18 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
19 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
20 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
21 import static android.util.DisplayMetrics.DENSITY_DEVICE_STABLE;
22 
23 import static com.android.launcher3.LauncherPrefs.ALLOW_ROTATION;
24 import static com.android.launcher3.Utilities.dpiFromPx;
25 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
26 import static com.android.launcher3.util.window.WindowManagerProxy.MIN_TABLET_WIDTH;
27 
28 import android.content.Context;
29 import android.content.SharedPreferences;
30 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
31 import android.os.Handler;
32 import android.os.Message;
33 
34 import androidx.annotation.NonNull;
35 import androidx.annotation.WorkerThread;
36 
37 import com.android.launcher3.BaseActivity;
38 import com.android.launcher3.DeviceProfile;
39 import com.android.launcher3.LauncherPrefs;
40 import com.android.launcher3.util.DisplayController;
41 
42 /**
43  * Utility class to manage launcher rotation
44  */
45 public class RotationHelper implements OnSharedPreferenceChangeListener,
46         DeviceProfile.OnDeviceProfileChangeListener,
47         DisplayController.DisplayInfoChangeListener {
48 
49     public static final String ALLOW_ROTATION_PREFERENCE_KEY = "pref_allowRotation";
50 
51     /**
52      * Returns the default value of {@link #ALLOW_ROTATION_PREFERENCE_KEY} preference.
53      */
getAllowRotationDefaultValue(DisplayController.Info info)54     public static boolean getAllowRotationDefaultValue(DisplayController.Info info) {
55         // If the device's pixel density was scaled (usually via settings for A11y), use the
56         // original dimensions to determine if rotation is allowed of not.
57         float originalSmallestWidth = dpiFromPx(Math.min(info.currentSize.x, info.currentSize.y),
58                 DENSITY_DEVICE_STABLE);
59         return originalSmallestWidth >= MIN_TABLET_WIDTH;
60     }
61 
62     public static final int REQUEST_NONE = 0;
63     public static final int REQUEST_ROTATE = 1;
64     public static final int REQUEST_LOCK = 2;
65 
66     @NonNull
67     private final BaseActivity mActivity;
68     private final Handler mRequestOrientationHandler;
69 
70     private boolean mIgnoreAutoRotateSettings;
71     private boolean mForceAllowRotationForTesting;
72     private boolean mHomeRotationEnabled;
73 
74     /**
75      * Rotation request made by
76      * {@link com.android.launcher3.util.ActivityTracker.SchedulerCallback}.
77      * This supersedes any other request.
78      */
79     private int mStateHandlerRequest = REQUEST_NONE;
80     /**
81      * Rotation request made by an app transition
82      */
83     private int mCurrentTransitionRequest = REQUEST_NONE;
84     /**
85      * Rotation request made by a Launcher State
86      */
87     private int mCurrentStateRequest = REQUEST_NONE;
88 
89     // This is used to defer setting rotation flags until the activity is being created
90     private boolean mInitialized;
91     private boolean mDestroyed;
92 
93     // Initialize mLastActivityFlags to a value not used by SCREEN_ORIENTATION flags
94     private int mLastActivityFlags = -999;
95 
RotationHelper(@onNull BaseActivity activity)96     public RotationHelper(@NonNull BaseActivity activity) {
97         mActivity = activity;
98         mRequestOrientationHandler =
99                 new Handler(UI_HELPER_EXECUTOR.getLooper(), this::setOrientationAsync);
100     }
101 
setIgnoreAutoRotateSettings(boolean ignoreAutoRotateSettings)102     private void setIgnoreAutoRotateSettings(boolean ignoreAutoRotateSettings) {
103         if (mDestroyed) return;
104         // On large devices we do not handle auto-rotate differently.
105         mIgnoreAutoRotateSettings = ignoreAutoRotateSettings;
106         if (!mIgnoreAutoRotateSettings) {
107             mHomeRotationEnabled = LauncherPrefs.get(mActivity).get(ALLOW_ROTATION);
108             LauncherPrefs.get(mActivity).addListener(this, ALLOW_ROTATION);
109         } else {
110             LauncherPrefs.get(mActivity).removeListener(this, ALLOW_ROTATION);
111         }
112     }
113 
114     @Override
onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s)115     public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
116         if (mDestroyed || mIgnoreAutoRotateSettings) return;
117         boolean wasRotationEnabled = mHomeRotationEnabled;
118         mHomeRotationEnabled = LauncherPrefs.get(mActivity).get(ALLOW_ROTATION);
119         if (mHomeRotationEnabled != wasRotationEnabled) {
120             notifyChange();
121         }
122     }
123 
124     /**
125      * Listening to both onDisplayInfoChanged and onDeviceProfileChanged to reduce delay. While
126      * onDeviceProfileChanged is triggered earlier, it only receives callback when Launcher is in
127      * the foreground. When in the background, we can still rely on onDisplayInfoChanged to update,
128      * assuming that the delay is tolerable since it takes time to change to foreground.
129      */
130     @Override
onDisplayInfoChanged(Context context, DisplayController.Info info, int flags)131     public void onDisplayInfoChanged(Context context, DisplayController.Info info, int flags) {
132         onIgnoreAutoRotateChanged(info.isTablet(info.realBounds));
133     }
134 
135     @Override
onDeviceProfileChanged(DeviceProfile dp)136     public void onDeviceProfileChanged(DeviceProfile dp) {
137         onIgnoreAutoRotateChanged(dp.isTablet);
138     }
139 
onIgnoreAutoRotateChanged(boolean ignoreAutoRotateSettings)140     private void onIgnoreAutoRotateChanged(boolean ignoreAutoRotateSettings) {
141         if (mDestroyed) return;
142         if (mIgnoreAutoRotateSettings != ignoreAutoRotateSettings) {
143             setIgnoreAutoRotateSettings(ignoreAutoRotateSettings);
144             notifyChange();
145         }
146     }
147 
setStateHandlerRequest(int request)148     public void setStateHandlerRequest(int request) {
149         if (mDestroyed || mStateHandlerRequest == request) return;
150         mStateHandlerRequest = request;
151         notifyChange();
152     }
153 
setCurrentTransitionRequest(int request)154     public void setCurrentTransitionRequest(int request) {
155         if (mDestroyed || mCurrentTransitionRequest == request) return;
156         mCurrentTransitionRequest = request;
157         notifyChange();
158     }
159 
setCurrentStateRequest(int request)160     public void setCurrentStateRequest(int request) {
161         if (mDestroyed || mCurrentStateRequest == request) return;
162         mCurrentStateRequest = request;
163         notifyChange();
164     }
165 
166     // Used by tests only.
forceAllowRotationForTesting(boolean allowRotation)167     public void forceAllowRotationForTesting(boolean allowRotation) {
168         if (mDestroyed) return;
169         mForceAllowRotationForTesting = allowRotation;
170         notifyChange();
171     }
172 
initialize()173     public void initialize() {
174         if (mInitialized) return;
175         mInitialized = true;
176         DisplayController displayController = DisplayController.INSTANCE.get(mActivity);
177         DisplayController.Info info = displayController.getInfo();
178         setIgnoreAutoRotateSettings(info.isTablet(info.realBounds));
179         displayController.addChangeListener(this);
180         mActivity.addOnDeviceProfileChangeListener(this);
181         notifyChange();
182     }
183 
destroy()184     public void destroy() {
185         if (mDestroyed) return;
186         mDestroyed = true;
187         mActivity.removeOnDeviceProfileChangeListener(this);
188         DisplayController.INSTANCE.get(mActivity).removeChangeListener(this);
189         LauncherPrefs.get(mActivity).removeListener(this, ALLOW_ROTATION);
190     }
191 
notifyChange()192     private void notifyChange() {
193         if (!mInitialized || mDestroyed) {
194             return;
195         }
196 
197         final int activityFlags;
198         if (mStateHandlerRequest != REQUEST_NONE) {
199             activityFlags = mStateHandlerRequest == REQUEST_LOCK ?
200                     SCREEN_ORIENTATION_LOCKED : SCREEN_ORIENTATION_UNSPECIFIED;
201         } else if (mCurrentTransitionRequest != REQUEST_NONE) {
202             activityFlags = mCurrentTransitionRequest == REQUEST_LOCK ?
203                     SCREEN_ORIENTATION_LOCKED : SCREEN_ORIENTATION_UNSPECIFIED;
204         } else if (mCurrentStateRequest == REQUEST_LOCK) {
205             activityFlags = SCREEN_ORIENTATION_LOCKED;
206         } else if (mIgnoreAutoRotateSettings || mCurrentStateRequest == REQUEST_ROTATE
207                 || mHomeRotationEnabled || mForceAllowRotationForTesting) {
208             activityFlags = SCREEN_ORIENTATION_UNSPECIFIED;
209         } else {
210             // If auto rotation is off, allow rotation on the activity, in case the user is using
211             // forced rotation.
212             activityFlags = SCREEN_ORIENTATION_NOSENSOR;
213         }
214         if (activityFlags != mLastActivityFlags) {
215             mLastActivityFlags = activityFlags;
216             mRequestOrientationHandler.sendEmptyMessage(activityFlags);
217         }
218     }
219 
220     @WorkerThread
setOrientationAsync(Message msg)221     private boolean setOrientationAsync(Message msg) {
222         if (mDestroyed) return true;
223         mActivity.setRequestedOrientation(msg.what);
224         return true;
225     }
226 
227     /**
228      * @return how many factors {@param newRotation} is rotated 90 degrees clockwise.
229      * E.g. 1->Rotated by 90 degrees clockwise, 2->Rotated 180 clockwise...
230      * A value of 0 means no rotation has been applied
231      */
deltaRotation(int oldRotation, int newRotation)232     public static int deltaRotation(int oldRotation, int newRotation) {
233         int delta = newRotation - oldRotation;
234         if (delta < 0) delta += 4;
235         return delta;
236     }
237 
238     @Override
toString()239     public String toString() {
240         return String.format("[mStateHandlerRequest=%d, mCurrentStateRequest=%d, "
241                         + "mLastActivityFlags=%d, mIgnoreAutoRotateSettings=%b, "
242                         + "mHomeRotationEnabled=%b, mForceAllowRotationForTesting=%b,"
243                         + " mDestroyed=%b]",
244                 mStateHandlerRequest, mCurrentStateRequest, mLastActivityFlags,
245                 mIgnoreAutoRotateSettings, mHomeRotationEnabled, mForceAllowRotationForTesting,
246                 mDestroyed);
247     }
248 }
249