Merge "Allow non-resizable apps in split-screen (10/n)" into sc-dev
This commit is contained in:
@@ -343,7 +343,9 @@ public class TaskInfo {
|
||||
// TopActivityToken and bounds are important if top activity is in size compat
|
||||
&& (!topActivityInSizeCompat || topActivityToken.equals(that.topActivityToken))
|
||||
&& (!topActivityInSizeCompat || configuration.windowConfiguration.getBounds()
|
||||
.equals(that.configuration.windowConfiguration.getBounds()));
|
||||
.equals(that.configuration.windowConfiguration.getBounds()))
|
||||
&& (!topActivityInSizeCompat || configuration.getLayoutDirection()
|
||||
== that.configuration.getLayoutDirection());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
30
libs/WindowManager/Shell/res/layout/size_compat_ui.xml
Normal file
30
libs/WindowManager/Shell/res/layout/size_compat_ui.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<com.android.wm.shell.sizecompatui.SizeCompatRestartButton
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/size_compat_restart_button"
|
||||
android:layout_width="@dimen/size_compat_button_size"
|
||||
android:layout_height="@dimen/size_compat_button_size"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/size_compat_restart_button"
|
||||
android:contentDescription="@string/restart_button_description"/>
|
||||
|
||||
</com.android.wm.shell.sizecompatui.SizeCompatRestartButton>
|
||||
@@ -174,6 +174,9 @@
|
||||
<!-- The width/height of the icon view on staring surface. -->
|
||||
<dimen name="starting_surface_icon_size">108dp</dimen>
|
||||
|
||||
<!-- The width/height of the size compat restart button. -->
|
||||
<dimen name="size_compat_button_size">48dp</dimen>
|
||||
|
||||
<!-- The width of the brand image on staring surface. -->
|
||||
<dimen name="starting_surface_brand_image_width">200dp</dimen>
|
||||
|
||||
|
||||
@@ -94,10 +94,22 @@ public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener {
|
||||
taskInfo.taskId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportSizeCompatUI() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) {
|
||||
if (!mLeashByTaskId.contains(taskId)) {
|
||||
throw new IllegalArgumentException("There is no surface for taskId=" + taskId);
|
||||
}
|
||||
b.setParent(mLeashByTaskId.get(taskId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dump(@NonNull PrintWriter pw, String prefix) {
|
||||
final String innerPrefix = prefix + " ";
|
||||
final String childPrefix = innerPrefix + " ";
|
||||
pw.println(prefix + this);
|
||||
pw.println(innerPrefix + mLeashByTaskId.size() + " Tasks");
|
||||
}
|
||||
|
||||
@@ -82,6 +82,15 @@ public class ShellTaskOrganizer extends TaskOrganizer {
|
||||
default void onTaskInfoChanged(RunningTaskInfo taskInfo) {}
|
||||
default void onTaskVanished(RunningTaskInfo taskInfo) {}
|
||||
default void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) {}
|
||||
/** Whether this task listener supports size compat UI. */
|
||||
default boolean supportSizeCompatUI() {
|
||||
return false;
|
||||
}
|
||||
/** Attaches the a child window surface to the task surface. */
|
||||
default void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) {
|
||||
throw new IllegalStateException(
|
||||
"This task listener doesn't support child surface attachment.");
|
||||
}
|
||||
default void dump(@NonNull PrintWriter pw, String prefix) {};
|
||||
}
|
||||
|
||||
@@ -358,8 +367,10 @@ public class ShellTaskOrganizer extends TaskOrganizer {
|
||||
return;
|
||||
}
|
||||
|
||||
// The task is vanished, notify to remove size compat UI on this Task if there is any.
|
||||
if (taskListener == null) {
|
||||
// The task is vanished or doesn't support size compat UI, notify to remove size compat UI
|
||||
// on this Task if there is any.
|
||||
if (taskListener == null || !taskListener.supportSizeCompatUI()
|
||||
|| !taskInfo.topActivityInSizeCompat) {
|
||||
mSizeCompatUI.onSizeCompatInfoChanged(taskInfo.displayId, taskInfo.taskId,
|
||||
null /* taskConfig */, null /* sizeCompatActivity*/,
|
||||
null /* taskListener */);
|
||||
@@ -367,10 +378,7 @@ public class ShellTaskOrganizer extends TaskOrganizer {
|
||||
}
|
||||
|
||||
mSizeCompatUI.onSizeCompatInfoChanged(taskInfo.displayId, taskInfo.taskId,
|
||||
taskInfo.configuration.windowConfiguration.getBounds(),
|
||||
// null if the top activity not in size compat.
|
||||
taskInfo.topActivityInSizeCompat ? taskInfo.topActivityToken : null,
|
||||
taskListener);
|
||||
taskInfo.configuration, taskInfo.topActivityToken, taskListener);
|
||||
}
|
||||
|
||||
private TaskListener getTaskListener(RunningTaskInfo runningTaskInfo) {
|
||||
|
||||
@@ -16,101 +16,80 @@
|
||||
|
||||
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.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. */
|
||||
class SizeCompatRestartButton extends ImageButton implements View.OnClickListener,
|
||||
public class SizeCompatRestartButton extends FrameLayout 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 SizeCompatUILayout mLayout;
|
||||
private ImageButton mRestartButton;
|
||||
@VisibleForTesting
|
||||
PopupWindow mShowingHint;
|
||||
private WindowManager.LayoutParams mWinParams;
|
||||
|
||||
private IBinder mLastActivityToken;
|
||||
private PopupWindow mShowingHint;
|
||||
|
||||
SizeCompatRestartButton(Context context, int displayId, boolean hasShownHint) {
|
||||
public SizeCompatRestartButton(@NonNull Context context) {
|
||||
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;
|
||||
public SizeCompatRestartButton(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public SizeCompatRestartButton(@NonNull Context context, @Nullable AttributeSet attrs,
|
||||
int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public SizeCompatRestartButton(@NonNull Context context, @Nullable AttributeSet attrs,
|
||||
int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
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 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);
|
||||
mRestartButton.setBackground(new RippleDrawable(color, null /* content */, mask));
|
||||
mRestartButton.setOnClickListener(this);
|
||||
mRestartButton.setOnLongClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
ActivityClient.getInstance().restartActivityProcessIfVisible(mLastActivityToken);
|
||||
mLayout.onRestartButtonClicked();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -122,20 +101,26 @@ class SizeCompatRestartButton extends ImageButton implements View.OnClickListene
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
if (mShouldShowHint) {
|
||||
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 = getGravity(layoutDirection);
|
||||
final int gravity = SizeCompatUILayout.getGravity(layoutDirection);
|
||||
if (mWinParams.gravity != gravity) {
|
||||
mWinParams.gravity = gravity;
|
||||
if (mShowingHint != null) {
|
||||
mShowingHint.dismiss();
|
||||
showHint();
|
||||
}
|
||||
getContext().getSystemService(WindowManager.class).updateViewLayout(this,
|
||||
mWinParams);
|
||||
}
|
||||
@@ -147,8 +132,10 @@ class SizeCompatRestartButton extends ImageButton implements View.OnClickListene
|
||||
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 /* root */);
|
||||
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);
|
||||
@@ -161,12 +148,15 @@ class SizeCompatRestartButton extends ImageButton implements View.OnClickListene
|
||||
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);
|
||||
gotItButton.setOnClickListener(view -> dismissHint());
|
||||
popupWindow.showAtLocation(mRestartButton, mWinParams.gravity, mLayout.mPopupOffsetX,
|
||||
mLayout.mPopupOffsetY);
|
||||
}
|
||||
|
||||
private static int getGravity(int layoutDirection) {
|
||||
return Gravity.BOTTOM
|
||||
| (layoutDirection == View.LAYOUT_DIRECTION_RTL ? Gravity.START : Gravity.END);
|
||||
void dismissHint() {
|
||||
if (mShowingHint != null) {
|
||||
mShowingHint.dismiss();
|
||||
mShowingHint = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,134 +18,166 @@ package com.android.wm.shell.sizecompatui;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.content.res.Configuration;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.os.IBinder;
|
||||
import android.util.ArraySet;
|
||||
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;
|
||||
import com.android.wm.shell.common.DisplayController;
|
||||
import com.android.wm.shell.common.DisplayImeController;
|
||||
import com.android.wm.shell.common.ShellExecutor;
|
||||
import com.android.wm.shell.common.DisplayLayout;
|
||||
import com.android.wm.shell.common.SyncTransactionQueue;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Shows a restart-activity button on Task when the foreground activity is in size compatibility
|
||||
* mode.
|
||||
* Controls to show/update restart-activity buttons on Tasks based on whether the foreground
|
||||
* activities are in size compatibility mode.
|
||||
*/
|
||||
public class SizeCompatUIController implements DisplayController.OnDisplaysChangedListener,
|
||||
DisplayImeController.ImePositionProcessor {
|
||||
private static final String TAG = "SizeCompatUI";
|
||||
private static final String TAG = "SizeCompatUIController";
|
||||
|
||||
/** Whether the IME is shown on display id. */
|
||||
private final Set<Integer> mDisplaysWithIme = new ArraySet<>(1);
|
||||
|
||||
/** The showing buttons by task id. */
|
||||
private final SparseArray<SizeCompatRestartButton> mActiveButtons = new SparseArray<>(1);
|
||||
private final SparseArray<SizeCompatUILayout> mActiveLayouts = new SparseArray<>(0);
|
||||
|
||||
/** Avoid creating display context frequently for non-default display. */
|
||||
private final SparseArray<WeakReference<Context>> mDisplayContextCache = new SparseArray<>(0);
|
||||
|
||||
@VisibleForTesting
|
||||
private final Context mContext;
|
||||
private final ShellExecutor mMainExecutor;
|
||||
private final DisplayController mDisplayController;
|
||||
private final DisplayImeController mImeController;
|
||||
private final SyncTransactionQueue mSyncQueue;
|
||||
|
||||
/** Only show once automatically in the process life. */
|
||||
private boolean mHasShownHint;
|
||||
|
||||
@VisibleForTesting
|
||||
public SizeCompatUIController(Context context,
|
||||
DisplayController displayController,
|
||||
DisplayImeController imeController,
|
||||
ShellExecutor mainExecutor) {
|
||||
SyncTransactionQueue syncQueue) {
|
||||
mContext = context;
|
||||
mMainExecutor = mainExecutor;
|
||||
mDisplayController = displayController;
|
||||
mImeController = imeController;
|
||||
mSyncQueue = syncQueue;
|
||||
mDisplayController.addDisplayWindowListener(this);
|
||||
mImeController.addPositionProcessor(this);
|
||||
}
|
||||
|
||||
public void onSizeCompatInfoChanged(int displayId, int taskId, @Nullable Rect taskBounds,
|
||||
@Nullable IBinder sizeCompatActivity,
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @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 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.
|
||||
*/
|
||||
public void onSizeCompatInfoChanged(int displayId, int taskId,
|
||||
@Nullable Configuration taskConfig, @Nullable IBinder sizeCompatActivity,
|
||||
@Nullable ShellTaskOrganizer.TaskListener taskListener) {
|
||||
// TODO Draw button on Task surface
|
||||
if (taskBounds == null || sizeCompatActivity == null || taskListener == null) {
|
||||
if (taskConfig == null || sizeCompatActivity == null || taskListener == null) {
|
||||
// Null token means the current foreground activity is not in size compatibility mode.
|
||||
removeRestartButton(taskId);
|
||||
removeLayout(taskId);
|
||||
} else if (mActiveLayouts.contains(taskId)) {
|
||||
// Button already exists, update the button layout.
|
||||
updateLayout(taskId, taskConfig, sizeCompatActivity, taskListener);
|
||||
} else {
|
||||
updateRestartButton(displayId, taskId, sizeCompatActivity);
|
||||
// Create a new restart button.
|
||||
createLayout(displayId, taskId, taskConfig, sizeCompatActivity, taskListener);
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
// Remove all buttons 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--) {
|
||||
removeLayout(toRemoveTaskIds.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
|
||||
final DisplayLayout displayLayout = mDisplayController.getDisplayLayout(displayId);
|
||||
forAllLayoutsOnDisplay(displayId, layout -> layout.updateDisplayLayout(displayLayout));
|
||||
}
|
||||
|
||||
private void updateRestartButton(int displayId, int taskId, IBinder activityToken) {
|
||||
SizeCompatRestartButton restartButton = mActiveButtons.get(taskId);
|
||||
if (restartButton != null) {
|
||||
restartButton.updateLastTargetActivity(activityToken);
|
||||
return;
|
||||
@Override
|
||||
public void onImeVisibilityChanged(int displayId, boolean isShowing) {
|
||||
if (isShowing) {
|
||||
mDisplaysWithIme.add(displayId);
|
||||
} else {
|
||||
mDisplaysWithIme.remove(displayId);
|
||||
}
|
||||
|
||||
// Hide the button when input method is showing.
|
||||
forAllLayoutsOnDisplay(displayId, layout -> layout.updateImeVisibility(isShowing));
|
||||
}
|
||||
|
||||
private boolean isImeShowingOnDisplay(int displayId) {
|
||||
return mDisplaysWithIme.contains(displayId);
|
||||
}
|
||||
|
||||
private void createLayout(int displayId, int taskId, Configuration taskConfig,
|
||||
IBinder activityToken, ShellTaskOrganizer.TaskListener taskListener) {
|
||||
final Context context = getOrCreateDisplayContext(displayId);
|
||||
if (context == null) {
|
||||
Log.i(TAG, "Cannot get context for display " + displayId);
|
||||
Log.e(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);
|
||||
}
|
||||
final SizeCompatUILayout layout = createLayout(context, displayId, taskId, taskConfig,
|
||||
activityToken, taskListener);
|
||||
mActiveLayouts.put(taskId, layout);
|
||||
layout.createSizeCompatButton(isImeShowingOnDisplay(displayId));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
SizeCompatRestartButton createRestartButton(Context context, int displayId) {
|
||||
final SizeCompatRestartButton button = new SizeCompatRestartButton(context, displayId,
|
||||
SizeCompatUILayout createLayout(Context context, int displayId, int taskId,
|
||||
Configuration taskConfig, IBinder activityToken,
|
||||
ShellTaskOrganizer.TaskListener taskListener) {
|
||||
final SizeCompatUILayout layout = new SizeCompatUILayout(mSyncQueue, context, taskConfig,
|
||||
taskId, activityToken, taskListener, mDisplayController.getDisplayLayout(displayId),
|
||||
mHasShownHint);
|
||||
// Only show hint for the first time.
|
||||
mHasShownHint = true;
|
||||
return button;
|
||||
return layout;
|
||||
}
|
||||
|
||||
private void removeRestartButton(int taskId) {
|
||||
final SizeCompatRestartButton button = mActiveButtons.get(taskId);
|
||||
if (button != null) {
|
||||
button.remove();
|
||||
mActiveButtons.remove(taskId);
|
||||
private void updateLayout(int taskId, Configuration taskConfig,
|
||||
IBinder sizeCompatActivity,
|
||||
ShellTaskOrganizer.TaskListener taskListener) {
|
||||
final SizeCompatUILayout layout = mActiveLayouts.get(taskId);
|
||||
if (layout == null) {
|
||||
return;
|
||||
}
|
||||
layout.updateSizeCompatInfo(taskConfig, sizeCompatActivity, taskListener,
|
||||
isImeShowingOnDisplay(layout.getDisplayId()));
|
||||
}
|
||||
|
||||
private void removeLayout(int taskId) {
|
||||
final SizeCompatUILayout layout = mActiveLayouts.get(taskId);
|
||||
if (layout != null) {
|
||||
layout.release();
|
||||
mActiveLayouts.remove(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,4 +199,14 @@ public class SizeCompatUIController implements DisplayController.OnDisplaysChang
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
private void forAllLayoutsOnDisplay(int displayId, Consumer<SizeCompatUILayout> callback) {
|
||||
for (int i = 0; i < mActiveLayouts.size(); i++) {
|
||||
final int taskId = mActiveLayouts.keyAt(i);
|
||||
final SizeCompatUILayout layout = mActiveLayouts.get(taskId);
|
||||
if (layout != null && layout.getDisplayId() == displayId) {
|
||||
callback.accept(layout);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* 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 static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
|
||||
import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
|
||||
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
|
||||
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
|
||||
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.app.ActivityClient;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
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;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.wm.shell.R;
|
||||
import com.android.wm.shell.ShellTaskOrganizer;
|
||||
import com.android.wm.shell.common.DisplayLayout;
|
||||
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.
|
||||
*/
|
||||
class SizeCompatUILayout {
|
||||
private static final String TAG = "SizeCompatUILayout";
|
||||
|
||||
private final SyncTransactionQueue mSyncQueue;
|
||||
private Context mContext;
|
||||
private Configuration mTaskConfig;
|
||||
private final int mDisplayId;
|
||||
private final int mTaskId;
|
||||
private IBinder mActivityToken;
|
||||
private ShellTaskOrganizer.TaskListener mTaskListener;
|
||||
private DisplayLayout mDisplayLayout;
|
||||
@VisibleForTesting
|
||||
final SizeCompatUIWindowManager mWindowManager;
|
||||
|
||||
@VisibleForTesting
|
||||
@Nullable
|
||||
SizeCompatRestartButton mButton;
|
||||
final int mButtonSize;
|
||||
final int mPopupOffsetX;
|
||||
final int mPopupOffsetY;
|
||||
boolean mShouldShowHint;
|
||||
|
||||
SizeCompatUILayout(SyncTransactionQueue syncQueue, Context context, Configuration taskConfig,
|
||||
int taskId, IBinder activityToken, ShellTaskOrganizer.TaskListener taskListener,
|
||||
DisplayLayout displayLayout, boolean hasShownHint) {
|
||||
mSyncQueue = syncQueue;
|
||||
mContext = context.createConfigurationContext(taskConfig);
|
||||
mTaskConfig = taskConfig;
|
||||
mDisplayId = mContext.getDisplayId();
|
||||
mTaskId = taskId;
|
||||
mActivityToken = activityToken;
|
||||
mTaskListener = taskListener;
|
||||
mDisplayLayout = displayLayout;
|
||||
mShouldShowHint = !hasShownHint;
|
||||
mWindowManager = new SizeCompatUIWindowManager(mContext, taskConfig, this);
|
||||
|
||||
mButtonSize =
|
||||
mContext.getResources().getDimensionPixelSize(R.dimen.size_compat_button_size);
|
||||
mPopupOffsetX = mButtonSize / 4;
|
||||
mPopupOffsetY = mButtonSize;
|
||||
}
|
||||
|
||||
/** Creates the 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();
|
||||
}
|
||||
|
||||
/** Releases the button window. */
|
||||
void release() {
|
||||
mButton.remove();
|
||||
mButton = null;
|
||||
mWindowManager.release();
|
||||
}
|
||||
|
||||
/** Called when size compat info changed. */
|
||||
void updateSizeCompatInfo(Configuration taskConfig, IBinder activityToken,
|
||||
ShellTaskOrganizer.TaskListener taskListener, boolean isImeShowing) {
|
||||
final Configuration prevTaskConfig = mTaskConfig;
|
||||
final ShellTaskOrganizer.TaskListener prevTaskListener = mTaskListener;
|
||||
mTaskConfig = taskConfig;
|
||||
mActivityToken = activityToken;
|
||||
mTaskListener = taskListener;
|
||||
|
||||
// Update configuration.
|
||||
mContext = mContext.createConfigurationContext(taskConfig);
|
||||
mWindowManager.setConfiguration(taskConfig);
|
||||
|
||||
if (mButton == null || prevTaskListener != taskListener) {
|
||||
// TaskListener changed, recreate the button for new surface parent.
|
||||
release();
|
||||
createSizeCompatButton(isImeShowing);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!taskConfig.windowConfiguration.getBounds()
|
||||
.equals(prevTaskConfig.windowConfiguration.getBounds())) {
|
||||
// Reposition the button surface.
|
||||
updateSurfacePosition();
|
||||
}
|
||||
|
||||
if (taskConfig.getLayoutDirection() != prevTaskConfig.getLayoutDirection()) {
|
||||
// Update layout for RTL.
|
||||
mButton.setLayoutDirection(taskConfig.getLayoutDirection());
|
||||
updateSurfacePosition();
|
||||
}
|
||||
}
|
||||
|
||||
/** Called when display layout changed. */
|
||||
void updateDisplayLayout(DisplayLayout displayLayout) {
|
||||
if (displayLayout == mDisplayLayout) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Rect prevStableBounds = new Rect();
|
||||
final Rect curStableBounds = new Rect();
|
||||
mDisplayLayout.getStableBounds(prevStableBounds);
|
||||
displayLayout.getStableBounds(curStableBounds);
|
||||
mDisplayLayout = displayLayout;
|
||||
if (!prevStableBounds.equals(curStableBounds)) {
|
||||
// Stable bounds changed, update button surface position.
|
||||
updateSurfacePosition();
|
||||
}
|
||||
}
|
||||
|
||||
/** Called when IME visibility changed. */
|
||||
void updateImeVisibility(boolean isImeShowing) {
|
||||
if (mButton == null) {
|
||||
// Button may not be created because ime is previous showing.
|
||||
createSizeCompatButton(isImeShowing);
|
||||
return;
|
||||
}
|
||||
|
||||
final int newVisibility = isImeShowing ? View.GONE : View.VISIBLE;
|
||||
if (mButton.getVisibility() != newVisibility) {
|
||||
mButton.setVisibility(newVisibility);
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets the layout params for restart button. */
|
||||
WindowManager.LayoutParams getWindowLayoutParams() {
|
||||
final WindowManager.LayoutParams winParams = new WindowManager.LayoutParams(
|
||||
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.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY;
|
||||
return winParams;
|
||||
}
|
||||
|
||||
/** Called when it is ready to be placed button surface button. */
|
||||
void attachToParentSurface(SurfaceControl.Builder b) {
|
||||
mTaskListener.attachChildSurfaceToTask(mTaskId, b);
|
||||
}
|
||||
|
||||
/** Called when the restart button is clicked. */
|
||||
void onRestartButtonClicked() {
|
||||
ActivityClient.getInstance().restartActivityProcessIfVisible(mActivityToken);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void updateSurfacePosition() {
|
||||
if (mButton == null || mWindowManager.getSurfaceControl() == null) {
|
||||
return;
|
||||
}
|
||||
// The hint popup won't be at the correct position.
|
||||
mButton.dismissHint();
|
||||
|
||||
// Use stable bounds to prevent the button 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 button in the container coordinate.
|
||||
final int positionX = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
|
||||
? stableBounds.left - taskBounds.left
|
||||
: stableBounds.right - taskBounds.left - mButtonSize;
|
||||
final int positionY = stableBounds.bottom - taskBounds.top - mButtonSize;
|
||||
|
||||
mSyncQueue.runInSync(t ->
|
||||
t.setPosition(mWindowManager.getSurfaceControl(), positionX, positionY));
|
||||
}
|
||||
|
||||
int getDisplayId() {
|
||||
return mDisplayId;
|
||||
}
|
||||
|
||||
int getTaskId() {
|
||||
return mTaskId;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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.annotation.Nullable;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.view.IWindow;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.SurfaceControl;
|
||||
import android.view.SurfaceControlViewHost;
|
||||
import android.view.SurfaceSession;
|
||||
import android.view.WindowlessWindowManager;
|
||||
|
||||
import com.android.wm.shell.R;
|
||||
|
||||
/**
|
||||
* Holds view hierarchy of a root surface and helps to inflate {@link SizeCompatRestartButton}.
|
||||
*/
|
||||
class SizeCompatUIWindowManager extends WindowlessWindowManager {
|
||||
|
||||
private Context mContext;
|
||||
private final SizeCompatUILayout mLayout;
|
||||
|
||||
@Nullable
|
||||
private SurfaceControlViewHost mViewHost;
|
||||
@Nullable
|
||||
private SurfaceControl mLeash;
|
||||
|
||||
SizeCompatUIWindowManager(Context context, Configuration config, SizeCompatUILayout layout) {
|
||||
super(config, null /* rootSurface */, null /* hostInputToken */);
|
||||
mContext = context;
|
||||
mLayout = layout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setConfiguration(Configuration configuration) {
|
||||
super.setConfiguration(configuration);
|
||||
mContext = mContext.createConfigurationContext(configuration);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) {
|
||||
// Can't set position for the ViewRootImpl SC directly. Create a leash to manipulate later.
|
||||
final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
|
||||
.setContainerLayer()
|
||||
.setName("SizeCompatUILeash")
|
||||
.setHidden(false)
|
||||
.setCallsite("SizeCompatUIWindowManager#attachToParentSurface");
|
||||
mLayout.attachToParentSurface(builder);
|
||||
mLeash = builder.build();
|
||||
b.setParent(mLeash);
|
||||
}
|
||||
|
||||
/** Inflates {@link SizeCompatRestartButton} on to the root surface. */
|
||||
SizeCompatRestartButton createSizeCompatUI() {
|
||||
if (mViewHost == null) {
|
||||
mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this);
|
||||
}
|
||||
|
||||
final SizeCompatRestartButton button = (SizeCompatRestartButton)
|
||||
LayoutInflater.from(mContext).inflate(R.layout.size_compat_ui, null);
|
||||
button.inject(mLayout);
|
||||
mViewHost.setView(button, mLayout.getWindowLayoutParams());
|
||||
return button;
|
||||
}
|
||||
|
||||
/** Releases the surface control and tears down the view hierarchy. */
|
||||
void release() {
|
||||
if (mViewHost != null) {
|
||||
mViewHost.release();
|
||||
mViewHost = null;
|
||||
}
|
||||
|
||||
if (mLeash != null) {
|
||||
new SurfaceControl.Transaction().remove(mLeash).apply();
|
||||
mLeash = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets {@link SurfaceControl} of the surface holding size compat UI view. @return {@code null}
|
||||
* if not feasible.
|
||||
*/
|
||||
@Nullable
|
||||
SurfaceControl getSurfaceControl() {
|
||||
return mLeash;
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.clearInvocations;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@@ -105,6 +106,11 @@ public class ShellTaskOrganizerTests {
|
||||
public void onTaskVanished(RunningTaskInfo taskInfo) {
|
||||
vanished.add(taskInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportSizeCompatUI() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Before
|
||||
@@ -282,10 +288,10 @@ public class ShellTaskOrganizerTests {
|
||||
|
||||
// sizeCompatActivity is null if top activity is not in size compat.
|
||||
verify(mSizeCompatUI).onSizeCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId,
|
||||
taskInfo1.configuration.windowConfiguration.getBounds(),
|
||||
null /* sizeCompatActivity*/ , taskListener);
|
||||
null /* taskConfig */, null /* sizeCompatActivity*/, null /* taskListener */);
|
||||
|
||||
// sizeCompatActivity is non-null if top activity is in size compat.
|
||||
clearInvocations(mSizeCompatUI);
|
||||
final RunningTaskInfo taskInfo2 =
|
||||
createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode());
|
||||
taskInfo2.displayId = taskInfo1.displayId;
|
||||
@@ -293,14 +299,12 @@ public class ShellTaskOrganizerTests {
|
||||
taskInfo2.topActivityInSizeCompat = true;
|
||||
mOrganizer.onTaskInfoChanged(taskInfo2);
|
||||
verify(mSizeCompatUI).onSizeCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId,
|
||||
taskInfo1.configuration.windowConfiguration.getBounds(),
|
||||
taskInfo1.topActivityToken,
|
||||
taskListener);
|
||||
taskInfo1.configuration, taskInfo1.topActivityToken, taskListener);
|
||||
|
||||
clearInvocations(mSizeCompatUI);
|
||||
mOrganizer.onTaskVanished(taskInfo1);
|
||||
verify(mSizeCompatUI).onSizeCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId,
|
||||
null /* taskConfig */, null /* sizeCompatActivity*/,
|
||||
null /* taskListener */);
|
||||
null /* taskConfig */, null /* sizeCompatActivity*/, null /* taskListener */);
|
||||
}
|
||||
|
||||
private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode) {
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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 static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
|
||||
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.os.IBinder;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.view.LayoutInflater;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.wm.shell.R;
|
||||
import com.android.wm.shell.ShellTaskOrganizer;
|
||||
import com.android.wm.shell.ShellTestCase;
|
||||
import com.android.wm.shell.common.DisplayLayout;
|
||||
import com.android.wm.shell.common.SyncTransactionQueue;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
/**
|
||||
* Tests for {@link SizeCompatRestartButton}.
|
||||
*
|
||||
* Build/Install/Run:
|
||||
* atest WMShellUnitTests:SizeCompatRestartButtonTest
|
||||
*/
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
@SmallTest
|
||||
public class SizeCompatRestartButtonTest extends ShellTestCase {
|
||||
|
||||
@Mock private SyncTransactionQueue mSyncTransactionQueue;
|
||||
@Mock private IBinder mActivityToken;
|
||||
@Mock private ShellTaskOrganizer.TaskListener mTaskListener;
|
||||
@Mock private DisplayLayout mDisplayLayout;
|
||||
|
||||
private SizeCompatUILayout mLayout;
|
||||
private SizeCompatRestartButton mButton;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
final int taskId = 1;
|
||||
mLayout = new SizeCompatUILayout(mSyncTransactionQueue, mContext, new Configuration(),
|
||||
taskId, mActivityToken, mTaskListener, mDisplayLayout, false /* hasShownHint*/);
|
||||
mButton = (SizeCompatRestartButton)
|
||||
LayoutInflater.from(mContext).inflate(R.layout.size_compat_ui, null);
|
||||
mButton.inject(mLayout);
|
||||
|
||||
spyOn(mLayout);
|
||||
spyOn(mButton);
|
||||
doNothing().when(mButton).showHint();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnClick() {
|
||||
doNothing().when(mLayout).onRestartButtonClicked();
|
||||
|
||||
mButton.onClick(mButton);
|
||||
|
||||
verify(mLayout).onRestartButtonClicked();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnLongClick() {
|
||||
verify(mButton, never()).showHint();
|
||||
|
||||
mButton.onLongClick(mButton);
|
||||
|
||||
verify(mButton).showHint();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnAttachedToWindow_showHint() {
|
||||
mLayout.mShouldShowHint = false;
|
||||
mButton.onAttachedToWindow();
|
||||
|
||||
verify(mButton, never()).showHint();
|
||||
|
||||
mLayout.mShouldShowHint = true;
|
||||
mButton.onAttachedToWindow();
|
||||
|
||||
verify(mButton).showHint();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemove() {
|
||||
mButton.remove();
|
||||
|
||||
verify(mButton).dismissHint();
|
||||
}
|
||||
}
|
||||
@@ -16,23 +16,28 @@
|
||||
|
||||
package com.android.wm.shell.sizecompatui;
|
||||
|
||||
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.IBinder;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.wm.shell.ShellTaskOrganizer;
|
||||
import com.android.wm.shell.ShellTestCase;
|
||||
import com.android.wm.shell.TestShellExecutor;
|
||||
import com.android.wm.shell.common.DisplayController;
|
||||
import com.android.wm.shell.common.DisplayImeController;
|
||||
import com.android.wm.shell.common.DisplayLayout;
|
||||
import com.android.wm.shell.common.SyncTransactionQueue;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@@ -50,28 +55,34 @@ import org.mockito.MockitoAnnotations;
|
||||
@SmallTest
|
||||
public class SizeCompatUIControllerTest extends ShellTestCase {
|
||||
private static final int DISPLAY_ID = 0;
|
||||
|
||||
private final TestShellExecutor mShellMainExecutor = new TestShellExecutor();
|
||||
private static final int TASK_ID = 12;
|
||||
|
||||
private SizeCompatUIController mController;
|
||||
private @Mock DisplayController mMockDisplayController;
|
||||
private @Mock DisplayLayout mMockDisplayLayout;
|
||||
private @Mock DisplayImeController mMockImeController;
|
||||
private @Mock SizeCompatRestartButton mMockButton;
|
||||
private @Mock IBinder mMockActivityToken;
|
||||
private @Mock ShellTaskOrganizer.TaskListener mMockTaskListener;
|
||||
private @Mock SyncTransactionQueue mMockSyncQueue;
|
||||
private @Mock SizeCompatUILayout mMockLayout;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
doReturn(true).when(mMockButton).show();
|
||||
|
||||
doReturn(mMockDisplayLayout).when(mMockDisplayController).getDisplayLayout(anyInt());
|
||||
doReturn(DISPLAY_ID).when(mMockLayout).getDisplayId();
|
||||
doReturn(TASK_ID).when(mMockLayout).getTaskId();
|
||||
mController = new SizeCompatUIController(mContext, mMockDisplayController,
|
||||
mMockImeController, mShellMainExecutor) {
|
||||
mMockImeController, mMockSyncQueue) {
|
||||
@Override
|
||||
SizeCompatRestartButton createRestartButton(Context context, int displayId) {
|
||||
return mMockButton;
|
||||
SizeCompatUILayout createLayout(Context context, int displayId, int taskId,
|
||||
Configuration taskConfig, IBinder activityToken,
|
||||
ShellTaskOrganizer.TaskListener taskListener) {
|
||||
return mMockLayout;
|
||||
}
|
||||
};
|
||||
spyOn(mController);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -82,42 +93,72 @@ public class SizeCompatUIControllerTest extends ShellTestCase {
|
||||
|
||||
@Test
|
||||
public void testOnSizeCompatInfoChanged() {
|
||||
final int taskId = 12;
|
||||
final Rect taskBounds = new Rect(0, 0, 1000, 2000);
|
||||
final Configuration taskConfig = new Configuration();
|
||||
|
||||
// Verify that the restart button is added with non-null size compat activity.
|
||||
mController.onSizeCompatInfoChanged(DISPLAY_ID, taskId, taskBounds,
|
||||
// Verify that the restart button is added with non-null size compat info.
|
||||
mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig,
|
||||
mMockActivityToken, mMockTaskListener);
|
||||
mShellMainExecutor.flushAll();
|
||||
|
||||
verify(mMockButton).show();
|
||||
verify(mMockButton).updateLastTargetActivity(eq(mMockActivityToken));
|
||||
verify(mController).createLayout(any(), eq(DISPLAY_ID), eq(TASK_ID), eq(taskConfig),
|
||||
eq(mMockActivityToken), eq(mMockTaskListener));
|
||||
|
||||
// Verify that the restart button is removed with null size compat activity.
|
||||
mController.onSizeCompatInfoChanged(DISPLAY_ID, taskId, null, null, null);
|
||||
// Verify that the restart button is updated with non-null new size compat info.
|
||||
final Configuration newTaskConfig = new Configuration();
|
||||
mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, newTaskConfig,
|
||||
mMockActivityToken, mMockTaskListener);
|
||||
|
||||
mShellMainExecutor.flushAll();
|
||||
verify(mMockButton).remove();
|
||||
verify(mMockLayout).updateSizeCompatInfo(taskConfig, mMockActivityToken, mMockTaskListener,
|
||||
false /* isImeShowing */);
|
||||
|
||||
// Verify that the restart button is removed with null size compat info.
|
||||
mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, null, null, mMockTaskListener);
|
||||
|
||||
verify(mMockLayout).release();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnDisplayRemoved() {
|
||||
final Configuration taskConfig = new Configuration();
|
||||
mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig,
|
||||
mMockActivityToken, mMockTaskListener);
|
||||
|
||||
mController.onDisplayRemoved(DISPLAY_ID + 1);
|
||||
|
||||
verify(mMockLayout, never()).release();
|
||||
|
||||
mController.onDisplayRemoved(DISPLAY_ID);
|
||||
|
||||
verify(mMockLayout).release();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnDisplayConfigurationChanged() {
|
||||
final Configuration taskConfig = new Configuration();
|
||||
mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig,
|
||||
mMockActivityToken, mMockTaskListener);
|
||||
|
||||
final Configuration newTaskConfig = new Configuration();
|
||||
mController.onDisplayConfigurationChanged(DISPLAY_ID + 1, newTaskConfig);
|
||||
|
||||
verify(mMockLayout, never()).updateDisplayLayout(any());
|
||||
|
||||
mController.onDisplayConfigurationChanged(DISPLAY_ID, newTaskConfig);
|
||||
|
||||
verify(mMockLayout).updateDisplayLayout(mMockDisplayLayout);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChangeButtonVisibilityOnImeShowHide() {
|
||||
final int taskId = 12;
|
||||
final Rect taskBounds = new Rect(0, 0, 1000, 2000);
|
||||
mController.onSizeCompatInfoChanged(DISPLAY_ID, taskId, taskBounds,
|
||||
final Configuration taskConfig = new Configuration();
|
||||
mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig,
|
||||
mMockActivityToken, mMockTaskListener);
|
||||
mShellMainExecutor.flushAll();
|
||||
|
||||
// Verify that the restart button is hidden when IME is visible.
|
||||
doReturn(View.VISIBLE).when(mMockButton).getVisibility();
|
||||
mController.onImeVisibilityChanged(DISPLAY_ID, true /* isShowing */);
|
||||
|
||||
verify(mMockButton).setVisibility(eq(View.GONE));
|
||||
verify(mMockLayout).updateImeVisibility(true);
|
||||
|
||||
// Verify that the restart button is visible when IME is hidden.
|
||||
doReturn(View.GONE).when(mMockButton).getVisibility();
|
||||
mController.onImeVisibilityChanged(DISPLAY_ID, false /* isShowing */);
|
||||
|
||||
verify(mMockButton).setVisibility(eq(View.VISIBLE));
|
||||
verify(mMockLayout).updateImeVisibility(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* 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 static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
|
||||
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.Mockito.clearInvocations;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.app.ActivityClient;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Rect;
|
||||
import android.os.IBinder;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.view.DisplayInfo;
|
||||
import android.view.SurfaceControl;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.wm.shell.ShellTaskOrganizer;
|
||||
import com.android.wm.shell.ShellTestCase;
|
||||
import com.android.wm.shell.common.DisplayLayout;
|
||||
import com.android.wm.shell.common.SyncTransactionQueue;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
/**
|
||||
* Tests for {@link SizeCompatUILayout}.
|
||||
*
|
||||
* Build/Install/Run:
|
||||
* atest WMShellUnitTests:SizeCompatUILayoutTest
|
||||
*/
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
@SmallTest
|
||||
public class SizeCompatUILayoutTest extends ShellTestCase {
|
||||
|
||||
private static final int TASK_ID = 1;
|
||||
|
||||
@Mock private SyncTransactionQueue mSyncTransactionQueue;
|
||||
@Mock private IBinder mActivityToken;
|
||||
@Mock private ShellTaskOrganizer.TaskListener mTaskListener;
|
||||
@Mock private DisplayLayout mDisplayLayout;
|
||||
@Mock private SizeCompatRestartButton mButton;
|
||||
private Configuration mTaskConfig;
|
||||
|
||||
private SizeCompatUILayout mLayout;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mTaskConfig = new Configuration();
|
||||
|
||||
mLayout = new SizeCompatUILayout(mSyncTransactionQueue, mContext, new Configuration(),
|
||||
TASK_ID, mActivityToken, mTaskListener, mDisplayLayout, false /* hasShownHint*/);
|
||||
|
||||
spyOn(mLayout);
|
||||
spyOn(mLayout.mWindowManager);
|
||||
doReturn(mButton).when(mLayout.mWindowManager).createSizeCompatUI();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateSizeCompatButton() {
|
||||
// Not create button if IME is showing.
|
||||
mLayout.createSizeCompatButton(true /* isImeShowing */);
|
||||
|
||||
verify(mLayout.mWindowManager, never()).createSizeCompatUI();
|
||||
assertNull(mLayout.mButton);
|
||||
|
||||
mLayout.createSizeCompatButton(false /* isImeShowing */);
|
||||
|
||||
verify(mLayout.mWindowManager).createSizeCompatUI();
|
||||
assertNotNull(mLayout.mButton);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRelease() {
|
||||
mLayout.createSizeCompatButton(false /* isImeShowing */);
|
||||
|
||||
mLayout.release();
|
||||
|
||||
assertNull(mLayout.mButton);
|
||||
verify(mButton).remove();
|
||||
verify(mLayout.mWindowManager).release();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateSizeCompatInfo() {
|
||||
mLayout.createSizeCompatButton(false /* isImeShowing */);
|
||||
|
||||
// No diff
|
||||
clearInvocations(mLayout);
|
||||
mLayout.updateSizeCompatInfo(mTaskConfig, mActivityToken, mTaskListener,
|
||||
false /* isImeShowing */);
|
||||
|
||||
verify(mLayout, never()).updateSurfacePosition();
|
||||
verify(mLayout, never()).release();
|
||||
verify(mLayout, never()).createSizeCompatButton(anyBoolean());
|
||||
|
||||
// Change task listener, recreate button.
|
||||
clearInvocations(mLayout);
|
||||
final ShellTaskOrganizer.TaskListener newTaskListener = mock(
|
||||
ShellTaskOrganizer.TaskListener.class);
|
||||
mLayout.updateSizeCompatInfo(mTaskConfig, mActivityToken, newTaskListener,
|
||||
false /* isImeShowing */);
|
||||
|
||||
verify(mLayout).release();
|
||||
verify(mLayout).createSizeCompatButton(anyBoolean());
|
||||
|
||||
// Change task bounds, update position.
|
||||
clearInvocations(mLayout);
|
||||
final Configuration newTaskConfiguration = new Configuration();
|
||||
newTaskConfiguration.windowConfiguration.setBounds(new Rect(0, 1000, 0, 2000));
|
||||
mLayout.updateSizeCompatInfo(newTaskConfiguration, mActivityToken, newTaskListener,
|
||||
false /* isImeShowing */);
|
||||
|
||||
verify(mLayout).updateSurfacePosition();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateDisplayLayout() {
|
||||
final DisplayInfo displayInfo = new DisplayInfo();
|
||||
displayInfo.logicalWidth = 1000;
|
||||
displayInfo.logicalHeight = 2000;
|
||||
final DisplayLayout displayLayout1 = new DisplayLayout(displayInfo,
|
||||
mContext.getResources(), false, false);
|
||||
|
||||
mLayout.updateDisplayLayout(displayLayout1);
|
||||
verify(mLayout).updateSurfacePosition();
|
||||
|
||||
// No update if the display bounds is the same.
|
||||
clearInvocations(mLayout);
|
||||
final DisplayLayout displayLayout2 = new DisplayLayout(displayInfo,
|
||||
mContext.getResources(), false, false);
|
||||
mLayout.updateDisplayLayout(displayLayout2);
|
||||
verify(mLayout, never()).updateSurfacePosition();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateImeVisibility() {
|
||||
// Create button if it is not created.
|
||||
mLayout.mButton = null;
|
||||
mLayout.updateImeVisibility(false /* isImeShowing */);
|
||||
|
||||
verify(mLayout).createSizeCompatButton(false /* isImeShowing */);
|
||||
|
||||
// Hide button if ime is shown.
|
||||
clearInvocations(mLayout);
|
||||
doReturn(View.VISIBLE).when(mButton).getVisibility();
|
||||
mLayout.updateImeVisibility(true /* isImeShowing */);
|
||||
|
||||
verify(mLayout, never()).createSizeCompatButton(anyBoolean());
|
||||
verify(mButton).setVisibility(View.GONE);
|
||||
|
||||
// Show button if ime is not shown.
|
||||
doReturn(View.GONE).when(mButton).getVisibility();
|
||||
mLayout.updateImeVisibility(false /* isImeShowing */);
|
||||
|
||||
verify(mLayout, never()).createSizeCompatButton(anyBoolean());
|
||||
verify(mButton).setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAttachToParentSurface() {
|
||||
final SurfaceControl.Builder b = new SurfaceControl.Builder();
|
||||
mLayout.attachToParentSurface(b);
|
||||
|
||||
verify(mTaskListener).attachChildSurfaceToTask(TASK_ID, b);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnRestartButtonClicked() {
|
||||
spyOn(ActivityClient.getInstance());
|
||||
doNothing().when(ActivityClient.getInstance()).restartActivityProcessIfVisible(any());
|
||||
|
||||
mLayout.onRestartButtonClicked();
|
||||
|
||||
verify(ActivityClient.getInstance()).restartActivityProcessIfVisible(mActivityToken);
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ import static android.os.Process.THREAD_PRIORITY_DISPLAY;
|
||||
|
||||
import android.animation.AnimationHandler;
|
||||
import android.app.ActivityTaskManager;
|
||||
import android.app.IActivityManager;
|
||||
import android.content.Context;
|
||||
import android.content.pm.LauncherApps;
|
||||
import android.content.pm.PackageManager;
|
||||
@@ -73,7 +72,6 @@ import com.android.wm.shell.pip.PipMediaController;
|
||||
import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
|
||||
import com.android.wm.shell.pip.PipUiEventLogger;
|
||||
import com.android.wm.shell.pip.phone.PipAppOpsListener;
|
||||
import com.android.wm.shell.pip.phone.PipController;
|
||||
import com.android.wm.shell.pip.phone.PipTouchHandler;
|
||||
import com.android.wm.shell.sizecompatui.SizeCompatUIController;
|
||||
import com.android.wm.shell.splitscreen.SplitScreen;
|
||||
@@ -211,8 +209,8 @@ public abstract class WMShellBaseModule {
|
||||
@Provides
|
||||
static SizeCompatUIController provideSizeCompatUIController(Context context,
|
||||
DisplayController displayController, DisplayImeController imeController,
|
||||
@ShellMainThread ShellExecutor mainExecutor) {
|
||||
return new SizeCompatUIController(context, displayController, imeController, mainExecutor);
|
||||
SyncTransactionQueue syncQueue) {
|
||||
return new SizeCompatUIController(context, displayController, imeController, syncQueue);
|
||||
}
|
||||
|
||||
@WMSingleton
|
||||
|
||||
Reference in New Issue
Block a user