From 6d4fc4ca09ed6d52a88ab6e27c839105af3ff8d9 Mon Sep 17 00:00:00 2001 From: Jason Chang Date: Mon, 13 Dec 2021 16:44:16 +0800 Subject: [PATCH] (2/n) Implement BackgroundWindowManager to manage tutorial background Using WindowlessWindowManager for adding a surface wihtout depend on Window Hierarchy, we just add leash with specific theme color at bottom z-order while One-handed trigger, and remove it when exited. Bug: 197165590 Test: manually test One-handed mode basic operations. Test: atest WMShellUnitTests Change-Id: Ie53684edd4511719516c00dd64c2dfad360a593b --- .../Shell/res/layout/background_panel.xml | 26 ++ .../onehanded/BackgroundWindowManager.java | 246 ++++++++++++++++++ .../shell/onehanded/OneHandedController.java | 3 +- .../onehanded/OneHandedTutorialHandler.java | 39 ++- .../BackgroundWindowManagerTest.java | 61 +++++ .../OneHandedTutorialHandlerTest.java | 7 +- 6 files changed, 376 insertions(+), 6 deletions(-) create mode 100644 libs/WindowManager/Shell/res/layout/background_panel.xml create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java create mode 100644 libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/BackgroundWindowManagerTest.java diff --git a/libs/WindowManager/Shell/res/layout/background_panel.xml b/libs/WindowManager/Shell/res/layout/background_panel.xml new file mode 100644 index 0000000000000..c3569d80fa1ea --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/background_panel.xml @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java new file mode 100644 index 0000000000000..c20b7d9b27472 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2022 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.wm.shell.onehanded; + +import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; +import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; +import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; + +import static com.android.wm.shell.onehanded.OneHandedState.STATE_ACTIVE; +import static com.android.wm.shell.onehanded.OneHandedState.STATE_ENTERING; + +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.os.Binder; +import android.util.Slog; +import android.view.ContextThemeWrapper; +import android.view.IWindow; +import android.view.LayoutInflater; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.SurfaceSession; +import android.view.View; +import android.view.WindowManager; +import android.view.WindowlessWindowManager; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.wm.shell.R; +import com.android.wm.shell.common.DisplayLayout; + +import java.io.PrintWriter; + +/** + * Holds view hierarchy of a root surface and helps inflate a themeable view for background. + */ +public final class BackgroundWindowManager extends WindowlessWindowManager { + private static final String TAG = BackgroundWindowManager.class.getSimpleName(); + private static final int THEME_COLOR_OFFSET = 10; + + private final OneHandedSurfaceTransactionHelper.SurfaceControlTransactionFactory + mTransactionFactory; + + private Context mContext; + private Rect mDisplayBounds; + private SurfaceControlViewHost mViewHost; + private SurfaceControl mLeash; + private View mBackgroundView; + private @OneHandedState.State int mCurrentState; + + public BackgroundWindowManager(Context context) { + super(context.getResources().getConfiguration(), null /* rootSurface */, + null /* hostInputToken */); + mContext = context; + mTransactionFactory = SurfaceControl.Transaction::new; + } + + @Override + public SurfaceControl getSurfaceControl(IWindow window) { + return super.getSurfaceControl(window); + } + + @Override + public void setConfiguration(Configuration configuration) { + super.setConfiguration(configuration); + mContext = mContext.createConfigurationContext(configuration); + } + + /** + * onConfigurationChanged events for updating background theme color. + */ + public void onConfigurationChanged() { + if (mCurrentState == STATE_ENTERING || mCurrentState == STATE_ACTIVE) { + updateThemeOnly(); + } + } + + /** + * One-handed mode state changed callback + * @param newState of One-handed mode representing by {@link OneHandedState} + */ + public void onStateChanged(int newState) { + mCurrentState = newState; + } + + @Override + protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) { + final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) + .setColorLayer() + .setBufferSize(mDisplayBounds.width(), mDisplayBounds.height()) + .setFormat(PixelFormat.RGB_888) + .setOpaque(true) + .setName(TAG) + .setCallsite("BackgroundWindowManager#attachToParentSurface"); + mLeash = builder.build(); + b.setParent(mLeash); + } + + /** Inflates background view on to the root surface. */ + boolean initView() { + if (mBackgroundView != null || mViewHost != null) { + return false; + } + + mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this); + mBackgroundView = (View) LayoutInflater.from(mContext) + .inflate(R.layout.background_panel, null /* root */); + WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + mDisplayBounds.width(), mDisplayBounds.height(), 0 /* TYPE NONE */, + FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_WATCH_OUTSIDE_TOUCH + | FLAG_SLIPPERY, PixelFormat.TRANSLUCENT); + lp.token = new Binder(); + lp.setTitle("background-panel"); + lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY; + mBackgroundView.setBackgroundColor(getThemeColorForBackground()); + mViewHost.setView(mBackgroundView, lp); + return true; + } + + /** + * Called when onDisplayAdded() or onDisplayRemoved() callback. + * @param displayLayout The latest {@link DisplayLayout} for display bounds. + */ + public void onDisplayChanged(DisplayLayout displayLayout) { + // One-handed mode is only available on portrait. + if (displayLayout.height() > displayLayout.width()) { + mDisplayBounds = new Rect(0, 0, displayLayout.width(), displayLayout.height()); + } else { + mDisplayBounds = new Rect(0, 0, displayLayout.height(), displayLayout.width()); + } + } + + private void updateThemeOnly() { + if (mBackgroundView == null || mViewHost == null || mLeash == null) { + Slog.w(TAG, "Background view or SurfaceControl does not exist when trying to " + + "update theme only!"); + return; + } + + WindowManager.LayoutParams lp = (WindowManager.LayoutParams) + mBackgroundView.getLayoutParams(); + mBackgroundView.setBackgroundColor(getThemeColorForBackground()); + mViewHost.setView(mBackgroundView, lp); + } + + /** + * Shows the background layer when One-handed mode triggered. + */ + public void showBackgroundLayer() { + if (!initView()) { + updateThemeOnly(); + return; + } + if (mLeash == null) { + Slog.w(TAG, "SurfaceControl mLeash is null, can't show One-handed mode " + + "background panel!"); + return; + } + + mTransactionFactory.getTransaction() + .setAlpha(mLeash, 1.0f) + .setLayer(mLeash, -1 /* at bottom-most layer */) + .show(mLeash) + .apply(); + } + + /** + * Remove the leash of background layer after stop One-handed mode. + */ + public void removeBackgroundLayer() { + if (mBackgroundView != null) { + mBackgroundView = null; + } + + if (mViewHost != null) { + mViewHost.release(); + mViewHost = null; + } + + if (mLeash != null) { + mTransactionFactory.getTransaction().remove(mLeash).apply(); + mLeash = null; + } + } + + /** + * Gets {@link SurfaceControl} of the background layer. + * @return {@code null} if not exist. + */ + @Nullable + SurfaceControl getSurfaceControl() { + return mLeash; + } + + private int getThemeColor() { + final Context themedContext = new ContextThemeWrapper(mContext, + com.android.internal.R.style.Theme_DeviceDefault_DayNight); + return themedContext.getColor(R.color.one_handed_tutorial_background_color); + } + + int getThemeColorForBackground() { + final int origThemeColor = getThemeColor(); + return android.graphics.Color.argb(Color.alpha(origThemeColor), + Color.red(origThemeColor) - THEME_COLOR_OFFSET, + Color.green(origThemeColor) - THEME_COLOR_OFFSET, + Color.blue(origThemeColor) - THEME_COLOR_OFFSET); + } + + private float adjustColor(int origColor) { + return Math.max(origColor - THEME_COLOR_OFFSET, 0) / 255.0f; + } + + void dump(@NonNull PrintWriter pw) { + final String innerPrefix = " "; + pw.println(TAG); + pw.print(innerPrefix + "mDisplayBounds="); + pw.println(mDisplayBounds); + pw.print(innerPrefix + "mViewHost="); + pw.println(mViewHost); + pw.print(innerPrefix + "mLeash="); + pw.println(mLeash); + pw.print(innerPrefix + "mBackgroundView="); + pw.println(mBackgroundView); + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java index 1aaca60538e73..48acfc1c76e70 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java @@ -199,8 +199,9 @@ public class OneHandedController implements RemoteCallable, OneHandedAccessibilityUtil accessibilityUtil = new OneHandedAccessibilityUtil(context); OneHandedTimeoutHandler timeoutHandler = new OneHandedTimeoutHandler(mainExecutor); OneHandedState oneHandedState = new OneHandedState(); + BackgroundWindowManager backgroundWindowManager = new BackgroundWindowManager(context); OneHandedTutorialHandler tutorialHandler = new OneHandedTutorialHandler(context, - settingsUtil, windowManager); + settingsUtil, windowManager, backgroundWindowManager); OneHandedAnimationController animationController = new OneHandedAnimationController(context); OneHandedTouchHandler touchHandler = new OneHandedTouchHandler(timeoutHandler, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTutorialHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTutorialHandler.java index 88f33755fa2da..04e8cf9d2c44b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTutorialHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedTutorialHandler.java @@ -32,7 +32,6 @@ import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.PixelFormat; import android.graphics.Rect; -import android.os.SystemProperties; import android.view.ContextThemeWrapper; import android.view.Gravity; import android.view.LayoutInflater; @@ -65,6 +64,7 @@ public class OneHandedTutorialHandler implements OneHandedTransitionCallback, private final float mTutorialHeightRatio; private final WindowManager mWindowManager; + private final BackgroundWindowManager mBackgroundWindowManager; private @OneHandedState.State int mCurrentState; private int mTutorialAreaHeight; @@ -79,9 +79,10 @@ public class OneHandedTutorialHandler implements OneHandedTransitionCallback, private int mAlphaAnimationDurationMs; public OneHandedTutorialHandler(Context context, OneHandedSettingsUtil settingsUtil, - WindowManager windowManager) { + WindowManager windowManager, BackgroundWindowManager backgroundWindowManager) { mContext = context; mWindowManager = windowManager; + mBackgroundWindowManager = backgroundWindowManager; mTutorialHeightRatio = settingsUtil.getTranslationFraction(context); mAlphaAnimationDurationMs = settingsUtil.getTransitionDuration(context); } @@ -109,9 +110,20 @@ public class OneHandedTutorialHandler implements OneHandedTransitionCallback, mAlphaAnimator.start(); } + @Override + public void onStartFinished(Rect bounds) { + fillBackgroundColor(); + } + + @Override + public void onStopFinished(Rect bounds) { + removeBackgroundSurface(); + } + @Override public void onStateChanged(int newState) { mCurrentState = newState; + mBackgroundWindowManager.onStateChanged(newState); switch (newState) { case STATE_ENTERING: createViewAndAttachToWindow(mContext); @@ -126,7 +138,6 @@ public class OneHandedTutorialHandler implements OneHandedTransitionCallback, case STATE_NONE: checkTransitionEnd(); removeTutorialFromWindowManager(); - break; default: break; } @@ -146,6 +157,7 @@ public class OneHandedTutorialHandler implements OneHandedTransitionCallback, } mTutorialAreaHeight = Math.round(mDisplayBounds.height() * mTutorialHeightRatio); mAlphaTransitionStart = mTutorialAreaHeight * START_TRANSITION_FRACTION; + mBackgroundWindowManager.onDisplayChanged(displayLayout); } @VisibleForTesting @@ -169,6 +181,7 @@ public class OneHandedTutorialHandler implements OneHandedTransitionCallback, private void attachTargetToWindow() { try { mWindowManager.addView(mTargetViewContainer, getTutorialTargetLayoutParams()); + mBackgroundWindowManager.showBackgroundLayer(); } catch (IllegalStateException e) { // This shouldn't happen, but if the target is already added, just update its // layout params. @@ -186,6 +199,11 @@ public class OneHandedTutorialHandler implements OneHandedTransitionCallback, mTargetViewContainer = null; } + @VisibleForTesting + void removeBackgroundSurface() { + mBackgroundWindowManager.removeBackgroundLayer(); + } + /** * Returns layout params for the dismiss target, using the latest display metrics. */ @@ -213,9 +231,12 @@ public class OneHandedTutorialHandler implements OneHandedTransitionCallback, * onConfigurationChanged events for updating tutorial text. */ public void onConfigurationChanged() { + mBackgroundWindowManager.onConfigurationChanged(); + removeTutorialFromWindowManager(); if (mCurrentState == STATE_ENTERING || mCurrentState == STATE_ACTIVE) { createViewAndAttachToWindow(mContext); + fillBackgroundColor(); updateThemeColor(); checkTransitionEnd(); } @@ -247,6 +268,14 @@ public class OneHandedTutorialHandler implements OneHandedTransitionCallback, tutorialDesc.setTextColor(themedTextColorSecondary); } + private void fillBackgroundColor() { + if (mTargetViewContainer == null || mBackgroundWindowManager == null) { + return; + } + mTargetViewContainer.setBackgroundColor( + mBackgroundWindowManager.getThemeColorForBackground()); + } + private void setupAlphaTransition(boolean isEntering) { final float start = isEntering ? 0.0f : 1.0f; final float end = isEntering ? 1.0f : 0.0f; @@ -282,5 +311,9 @@ public class OneHandedTutorialHandler implements OneHandedTransitionCallback, pw.println(mAlphaTransitionStart); pw.print(innerPrefix + "mAlphaAnimationDurationMs="); pw.println(mAlphaAnimationDurationMs); + + if (mBackgroundWindowManager != null) { + mBackgroundWindowManager.dump(pw); + } } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/BackgroundWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/BackgroundWindowManagerTest.java new file mode 100644 index 0000000000000..f3f70673b332f --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/BackgroundWindowManagerTest.java @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2022 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.wm.shell.onehanded; + +import static com.google.common.truth.Truth.assertThat; + +import android.testing.TestableLooper; + +import androidx.test.annotation.UiThreadTest; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.DisplayLayout; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** Tests for {@link BackgroundWindowManager} */ +@SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@RunWith(AndroidJUnit4.class) +public class BackgroundWindowManagerTest extends ShellTestCase { + private BackgroundWindowManager mBackgroundWindowManager; + @Mock + private DisplayLayout mMockDisplayLayout; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mBackgroundWindowManager = new BackgroundWindowManager(mContext); + mBackgroundWindowManager.onDisplayChanged(mMockDisplayLayout); + } + + @Test + @UiThreadTest + public void testInitRelease() { + mBackgroundWindowManager.initView(); + assertThat(mBackgroundWindowManager.getSurfaceControl()).isNotNull(); + + mBackgroundWindowManager.removeBackgroundLayer(); + assertThat(mBackgroundWindowManager.getSurfaceControl()).isNull(); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTutorialHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTutorialHandlerTest.java index b1434ca325b73..63d8bfd1e7ef4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTutorialHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedTutorialHandlerTest.java @@ -56,6 +56,8 @@ public class OneHandedTutorialHandlerTest extends OneHandedTestCase { OneHandedSettingsUtil mMockSettingsUtil; @Mock WindowManager mMockWindowManager; + @Mock + BackgroundWindowManager mMockBackgroundWindowManager; @Before public void setUp() { @@ -63,10 +65,11 @@ public class OneHandedTutorialHandlerTest extends OneHandedTestCase { when(mMockSettingsUtil.getTutorialShownCounts(any(), anyInt())).thenReturn(0); mDisplay = mContext.getDisplay(); - mDisplayLayout = new DisplayLayout(mContext, mDisplay); + mDisplayLayout = new DisplayLayout(getTestContext().getApplicationContext(), mDisplay); mSpiedTransitionState = spy(new OneHandedState()); mSpiedTutorialHandler = spy( - new OneHandedTutorialHandler(mContext, mMockSettingsUtil, mMockWindowManager)); + new OneHandedTutorialHandler(mContext, mMockSettingsUtil, mMockWindowManager, + mMockBackgroundWindowManager)); mTimeoutHandler = new OneHandedTimeoutHandler(mMockShellMainExecutor); }