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