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

Commit 5951e172 authored by Chris Li's avatar Chris Li Committed by Automerger Merge Worker
Browse files

Merge "Allow non-resizable apps in split-screen (8/n)" into sc-dev am: 5c3ad3a5

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/13445762

MUST ONLY BE SUBMITTED BY AUTOMERGER

Change-Id: I7c5d5710006ee75f042d8975e5b72b1df1d7b88c
parents dd82e9ed 5c3ad3a5
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