(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
This commit is contained in:
26
libs/WindowManager/Shell/res/layout/background_panel.xml
Normal file
26
libs/WindowManager/Shell/res/layout/background_panel.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
~ 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
|
||||
-->
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/background_panel_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_horizontal | center_vertical"
|
||||
android:background="@android:color/transparent">
|
||||
</LinearLayout>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -199,8 +199,9 @@ public class OneHandedController implements RemoteCallable<OneHandedController>,
|
||||
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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user