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 */); } }