diff --git a/packages/SystemUI/res/layout/start_contextual.xml b/packages/SystemUI/res/layout/start_contextual.xml
deleted file mode 100644
index e022c7301e3b6..0000000000000
--- a/packages/SystemUI/res/layout/start_contextual.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index d774c551b5475..27431f7257f22 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -331,7 +331,7 @@
left[.5W],back[1WC];home;recent[1WC],right[.5W]
back[1.7WC];home;contextual[1.7WC]
- start_contextual[40AC];home_handle;ime_switcher[40AC]
+ back[40AC];home_handle;ime_switcher[40AC]
false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/FloatingRotationButton.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/FloatingRotationButton.java
new file mode 100644
index 0000000000000..215aa77920d91
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/FloatingRotationButton.java
@@ -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();
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java
index c9ba76c2be9e3..337c6b167cdf9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java
@@ -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;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
index a12ae96f57b06..662c744f8c95b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
@@ -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);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java
index 3b3336b3f14d7..8a28c6fbff295 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarTransitions.java
@@ -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);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
index 5f619759d1ffd..7682e8a594cef 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java
@@ -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 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(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationButton.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationButton.java
new file mode 100644
index 0000000000000..2580c0e770134
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationButton.java
@@ -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;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationButtonController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationButtonController.java
new file mode 100644
index 0000000000000..1e5406f3a5370
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationButtonController.java
@@ -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 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 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 */);
+ }
+ };
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationContextButton.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationContextButton.java
index 7203e57c3d23f..f1460a62903db 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationContextButton.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationContextButton.java
@@ -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 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 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();
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarFragmentTest.java
index ad9c7299f8ed7..4181d38ad2c57 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarFragmentTest.java
@@ -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;
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarRotationContextTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarRotationContextTest.java
index 28e91eaadd491..cf084287a7b73 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarRotationContextTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarRotationContextTest.java
@@ -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 */);
}
}