/* * Copyright (C) 2018 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.launcher3.states; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; import static android.util.DisplayMetrics.DENSITY_DEVICE_STABLE; import static com.android.launcher3.LauncherPrefs.ALLOW_ROTATION; import static com.android.launcher3.Utilities.dpiFromPx; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static com.android.launcher3.util.window.WindowManagerProxy.MIN_TABLET_WIDTH; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.os.Handler; import android.os.Message; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import com.android.launcher3.BaseActivity; import com.android.launcher3.DeviceProfile; import com.android.launcher3.LauncherPrefs; import com.android.launcher3.util.DisplayController; /** * Utility class to manage launcher rotation */ public class RotationHelper implements OnSharedPreferenceChangeListener, DeviceProfile.OnDeviceProfileChangeListener, DisplayController.DisplayInfoChangeListener { public static final String ALLOW_ROTATION_PREFERENCE_KEY = "pref_allowRotation"; /** * Returns the default value of {@link #ALLOW_ROTATION_PREFERENCE_KEY} preference. */ public static boolean getAllowRotationDefaultValue(DisplayController.Info info) { // If the device's pixel density was scaled (usually via settings for A11y), use the // original dimensions to determine if rotation is allowed of not. float originalSmallestWidth = dpiFromPx(Math.min(info.currentSize.x, info.currentSize.y), DENSITY_DEVICE_STABLE); return originalSmallestWidth >= MIN_TABLET_WIDTH; } public static final int REQUEST_NONE = 0; public static final int REQUEST_ROTATE = 1; public static final int REQUEST_LOCK = 2; @NonNull private final BaseActivity mActivity; private final Handler mRequestOrientationHandler; private boolean mIgnoreAutoRotateSettings; private boolean mForceAllowRotationForTesting; private boolean mHomeRotationEnabled; /** * Rotation request made by * {@link com.android.launcher3.util.ActivityTracker.SchedulerCallback}. * This supersedes any other request. */ private int mStateHandlerRequest = REQUEST_NONE; /** * Rotation request made by an app transition */ private int mCurrentTransitionRequest = REQUEST_NONE; /** * Rotation request made by a Launcher State */ private int mCurrentStateRequest = REQUEST_NONE; // This is used to defer setting rotation flags until the activity is being created private boolean mInitialized; private boolean mDestroyed; // Initialize mLastActivityFlags to a value not used by SCREEN_ORIENTATION flags private int mLastActivityFlags = -999; public RotationHelper(@NonNull BaseActivity activity) { mActivity = activity; mRequestOrientationHandler = new Handler(UI_HELPER_EXECUTOR.getLooper(), this::setOrientationAsync); } private void setIgnoreAutoRotateSettings(boolean ignoreAutoRotateSettings) { if (mDestroyed) return; // On large devices we do not handle auto-rotate differently. mIgnoreAutoRotateSettings = ignoreAutoRotateSettings; if (!mIgnoreAutoRotateSettings) { mHomeRotationEnabled = LauncherPrefs.get(mActivity).get(ALLOW_ROTATION); LauncherPrefs.get(mActivity).addListener(this, ALLOW_ROTATION); } else { LauncherPrefs.get(mActivity).removeListener(this, ALLOW_ROTATION); } } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) { if (mDestroyed || mIgnoreAutoRotateSettings) return; boolean wasRotationEnabled = mHomeRotationEnabled; mHomeRotationEnabled = LauncherPrefs.get(mActivity).get(ALLOW_ROTATION); if (mHomeRotationEnabled != wasRotationEnabled) { notifyChange(); } } /** * Listening to both onDisplayInfoChanged and onDeviceProfileChanged to reduce delay. While * onDeviceProfileChanged is triggered earlier, it only receives callback when Launcher is in * the foreground. When in the background, we can still rely on onDisplayInfoChanged to update, * assuming that the delay is tolerable since it takes time to change to foreground. */ @Override public void onDisplayInfoChanged(Context context, DisplayController.Info info, int flags) { onIgnoreAutoRotateChanged(info.isTablet(info.realBounds)); } @Override public void onDeviceProfileChanged(DeviceProfile dp) { onIgnoreAutoRotateChanged(dp.isTablet); } private void onIgnoreAutoRotateChanged(boolean ignoreAutoRotateSettings) { if (mDestroyed) return; if (mIgnoreAutoRotateSettings != ignoreAutoRotateSettings) { setIgnoreAutoRotateSettings(ignoreAutoRotateSettings); notifyChange(); } } public void setStateHandlerRequest(int request) { if (mDestroyed || mStateHandlerRequest == request) return; mStateHandlerRequest = request; notifyChange(); } public void setCurrentTransitionRequest(int request) { if (mDestroyed || mCurrentTransitionRequest == request) return; mCurrentTransitionRequest = request; notifyChange(); } public void setCurrentStateRequest(int request) { if (mDestroyed || mCurrentStateRequest == request) return; mCurrentStateRequest = request; notifyChange(); } // Used by tests only. public void forceAllowRotationForTesting(boolean allowRotation) { if (mDestroyed) return; mForceAllowRotationForTesting = allowRotation; notifyChange(); } public void initialize() { if (mInitialized) return; mInitialized = true; DisplayController displayController = DisplayController.INSTANCE.get(mActivity); DisplayController.Info info = displayController.getInfo(); setIgnoreAutoRotateSettings(info.isTablet(info.realBounds)); displayController.addChangeListener(this); mActivity.addOnDeviceProfileChangeListener(this); notifyChange(); } public void destroy() { if (mDestroyed) return; mDestroyed = true; mActivity.removeOnDeviceProfileChangeListener(this); DisplayController.INSTANCE.get(mActivity).removeChangeListener(this); LauncherPrefs.get(mActivity).removeListener(this, ALLOW_ROTATION); } private void notifyChange() { if (!mInitialized || mDestroyed) { return; } final int activityFlags; if (mStateHandlerRequest != REQUEST_NONE) { activityFlags = mStateHandlerRequest == REQUEST_LOCK ? SCREEN_ORIENTATION_LOCKED : SCREEN_ORIENTATION_UNSPECIFIED; } else if (mCurrentTransitionRequest != REQUEST_NONE) { activityFlags = mCurrentTransitionRequest == REQUEST_LOCK ? SCREEN_ORIENTATION_LOCKED : SCREEN_ORIENTATION_UNSPECIFIED; } else if (mCurrentStateRequest == REQUEST_LOCK) { activityFlags = SCREEN_ORIENTATION_LOCKED; } else if (mIgnoreAutoRotateSettings || mCurrentStateRequest == REQUEST_ROTATE || mHomeRotationEnabled || mForceAllowRotationForTesting) { activityFlags = SCREEN_ORIENTATION_UNSPECIFIED; } else { // If auto rotation is off, allow rotation on the activity, in case the user is using // forced rotation. activityFlags = SCREEN_ORIENTATION_NOSENSOR; } if (activityFlags != mLastActivityFlags) { mLastActivityFlags = activityFlags; mRequestOrientationHandler.sendEmptyMessage(activityFlags); } } @WorkerThread private boolean setOrientationAsync(Message msg) { if (mDestroyed) return true; mActivity.setRequestedOrientation(msg.what); return true; } /** * @return how many factors {@param newRotation} is rotated 90 degrees clockwise. * E.g. 1->Rotated by 90 degrees clockwise, 2->Rotated 180 clockwise... * A value of 0 means no rotation has been applied */ public static int deltaRotation(int oldRotation, int newRotation) { int delta = newRotation - oldRotation; if (delta < 0) delta += 4; return delta; } @Override public String toString() { return String.format("[mStateHandlerRequest=%d, mCurrentStateRequest=%d, " + "mLastActivityFlags=%d, mIgnoreAutoRotateSettings=%b, " + "mHomeRotationEnabled=%b, mForceAllowRotationForTesting=%b," + " mDestroyed=%b]", mStateHandlerRequest, mCurrentStateRequest, mLastActivityFlags, mIgnoreAutoRotateSettings, mHomeRotationEnabled, mForceAllowRotationForTesting, mDestroyed); } }