Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6d4fc4ca authored by Jason Chang's avatar Jason Chang
Browse files

(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
parent f25e1e90
Loading
Loading
Loading
Loading
+26 −0
Original line number Diff line number Diff line
<?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
+246 −0
Original line number Diff line number Diff line
/*
 * 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);
    }

}
+2 −1
Original line number Diff line number Diff line
@@ -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,
+36 −3
Original line number Diff line number Diff line
@@ -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);
        }
    }
}
+61 −0
Original line number Diff line number Diff line
/**
 * 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();
    }
}
Loading