Move floating rotation button handling to Launcher

Moves handling of floating rotation button when navigation
bar is not created to the launcher. This button was not
showing when taskbar is visible as it was initialized in
navigation bar (which is not created for large screens).

Bug: 200103245
Test: rotate phone when autorotate disabled on inner screen
Test: showing rotate suggestion when gesture nav enabled/disabled
Change-Id: I13dd555bcd811f1524be7ab9ad51b2b012b3b749
This commit is contained in:
Nick Chameyev 2021-10-21 15:31:51 +01:00
parent e6a04336eb
commit 0288d2e8bb
11 changed files with 81 additions and 594 deletions

View File

@ -30,6 +30,9 @@
<color name="taskbar_stashed_handle_light_color">#EBffffff</color>
<color name="taskbar_stashed_handle_dark_color">#99000000</color>
<color name="rotation_button_light_color">#FFF</color>
<color name="rotation_button_dark_color">#99000000</color>
<!-- Gesture navigation tutorial -->
<color name="gesture_tutorial_back_arrow_color">#FFFFFFFF</color>

View File

@ -208,6 +208,9 @@
<!-- Button text shown on a button on the tutorial skip dialog to exit the tutorial. [CHAR LIMIT=14] -->
<string name="gesture_tutorial_action_button_label_skip">Skip</string>
<!-- Accessibility label for the rotation suggestion button -->
<string name="accessibility_rotate_button">Rotate screen</string>
<!-- ******* Taskbar Edu ******* -->
<!-- Accessibility text spoken when the taskbar education panel appears [CHAR_LIMIT=NONE] -->
<string name="taskbar_edu_opened">Taskbar education appeared</string>

View File

@ -345,7 +345,8 @@ public class LauncherTaskbarUIController extends TaskbarUIController {
* @return Whether any Taskbar item could handle the given MotionEvent if given the chance.
*/
public boolean isEventOverAnyTaskbarItem(MotionEvent ev) {
return mControllers.taskbarViewController.isEventOverAnyItem(ev);
return mControllers.taskbarViewController.isEventOverAnyItem(ev)
|| mControllers.navbarButtonsViewController.isEventOverAnyItem(ev);
}
public boolean isDraggingItem() {

View File

@ -15,6 +15,8 @@
*/
package com.android.launcher3.taskbar;
import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_A11Y;
import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_A11Y_LONG_CLICK;
@ -38,6 +40,7 @@ import android.animation.ObjectAnimator;
import android.annotation.DrawableRes;
import android.annotation.IdRes;
import android.annotation.LayoutRes;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Rect;
import android.graphics.Region;
@ -45,6 +48,7 @@ import android.graphics.Region.Op;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.util.Property;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnHoverListener;
@ -57,11 +61,12 @@ import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AlphaUpdateListener;
import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarButton;
import com.android.launcher3.taskbar.contextual.RotationButton;
import com.android.launcher3.taskbar.contextual.RotationButtonController;
import com.android.launcher3.util.MultiValueAlpha;
import com.android.launcher3.util.Themes;
import com.android.quickstep.AnimatedFloat;
import com.android.systemui.shared.rotation.FloatingRotationButton;
import com.android.systemui.shared.rotation.RotationButton;
import com.android.systemui.shared.rotation.RotationButtonController;
import java.util.ArrayList;
import java.util.function.IntPredicate;
@ -103,12 +108,16 @@ public class NavbarButtonsViewController {
this::updateNavButtonTranslationY);
private final AnimatedFloat mNavButtonTranslationYMultiplier = new AnimatedFloat(
this::updateNavButtonTranslationY);
private final RotationButtonListener mRotationButtonListener = new RotationButtonListener();
private final Rect mFloatingRotationButtonBounds = new Rect();
// Initialized in init.
private TaskbarControllers mControllers;
private View mA11yButton;
private int mSysuiStateFlags;
private View mBackButton;
private FloatingRotationButton mFloatingRotationButton;
public NavbarButtonsViewController(TaskbarActivityContext context, FrameLayout navButtonsView) {
mContext = context;
@ -198,9 +207,13 @@ public class NavbarButtonsViewController {
addButton(mEndContextualContainer, R.id.rotate_suggestion,
R.layout.taskbar_contextual_button));
rotationButton.hide();
mControllers.rotationButtonController.setRotationButton(rotationButton);
mControllers.rotationButtonController.setRotationButton(rotationButton, null);
} else {
mControllers.rotationButtonController.setRotationButton(new RotationButton() {});
mFloatingRotationButton = new FloatingRotationButton(mContext,
R.string.accessibility_rotate_button);
mControllers.rotationButtonController.setRotationButton(mFloatingRotationButton,
mRotationButtonListener);
View imeDownButton = addButton(R.drawable.ic_sysbar_back, BUTTON_BACK,
mStartContextualContainer, mControllers.navButtonController, R.id.back);
imeDownButton.setRotation(Utilities.isRtl(mContext.getResources()) ? 90 : -90);
@ -405,8 +418,28 @@ public class NavbarButtonsViewController {
return buttonView;
}
public boolean isEventOverAnyItem(MotionEvent ev) {
return mFloatingRotationButtonBounds.contains((int) ev.getX(), (int) ev.getY());
}
public void onDestroy() {
mPropertyHolders.clear();
mControllers.rotationButtonController.unregisterListeners();
if (mFloatingRotationButton != null) {
mFloatingRotationButton.hide();
}
}
private class RotationButtonListener implements RotationButton.RotationButtonUpdatesCallback {
@Override
public void onVisibilityChanged(boolean isVisible) {
if (isVisible) {
mFloatingRotationButton.getCurrentView()
.getBoundsOnScreen(mFloatingRotationButtonBounds);
} else {
mFloatingRotationButtonBounds.setEmpty();
}
}
}
private class RotationButtonImpl implements RotationButton {
@ -424,6 +457,8 @@ public class NavbarButtonsViewController {
mImageDrawable = (AnimatedVectorDrawable) mButton.getContext()
.getDrawable(rotationButtonController.getIconResId());
mButton.setImageDrawable(mImageDrawable);
mButton.setContentDescription(mButton.getResources()
.getString(R.string.accessibility_rotate_button));
mImageDrawable.setCallback(mButton);
}
@ -433,17 +468,19 @@ public class NavbarButtonsViewController {
}
@Override
public void show() {
public boolean show() {
mButton.setVisibility(View.VISIBLE);
mState |= FLAG_ROTATION_BUTTON_VISIBLE;
applyState();
return true;
}
@Override
public void hide() {
public boolean hide() {
mButton.setVisibility(View.GONE);
mState &= ~FLAG_ROTATION_BUTTON_VISIBLE;
applyState();
return true;
}
@Override

View File

@ -60,7 +60,6 @@ import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.logger.LauncherAtom;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.taskbar.contextual.RotationButtonController;
import com.android.launcher3.touch.ItemClickHandler;
import com.android.launcher3.util.PackageManagerHelper;
import com.android.launcher3.util.SettingsCache;
@ -71,6 +70,7 @@ import com.android.launcher3.views.ActivityContext;
import com.android.quickstep.SysUINavigationMode;
import com.android.quickstep.SysUINavigationMode.Mode;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.rotation.RotationButtonController;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.WindowManagerWrapper;
import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider;
@ -147,8 +147,14 @@ public class TaskbarActivityContext extends ContextThemeWrapper implements Activ
new TaskbarDragController(this),
buttonController,
new NavbarButtonsViewController(this, navButtonsView),
new RotationButtonController(this, R.color.popup_color_primary_light,
R.color.popup_color_primary_light),
new RotationButtonController(this,
c.getColor(R.color.rotation_button_light_color),
c.getColor(R.color.rotation_button_dark_color),
R.drawable.ic_sysbar_rotate_button_ccw_start_0,
R.drawable.ic_sysbar_rotate_button_ccw_start_90,
R.drawable.ic_sysbar_rotate_button_cw_start_0,
R.drawable.ic_sysbar_rotate_button_cw_start_90,
() -> getDisplay().getRotation()),
new TaskbarDragLayerController(this, mDragLayer),
new TaskbarViewController(this, taskbarView),
new TaskbarScrimViewController(this, taskbarScrimView),

View File

@ -17,7 +17,7 @@ package com.android.launcher3.taskbar;
import androidx.annotation.NonNull;
import com.android.launcher3.taskbar.contextual.RotationButtonController;
import com.android.systemui.shared.rotation.RotationButtonController;
/**
* Hosts various taskbar controllers to facilitate passing between one another.
@ -80,9 +80,7 @@ public class TaskbarControllers {
public void init(TaskbarSharedState sharedState) {
taskbarDragController.init(this);
navbarButtonsViewController.init(this);
if (taskbarActivityContext.isThreeButtonNav()) {
rotationButtonController.init();
}
rotationButtonController.init();
taskbarDragLayerController.init(this);
taskbarViewController.init(this);
taskbarScrimViewController.init(this);

View File

@ -16,8 +16,7 @@
package com.android.launcher3.taskbar;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
import static com.android.launcher3.util.DisplayController.CHANGE_ACTIVE_SCREEN;
import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
import static com.android.launcher3.util.DisplayController.CHANGE_SUPPORTED_BOUNDS;
@ -91,7 +90,7 @@ public class TaskbarManager implements DisplayController.DisplayInfoChangeListen
mSysUINavigationMode = SysUINavigationMode.INSTANCE.get(service);
Display display =
service.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY);
mContext = service.createWindowContext(display, TYPE_APPLICATION_OVERLAY, null);
mContext = service.createWindowContext(display, TYPE_NAVIGATION_BAR_PANEL, null);
mNavButtonController = new TaskbarNavButtonController(service);
mUserSetupCompleteListener = isUserSetupComplete -> recreateTaskbar();
mComponentCallbacks = new ComponentCallbacks() {

View File

@ -94,6 +94,7 @@ public class TaskbarStashController {
private final SharedPreferences mPrefs;
private final int mStashedHeight;
private final int mUnstashedHeight;
private final SystemUiProxy mSystemUiProxy;
// Initialized in init.
private TaskbarControllers mControllers;
@ -127,6 +128,7 @@ public class TaskbarStashController {
mPrefs = Utilities.getPrefs(mActivity);
final Resources resources = mActivity.getResources();
mStashedHeight = resources.getDimensionPixelSize(R.dimen.taskbar_stashed_size);
mSystemUiProxy = SystemUiProxy.INSTANCE.get(activity);
mUnstashedHeight = mActivity.getDeviceProfile().taskbarSize;
}
@ -155,8 +157,7 @@ public class TaskbarStashController {
!mActivity.isUserSetupComplete() || sharedState.setupUIVisible);
applyState();
SystemUiProxy.INSTANCE.get(mActivity)
.notifyTaskbarStatus(/* visible */ false, /* stashed */ isStashedInApp());
notifyStashChange(/* visible */ false, /* stashed */ isStashedInApp());
}
/**
@ -440,8 +441,7 @@ public class TaskbarStashController {
mControllers.uiController.onStashedInAppChanged();
}
if (hasAnyFlag(changedFlags, FLAGS_STASHED_IN_APP | FLAG_IN_APP)) {
SystemUiProxy.INSTANCE.get(mActivity)
.notifyTaskbarStatus(/* visible */ hasAnyFlag(FLAG_IN_APP),
notifyStashChange(/* visible */ hasAnyFlag(FLAG_IN_APP),
/* stashed */ isStashedInApp());
}
if (hasAnyFlag(changedFlags, FLAG_STASHED_IN_APP_MANUAL)) {
@ -453,6 +453,11 @@ public class TaskbarStashController {
}
}
private void notifyStashChange(boolean visible, boolean stashed) {
mSystemUiProxy.notifyTaskbarStatus(visible, stashed);
mControllers.rotationButtonController.onTaskbarStateChange(visible, stashed);
}
private class StatePropertyHolder {
private final IntPredicate mStashCondition;

View File

@ -1,54 +0,0 @@
/*
* Copyright 2021 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.taskbar.contextual;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.view.View;
/**
* Interface of a rotation button that interacts {@link RotationButtonController}.
* This interface exists because of the two different styles of rotation button in Sysui,
* one in contextual for 3 button nav and a floating rotation button for gestural.
* Keeping the interface for eventual migration of floating button, so some methods are
* pass through to "super" while others are trivially implemented.
*
* Changes:
* * Directly use AnimatedVectorDrawable instead of KeyButtonDrawable
*/
public interface RotationButton {
default void setRotationButtonController(RotationButtonController rotationButtonController) { }
default View getCurrentView() {
return null;
}
default void show() { }
default void hide() { }
default boolean isVisible() {
return false;
}
default void updateIcon(int lightIconColor, int darkIconColor) { }
default void setOnClickListener(View.OnClickListener onClickListener) { }
default void setOnHoverListener(View.OnHoverListener onHoverListener) { }
default AnimatedVectorDrawable getImageDrawable() {
return null;
}
default void setDarkIntensity(float darkIntensity) { }
default boolean acceptRotationProposal() {
return getCurrentView() != null;
}
}

View File

@ -1,512 +0,0 @@
/*
* Copyright 2021 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.taskbar.contextual;
import static android.view.Display.DEFAULT_DISPLAY;
import static com.android.internal.view.RotationPolicy.NATURAL_ROTATION;
import static com.android.launcher3.anim.Interpolators.LINEAR;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.annotation.ColorInt;
import android.annotation.DrawableRes;
import android.annotation.SuppressLint;
import android.app.StatusBarManager;
import android.content.ContentResolver;
import android.content.Context;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.provider.Settings;
import android.util.Log;
import android.view.IRotationWatcher;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.View;
import android.view.WindowInsetsController;
import android.view.WindowManagerGlobal;
import android.view.accessibility.AccessibilityManager;
import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.logging.UiEventLoggerImpl;
import com.android.internal.view.RotationPolicy;
import com.android.launcher3.R;
import com.android.launcher3.util.DisplayController;
import com.android.systemui.shared.recents.utilities.Utilities;
import com.android.systemui.shared.recents.utilities.ViewRippler;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.TaskStackChangeListeners;
import java.util.Optional;
/**
* Copied over from the SysUI equivalent class. Known issues/things not ported over
* * When rotation button visible and in auto-hide mode, we ask auto-hide controller to
* keep the navbar around longer. Will need to implement if we use auto-hide on taskbar
*
* Contains logic that deals with showing a rotate suggestion button with animation.
*/
public class RotationButtonController {
private static final String TAG = "StatusBar/RotationButtonController";
private static final int BUTTON_FADE_IN_OUT_DURATION_MS = 100;
private static final int NAVBAR_HIDDEN_PENDING_ICON_TIMEOUT_MS = 20000;
private static final int NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION = 3;
private final Context mContext;
private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
private final UiEventLogger mUiEventLogger = new UiEventLoggerImpl();
private final ViewRippler mViewRippler = new ViewRippler();
private final DisplayController mDisplayController;
private RotationButton mRotationButton;
private int mLastRotationSuggestion;
private boolean mPendingRotationSuggestion;
private boolean mHoveringRotationSuggestion;
private final AccessibilityManager mAccessibilityManager;
private final TaskStackListenerImpl mTaskStackListener;
private boolean mListenersRegistered = false;
private boolean mIsTaskbarShowing;
@SuppressLint("InlinedApi")
private @WindowInsetsController.Behavior
int mBehavior = WindowInsetsController.BEHAVIOR_DEFAULT;
private boolean mSkipOverrideUserLockPrefsOnce;
private final int mLightIconColor;
private final int mDarkIconColor;
private int mIconResId = R.drawable.ic_sysbar_rotate_button_ccw_start_90;
private final Runnable mRemoveRotationProposal =
() -> setRotateSuggestionButtonState(false /* visible */);
private final Runnable mCancelPendingRotationProposal =
() -> mPendingRotationSuggestion = false;
private Animator mRotateHideAnimator;
private final IRotationWatcher.Stub mRotationWatcher = new IRotationWatcher.Stub() {
@Override
public void onRotationChanged(final int rotation) {
// We need this to be scheduled as early as possible to beat the redrawing of
// window in response to the orientation change.
mMainThreadHandler.postAtFrontOfQueue(() -> {
// If the screen rotation changes while locked, potentially update lock to flow with
// new screen rotation and hide any showing suggestions.
if (isRotationLocked()) {
if (shouldOverrideUserLockPrefs(rotation)) {
setRotationLockedAtAngle(rotation);
}
setRotateSuggestionButtonState(false /* visible */, true /* forced */);
}
});
}
};
/**
* Determines if rotation suggestions disabled2 flag exists in flag
* @param disable2Flags see if rotation suggestion flag exists in this flag
* @return whether flag exists
*/
static boolean hasDisable2RotateSuggestionFlag(int disable2Flags) {
return (disable2Flags & StatusBarManager.DISABLE2_ROTATE_SUGGESTIONS) != 0;
}
public RotationButtonController(Context context, @ColorInt int lightIconColor,
@ColorInt int darkIconColor) {
mContext = context;
mLightIconColor = lightIconColor;
mDarkIconColor = darkIconColor;
mAccessibilityManager = AccessibilityManager.getInstance(context);
mTaskStackListener = new TaskStackListenerImpl();
mDisplayController = DisplayController.INSTANCE.get(context);
}
public void setRotationButton(RotationButton rotationButton) {
mRotationButton = rotationButton;
mRotationButton.setRotationButtonController(this);
mRotationButton.setOnClickListener(this::onRotateSuggestionClick);
mRotationButton.setOnHoverListener(this::onRotateSuggestionHover);
}
public void init() {
registerListeners();
if (mContext.getDisplay().getDisplayId() != DEFAULT_DISPLAY) {
// Currently there is no accelerometer sensor on non-default display, disable fixed
// rotation for non-default display
onDisable2FlagChanged(StatusBarManager.DISABLE2_ROTATE_SUGGESTIONS);
}
}
public void onDestroy() {
unregisterListeners();
}
private void registerListeners() {
if (mListenersRegistered) {
return;
}
mListenersRegistered = true;
try {
WindowManagerGlobal.getWindowManagerService()
.watchRotation(mRotationWatcher, DEFAULT_DISPLAY);
} catch (IllegalArgumentException e) {
mListenersRegistered = false;
Log.w(TAG, "RegisterListeners for the display failed");
} catch (RemoteException e) {
Log.e(TAG, "RegisterListeners caught a RemoteException", e);
return;
}
TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener);
}
void unregisterListeners() {
if (!mListenersRegistered) {
return;
}
mListenersRegistered = false;
try {
WindowManagerGlobal.getWindowManagerService().removeRotationWatcher(mRotationWatcher);
} catch (RemoteException e) {
Log.e(TAG, "UnregisterListeners caught a RemoteException", e);
return;
}
TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener);
}
void setRotationLockedAtAngle(int rotationSuggestion) {
RotationPolicy.setRotationLockAtAngle(mContext, true, rotationSuggestion);
}
public boolean isRotationLocked() {
return RotationPolicy.isRotationLocked(mContext);
}
public void setRotateSuggestionButtonState(boolean visible) {
setRotateSuggestionButtonState(visible, false /* force */);
}
void setRotateSuggestionButtonState(final boolean visible, final boolean force) {
// At any point the button can become invisible because an a11y service became active.
// Similarly, a call to make the button visible may be rejected because an a11y service is
// active. Must account for this.
// Rerun a show animation to indicate change but don't rerun a hide animation
if (!visible && !mRotationButton.isVisible()) return;
final View view = mRotationButton.getCurrentView();
if (view == null) return;
final AnimatedVectorDrawable currentDrawable = mRotationButton.getImageDrawable();
if (currentDrawable == null) return;
// Clear any pending suggestion flag as it has either been nullified or is being shown
mPendingRotationSuggestion = false;
mMainThreadHandler.removeCallbacks(mCancelPendingRotationProposal);
// Handle the visibility change and animation
if (visible) { // Appear and change (cannot force)
// Stop and clear any currently running hide animations
if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) {
mRotateHideAnimator.cancel();
}
mRotateHideAnimator = null;
// Reset the alpha if any has changed due to hide animation
view.setAlpha(1f);
// Run the rotate icon's animation if it has one
currentDrawable.reset();
currentDrawable.start();
// TODO(b/187754252): No idea why this doesn't work. If we remove the "false"
// we see the animation show the pressed state... but it only shows the first time.
if (!isRotateSuggestionIntroduced()) mViewRippler.start(view);
// Set visibility unless a11y service is active.
mRotationButton.show();
} else { // Hide
mViewRippler.stop(); // Prevent any pending ripples, force hide or not
if (force) {
// If a hide animator is running stop it and make invisible
if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) {
mRotateHideAnimator.pause();
}
mRotationButton.hide();
return;
}
// Don't start any new hide animations if one is running
if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) return;
ObjectAnimator fadeOut = ObjectAnimator.ofFloat(view, "alpha", 0f);
fadeOut.setDuration(BUTTON_FADE_IN_OUT_DURATION_MS);
fadeOut.setInterpolator(LINEAR);
fadeOut.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mRotationButton.hide();
}
});
mRotateHideAnimator = fadeOut;
fadeOut.start();
}
}
void setDarkIntensity(float darkIntensity) {
mRotationButton.setDarkIntensity(darkIntensity);
}
public void onRotationProposal(int rotation, boolean isValid) {
int windowRotation = mDisplayController.getInfo().rotation;
if (!mRotationButton.acceptRotationProposal()) {
return;
}
// This method will be called on rotation suggestion changes even if the proposed rotation
// is not valid for the top app. Use invalid rotation choices as a signal to remove the
// rotate button if shown.
if (!isValid) {
setRotateSuggestionButtonState(false /* visible */);
return;
}
// If window rotation matches suggested rotation, remove any current suggestions
if (rotation == windowRotation) {
mMainThreadHandler.removeCallbacks(mRemoveRotationProposal);
setRotateSuggestionButtonState(false /* visible */);
return;
}
// Prepare to show the navbar icon by updating the icon style to change anim params
mLastRotationSuggestion = rotation; // Remember rotation for click
final boolean rotationCCW = Utilities.isRotationAnimationCCW(windowRotation, rotation);
if (windowRotation == Surface.ROTATION_0 || windowRotation == Surface.ROTATION_180) {
mIconResId = rotationCCW
? R.drawable.ic_sysbar_rotate_button_ccw_start_90
: R.drawable.ic_sysbar_rotate_button_cw_start_90;
} else { // 90 or 270
mIconResId = rotationCCW
? R.drawable.ic_sysbar_rotate_button_ccw_start_0
: R.drawable.ic_sysbar_rotate_button_ccw_start_0;
}
mRotationButton.updateIcon(mLightIconColor, mDarkIconColor);
if (canShowRotationButton()) {
// The navbar is visible / it's in visual immersive mode, so show the icon right away
showAndLogRotationSuggestion();
} else {
// If the navbar isn't shown, flag the rotate icon to be shown should the navbar become
// visible given some time limit.
mPendingRotationSuggestion = true;
mMainThreadHandler.removeCallbacks(mCancelPendingRotationProposal);
mMainThreadHandler.postDelayed(mCancelPendingRotationProposal,
NAVBAR_HIDDEN_PENDING_ICON_TIMEOUT_MS);
}
}
public void onDisable2FlagChanged(int state2) {
final boolean rotateSuggestionsDisabled = hasDisable2RotateSuggestionFlag(state2);
if (rotateSuggestionsDisabled) onRotationSuggestionsDisabled();
}
public void onBehaviorChanged(int displayId, @WindowInsetsController.Behavior int behavior) {
if (DEFAULT_DISPLAY != displayId) {
return;
}
if (mBehavior != behavior) {
mBehavior = behavior;
showPendingRotationButtonIfNeeded();
}
}
public void onTaskBarVisibilityChange(boolean showing) {
if (mIsTaskbarShowing != showing) {
mIsTaskbarShowing = showing;
showPendingRotationButtonIfNeeded();
}
}
private void showPendingRotationButtonIfNeeded() {
if (canShowRotationButton() && mPendingRotationSuggestion) {
showAndLogRotationSuggestion();
}
}
/** Return true when either the task bar is visible or it's in visual immersive mode. */
@SuppressLint("InlinedApi")
private boolean canShowRotationButton() {
return mIsTaskbarShowing || mBehavior == WindowInsetsController.BEHAVIOR_DEFAULT;
}
public @DrawableRes
int getIconResId() {
return mIconResId;
}
public @ColorInt int getLightIconColor() {
return mLightIconColor;
}
public @ColorInt int getDarkIconColor() {
return mDarkIconColor;
}
private void onRotateSuggestionClick(View v) {
mUiEventLogger.log(RotationButtonEvent.ROTATION_SUGGESTION_ACCEPTED);
incrementNumAcceptedRotationSuggestionsIfNeeded();
setRotationLockedAtAngle(mLastRotationSuggestion);
}
private boolean onRotateSuggestionHover(View v, MotionEvent event) {
final int action = event.getActionMasked();
mHoveringRotationSuggestion = (action == MotionEvent.ACTION_HOVER_ENTER)
|| (action == MotionEvent.ACTION_HOVER_MOVE);
rescheduleRotationTimeout(true /* reasonHover */);
return false; // Must return false so a11y hover events are dispatched correctly.
}
private void onRotationSuggestionsDisabled() {
// Immediately hide the rotate button and clear any planned removal
setRotateSuggestionButtonState(false /* visible */, true /* force */);
mMainThreadHandler.removeCallbacks(mRemoveRotationProposal);
}
private void showAndLogRotationSuggestion() {
setRotateSuggestionButtonState(true /* visible */);
rescheduleRotationTimeout(false /* reasonHover */);
mUiEventLogger.log(RotationButtonEvent.ROTATION_SUGGESTION_SHOWN);
}
/**
* Makes {@link #shouldOverrideUserLockPrefs} always return {@code false} once. It is used to
* avoid losing original user rotation when display rotation is changed by entering the fixed
* orientation overview.
*/
void setSkipOverrideUserLockPrefsOnce() {
mSkipOverrideUserLockPrefsOnce = true;
}
private boolean shouldOverrideUserLockPrefs(final int rotation) {
if (mSkipOverrideUserLockPrefsOnce) {
mSkipOverrideUserLockPrefsOnce = false;
return false;
}
// Only override user prefs when returning to the natural rotation (normally portrait).
// Don't let apps that force landscape or 180 alter user lock.
return rotation == NATURAL_ROTATION;
}
private void rescheduleRotationTimeout(final boolean reasonHover) {
// May be called due to a new rotation proposal or a change in hover state
if (reasonHover) {
// Don't reschedule if a hide animator is running
if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) return;
// Don't reschedule if not visible
if (!mRotationButton.isVisible()) return;
}
// Stop any pending removal
mMainThreadHandler.removeCallbacks(mRemoveRotationProposal);
// Schedule timeout
mMainThreadHandler.postDelayed(mRemoveRotationProposal,
computeRotationProposalTimeout());
}
private int computeRotationProposalTimeout() {
return mAccessibilityManager.getRecommendedTimeoutMillis(
mHoveringRotationSuggestion ? 16000 : 5000,
AccessibilityManager.FLAG_CONTENT_CONTROLS);
}
private boolean isRotateSuggestionIntroduced() {
ContentResolver cr = mContext.getContentResolver();
return Settings.Secure.getInt(cr, Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED, 0)
>= NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION;
}
private void incrementNumAcceptedRotationSuggestionsIfNeeded() {
// Get the number of accepted suggestions
ContentResolver cr = mContext.getContentResolver();
final int numSuggestions = Settings.Secure.getInt(cr,
Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED, 0);
// Increment the number of accepted suggestions only if it would change intro mode
if (numSuggestions < NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION) {
Settings.Secure.putInt(cr, Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED,
numSuggestions + 1);
}
}
private class TaskStackListenerImpl extends TaskStackChangeListener {
// Invalidate any rotation suggestion on task change or activity orientation change
// Note: all callbacks happen on main thread
@Override
public void onTaskStackChanged() {
setRotateSuggestionButtonState(false /* visible */);
}
@Override
public void onTaskRemoved(int taskId) {
setRotateSuggestionButtonState(false /* visible */);
}
@Override
public void onTaskMovedToFront(int taskId) {
setRotateSuggestionButtonState(false /* visible */);
}
@Override
public void onActivityRequestedOrientationChanged(int taskId, int requestedOrientation) {
// Only hide the icon if the top task changes its requestedOrientation
// Launcher can alter its requestedOrientation while it's not on top, don't hide on this
Optional.ofNullable(ActivityManagerWrapper.getInstance())
.map(ActivityManagerWrapper::getRunningTask)
.ifPresent(a -> {
if (a.id == taskId) setRotateSuggestionButtonState(false /* visible */);
});
}
}
enum RotationButtonEvent implements UiEventLogger.UiEventEnum {
@UiEvent(doc = "The rotation button was shown")
ROTATION_SUGGESTION_SHOWN(206),
@UiEvent(doc = "The rotation button was clicked")
ROTATION_SUGGESTION_ACCEPTED(207);
private final int mId;
RotationButtonEvent(int id) {
mId = id;
}
@Override public int getId() {
return mId;
}
}
}

View File

@ -31,8 +31,8 @@ import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.launcher3.taskbar.contextual.RotationButton;
import com.android.launcher3.taskbar.contextual.RotationButtonController;
import com.android.systemui.shared.rotation.RotationButton;
import com.android.systemui.shared.rotation.RotationButtonController;
import org.junit.Before;
import org.junit.Test;
@ -55,8 +55,9 @@ public class NavigationBarRotationContextTest {
Context mTargetContext = InstrumentationRegistry.getTargetContext();
final View view = new View(mTargetContext);
RotationButton rotationButton = mock(RotationButton.class);
mRotationButtonController = new RotationButtonController(mTargetContext, 0, 0);
mRotationButtonController.setRotationButton(rotationButton);
mRotationButtonController = new RotationButtonController(mTargetContext, 0, 0, 0, 0, 0, 0,
() -> 0);
mRotationButtonController.setRotationButton(rotationButton, null);
// Due to a mockito issue, only spy the object after setting the initial state
mRotationButtonController = spy(mRotationButtonController);
final AnimatedVectorDrawable kbd = mock(AnimatedVectorDrawable.class);
@ -85,7 +86,7 @@ public class NavigationBarRotationContextTest {
// No navigation bar should not call to set visibility state
mRotationButtonController.onBehaviorChanged(DEFAULT_DISPLAY,
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
mRotationButtonController.onTaskBarVisibilityChange(false /* showing */);
mRotationButtonController.onNavigationBarWindowVisibilityChange(false /* showing */);
verify(mRotationButtonController, times(0)).setRotateSuggestionButtonState(
false /* visible */);
verify(mRotationButtonController, times(0)).setRotateSuggestionButtonState(
@ -100,7 +101,7 @@ public class NavigationBarRotationContextTest {
true /* visible */);
// Since rotation has changed rotation should be pending, show mButton when showing nav bar
mRotationButtonController.onTaskBarVisibilityChange(true /* showing */);
mRotationButtonController.onNavigationBarWindowVisibilityChange(true /* showing */);
verify(mRotationButtonController, times(1)).setRotateSuggestionButtonState(
true /* visible */);
}
@ -108,7 +109,7 @@ public class NavigationBarRotationContextTest {
@Test
public void testOnRotationProposalShowButton() {
// Navigation bar being visible should not call to set visibility state
mRotationButtonController.onTaskBarVisibilityChange(true /* showing */);
mRotationButtonController.onNavigationBarWindowVisibilityChange(true /* showing */);
verify(mRotationButtonController, times(0))
.setRotateSuggestionButtonState(false /* visible */);
verify(mRotationButtonController, times(0))