Refactor how we show rotation button in fully gestural mode.

Fixes: 132201048
Test: manual
Change-Id: I56d04dfabb4db741cd5dce72f01439c777d668c5
This commit is contained in:
Tracy Zhou
2019-05-20 14:40:38 -07:00
parent e02360b53a
commit 24fd028ea4
12 changed files with 781 additions and 540 deletions

View File

@@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:systemui="http://schemas.android.com/apk/res-auto"
android:id="@+id/start_menu_container"
android:layout_width="@dimen/navigation_key_width"
android:layout_height="match_parent"
android:importantForAccessibility="no"
android:focusable="false"
android:clipChildren="false"
android:clipToPadding="false"
>
<include layout="@layout/rotate_suggestion"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="invisible"
/>
<include layout="@layout/back"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="invisible"
/>
</FrameLayout>

View File

@@ -331,7 +331,7 @@
<!-- Nav bar button default ordering/layout -->
<string name="config_navBarLayout" translatable="false">left[.5W],back[1WC];home;recent[1WC],right[.5W]</string>
<string name="config_navBarLayoutQuickstep" translatable="false">back[1.7WC];home;contextual[1.7WC]</string>
<string name="config_navBarLayoutHandle" translatable="false">start_contextual[40AC];home_handle;ime_switcher[40AC]</string>
<string name="config_navBarLayoutHandle" translatable="false">back[40AC];home_handle;ime_switcher[40AC]</string>
<bool name="quick_settings_show_full_alarm">false</bool>

View File

@@ -0,0 +1,162 @@
/*
* Copyright (C) 2019 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.systemui.statusbar.phone;
import android.content.Context;
import android.graphics.PixelFormat;
import android.view.ContextThemeWrapper;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Surface;
import android.view.View;
import android.view.WindowManager;
import com.android.systemui.R;
import com.android.systemui.statusbar.policy.KeyButtonDrawable;
import com.android.systemui.statusbar.policy.KeyButtonView;
/** Containing logic for the rotation button on the physical left bottom corner of the screen. */
public class FloatingRotationButton implements RotationButton {
private final Context mContext;
private final WindowManager mWindowManager;
private final KeyButtonView mKeyButtonView;
private KeyButtonDrawable mKeyButtonDrawable;
private boolean mIsShowing;
private boolean mCanShow = true;
private RotationButtonController mRotationButtonController;
FloatingRotationButton(Context context) {
mContext = context;
mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
mKeyButtonView = (KeyButtonView) LayoutInflater.from(mContext).inflate(
R.layout.rotate_suggestion, null);
}
@Override
public void setRotationButtonController(RotationButtonController rotationButtonController) {
mRotationButtonController = rotationButtonController;
}
@Override
public View getCurrentView() {
return mKeyButtonView;
}
@Override
public boolean show() {
if (!mCanShow || mIsShowing) {
return false;
}
mIsShowing = true;
int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
float density = mContext.getResources().getDisplayMetrics().density;
int diameter = (int) density * 48;
int margin = (int) density * 4;
final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(diameter, diameter,
margin, margin, WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, flags,
PixelFormat.TRANSLUCENT);
lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
lp.setTitle("FloatingRotationButton");
switch (mWindowManager.getDefaultDisplay().getRotation()) {
case Surface.ROTATION_0:
lp.gravity = Gravity.BOTTOM | Gravity.LEFT;
break;
case Surface.ROTATION_90:
lp.gravity = Gravity.BOTTOM | Gravity.RIGHT;
break;
case Surface.ROTATION_180:
lp.gravity = Gravity.TOP | Gravity.RIGHT;
break;
case Surface.ROTATION_270:
lp.gravity = Gravity.TOP | Gravity.LEFT;
break;
default:
break;
}
updateIcon();
mWindowManager.addView(mKeyButtonView, lp);
if (mKeyButtonDrawable != null && mKeyButtonDrawable.canAnimate()) {
mKeyButtonDrawable.resetAnimation();
mKeyButtonDrawable.startAnimation();
}
return true;
}
@Override
public boolean hide() {
if (!mIsShowing) {
return false;
}
mWindowManager.removeViewImmediate(mKeyButtonView);
mRotationButtonController.cleanUp();
mIsShowing = false;
return true;
}
@Override
public boolean isVisible() {
return mIsShowing;
}
@Override
public void updateIcon() {
if (!mIsShowing) {
return;
}
mKeyButtonDrawable = getImageDrawable();
mKeyButtonView.setImageDrawable(mKeyButtonDrawable);
mKeyButtonDrawable.setCallback(mKeyButtonView);
if (mKeyButtonDrawable != null && mKeyButtonDrawable.canAnimate()) {
mKeyButtonDrawable.resetAnimation();
mKeyButtonDrawable.startAnimation();
}
}
@Override
public void setOnClickListener(View.OnClickListener onClickListener) {
mKeyButtonView.setOnClickListener(onClickListener);
}
@Override
public void setOnHoverListener(View.OnHoverListener onHoverListener) {
mKeyButtonView.setOnHoverListener(onHoverListener);
}
@Override
public KeyButtonDrawable getImageDrawable() {
Context context = new ContextThemeWrapper(mContext.getApplicationContext(),
mRotationButtonController.getStyleRes());
return KeyButtonDrawable.create(context, R.drawable.ic_sysbar_rotate_button,
false /* shadow */, true /* hasOvalBg */);
}
@Override
public void setDarkIntensity(float darkIntensity) {
mKeyButtonView.setDarkIntensity(darkIntensity);
}
@Override
public void setCanShowRotationButton(boolean canShow) {
mCanShow = canShow;
if (!mCanShow) {
hide();
}
}
}

View File

@@ -188,7 +188,7 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback
@Override
public void onQuickStepStarted() {
// Use navbar dragging as a signal to hide the rotate button
mNavigationBarView.getRotateSuggestionButton().setRotateSuggestionButtonState(false);
mNavigationBarView.getRotationButtonController().setRotateSuggestionButtonState(false);
// Hide the notifications panel when quick step starts
mStatusBar.collapsePanel(true /* animate */);
@@ -333,16 +333,16 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback
// Currently there is no accelerometer sensor on non-default display.
if (mIsOnDefaultDisplay) {
final RotationContextButton rotationButton =
mNavigationBarView.getRotateSuggestionButton();
rotationButton.setListener(mRotationButtonListener);
rotationButton.addRotationCallback(mRotationWatcher);
mNavigationBarView.getRotateSuggestionButton().setListener(mRotationButtonListener);
final RotationButtonController rotationButtonController =
mNavigationBarView.getRotationButtonController();
rotationButtonController.addRotationCallback(mRotationWatcher);
// Reset user rotation pref to match that of the WindowManager if starting in locked
// mode. This will automatically happen when switching from auto-rotate to locked mode.
if (display != null && rotationButton.isRotationLocked()) {
final int winRotation = display.getRotation();
rotationButton.setRotationLockedAtAngle(winRotation);
if (display != null && rotationButtonController.isRotationLocked()) {
rotationButtonController.setRotationLockedAtAngle(display.getRotation());
}
} else {
mDisabledFlags2 |= StatusBarManager.DISABLE2_ROTATE_SUGGESTIONS;
@@ -458,34 +458,34 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback
if (DEBUG_WINDOW_STATE) Log.d(TAG, "Navigation bar " + windowStateToString(state));
updateSystemUiStateFlags(-1);
mNavigationBarView.getRotateSuggestionButton()
.onNavigationBarWindowVisibilityChange(isNavBarWindowVisible());
mNavigationBarView.getRotationButtonController().onNavigationBarWindowVisibilityChange(
isNavBarWindowVisible());
}
}
@Override
public void onRotationProposal(final int rotation, boolean isValid) {
final int winRotation = mNavigationBarView.getDisplay().getRotation();
final boolean rotateSuggestionsDisabled = RotationContextButton
final boolean rotateSuggestionsDisabled = RotationButtonController
.hasDisable2RotateSuggestionFlag(mDisabledFlags2);
final RotationButtonController rotationButtonController =
mNavigationBarView.getRotationButtonController();
final RotationButton rotationButton = rotationButtonController.getRotationButton();
if (RotationContextButton.DEBUG_ROTATION) {
Log.v(TAG, "onRotationProposal proposedRotation=" + Surface.rotationToString(rotation)
+ ", winRotation=" + Surface.rotationToString(winRotation)
+ ", isValid=" + isValid + ", mNavBarWindowState="
+ StatusBarManager.windowStateToString(mNavigationBarWindowState)
+ ", rotateSuggestionsDisabled=" + rotateSuggestionsDisabled
+ ", isRotateButtonVisible=" + (mNavigationBarView == null ? "null" :
mNavigationBarView.getRotateSuggestionButton().isVisible()));
+ ", isRotateButtonVisible=" + (mNavigationBarView == null ? "null"
: rotationButton.isVisible()));
}
// Respect the disabled flag, no need for action as flag change callback will handle hiding
if (rotateSuggestionsDisabled) return;
View rotationButton = mNavigationBarView.getRotateSuggestionButton().getCurrentView();
if (rotationButton != null && rotationButton.isAttachedToWindow()) {
mNavigationBarView.getRotateSuggestionButton()
.onRotationProposal(rotation, winRotation, isValid);
}
rotationButtonController.onRotationProposal(rotation, winRotation, isValid);
}
/** Restores the System UI flags saved state to {@link NavigationBarFragment}. */
@@ -593,7 +593,7 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback
private void setDisabled2Flags(int state2) {
// Method only called on change of disable2 flags
if (mNavigationBarView != null) {
mNavigationBarView.getRotateSuggestionButton().onDisable2FlagChanged(state2);
mNavigationBarView.getRotationButtonController().onDisable2FlagChanged(state2);
}
}
@@ -862,8 +862,8 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback
boolean[] feedbackEnabled = new boolean[1];
int a11yFlags = getA11yButtonState(feedbackEnabled);
mNavigationBarView.getRotateSuggestionButton()
.setAccessibilityFeedbackEnabled(feedbackEnabled[0]);
mNavigationBarView.getRotationButtonController().setAccessibilityFeedbackEnabled(
feedbackEnabled[0]);
boolean clickable = (a11yFlags & SYSUI_STATE_A11Y_BUTTON_CLICKABLE) != 0;
boolean longClickable = (a11yFlags & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE) != 0;

View File

@@ -63,7 +63,6 @@ public class NavigationBarInflaterView extends FrameLayout
public static final String RIGHT = "right";
public static final String CONTEXTUAL = "contextual";
public static final String IME_SWITCHER = "ime_switcher";
public static final String START_CONTEXTUAL = "start_contextual";
public static final String GRAVITY_SEPARATOR = ";";
public static final String BUTTON_SEPARATOR = ",";
@@ -395,8 +394,6 @@ public class NavigationBarInflaterView extends FrameLayout
v = inflater.inflate(R.layout.home_handle, parent, false);
} else if (IME_SWITCHER.equals(button)) {
v = inflater.inflate(R.layout.ime_switcher, parent, false);
} else if (START_CONTEXTUAL.equals(button)) {
v = inflater.inflate(R.layout.start_contextual, parent, false);
} else if (button.startsWith(KEY)) {
String uri = extractImage(button);
int code = extractKeycode(button);

View File

@@ -173,6 +173,7 @@ public final class NavigationBarTransitions extends BarTransitions implements
for (int i = buttonDispatchers.size() - 1; i >= 0; i--) {
buttonDispatchers.valueAt(i).setDarkIntensity(darkIntensity);
}
mView.getRotationButtonController().setDarkIntensity(darkIntensity);
if (mAutoDim) {
applyLightsOut(false, true);
}

View File

@@ -22,6 +22,7 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_H
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
import static com.android.systemui.shared.system.QuickStepContract.isGesturalMode;
import static com.android.systemui.statusbar.phone.BarTransitions.MODE_OPAQUE;
import android.animation.LayoutTransition;
@@ -130,7 +131,6 @@ public class NavigationBarView extends FrameLayout implements
private boolean mImeVisible;
private final SparseArray<ButtonDispatcher> mButtonDispatchers = new SparseArray<>();
private final ContextualButtonGroup mStartContextualButtonGroup;
private final ContextualButtonGroup mContextualButtonGroup;
private Configuration mConfiguration;
private Configuration mTmpLastConfiguration;
@@ -138,6 +138,8 @@ public class NavigationBarView extends FrameLayout implements
private NavigationBarInflaterView mNavigationInflaterView;
private RecentsOnboarding mRecentsOnboarding;
private NotificationPanelView mPanelView;
private FloatingRotationButton mFloatingRotationButton;
private RotationButtonController mRotationButtonController;
private NavBarTintController mTintController;
@@ -232,24 +234,12 @@ public class NavigationBarView extends FrameLayout implements
private final OnComputeInternalInsetsListener mOnComputeInternalInsetsListener = info -> {
// When the nav bar is in 2-button or 3-button mode, or when IME is visible in fully
// gestural mode, the entire nav bar should be touchable.
if (!QuickStepContract.isGesturalMode(mNavBarMode) || mImeVisible) {
if (!isGesturalMode(mNavBarMode) || mImeVisible) {
info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_FRAME);
return;
}
info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
RotationContextButton rotationContextButton = getRotateSuggestionButton();
// If the rotate suggestion button is not visible in fully gestural mode, the entire nav bar
// is not touchable so that the app underneath can be clicked through.
if (rotationContextButton.getVisibility() != VISIBLE) {
info.touchableRegion.setEmpty();
} else {
// Set the rotate suggestion button area to be touchable.
rotationContextButton.getCurrentView().getLocationInWindow(mTmpPosition);
Rect rect = new Rect(mTmpPosition[0], mTmpPosition[1],
mTmpPosition[0] + mRotationButtonBounds.width(),
mTmpPosition[1] + mRotationButtonBounds.height());
info.touchableRegion.union(rect);
}
info.touchableRegion.setEmpty();
};
public NavigationBarView(Context context, AttributeSet attrs) {
@@ -258,15 +248,14 @@ public class NavigationBarView extends FrameLayout implements
mIsVertical = false;
mLongClickableAccessibilityButton = false;
mNavBarMode = Dependency.get(NavigationModeController.class).addListener(this);
boolean isGesturalMode = QuickStepContract.isGesturalMode(mNavBarMode);
boolean isGesturalMode = isGesturalMode(mNavBarMode);
// Set up the context group of buttons
mContextualButtonGroup = new ContextualButtonGroup(R.id.menu_container);
final ContextualButton imeSwitcherButton = new ContextualButton(R.id.ime_switcher,
R.drawable.ic_ime_switcher_default);
final RotationContextButton rotateSuggestionButton = new RotationContextButton(
R.id.rotate_suggestion, R.drawable.ic_sysbar_rotate_button, getContext(),
R.style.RotateButtonCCWStart90);
R.id.rotate_suggestion, R.drawable.ic_sysbar_rotate_button);
final ContextualButton accessibilityButton =
new ContextualButton(R.id.accessibility_button,
R.drawable.ic_sysbar_accessibility_button);
@@ -278,13 +267,12 @@ public class NavigationBarView extends FrameLayout implements
mOverviewProxyService = Dependency.get(OverviewProxyService.class);
mRecentsOnboarding = new RecentsOnboarding(context, mOverviewProxyService);
mFloatingRotationButton = new FloatingRotationButton(context);
mRotationButtonController = new RotationButtonController(context,
R.style.RotateButtonCCWStart90,
isGesturalMode ? mFloatingRotationButton : rotateSuggestionButton);
final ContextualButton backButton = new ContextualButton(R.id.back, 0);
mStartContextualButtonGroup = new ContextualButtonGroup(R.id.start_menu_container);
if (isGesturalMode) {
mStartContextualButtonGroup.addButton(rotateSuggestionButton);
}
mStartContextualButtonGroup.addButton(backButton);
mConfiguration = new Configuration();
mTmpLastConfiguration = new Configuration();
@@ -301,7 +289,6 @@ public class NavigationBarView extends FrameLayout implements
mButtonDispatchers.put(R.id.accessibility_button, accessibilityButton);
mButtonDispatchers.put(R.id.rotate_suggestion, rotateSuggestionButton);
mButtonDispatchers.put(R.id.menu_container, mContextualButtonGroup);
mButtonDispatchers.put(R.id.start_menu_container, mStartContextualButtonGroup);
mDeadZone = new DeadZone(this);
mEdgeBackGestureHandler = new EdgeBackGestureHandler(context, mOverviewProxyService);
@@ -390,6 +377,14 @@ public class NavigationBarView extends FrameLayout implements
return mCurrentView;
}
public RotationButtonController getRotationButtonController() {
return mRotationButtonController;
}
public FloatingRotationButton getFloatingRotationButton() {
return mFloatingRotationButton;
}
public ButtonDispatcher getRecentsButton() {
return mButtonDispatchers.get(R.id.recent_apps);
}
@@ -414,10 +409,6 @@ public class NavigationBarView extends FrameLayout implements
return (RotationContextButton) mButtonDispatchers.get(R.id.rotate_suggestion);
}
public ContextualButtonGroup getStartContextualButtonGroup() {
return mStartContextualButtonGroup;
}
public ButtonDispatcher getHomeHandle() {
return mButtonDispatchers.get(R.id.home_handle);
}
@@ -454,7 +445,6 @@ public class NavigationBarView extends FrameLayout implements
if (densityChange || dirChange) {
mRecentIcon = getDrawable(R.drawable.ic_sysbar_recent);
mContextualButtonGroup.updateIcons();
mStartContextualButtonGroup.updateIcons();
}
if (orientationChange || densityChange || dirChange) {
mBackIcon = getBackDrawable();
@@ -490,7 +480,7 @@ public class NavigationBarView extends FrameLayout implements
return;
}
if (QuickStepContract.isGesturalMode(mNavBarMode)) {
if (isGesturalMode(mNavBarMode)) {
drawable.setRotation(degrees);
return;
}
@@ -561,6 +551,7 @@ public class NavigationBarView extends FrameLayout implements
mTransitionListener.onBackAltCleared();
}
mImeVisible = visible;
mRotationButtonController.getRotationButton().setCanShowRotationButton(!mImeVisible);
}
public void setDisabledFlags(int disabledFlags) {
@@ -603,7 +594,7 @@ public class NavigationBarView extends FrameLayout implements
mBarTransitions.reapplyDarkIntensity();
boolean disableHome = QuickStepContract.isGesturalMode(mNavBarMode)
boolean disableHome = isGesturalMode(mNavBarMode)
|| ((mDisabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0);
// TODO(b/113914868): investigation log for disappearing home button
@@ -613,7 +604,7 @@ public class NavigationBarView extends FrameLayout implements
// Always disable recents when alternate car mode UI is active and for secondary displays.
boolean disableRecent = isRecentsButtonDisabled();
boolean disableBack = !useAltBack && (QuickStepContract.isGesturalMode(mNavBarMode)
boolean disableBack = !useAltBack && (isGesturalMode(mNavBarMode)
|| ((mDisabledFlags & View.STATUS_BAR_DISABLE_BACK) != 0));
// When screen pinning, don't hide back and home when connected service or back and
@@ -641,7 +632,6 @@ public class NavigationBarView extends FrameLayout implements
}
getBackButton().setVisibility(disableBack ? View.INVISIBLE : View.VISIBLE);
mStartContextualButtonGroup.setButtonVisibility(R.id.back, !disableBack);
getHomeButton().setVisibility(disableHome ? View.INVISIBLE : View.VISIBLE);
getRecentsButton().setVisibility(disableRecent ? View.INVISIBLE : View.VISIBLE);
}
@@ -782,7 +772,7 @@ public class NavigationBarView extends FrameLayout implements
// Color adaption is tied with showing home handle, only avaliable if visible
mTintController.onNavigationModeChanged(mNavBarMode);
if (QuickStepContract.isGesturalMode(mNavBarMode)) {
if (isGesturalMode(mNavBarMode)) {
mTintController.start();
} else {
mTintController.stop();
@@ -939,7 +929,7 @@ public class NavigationBarView extends FrameLayout implements
"onMeasure: (%dx%d) old: (%dx%d)", w, h, getMeasuredWidth(), getMeasuredHeight()));
final boolean newVertical = w > 0 && h > w
&& !QuickStepContract.isGesturalMode(mNavBarMode);
&& !isGesturalMode(mNavBarMode);
if (newVertical != mIsVertical) {
mIsVertical = newVertical;
if (DEBUG) {
@@ -950,7 +940,7 @@ public class NavigationBarView extends FrameLayout implements
notifyVerticalChangedListener(newVertical);
}
if (QuickStepContract.isGesturalMode(mNavBarMode)) {
if (isGesturalMode(mNavBarMode)) {
// Update the nav bar background to match the height of the visible nav bar
int height = mIsVertical
? getResources().getDimensionPixelSize(

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2019 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.systemui.statusbar.phone;
import android.view.View;
import com.android.systemui.statusbar.policy.KeyButtonDrawable;
/** Interface of a rotation button that interacts {@link RotationButtonController}. */
interface RotationButton {
void setRotationButtonController(RotationButtonController rotationButtonController);
View getCurrentView();
boolean show();
boolean hide();
boolean isVisible();
void updateIcon();
void setOnClickListener(View.OnClickListener onClickListener);
void setOnHoverListener(View.OnHoverListener onHoverListener);
KeyButtonDrawable getImageDrawable();
void setDarkIntensity(float darkIntensity);
default void setCanShowRotationButton(boolean canShow) {}
default boolean acceptRotationProposal() {
return getCurrentView() != null;
}
}

View File

@@ -0,0 +1,478 @@
/*
* Copyright (C) 2019 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.systemui.statusbar.phone;
import static com.android.internal.view.RotationPolicy.NATURAL_ROTATION;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.annotation.StyleRes;
import android.app.StatusBarManager;
import android.content.ContentResolver;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.os.RemoteException;
import android.provider.Settings;
import android.view.IRotationWatcher.Stub;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.View;
import android.view.WindowManagerGlobal;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.systemui.Dependency;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.statusbar.policy.KeyButtonDrawable;
import com.android.systemui.statusbar.policy.RotationLockController;
import java.util.Optional;
import java.util.function.Consumer;
/** Contains logic that deals with showing a rotate suggestion button with animation. */
public class 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 MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
private final ViewRippler mViewRippler = new ViewRippler();
private @StyleRes int mStyleRes;
private int mLastRotationSuggestion;
private boolean mPendingRotationSuggestion;
private boolean mHoveringRotationSuggestion;
private RotationLockController mRotationLockController;
private TaskStackListenerImpl mTaskStackListener;
private Consumer<Integer> mRotWatcherListener;
private boolean mIsNavigationBarShowing;
private final Runnable mRemoveRotationProposal =
() -> setRotateSuggestionButtonState(false /* visible */);
private final Runnable mCancelPendingRotationProposal =
() -> mPendingRotationSuggestion = false;
private Animator mRotateHideAnimator;
private boolean mAccessibilityFeedbackEnabled;
private final Context mContext;
private final RotationButton mRotationButton;
private final Stub mRotationWatcher = new Stub() {
@Override
public void onRotationChanged(final int rotation) throws RemoteException {
if (mRotationButton.getCurrentView() == null) {
return;
}
// We need this to be scheduled as early as possible to beat the redrawing of
// window in response to the orientation change.
Handler h = mRotationButton.getCurrentView().getHandler();
Message msg = Message.obtain(h, () -> {
// If the screen rotation changes while locked, potentially update lock to flow with
// new screen rotation and hide any showing suggestions.
if (mRotationLockController.isRotationLocked()) {
if (shouldOverrideUserLockPrefs(rotation)) {
setRotationLockedAtAngle(rotation);
}
setRotateSuggestionButtonState(false /* visible */, true /* forced */);
}
if (mRotWatcherListener != null) {
mRotWatcherListener.accept(rotation);
}
});
msg.setAsynchronous(true);
h.sendMessageAtFrontOfQueue(msg);
}
};
/**
* 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;
}
RotationButtonController(Context context, @StyleRes int style, RotationButton rotationButton) {
mContext = context;
mRotationButton = rotationButton;
mRotationButton.setRotationButtonController(this);
mStyleRes = style;
mIsNavigationBarShowing = true;
mRotationLockController = Dependency.get(RotationLockController.class);
// Register the task stack listener
mTaskStackListener = new TaskStackListenerImpl();
ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
mRotationButton.setOnClickListener(this::onRotateSuggestionClick);
mRotationButton.setOnHoverListener(this::onRotateSuggestionHover);
try {
WindowManagerGlobal.getWindowManagerService()
.watchRotation(mRotationWatcher, mContext.getDisplay().getDisplayId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
void cleanUp() {
// Unregister the task stack listener
ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener);
try {
WindowManagerGlobal.getWindowManagerService().removeRotationWatcher(mRotationWatcher);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
void addRotationCallback(Consumer<Integer> watcher) {
mRotWatcherListener = watcher;
}
void setAccessibilityFeedbackEnabled(boolean flag) {
mAccessibilityFeedbackEnabled = flag;
}
void setRotationLockedAtAngle(int rotationSuggestion) {
mRotationLockController.setRotationLockedAtAngle(true /* locked */, rotationSuggestion);
}
public boolean isRotationLocked() {
return mRotationLockController.isRotationLocked();
}
void setRotateSuggestionButtonState(boolean visible) {
setRotateSuggestionButtonState(visible, false /* force */);
}
void setRotateSuggestionButtonState(final boolean visible, final boolean force) {
// At any point the 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 KeyButtonDrawable currentDrawable = mRotationButton.getImageDrawable();
if (currentDrawable == null) return;
// Clear any pending suggestion flag as it has either been nullified or is being shown
mPendingRotationSuggestion = false;
view.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
if (currentDrawable.canAnimate()) {
currentDrawable.resetAnimation();
currentDrawable.startAnimation();
}
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(Interpolators.LINEAR);
fadeOut.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mRotationButton.hide();
}
});
mRotateHideAnimator = fadeOut;
fadeOut.start();
}
}
void setDarkIntensity(float darkIntensity) {
mRotationButton.setDarkIntensity(darkIntensity);
}
void onRotationProposal(int rotation, int windowRotation, boolean isValid) {
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;
}
final View currentView = mRotationButton.getCurrentView();
// If window rotation matches suggested rotation, remove any current suggestions
if (rotation == windowRotation) {
if (currentView != null) {
currentView.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 = isRotationAnimationCCW(windowRotation, rotation);
int style;
if (windowRotation == Surface.ROTATION_0 || windowRotation == Surface.ROTATION_180) {
style = rotationCCW ? R.style.RotateButtonCCWStart90 : R.style.RotateButtonCWStart90;
} else { // 90 or 270
style = rotationCCW ? R.style.RotateButtonCCWStart0 : R.style.RotateButtonCWStart0;
}
mStyleRes = style;
mRotationButton.updateIcon();
if (mIsNavigationBarShowing) {
// The navbar is visible 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;
if (currentView != null) {
currentView.removeCallbacks(mCancelPendingRotationProposal);
currentView.postDelayed(mCancelPendingRotationProposal,
NAVBAR_HIDDEN_PENDING_ICON_TIMEOUT_MS);
}
}
}
void onDisable2FlagChanged(int state2) {
final boolean rotateSuggestionsDisabled = hasDisable2RotateSuggestionFlag(state2);
if (rotateSuggestionsDisabled) onRotationSuggestionsDisabled();
}
void onNavigationBarWindowVisibilityChange(boolean showing) {
if (mIsNavigationBarShowing != showing) {
mIsNavigationBarShowing = showing;
// If the navbar is visible, show the rotate button if there's a pending suggestion
if (showing && mPendingRotationSuggestion) {
showAndLogRotationSuggestion();
}
}
}
@StyleRes int getStyleRes() {
return mStyleRes;
}
RotationButton getRotationButton() {
return mRotationButton;
}
private void onRotateSuggestionClick(View v) {
mMetricsLogger.action(MetricsEvent.ACTION_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 */);
if (mRotationButton.getCurrentView() != null) {
mRotationButton.getCurrentView().removeCallbacks(mRemoveRotationProposal);
}
}
private void showAndLogRotationSuggestion() {
setRotateSuggestionButtonState(true /* visible */);
rescheduleRotationTimeout(false /* reasonHover */);
mMetricsLogger.visible(MetricsEvent.ROTATION_SUGGESTION_SHOWN);
}
private boolean shouldOverrideUserLockPrefs(final int rotation) {
// 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 boolean isRotationAnimationCCW(int from, int to) {
// All 180deg WM rotation animations are CCW, match that
if (from == Surface.ROTATION_0 && to == Surface.ROTATION_90) return false;
if (from == Surface.ROTATION_0 && to == Surface.ROTATION_180) return true; //180d so CCW
if (from == Surface.ROTATION_0 && to == Surface.ROTATION_270) return true;
if (from == Surface.ROTATION_90 && to == Surface.ROTATION_0) return true;
if (from == Surface.ROTATION_90 && to == Surface.ROTATION_180) return false;
if (from == Surface.ROTATION_90 && to == Surface.ROTATION_270) return true; //180d so CCW
if (from == Surface.ROTATION_180 && to == Surface.ROTATION_0) return true; //180d so CCW
if (from == Surface.ROTATION_180 && to == Surface.ROTATION_90) return true;
if (from == Surface.ROTATION_180 && to == Surface.ROTATION_270) return false;
if (from == Surface.ROTATION_270 && to == Surface.ROTATION_0) return false;
if (from == Surface.ROTATION_270 && to == Surface.ROTATION_90) return true; //180d so CCW
if (from == Surface.ROTATION_270 && to == Surface.ROTATION_180) return true;
return false; // Default
}
private void rescheduleRotationTimeout(final boolean reasonHover) {
if (mRotationButton.getCurrentView() == null) {
return;
}
// 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
mRotationButton.getCurrentView().removeCallbacks(mRemoveRotationProposal);
// Schedule timeout
mRotationButton.getCurrentView().postDelayed(mRemoveRotationProposal,
computeRotationProposalTimeout());
}
private int computeRotationProposalTimeout() {
if (mAccessibilityFeedbackEnabled) return 10000;
if (mHoveringRotationSuggestion) return 8000;
return 5000;
}
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 */);
});
}
}
private class ViewRippler {
private static final int RIPPLE_OFFSET_MS = 50;
private static final int RIPPLE_INTERVAL_MS = 2000;
private View mRoot;
public void start(View root) {
stop(); // Stop any pending ripple animations
mRoot = root;
// Schedule pending ripples, offset the 1st to avoid problems with visibility change
mRoot.postOnAnimationDelayed(mRipple, RIPPLE_OFFSET_MS);
mRoot.postOnAnimationDelayed(mRipple, RIPPLE_INTERVAL_MS);
mRoot.postOnAnimationDelayed(mRipple, 2 * RIPPLE_INTERVAL_MS);
mRoot.postOnAnimationDelayed(mRipple, 3 * RIPPLE_INTERVAL_MS);
mRoot.postOnAnimationDelayed(mRipple, 4 * RIPPLE_INTERVAL_MS);
}
public void stop() {
if (mRoot != null) mRoot.removeCallbacks(mRipple);
}
private final Runnable mRipple = new Runnable() {
@Override
public void run() { // Cause the ripple to fire via false presses
if (!mRoot.isAttachedToWindow()) return;
mRoot.setPressed(true /* pressed */);
mRoot.setPressed(false /* pressed */);
}
};
}
}

View File

@@ -18,281 +18,30 @@ package com.android.systemui.statusbar.phone;
import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON;
import static com.android.internal.view.RotationPolicy.NATURAL_ROTATION;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.annotation.DrawableRes;
import android.annotation.IdRes;
import android.annotation.NonNull;
import android.annotation.StyleRes;
import android.app.StatusBarManager;
import android.content.ContentResolver;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.os.RemoteException;
import android.provider.Settings;
import android.view.ContextThemeWrapper;
import android.view.IRotationWatcher.Stub;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.View;
import android.view.WindowManagerGlobal;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.systemui.Dependency;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.statusbar.policy.KeyButtonDrawable;
import com.android.systemui.statusbar.policy.RotationLockController;
import java.util.Optional;
import java.util.function.Consumer;
/** Containing logic for the rotation button in nav bar. */
public class RotationContextButton extends ContextualButton implements
NavigationModeController.ModeChangedListener {
NavigationModeController.ModeChangedListener, RotationButton {
public static final boolean DEBUG_ROTATION = false;
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 @StyleRes int mStyleRes;
private int mLastRotationSuggestion;
private boolean mPendingRotationSuggestion;
private boolean mHoveringRotationSuggestion;
private RotationLockController mRotationLockController;
private TaskStackListenerImpl mTaskStackListener;
private Consumer<Integer> mRotWatcherListener;
private boolean mIsNavigationBarShowing;
private final Runnable mRemoveRotationProposal =
() -> setRotateSuggestionButtonState(false /* visible */);
private final Runnable mCancelPendingRotationProposal =
() -> mPendingRotationSuggestion = false;
private Animator mRotateHideAnimator;
private boolean mAccessibilityFeedbackEnabled;
private int mNavBarMode = NAV_BAR_MODE_3BUTTON;
private RotationButtonController mRotationButtonController;
private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
private final ViewRippler mViewRippler = new ViewRippler();
private final Stub mRotationWatcher = new Stub() {
@Override
public void onRotationChanged(final int rotation) throws RemoteException {
if (getCurrentView() == null) {
return;
}
// We need this to be scheduled as early as possible to beat the redrawing of
// window in response to the orientation change.
Handler h = getCurrentView().getHandler();
Message msg = Message.obtain(h, () -> {
// If the screen rotation changes while locked, potentially update lock to flow with
// new screen rotation and hide any showing suggestions.
if (mRotationLockController.isRotationLocked()) {
if (shouldOverrideUserLockPrefs(rotation)) {
setRotationLockedAtAngle(rotation);
}
setRotateSuggestionButtonState(false /* visible */, true /* forced */);
}
if (mRotWatcherListener != null) {
mRotWatcherListener.accept(rotation);
}
});
msg.setAsynchronous(true);
h.sendMessageAtFrontOfQueue(msg);
}
};
/**
* 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 RotationContextButton(@IdRes int buttonResId, @DrawableRes int iconResId,
@NonNull Context context, @StyleRes int style) {
public RotationContextButton(@IdRes int buttonResId, @DrawableRes int iconResId) {
super(buttonResId, iconResId);
mStyleRes = style;
mIsNavigationBarShowing = true;
mRotationLockController = Dependency.get(RotationLockController.class);
// Register the task stack listener
mTaskStackListener = new TaskStackListenerImpl();
ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
setOnClickListener(this::onRotateSuggestionClick);
setOnHoverListener(this::onRotateSuggestionHover);
try {
WindowManagerGlobal.getWindowManagerService()
.watchRotation(mRotationWatcher, context.getDisplay().getDisplayId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
public void addRotationCallback(Consumer<Integer> watcher) {
mRotWatcherListener = watcher;
}
public void setRotateSuggestionButtonState(boolean visible) {
setRotateSuggestionButtonState(visible, false /* force */);
}
public void setRotateSuggestionButtonState(final boolean visible, final boolean force) {
// At any point the 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 && !isVisible()) return;
final View view = getCurrentView();
if (view == null) return;
final KeyButtonDrawable currentDrawable = getImageDrawable();
if (currentDrawable == null) return;
// Clear any pending suggestion flag as it has either been nullified or is being shown
mPendingRotationSuggestion = false;
view.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
if (currentDrawable.canAnimate()) {
currentDrawable.resetAnimation();
currentDrawable.startAnimation();
}
if (!isRotateSuggestionIntroduced()) mViewRippler.start(view);
// Set visibility unless a11y service is active.
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();
}
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(Interpolators.LINEAR);
fadeOut.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
hide();
}
});
mRotateHideAnimator = fadeOut;
fadeOut.start();
}
}
public void setAccessibilityFeedbackEnabled(boolean flag) {
mAccessibilityFeedbackEnabled = flag;
}
public void setRotationLockedAtAngle(int rotationSuggestion) {
mRotationLockController.setRotationLockedAtAngle(true /* locked */, rotationSuggestion);
}
public boolean isRotationLocked() {
return mRotationLockController.isRotationLocked();
}
public void onRotationProposal(int rotation, int windowRotation, boolean isValid) {
// 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) {
if (getCurrentView() != null) {
getCurrentView().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 = isRotationAnimationCCW(windowRotation, rotation);
int style;
if (windowRotation == Surface.ROTATION_0 || windowRotation == Surface.ROTATION_180) {
style = rotationCCW ? R.style.RotateButtonCCWStart90 : R.style.RotateButtonCWStart90;
} else { // 90 or 270
style = rotationCCW ? R.style.RotateButtonCCWStart0 : R.style.RotateButtonCWStart0;
}
mStyleRes = style;
updateIcon();
if (mIsNavigationBarShowing) {
// The navbar is visible 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;
if (getCurrentView() != null) {
getCurrentView().removeCallbacks(mCancelPendingRotationProposal);
getCurrentView().postDelayed(mCancelPendingRotationProposal,
NAVBAR_HIDDEN_PENDING_ICON_TIMEOUT_MS);
}
}
}
public void onDisable2FlagChanged(int state2) {
final boolean rotateSuggestionsDisabled = hasDisable2RotateSuggestionFlag(state2);
if (rotateSuggestionsDisabled) onRotationSuggestionsDisabled();
}
public void onNavigationBarWindowVisibilityChange(boolean showing) {
if (mIsNavigationBarShowing != showing) {
mIsNavigationBarShowing = showing;
// If the navbar is visible, show the rotate button if there's a pending suggestion
if (showing && mPendingRotationSuggestion) {
showAndLogRotationSuggestion();
}
}
@Override
public void setRotationButtonController(RotationButtonController rotationButtonController) {
mRotationButtonController = rotationButtonController;
}
@Override
@@ -309,116 +58,15 @@ public class RotationContextButton extends ContextualButton implements
@Override
protected KeyButtonDrawable getNewDrawable() {
Context context = new ContextThemeWrapper(getContext().getApplicationContext(), mStyleRes);
Context context = new ContextThemeWrapper(getContext().getApplicationContext(),
mRotationButtonController.getStyleRes());
return KeyButtonDrawable.create(context, mIconResId, false /* shadow */,
QuickStepContract.isGesturalMode(mNavBarMode));
}
@Override
public void onDestroy() {
// Unregister the task stack listener
ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener);
try {
WindowManagerGlobal.getWindowManagerService().removeRotationWatcher(mRotationWatcher);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
private void onRotateSuggestionClick(View v) {
mMetricsLogger.action(MetricsEvent.ACTION_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 */);
if (getCurrentView() != null) {
getCurrentView().removeCallbacks(mRemoveRotationProposal);
}
}
private void showAndLogRotationSuggestion() {
setRotateSuggestionButtonState(true /* visible */);
rescheduleRotationTimeout(false /* reasonHover */);
mMetricsLogger.visible(MetricsEvent.ROTATION_SUGGESTION_SHOWN);
}
private boolean shouldOverrideUserLockPrefs(final int rotation) {
// 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 boolean isRotationAnimationCCW(int from, int to) {
// All 180deg WM rotation animations are CCW, match that
if (from == Surface.ROTATION_0 && to == Surface.ROTATION_90) return false;
if (from == Surface.ROTATION_0 && to == Surface.ROTATION_180) return true; //180d so CCW
if (from == Surface.ROTATION_0 && to == Surface.ROTATION_270) return true;
if (from == Surface.ROTATION_90 && to == Surface.ROTATION_0) return true;
if (from == Surface.ROTATION_90 && to == Surface.ROTATION_180) return false;
if (from == Surface.ROTATION_90 && to == Surface.ROTATION_270) return true; //180d so CCW
if (from == Surface.ROTATION_180 && to == Surface.ROTATION_0) return true; //180d so CCW
if (from == Surface.ROTATION_180 && to == Surface.ROTATION_90) return true;
if (from == Surface.ROTATION_180 && to == Surface.ROTATION_270) return false;
if (from == Surface.ROTATION_270 && to == Surface.ROTATION_0) return false;
if (from == Surface.ROTATION_270 && to == Surface.ROTATION_90) return true; //180d so CCW
if (from == Surface.ROTATION_270 && to == Surface.ROTATION_180) return true;
return false; // Default
}
private void rescheduleRotationTimeout(final boolean reasonHover) {
if (getCurrentView() == null) {
return;
}
// 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 (!isVisible()) return;
}
// Stop any pending removal
getCurrentView().removeCallbacks(mRemoveRotationProposal);
// Schedule timeout
getCurrentView().postDelayed(mRemoveRotationProposal, computeRotationProposalTimeout());
}
private int computeRotationProposalTimeout() {
if (mAccessibilityFeedbackEnabled) return 10000;
if (mHoveringRotationSuggestion) return 8000;
return 5000;
}
private boolean isRotateSuggestionIntroduced() {
ContentResolver cr = getContext().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 = getContext().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);
}
mRotationButtonController.cleanUp();
}
@Override
@@ -426,66 +74,9 @@ public class RotationContextButton extends ContextualButton implements
mNavBarMode = mode;
}
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 */);
});
}
}
private class ViewRippler {
private static final int RIPPLE_OFFSET_MS = 50;
private static final int RIPPLE_INTERVAL_MS = 2000;
private View mRoot;
public void start(View root) {
stop(); // Stop any pending ripple animations
mRoot = root;
// Schedule pending ripples, offset the 1st to avoid problems with visibility change
mRoot.postOnAnimationDelayed(mRipple, RIPPLE_OFFSET_MS);
mRoot.postOnAnimationDelayed(mRipple, RIPPLE_INTERVAL_MS);
mRoot.postOnAnimationDelayed(mRipple, 2 * RIPPLE_INTERVAL_MS);
mRoot.postOnAnimationDelayed(mRipple, 3 * RIPPLE_INTERVAL_MS);
mRoot.postOnAnimationDelayed(mRipple, 4 * RIPPLE_INTERVAL_MS);
}
public void stop() {
if (mRoot != null) mRoot.removeCallbacks(mRipple);
}
private final Runnable mRipple = new Runnable() {
@Override
public void run() { // Cause the ripple to fire via false presses
if (!mRoot.isAttachedToWindow()) return;
mRoot.setPressed(true /* pressed */);
mRoot.setPressed(false /* pressed */);
}
};
@Override
public boolean acceptRotationProposal() {
View currentView = getCurrentView();
return currentView != null && currentView.isAttachedToWindow();
}
}

View File

@@ -271,6 +271,8 @@ public class NavigationBarFragmentTest extends SysuiBaseFragmentTest {
when(view.getBarTransitions()).thenReturn(mock(BarTransitions.class));
when(view.getLightTransitionsController()).thenReturn(
mock(LightBarTransitionsController.class));
when(view.getRotationButtonController()).thenReturn(
mock(RotationButtonController.class));
return view;
}
}

View File

@@ -51,7 +51,8 @@ public class NavigationBarRotationContextTest extends SysuiTestCase {
public final SysuiTestableContext mContext = new SysuiTestableContext(
InstrumentationRegistry.getContext(), getLeakCheck());
private final TestableDependency mDependency = new TestableDependency(mContext);
private RotationContextButton mButton;
private RotationButtonController mRotationButtonController;
private RotationButton mRotationButton;
@Before
public void setup() {
@@ -59,50 +60,66 @@ public class NavigationBarRotationContextTest extends SysuiTestCase {
mDependency.injectMockDependency(RotationLockController.class);
final View view = new View(mContext);
mButton = spy(new RotationContextButton(RES_UNDEF, RES_UNDEF, mContext, RES_UNDEF));
mRotationButton = mock(RotationButton.class);
mRotationButtonController = spy(
new RotationButtonController(mContext, RES_UNDEF, mRotationButton));
final KeyButtonDrawable kbd = mock(KeyButtonDrawable.class);
doReturn(view).when(mButton).getCurrentView();
doReturn(kbd).when(mButton).getNewDrawable();
doReturn(view).when(mRotationButton).getCurrentView();
doReturn(true).when(mRotationButton).acceptRotationProposal();
}
@Test
public void testOnInvalidRotationProposal() {
mButton.onRotationProposal(DEFAULT_ROTATE, DEFAULT_ROTATE + 1, false /* isValid */);
verify(mButton, times(1)).setRotateSuggestionButtonState(false /* visible */);
mRotationButtonController.onRotationProposal(DEFAULT_ROTATE, DEFAULT_ROTATE + 1,
false /* isValid */);
verify(mRotationButtonController, times(1)).setRotateSuggestionButtonState(
false /* visible */);
}
@Test
public void testOnSameRotationProposal() {
mButton.onRotationProposal(DEFAULT_ROTATE, DEFAULT_ROTATE, true /* isValid */);
verify(mButton, times(1)).setRotateSuggestionButtonState(false /* visible */);
mRotationButtonController.onRotationProposal(DEFAULT_ROTATE, DEFAULT_ROTATE,
true /* isValid */);
verify(mRotationButtonController, times(1)).setRotateSuggestionButtonState(
false /* visible */);
}
@Test
public void testOnRotationProposalShowButtonShowNav() {
// No navigation bar should not call to set visibility state
mButton.onNavigationBarWindowVisibilityChange(false /* showing */);
verify(mButton, times(0)).setRotateSuggestionButtonState(false /* visible */);
verify(mButton, times(0)).setRotateSuggestionButtonState(true /* visible */);
mRotationButtonController.onNavigationBarWindowVisibilityChange(false /* showing */);
verify(mRotationButtonController, times(0)).setRotateSuggestionButtonState(
false /* visible */);
verify(mRotationButtonController, times(0)).setRotateSuggestionButtonState(
true /* visible */);
// No navigation bar with rotation change should not call to set visibility state
mButton.onRotationProposal(DEFAULT_ROTATE, DEFAULT_ROTATE + 1, true /* isValid */);
verify(mButton, times(0)).setRotateSuggestionButtonState(false /* visible */);
verify(mButton, times(0)).setRotateSuggestionButtonState(true /* visible */);
mRotationButtonController.onRotationProposal(DEFAULT_ROTATE, DEFAULT_ROTATE + 1,
true /* isValid */);
verify(mRotationButtonController, times(0)).setRotateSuggestionButtonState(
false /* visible */);
verify(mRotationButtonController, times(0)).setRotateSuggestionButtonState(
true /* visible */);
// Since rotation has changed rotation should be pending, show mButton when showing nav bar
mButton.onNavigationBarWindowVisibilityChange(true /* showing */);
verify(mButton, times(1)).setRotateSuggestionButtonState(true /* visible */);
mRotationButtonController.onNavigationBarWindowVisibilityChange(true /* showing */);
verify(mRotationButtonController, times(1)).setRotateSuggestionButtonState(
true /* visible */);
}
@Test
public void testOnRotationProposalShowButton() {
// Navigation bar being visible should not call to set visibility state
mButton.onNavigationBarWindowVisibilityChange(true /* showing */);
verify(mButton, times(0)).setRotateSuggestionButtonState(false /* visible */);
verify(mButton, times(0)).setRotateSuggestionButtonState(true /* visible */);
mRotationButtonController.onNavigationBarWindowVisibilityChange(true /* showing */);
verify(mRotationButtonController, times(0)).setRotateSuggestionButtonState(
false /* visible */);
verify(mRotationButtonController, times(0)).setRotateSuggestionButtonState(
true /* visible */);
// Navigation bar is visible and rotation requested
mButton.onRotationProposal(DEFAULT_ROTATE, DEFAULT_ROTATE + 1, true /* isValid */);
verify(mButton, times(1)).setRotateSuggestionButtonState(true /* visible */);
mRotationButtonController.onRotationProposal(DEFAULT_ROTATE, DEFAULT_ROTATE + 1,
true /* isValid */);
verify(mRotationButtonController, times(1)).setRotateSuggestionButtonState(
true /* visible */);
}
}