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

Commit 27d983a0 authored by Chris Li's avatar Chris Li
Browse files

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

Refactor SizeCompatModeActivityController to WM Shell. The old one will
be removed in separate cl as there are some dependencies in other
packages.

This cl is mainly for refactor. Major change will come after.
Besides refactor, also changed the mActiveButtons key to TaskId, because
launcher Task will also trigger onTaskInfoChanged, but should not remove
the restart button on the same display.

Bug: 176061101
Bug: 178327644
Test: manually verify that the size compat restart button works as usual
Test: atest WMShellUnitTests:SizeCompatUIControllerTest
Test: atest WmTests:SizeCompatTests
Change-Id: I9143e079a1a945b76c3c56596976dd6ad2802897
parent 98d2d061
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -146,4 +146,10 @@

    <!-- Content description to tell the user a bubble has been dismissed. -->
    <string name="accessibility_bubble_dismissed">Bubble dismissed.</string>

    <!-- Description of the restart button in the hint of size compatibility mode. [CHAR LIMIT=NONE] -->
    <string name="restart_button_description">Tap to restart this app and go full screen.</string>

    <!-- Generic "got it" acceptance of dialog or cling [CHAR LIMIT=NONE] -->
    <string name="got_it">Got it</string>
</resources>
+172 −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.app.ActivityClient;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.RippleDrawable;
import android.os.IBinder;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.PopupWindow;

import com.android.wm.shell.R;

/** Button to restart the size compat activity. */
class SizeCompatRestartButton extends ImageButton implements View.OnClickListener,
        View.OnLongClickListener {
    private static final String TAG = "SizeCompatRestartButton";

    final WindowManager.LayoutParams mWinParams;
    final boolean mShouldShowHint;
    final int mDisplayId;
    final int mPopupOffsetX;
    final int mPopupOffsetY;

    private IBinder mLastActivityToken;
    private PopupWindow mShowingHint;

    SizeCompatRestartButton(Context context, int displayId, boolean hasShownHint) {
        super(context);
        mDisplayId = displayId;
        mShouldShowHint = !hasShownHint;
        final Drawable drawable = context.getDrawable(R.drawable.size_compat_restart_button);
        setImageDrawable(drawable);
        setContentDescription(context.getString(R.string.restart_button_description));

        final int drawableW = drawable.getIntrinsicWidth();
        final int drawableH = drawable.getIntrinsicHeight();
        mPopupOffsetX = drawableW / 2;
        mPopupOffsetY = drawableH * 2;

        final ColorStateList color = ColorStateList.valueOf(Color.LTGRAY);
        final GradientDrawable mask = new GradientDrawable();
        mask.setShape(GradientDrawable.OVAL);
        mask.setColor(color);
        setBackground(new RippleDrawable(color, null /* content */, mask));
        setOnClickListener(this);
        setOnLongClickListener(this);

        mWinParams = new WindowManager.LayoutParams();
        mWinParams.gravity = getGravity(getResources().getConfiguration().getLayoutDirection());
        mWinParams.width = drawableW * 2;
        mWinParams.height = drawableH * 2;
        mWinParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
        mWinParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
        mWinParams.format = PixelFormat.TRANSLUCENT;
        mWinParams.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
        mWinParams.setTitle(SizeCompatRestartButton.class.getSimpleName()
                + context.getDisplayId());
    }

    void updateLastTargetActivity(IBinder activityToken) {
        mLastActivityToken = activityToken;
    }

    /** @return {@code false} if the target display is invalid. */
    boolean show() {
        try {
            getContext().getSystemService(WindowManager.class).addView(this, mWinParams);
        } catch (WindowManager.InvalidDisplayException e) {
            // The target display may have been removed when the callback has just arrived.
            Log.w(TAG, "Cannot show on display " + getContext().getDisplayId(), e);
            return false;
        }
        return true;
    }

    void remove() {
        if (mShowingHint != null) {
            mShowingHint.dismiss();
        }
        getContext().getSystemService(WindowManager.class).removeViewImmediate(this);
    }

    @Override
    public void onClick(View v) {
        ActivityClient.getInstance().restartActivityProcessIfVisible(mLastActivityToken);
    }

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

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

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

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

        final View popupView = LayoutInflater.from(getContext()).inflate(
                R.layout.size_compat_mode_hint, null /* root */);
        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 -> popupWindow.dismiss());
        popupWindow.showAtLocation(this, mWinParams.gravity, mPopupOffsetX, mPopupOffsetY);
    }

    private static int getGravity(int layoutDirection) {
        return Gravity.BOTTOM
                | (layoutDirection == View.LAYOUT_DIRECTION_RTL ? Gravity.START : Gravity.END);
    }
}
+101 −1
Original line number Diff line number Diff line
@@ -19,7 +19,12 @@ package com.android.wm.shell.sizecompatui;
import android.annotation.Nullable;
import android.content.Context;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.os.IBinder;
import android.util.Log;
import android.util.SparseArray;
import android.view.Display;
import android.view.View;

import com.android.internal.annotations.VisibleForTesting;
import com.android.wm.shell.ShellTaskOrganizer;
@@ -27,6 +32,8 @@ import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.common.ShellExecutor;

import java.lang.ref.WeakReference;

/**
 * Shows a restart-activity button on Task when the foreground activity is in size compatibility
 * mode.
@@ -35,6 +42,11 @@ public class SizeCompatUIController implements DisplayController.OnDisplaysChang
        DisplayImeController.ImePositionProcessor {
    private static final String TAG = "SizeCompatUI";

    /** The showing buttons by task id. */
    private final SparseArray<SizeCompatRestartButton> mActiveButtons = new SparseArray<>(1);
    /** Avoid creating display context frequently for non-default display. */
    private final SparseArray<WeakReference<Context>> mDisplayContextCache = new SparseArray<>(0);

    @VisibleForTesting
    final SizeCompatUI mImpl = new SizeCompatUIImpl();
    private final Context mContext;
@@ -42,6 +54,9 @@ public class SizeCompatUIController implements DisplayController.OnDisplaysChang
    private final DisplayController mDisplayController;
    private final DisplayImeController mImeController;

    /** Only show once automatically in the process life. */
    private boolean mHasShownHint;

    /** Creates the {@link SizeCompatUIController}. */
    public static SizeCompatUI create(Context context,
            DisplayController displayController,
@@ -67,16 +82,101 @@ public class SizeCompatUIController implements DisplayController.OnDisplaysChang
    private void onSizeCompatInfoChanged(int displayId, int taskId, @Nullable Rect taskBounds,
            @Nullable IBinder sizeCompatActivity,
            @Nullable ShellTaskOrganizer.TaskListener taskListener) {
        // TODO need to deduplicate task info changed
        // TODO Draw button on Task surface
        if (taskBounds == null || sizeCompatActivity == null || taskListener == null) {
            // Null token means the current foreground activity is not in size compatibility mode.
            removeRestartButton(taskId);
        } else {
            updateRestartButton(displayId, taskId, sizeCompatActivity);
        }
    }

    // TODO move from SizeCompatModeActivityController from system UI.
    @Override
    public void onDisplayRemoved(int displayId) {
        mDisplayContextCache.remove(displayId);
        for (int i = 0; i < mActiveButtons.size(); i++) {
            final int taskId = mActiveButtons.keyAt(i);
            final SizeCompatRestartButton button = mActiveButtons.get(taskId);
            if (button != null && button.mDisplayId == displayId) {
                removeRestartButton(taskId);
            }
        }
    }

    @Override
    public void onImeVisibilityChanged(int displayId, boolean isShowing) {
        final int newVisibility = isShowing ? View.GONE : View.VISIBLE;
        for (int i = 0; i < mActiveButtons.size(); i++) {
            final int taskId = mActiveButtons.keyAt(i);
            final SizeCompatRestartButton button = mActiveButtons.get(taskId);
            if (button == null || button.mDisplayId != displayId) {
                continue;
            }

            // Hide the button when input method is showing.
            if (button.getVisibility() != newVisibility) {
                button.setVisibility(newVisibility);
            }
        }
    }

    private void updateRestartButton(int displayId, int taskId, IBinder activityToken) {
        SizeCompatRestartButton restartButton = mActiveButtons.get(taskId);
        if (restartButton != null) {
            restartButton.updateLastTargetActivity(activityToken);
            return;
        }

        final Context context = getOrCreateDisplayContext(displayId);
        if (context == null) {
            Log.i(TAG, "Cannot get context for display " + displayId);
            return;
        }

        restartButton = createRestartButton(context, displayId);
        restartButton.updateLastTargetActivity(activityToken);
        if (restartButton.show()) {
            mActiveButtons.append(taskId, restartButton);
        } else {
            onDisplayRemoved(displayId);
        }
    }

    @VisibleForTesting
    SizeCompatRestartButton createRestartButton(Context context, int displayId) {
        final SizeCompatRestartButton button = new SizeCompatRestartButton(context, displayId,
                mHasShownHint);
        // Only show hint for the first time.
        mHasShownHint = true;
        return button;
    }

    private void removeRestartButton(int taskId) {
        final SizeCompatRestartButton button = mActiveButtons.get(taskId);
        if (button != null) {
            button.remove();
            mActiveButtons.remove(taskId);
        }
    }

    private Context getOrCreateDisplayContext(int displayId) {
        if (displayId == Display.DEFAULT_DISPLAY) {
            return mContext;
        }
        Context context = null;
        final WeakReference<Context> ref = mDisplayContextCache.get(displayId);
        if (ref != null) {
            context = ref.get();
        }
        if (context == null) {
            Display display = mContext.getSystemService(DisplayManager.class).getDisplay(displayId);
            if (display != null) {
                context = mContext.createDisplayContext(display);
                mDisplayContextCache.put(displayId, new WeakReference<>(context));
            }
        }
        return context;
    }

    private class SizeCompatUIImpl implements SizeCompatUI {
Loading