(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
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 0000000..c3569d8
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/background_panel.xml
@@ -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>
\ 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 0000000..c20b7d9
--- /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 1aaca60..48acfc1 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 @@
         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 88f3375..04e8cf9 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.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 @@
 
     private final float mTutorialHeightRatio;
     private final WindowManager mWindowManager;
+    private final BackgroundWindowManager mBackgroundWindowManager;
 
     private @OneHandedState.State int mCurrentState;
     private int mTutorialAreaHeight;
@@ -79,9 +79,10 @@
     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);
     }
@@ -110,8 +111,19 @@
     }
 
     @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 @@
             case STATE_NONE:
                 checkTransitionEnd();
                 removeTutorialFromWindowManager();
-                break;
             default:
                 break;
         }
@@ -146,6 +157,7 @@
         }
         mTutorialAreaHeight = Math.round(mDisplayBounds.height() * mTutorialHeightRatio);
         mAlphaTransitionStart = mTutorialAreaHeight * START_TRANSITION_FRACTION;
+        mBackgroundWindowManager.onDisplayChanged(displayLayout);
     }
 
     @VisibleForTesting
@@ -169,6 +181,7 @@
     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 @@
         mTargetViewContainer = null;
     }
 
+    @VisibleForTesting
+    void removeBackgroundSurface() {
+        mBackgroundWindowManager.removeBackgroundLayer();
+    }
+
     /**
      * Returns layout params for the dismiss target, using the latest display metrics.
      */
@@ -213,9 +231,12 @@
      * 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 @@
         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 @@
         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 0000000..f3f7067
--- /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 b1434ca..63d8bfd 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 @@
     OneHandedSettingsUtil mMockSettingsUtil;
     @Mock
     WindowManager mMockWindowManager;
+    @Mock
+    BackgroundWindowManager mMockBackgroundWindowManager;
 
     @Before
     public void setUp() {
@@ -63,10 +65,11 @@
         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);
     }