Merge "Allow non-resizable apps in split-screen (8/n)" into sc-dev
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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 org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
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 org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
/**
|
||||
* Tests for {@link SizeCompatUIController}.
|
||||
*
|
||||
* Build/Install/Run:
|
||||
* atest WMShellUnitTests:SizeCompatUIControllerTest
|
||||
*/
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
@SmallTest
|
||||
public class SizeCompatUIControllerTest extends ShellTestCase {
|
||||
private static final int DISPLAY_ID = 0;
|
||||
|
||||
private final TestShellExecutor mShellMainExecutor = new TestShellExecutor();
|
||||
|
||||
private SizeCompatUIController mController;
|
||||
private @Mock DisplayController mMockDisplayController;
|
||||
private @Mock DisplayImeController mMockImeController;
|
||||
private @Mock SizeCompatRestartButton mMockButton;
|
||||
private @Mock IBinder mMockActivityToken;
|
||||
private @Mock ShellTaskOrganizer.TaskListener mMockTaskListener;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
doReturn(true).when(mMockButton).show();
|
||||
|
||||
mController = new SizeCompatUIController(mContext, mMockDisplayController,
|
||||
mMockImeController, mShellMainExecutor) {
|
||||
@Override
|
||||
SizeCompatRestartButton createRestartButton(Context context, int displayId) {
|
||||
return mMockButton;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testListenerRegistered() {
|
||||
verify(mMockDisplayController).addDisplayWindowListener(mController);
|
||||
verify(mMockImeController).addPositionProcessor(mController);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnSizeCompatInfoChanged() {
|
||||
final int taskId = 12;
|
||||
final Rect taskBounds = new Rect(0, 0, 1000, 2000);
|
||||
|
||||
// Verify that the restart button is added with non-null size compat activity.
|
||||
mController.mImpl.onSizeCompatInfoChanged(DISPLAY_ID, taskId, taskBounds,
|
||||
mMockActivityToken, mMockTaskListener);
|
||||
mShellMainExecutor.flushAll();
|
||||
|
||||
verify(mMockButton).show();
|
||||
verify(mMockButton).updateLastTargetActivity(eq(mMockActivityToken));
|
||||
|
||||
// Verify that the restart button is removed with null size compat activity.
|
||||
mController.mImpl.onSizeCompatInfoChanged(DISPLAY_ID, taskId, null, null, null);
|
||||
|
||||
mShellMainExecutor.flushAll();
|
||||
verify(mMockButton).remove();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChangeButtonVisibilityOnImeShowHide() {
|
||||
final int taskId = 12;
|
||||
final Rect taskBounds = new Rect(0, 0, 1000, 2000);
|
||||
mController.mImpl.onSizeCompatInfoChanged(DISPLAY_ID, taskId, taskBounds,
|
||||
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 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));
|
||||
}
|
||||
}
|
||||
@@ -2580,9 +2580,6 @@
|
||||
<!-- What to show on the ambient display player when song doesn't have a title. [CHAR LIMIT=20] -->
|
||||
<string name="music_controls_no_title">No title</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>
|
||||
|
||||
<!-- Action in accessibility menu to move the stack of bubbles [CHAR LIMIT=20] -->
|
||||
<string name="bubble_accessibility_action_move">Move</string>
|
||||
|
||||
|
||||
@@ -16,284 +16,27 @@
|
||||
|
||||
package com.android.systemui;
|
||||
|
||||
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.hardware.display.DisplayManager;
|
||||
import android.inputmethodservice.InputMethodService;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.view.Display;
|
||||
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.internal.annotations.VisibleForTesting;
|
||||
import com.android.systemui.dagger.SysUISingleton;
|
||||
import com.android.systemui.shared.system.TaskStackChangeListener;
|
||||
import com.android.systemui.shared.system.TaskStackChangeListeners;
|
||||
import com.android.systemui.statusbar.CommandQueue;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
/** Shows a restart-activity button when the foreground activity is in size compatibility mode. */
|
||||
/**
|
||||
* Shows a restart-activity button when the foreground activity is in size compatibility mode.
|
||||
*
|
||||
* // TODO remove this class after cleanup all dependencies.
|
||||
* @deprecated Use {@link com.android.wm.shell.sizecompatui.SizeCompatUIController}
|
||||
*/
|
||||
@Deprecated
|
||||
@SysUISingleton
|
||||
public class SizeCompatModeActivityController extends SystemUI implements CommandQueue.Callbacks {
|
||||
private static final String TAG = "SizeCompatMode";
|
||||
public class SizeCompatModeActivityController extends SystemUI {
|
||||
|
||||
/** The showing buttons by display id. */
|
||||
private final SparseArray<RestartActivityButton> mActiveButtons = new SparseArray<>(1);
|
||||
/** Avoid creating display context frequently for non-default display. */
|
||||
private final SparseArray<WeakReference<Context>> mDisplayContextCache = new SparseArray<>(0);
|
||||
private final CommandQueue mCommandQueue;
|
||||
|
||||
/** Only show once automatically in the process life. */
|
||||
private boolean mHasShownHint;
|
||||
|
||||
@VisibleForTesting
|
||||
@Inject
|
||||
SizeCompatModeActivityController(Context context, TaskStackChangeListeners listeners,
|
||||
CommandQueue commandQueue) {
|
||||
SizeCompatModeActivityController(Context context) {
|
||||
super(context);
|
||||
mCommandQueue = commandQueue;
|
||||
listeners.registerTaskStackListener(new TaskStackChangeListener() {
|
||||
@Override
|
||||
public void onSizeCompatModeActivityChanged(int displayId, IBinder activityToken) {
|
||||
// Note the callback already runs on main thread.
|
||||
updateRestartButton(displayId, activityToken);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
mCommandQueue.addCallback(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImeWindowStatus(int displayId, IBinder token, int vis, int backDisposition,
|
||||
boolean showImeSwitcher) {
|
||||
RestartActivityButton button = mActiveButtons.get(displayId);
|
||||
if (button == null) {
|
||||
return;
|
||||
}
|
||||
boolean imeShown = (vis & InputMethodService.IME_VISIBLE) != 0;
|
||||
int newVisibility = imeShown ? View.GONE : View.VISIBLE;
|
||||
// Hide the button when input method is showing.
|
||||
if (button.getVisibility() != newVisibility) {
|
||||
button.setVisibility(newVisibility);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisplayRemoved(int displayId) {
|
||||
mDisplayContextCache.remove(displayId);
|
||||
removeRestartButton(displayId);
|
||||
}
|
||||
|
||||
private void removeRestartButton(int displayId) {
|
||||
RestartActivityButton button = mActiveButtons.get(displayId);
|
||||
if (button != null) {
|
||||
button.remove();
|
||||
mActiveButtons.remove(displayId);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateRestartButton(int displayId, IBinder activityToken) {
|
||||
if (activityToken == null) {
|
||||
// Null token means the current foreground activity is not in size compatibility mode.
|
||||
removeRestartButton(displayId);
|
||||
return;
|
||||
}
|
||||
|
||||
RestartActivityButton restartButton = mActiveButtons.get(displayId);
|
||||
if (restartButton != null) {
|
||||
restartButton.updateLastTargetActivity(activityToken);
|
||||
return;
|
||||
}
|
||||
|
||||
Context context = getOrCreateDisplayContext(displayId);
|
||||
if (context == null) {
|
||||
Log.i(TAG, "Cannot get context for display " + displayId);
|
||||
return;
|
||||
}
|
||||
|
||||
restartButton = createRestartButton(context);
|
||||
restartButton.updateLastTargetActivity(activityToken);
|
||||
if (restartButton.show()) {
|
||||
mActiveButtons.append(displayId, restartButton);
|
||||
} else {
|
||||
onDisplayRemoved(displayId);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
RestartActivityButton createRestartButton(Context context) {
|
||||
RestartActivityButton button = new RestartActivityButton(context, mHasShownHint);
|
||||
mHasShownHint = true;
|
||||
return button;
|
||||
}
|
||||
|
||||
private Context getOrCreateDisplayContext(int displayId) {
|
||||
if (displayId == Display.DEFAULT_DISPLAY) {
|
||||
return mContext;
|
||||
}
|
||||
Context context = null;
|
||||
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>(context));
|
||||
}
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static class RestartActivityButton extends ImageButton implements View.OnClickListener,
|
||||
View.OnLongClickListener {
|
||||
|
||||
final WindowManager.LayoutParams mWinParams;
|
||||
final boolean mShouldShowHint;
|
||||
IBinder mLastActivityToken;
|
||||
|
||||
final int mPopupOffsetX;
|
||||
final int mPopupOffsetY;
|
||||
PopupWindow mShowingHint;
|
||||
|
||||
RestartActivityButton(Context context, boolean hasShownHint) {
|
||||
super(context);
|
||||
mShouldShowHint = !hasShownHint;
|
||||
Drawable drawable = context.getDrawable(R.drawable.btn_restart);
|
||||
setImageDrawable(drawable);
|
||||
setContentDescription(context.getString(R.string.restart_button_description));
|
||||
|
||||
int drawableW = drawable.getIntrinsicWidth();
|
||||
int drawableH = drawable.getIntrinsicHeight();
|
||||
mPopupOffsetX = drawableW / 2;
|
||||
mPopupOffsetY = drawableH * 2;
|
||||
|
||||
ColorStateList color = ColorStateList.valueOf(Color.LTGRAY);
|
||||
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(SizeCompatModeActivityController.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) {
|
||||
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;
|
||||
}
|
||||
|
||||
View popupView = LayoutInflater.from(getContext()).inflate(
|
||||
R.layout.size_compat_mode_hint, null /* root */);
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
public void start() { }
|
||||
}
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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.systemui;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.content.Context;
|
||||
import android.inputmethodservice.InputMethodService;
|
||||
import android.os.IBinder;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.systemui.SizeCompatModeActivityController.RestartActivityButton;
|
||||
import com.android.systemui.shared.system.TaskStackChangeListener;
|
||||
import com.android.systemui.shared.system.TaskStackChangeListeners;
|
||||
import com.android.systemui.statusbar.CommandQueue;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
/**
|
||||
* runtest systemui -c com.android.systemui.SizeCompatModeActivityControllerTest
|
||||
*/
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
@SmallTest
|
||||
public class SizeCompatModeActivityControllerTest extends SysuiTestCase {
|
||||
private static final int DISPLAY_ID = 0;
|
||||
|
||||
private SizeCompatModeActivityController mController;
|
||||
private TaskStackChangeListener mTaskStackListener;
|
||||
private @Mock TaskStackChangeListeners mMockTaskListeners;
|
||||
private @Mock RestartActivityButton mMockButton;
|
||||
private @Mock IBinder mMockActivityToken;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
doReturn(true).when(mMockButton).show();
|
||||
|
||||
mController = new SizeCompatModeActivityController(mContext, mMockTaskListeners,
|
||||
new CommandQueue(mContext)) {
|
||||
@Override
|
||||
RestartActivityButton createRestartButton(Context context) {
|
||||
return mMockButton;
|
||||
};
|
||||
};
|
||||
|
||||
ArgumentCaptor<TaskStackChangeListener> listenerCaptor =
|
||||
ArgumentCaptor.forClass(TaskStackChangeListener.class);
|
||||
verify(mMockTaskListeners).registerTaskStackListener(listenerCaptor.capture());
|
||||
mTaskStackListener = listenerCaptor.getValue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnSizeCompatModeActivityChanged() {
|
||||
// Verifies that the restart button is added with non-null component name.
|
||||
mTaskStackListener.onSizeCompatModeActivityChanged(DISPLAY_ID, mMockActivityToken);
|
||||
verify(mMockButton).show();
|
||||
verify(mMockButton).updateLastTargetActivity(eq(mMockActivityToken));
|
||||
|
||||
// Verifies that the restart button is removed with null component name.
|
||||
mTaskStackListener.onSizeCompatModeActivityChanged(DISPLAY_ID, null /* activityToken */);
|
||||
verify(mMockButton).remove();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChangeButtonVisibilityOnImeShowHide() {
|
||||
mTaskStackListener.onSizeCompatModeActivityChanged(DISPLAY_ID, mMockActivityToken);
|
||||
|
||||
// Verifies that the restart button is hidden when IME is visible.
|
||||
doReturn(View.VISIBLE).when(mMockButton).getVisibility();
|
||||
mController.setImeWindowStatus(DISPLAY_ID, null /* token */, InputMethodService.IME_VISIBLE,
|
||||
0 /* backDisposition */, false /* showImeSwitcher */);
|
||||
verify(mMockButton).setVisibility(eq(View.GONE));
|
||||
|
||||
// Verifies that the restart button is visible when IME is hidden.
|
||||
doReturn(View.GONE).when(mMockButton).getVisibility();
|
||||
mController.setImeWindowStatus(DISPLAY_ID, null /* token */, 0 /* vis */,
|
||||
0 /* backDisposition */, false /* showImeSwitcher */);
|
||||
verify(mMockButton).setVisibility(eq(View.VISIBLE));
|
||||
}
|
||||
}
|
||||
@@ -5500,12 +5500,9 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
|
||||
// The callback is only interested in the foreground changes of fullscreen activity.
|
||||
return;
|
||||
}
|
||||
// TODO(b/178327644) Update for per Task size compat
|
||||
if (!r.inSizeCompatMode()) {
|
||||
if (mLastCompatModeActivity != null) {
|
||||
// TODO(b/178327644) Remove notifySizeCompatModeActivityChanged
|
||||
mAtmService.getTaskChangeNotificationController()
|
||||
.notifySizeCompatModeActivityChanged(mDisplayId, null /* activityToken */);
|
||||
// This will do nothing until SizeCompatModeActivityController is moved to shell
|
||||
organizedTask.onSizeCompatActivityChanged();
|
||||
}
|
||||
mLastCompatModeActivity = null;
|
||||
@@ -5515,10 +5512,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
|
||||
return;
|
||||
}
|
||||
mLastCompatModeActivity = r;
|
||||
// TODO(b/178327644) Remove notifySizeCompatModeActivityChanged
|
||||
mAtmService.getTaskChangeNotificationController()
|
||||
.notifySizeCompatModeActivityChanged(mDisplayId, r.appToken);
|
||||
// This will do nothing until SizeCompatModeActivityController is moved to shell
|
||||
organizedTask.onSizeCompatActivityChanged();
|
||||
}
|
||||
|
||||
|
||||
@@ -52,14 +52,12 @@ import static org.mockito.Mockito.doCallRealMethod;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.app.ActivityManagerInternal;
|
||||
import android.app.TaskStackListener;
|
||||
import android.app.WindowConfiguration;
|
||||
import android.compat.testing.PlatformCompatChangeRule;
|
||||
import android.content.ComponentName;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Rect;
|
||||
import android.os.IBinder;
|
||||
import android.platform.test.annotations.Presubmit;
|
||||
import android.view.WindowManager;
|
||||
|
||||
@@ -72,8 +70,6 @@ import org.junit.Test;
|
||||
import org.junit.rules.TestRule;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Tests for Size Compatibility mode.
|
||||
*
|
||||
@@ -478,52 +474,6 @@ public class SizeCompatTests extends WindowTestsBase {
|
||||
eq(mActivity.app.mName), eq(mActivity.app.mUid), anyString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that {@link TaskStackListener} can receive callback about the activity in size
|
||||
* compatibility mode.
|
||||
*
|
||||
* TODO(b/178327644) Remove after update DC#handleActivitySizeCompatModeIfNeeded
|
||||
*/
|
||||
@Test
|
||||
public void testHandleActivitySizeCompatMode() {
|
||||
setUpDisplaySizeWithApp(1000, 2000);
|
||||
doReturn(true).when(mTask).isOrganized();
|
||||
ActivityRecord activity = mActivity;
|
||||
activity.setState(Task.ActivityState.RESUMED, "testHandleActivitySizeCompatMode");
|
||||
prepareUnresizable(mActivity, -1.f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT);
|
||||
assertFitted();
|
||||
|
||||
final ArrayList<IBinder> compatTokens = new ArrayList<>();
|
||||
mAtm.getTaskChangeNotificationController().registerTaskStackListener(
|
||||
new TaskStackListener() {
|
||||
@Override
|
||||
public void onSizeCompatModeActivityChanged(int displayId,
|
||||
IBinder activityToken) {
|
||||
compatTokens.add(activityToken);
|
||||
}
|
||||
});
|
||||
|
||||
// Resize the display so that the activity exercises size-compat mode.
|
||||
resizeDisplay(mTask.mDisplayContent, 1000, 2500);
|
||||
|
||||
// Expect the exact token when the activity is in size compatibility mode.
|
||||
assertEquals(1, compatTokens.size());
|
||||
assertEquals(activity.appToken, compatTokens.get(0));
|
||||
|
||||
compatTokens.clear();
|
||||
// Make the activity resizable again by restarting it
|
||||
activity.info.resizeMode = RESIZE_MODE_RESIZEABLE;
|
||||
activity.mVisibleRequested = true;
|
||||
activity.restartProcessIfVisible();
|
||||
// The full lifecycle isn't hooked up so manually set state to resumed
|
||||
activity.setState(Task.ActivityState.RESUMED, "testHandleActivitySizeCompatMode");
|
||||
mTask.mDisplayContent.handleActivitySizeCompatModeIfNeeded(activity);
|
||||
|
||||
// Expect null token when switching to non-size-compat mode activity.
|
||||
assertEquals(1, compatTokens.size());
|
||||
assertEquals(null, compatTokens.get(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that {@link TaskOrganizerController} can receive callback about the activity in size
|
||||
* compatibility mode.
|
||||
|
||||
Reference in New Issue
Block a user