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

Commit de551ad9 authored by Chris Li's avatar Chris Li
Browse files

Allow non-resizable apps in split-screen (12/n)

Before, the hint uses PopupWindow to be placed at the bottom right of the
Display, which can't be attached to Task. Now, render size compat button
 hint popup on Task surface with WindowlessWindowManager.

Fix: 178327644
Bug: 176061101
Test: atest WMShellUnitTests:SizeCompatUILayoutTest
Test: atest WMShellUnitTests:SizeCompatHintPopupTest
Change-Id: I6f8d7afc996d7f312516d85deb2bd113eae8bc3f
parent e3ca0e42
Loading
Loading
Loading
Loading
+45 −28
Original line number Diff line number Diff line
@@ -14,10 +14,23 @@
     See the License for the specific language governing permissions and
     limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<com.android.wm.shell.sizecompatui.SizeCompatHintPopup
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <FrameLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:clipToPadding="false"
        android:padding="@dimen/bubble_elevation">

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@android:color/background_light"
            android:elevation="@dimen/bubble_elevation"
            android:orientation="vertical">

            <TextView
@@ -46,3 +59,7 @@
                android:textStyle="bold"/>

        </LinearLayout>

    </FrameLayout>

</com.android.wm.shell.sizecompatui.SizeCompatHintPopup>
+71 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.sizecompatui;

import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.drawable.RippleDrawable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Button;
import android.widget.FrameLayout;

import androidx.annotation.Nullable;

import com.android.wm.shell.R;

/** Popup to show the hint about the {@link SizeCompatRestartButton}. */
public class SizeCompatHintPopup extends FrameLayout implements View.OnClickListener {

    private SizeCompatUILayout mLayout;

    public SizeCompatHintPopup(Context context) {
        super(context);
    }

    public SizeCompatHintPopup(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public SizeCompatHintPopup(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public SizeCompatHintPopup(Context context, AttributeSet attrs, int defStyleAttr,
            int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    void inject(SizeCompatUILayout layout) {
        mLayout = layout;
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        final Button gotItButton = findViewById(R.id.got_it);
        gotItButton.setBackground(new RippleDrawable(ColorStateList.valueOf(Color.LTGRAY),
                null /* content */, null /* mask */));
        gotItButton.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        mLayout.dismissHint();
    }
}
+5 −82
Original line number Diff line number Diff line
@@ -22,19 +22,13 @@ import android.graphics.Color;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.RippleDrawable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.PopupWindow;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.internal.annotations.VisibleForTesting;
import com.android.wm.shell.R;

/** Button to restart the size compat activity. */
@@ -42,10 +36,6 @@ public class SizeCompatRestartButton extends FrameLayout implements View.OnClick
        View.OnLongClickListener {

    private SizeCompatUILayout mLayout;
    private ImageButton mRestartButton;
    @VisibleForTesting
    PopupWindow mShowingHint;
    private WindowManager.LayoutParams mWinParams;

    public SizeCompatRestartButton(@NonNull Context context) {
        super(context);
@@ -67,24 +57,19 @@ public class SizeCompatRestartButton extends FrameLayout implements View.OnClick

    void inject(SizeCompatUILayout layout) {
        mLayout = layout;
        mWinParams = layout.getWindowLayoutParams();
    }

    void remove() {
        dismissHint();
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mRestartButton = findViewById(R.id.size_compat_restart_button);
        final ImageButton restartButton = findViewById(R.id.size_compat_restart_button);
        final ColorStateList color = ColorStateList.valueOf(Color.LTGRAY);
        final GradientDrawable mask = new GradientDrawable();
        mask.setShape(GradientDrawable.OVAL);
        mask.setColor(color);
        mRestartButton.setBackground(new RippleDrawable(color, null /* content */, mask));
        mRestartButton.setOnClickListener(this);
        mRestartButton.setOnLongClickListener(this);
        restartButton.setBackground(new RippleDrawable(color, null /* content */, mask));
        restartButton.setOnClickListener(this);
        restartButton.setOnLongClickListener(this);
    }

    @Override
@@ -94,69 +79,7 @@ public class SizeCompatRestartButton extends FrameLayout implements View.OnClick

    @Override
    public boolean onLongClick(View v) {
        showHint();
        mLayout.onRestartButtonLongClicked();
        return true;
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (mLayout.mShouldShowHint) {
            mLayout.mShouldShowHint = false;
            showHint();
        }
    }

    @Override
    public void setVisibility(@Visibility int visibility) {
        if (visibility == View.GONE && mShowingHint != null) {
            // Also dismiss the popup.
            dismissHint();
        }
        super.setVisibility(visibility);
    }

    @Override
    public void setLayoutDirection(int layoutDirection) {
        final int gravity = SizeCompatUILayout.getGravity(layoutDirection);
        if (mWinParams.gravity != gravity) {
            mWinParams.gravity = gravity;
            getContext().getSystemService(WindowManager.class).updateViewLayout(this,
                    mWinParams);
        }
        super.setLayoutDirection(layoutDirection);
    }

    void showHint() {
        if (mShowingHint != null) {
            return;
        }

        // TODO: popup is not attached to the button surface. Need to handle this differently for
        // non-fullscreen task.
        final View popupView = LayoutInflater.from(getContext()).inflate(
                R.layout.size_compat_mode_hint, null);
        final PopupWindow popupWindow = new PopupWindow(popupView,
                LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
        popupWindow.setWindowLayoutType(mWinParams.type);
        popupWindow.setElevation(getResources().getDimension(R.dimen.bubble_elevation));
        popupWindow.setAnimationStyle(android.R.style.Animation_InputMethod);
        popupWindow.setClippingEnabled(false);
        popupWindow.setOnDismissListener(() -> mShowingHint = null);
        mShowingHint = popupWindow;

        final Button gotItButton = popupView.findViewById(R.id.got_it);
        gotItButton.setBackground(new RippleDrawable(ColorStateList.valueOf(Color.LTGRAY),
                null /* content */, null /* mask */));
        gotItButton.setOnClickListener(view -> dismissHint());
        popupWindow.showAtLocation(mRestartButton, mWinParams.gravity, mLayout.mPopupOffsetX,
                mLayout.mPopupOffsetY);
    }

    void dismissHint() {
        if (mShowingHint != null) {
            mShowingHint.dismiss();
            mShowingHint = null;
        }
    }
}
+8 −8
Original line number Diff line number Diff line
@@ -50,7 +50,7 @@ public class SizeCompatUIController implements DisplayController.OnDisplaysChang
    /** Whether the IME is shown on display id. */
    private final Set<Integer> mDisplaysWithIme = new ArraySet<>(1);

    /** The showing buttons by task id. */
    /** The showing UIs by task id. */
    private final SparseArray<SizeCompatUILayout> mActiveLayouts = new SparseArray<>(0);

    /** Avoid creating display context frequently for non-default display. */
@@ -77,12 +77,12 @@ public class SizeCompatUIController implements DisplayController.OnDisplaysChang
    }

    /**
     * Called when the Task info changed. Creates and updates the restart button if there is an
     * activity in size compat, or removes the restart button if there is no size compat activity.
     * Called when the Task info changed. Creates and updates the size compat UI if there is an
     * activity in size compat, or removes the UI if there is no size compat activity.
     *
     * @param displayId display the task and activity are in.
     * @param taskId task the activity is in.
     * @param taskConfig task config to place the restart button with.
     * @param taskConfig task config to place the size compat UI with.
     * @param sizeCompatActivity the size compat activity in the task. Can be {@code null} if the
     *                           top activity in this Task is not in size compat.
     * @param taskListener listener to handle the Task Surface placement.
@@ -94,10 +94,10 @@ public class SizeCompatUIController implements DisplayController.OnDisplaysChang
            // Null token means the current foreground activity is not in size compatibility mode.
            removeLayout(taskId);
        } else if (mActiveLayouts.contains(taskId)) {
            // Button already exists, update the button layout.
            // UI already exists, update the UI layout.
            updateLayout(taskId, taskConfig, sizeCompatActivity, taskListener);
        } else {
            // Create a new restart button.
            // Create a new size compat UI.
            createLayout(displayId, taskId, taskConfig, sizeCompatActivity, taskListener);
        }
    }
@@ -106,7 +106,7 @@ public class SizeCompatUIController implements DisplayController.OnDisplaysChang
    public void onDisplayRemoved(int displayId) {
        mDisplayContextCache.remove(displayId);

        // Remove all buttons on the removed display.
        // Remove all size compat UIs on the removed display.
        final List<Integer> toRemoveTaskIds = new ArrayList<>();
        forAllLayoutsOnDisplay(displayId, layout -> toRemoveTaskIds.add(layout.getTaskId()));
        for (int i = toRemoveTaskIds.size() - 1; i >= 0; i--) {
@@ -128,7 +128,7 @@ public class SizeCompatUIController implements DisplayController.OnDisplaysChang
            mDisplaysWithIme.remove(displayId);
        }

        // Hide the button when input method is showing.
        // Hide the size compat UIs when input method is showing.
        forAllLayoutsOnDisplay(displayId, layout -> layout.updateImeVisibility(isShowing));
    }

+117 −32
Original line number Diff line number Diff line
@@ -30,7 +30,6 @@ import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.os.Binder;
import android.os.IBinder;
import android.view.Gravity;
import android.view.SurfaceControl;
import android.view.View;
import android.view.WindowManager;
@@ -43,7 +42,7 @@ import com.android.wm.shell.common.SyncTransactionQueue;

/**
 * Records and handles layout of size compat UI on a task with size compat activity. Helps to
 * calculate proper bounds when configuration or button position changes.
 * calculate proper bounds when configuration or UI position changes.
 */
class SizeCompatUILayout {
    private static final String TAG = "SizeCompatUILayout";
@@ -56,12 +55,18 @@ class SizeCompatUILayout {
    private IBinder mActivityToken;
    private ShellTaskOrganizer.TaskListener mTaskListener;
    private DisplayLayout mDisplayLayout;
    @VisibleForTesting
    final SizeCompatUIWindowManager mWindowManager;

    @VisibleForTesting
    final SizeCompatUIWindowManager mButtonWindowManager;
    @VisibleForTesting
    @Nullable
    SizeCompatUIWindowManager mHintWindowManager;
    @VisibleForTesting
    @Nullable
    SizeCompatRestartButton mButton;
    @VisibleForTesting
    @Nullable
    SizeCompatHintPopup mHint;
    final int mButtonSize;
    final int mPopupOffsetX;
    final int mPopupOffsetY;
@@ -79,7 +84,7 @@ class SizeCompatUILayout {
        mTaskListener = taskListener;
        mDisplayLayout = displayLayout;
        mShouldShowHint = !hasShownHint;
        mWindowManager = new SizeCompatUIWindowManager(mContext, taskConfig, this);
        mButtonWindowManager = new SizeCompatUIWindowManager(mContext, taskConfig, this);

        mButtonSize =
                mContext.getResources().getDimensionPixelSize(R.dimen.size_compat_button_size);
@@ -87,21 +92,52 @@ class SizeCompatUILayout {
        mPopupOffsetY = mButtonSize;
    }

    /** Creates the button window. */
    /** Creates the activity restart button window. */
    void createSizeCompatButton(boolean isImeShowing) {
        if (isImeShowing || mButton != null) {
            // When ime is showing, wait until ime is dismiss to create UI.
            return;
        }
        mButton = mWindowManager.createSizeCompatUI();
        updateSurfacePosition();
        mButton = mButtonWindowManager.createSizeCompatButton();
        updateButtonSurfacePosition();

        if (mShouldShowHint) {
            // Only show by default for the first time.
            mShouldShowHint = false;
            createSizeCompatHint();
        }
    }

    /** Creates the restart button hint window. */
    private void createSizeCompatHint() {
        if (mHint != null) {
            // Hint already shown.
            return;
        }
        mHintWindowManager = createHintWindowManager();
        mHint = mHintWindowManager.createSizeCompatHint();
        updateHintSurfacePosition();
    }

    /** Releases the button window. */
    @VisibleForTesting
    SizeCompatUIWindowManager createHintWindowManager() {
        return new SizeCompatUIWindowManager(mContext, mTaskConfig, this);
    }

    /** Dismisses the hint window. */
    void dismissHint() {
        mHint = null;
        if (mHintWindowManager != null) {
            mHintWindowManager.release();
            mHintWindowManager = null;
        }
    }

    /** Releases the UI windows. */
    void release() {
        mButton.remove();
        dismissHint();
        mButton = null;
        mWindowManager.release();
        mButtonWindowManager.release();
    }

    /** Called when size compat info changed. */
@@ -115,7 +151,10 @@ class SizeCompatUILayout {

        // Update configuration.
        mContext = mContext.createConfigurationContext(taskConfig);
        mWindowManager.setConfiguration(taskConfig);
        mButtonWindowManager.setConfiguration(taskConfig);
        if (mHintWindowManager != null) {
            mHintWindowManager.setConfiguration(taskConfig);
        }

        if (mButton == null || prevTaskListener != taskListener) {
            // TaskListener changed, recreate the button for new surface parent.
@@ -126,14 +165,19 @@ class SizeCompatUILayout {

        if (!taskConfig.windowConfiguration.getBounds()
                .equals(prevTaskConfig.windowConfiguration.getBounds())) {
            // Reposition the button surface.
            updateSurfacePosition();
            // Reposition the UI surfaces.
            updateButtonSurfacePosition();
            updateHintSurfacePosition();
        }

        if (taskConfig.getLayoutDirection() != prevTaskConfig.getLayoutDirection()) {
            // Update layout for RTL.
            mButton.setLayoutDirection(taskConfig.getLayoutDirection());
            updateSurfacePosition();
            updateButtonSurfacePosition();
            if (mHint != null) {
                mHint.setLayoutDirection(taskConfig.getLayoutDirection());
                updateHintSurfacePosition();
            }
        }
    }

@@ -149,8 +193,9 @@ class SizeCompatUILayout {
        displayLayout.getStableBounds(curStableBounds);
        mDisplayLayout = displayLayout;
        if (!prevStableBounds.equals(curStableBounds)) {
            // Stable bounds changed, update button surface position.
            updateSurfacePosition();
            // Stable bounds changed, update UI surface positions.
            updateButtonSurfacePosition();
            updateHintSurfacePosition();
        }
    }

@@ -162,27 +207,46 @@ class SizeCompatUILayout {
            return;
        }

        // Hide size compat UIs when IME is showing.
        final int newVisibility = isImeShowing ? View.GONE : View.VISIBLE;
        if (mButton.getVisibility() != newVisibility) {
            mButton.setVisibility(newVisibility);
        }
        if (mHint != null && mHint.getVisibility() != newVisibility) {
            mHint.setVisibility(newVisibility);
        }
    }

    /** Gets the layout params for restart button. */
    WindowManager.LayoutParams getWindowLayoutParams() {
    WindowManager.LayoutParams getButtonWindowLayoutParams() {
        final WindowManager.LayoutParams winParams = new WindowManager.LayoutParams(
                // Cannot be wrap_content as this determines the actual window size
                mButtonSize, mButtonSize,
                TYPE_APPLICATION_OVERLAY,
                FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL,
                PixelFormat.TRANSLUCENT);
        winParams.gravity = getGravity(getLayoutDirection());
        winParams.token = new Binder();
        winParams.setTitle(SizeCompatRestartButton.class.getSimpleName() + mContext.getDisplayId());
        winParams.setTitle(SizeCompatRestartButton.class.getSimpleName() + getTaskId());
        winParams.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY;
        return winParams;
    }

    /** Gets the layout params for hint popup. */
    WindowManager.LayoutParams getHintWindowLayoutParams(SizeCompatHintPopup hint) {
        final WindowManager.LayoutParams winParams = new WindowManager.LayoutParams(
                // Cannot be wrap_content as this determines the actual window size
                hint.getMeasuredWidth(), hint.getMeasuredHeight(),
                TYPE_APPLICATION_OVERLAY,
                FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL,
                PixelFormat.TRANSLUCENT);
        winParams.token = new Binder();
        winParams.setTitle(SizeCompatHintPopup.class.getSimpleName() + getTaskId());
        winParams.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY;
        winParams.windowAnimations = android.R.style.Animation_InputMethod;
        return winParams;
    }

    /** Called when it is ready to be placed button surface button. */
    /** Called when it is ready to be placed size compat UI surface. */
    void attachToParentSurface(SurfaceControl.Builder b) {
        mTaskListener.attachChildSurfaceToTask(mTaskId, b);
    }
@@ -192,13 +256,17 @@ class SizeCompatUILayout {
        ActivityClient.getInstance().restartActivityProcessIfVisible(mActivityToken);
    }

    /** Called when the restart button is long clicked. */
    void onRestartButtonLongClicked() {
        createSizeCompatHint();
    }

    @VisibleForTesting
    void updateSurfacePosition() {
        if (mButton == null || mWindowManager.getSurfaceControl() == null) {
    void updateButtonSurfacePosition() {
        if (mButton == null || mButtonWindowManager.getSurfaceControl() == null) {
            return;
        }
        // The hint popup won't be at the correct position.
        mButton.dismissHint();
        final SurfaceControl leash = mButtonWindowManager.getSurfaceControl();

        // Use stable bounds to prevent the button from overlapping with system bars.
        final Rect taskBounds = mTaskConfig.windowConfiguration.getBounds();
@@ -212,8 +280,30 @@ class SizeCompatUILayout {
                : stableBounds.right - taskBounds.left - mButtonSize;
        final int positionY = stableBounds.bottom - taskBounds.top - mButtonSize;

        mSyncQueue.runInSync(t ->
                t.setPosition(mWindowManager.getSurfaceControl(), positionX, positionY));
        mSyncQueue.runInSync(t -> t.setPosition(leash, positionX, positionY));
    }

    void updateHintSurfacePosition() {
        if (mHint == null || mHintWindowManager == null
                || mHintWindowManager.getSurfaceControl() == null) {
            return;
        }
        final SurfaceControl leash = mHintWindowManager.getSurfaceControl();

        // Use stable bounds to prevent the hint from overlapping with system bars.
        final Rect taskBounds = mTaskConfig.windowConfiguration.getBounds();
        final Rect stableBounds = new Rect();
        mDisplayLayout.getStableBounds(stableBounds);
        stableBounds.intersect(taskBounds);

        // Position of the hint in the container coordinate.
        final int positionX = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
                ? stableBounds.left - taskBounds.left + mPopupOffsetX
                : stableBounds.right - taskBounds.left - mPopupOffsetX - mHint.getMeasuredWidth();
        final int positionY =
                stableBounds.bottom - taskBounds.top - mPopupOffsetY - mHint.getMeasuredHeight();

        mSyncQueue.runInSync(t -> t.setPosition(leash, positionX, positionY));
    }

    int getDisplayId() {
@@ -227,9 +317,4 @@ class SizeCompatUILayout {
    private int getLayoutDirection() {
        return mContext.getResources().getConfiguration().getLayoutDirection();
    }

    static int getGravity(int layoutDirection) {
        return Gravity.BOTTOM
                | (layoutDirection == View.LAYOUT_DIRECTION_RTL ? Gravity.START : Gravity.END);
    }
}
Loading