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

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

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

Bug: 176061101
Bug: 178327644
Test: manually verify that the size compat restart button works as usual
Test: atest WMShellUnitTests:SizeCompatUIControllerTest
Test: atest WmTests:SizeCompatTests
Change-Id: I9143e079a1a945b76c3c56596976dd6ad2802897
This commit is contained in:
Chris Li
2021-01-30 22:11:44 -08:00
parent 98d2d06106
commit 27d983a098
11 changed files with 413 additions and 433 deletions

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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 {

View File

@@ -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));
}
}

View File

@@ -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>

View File

@@ -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() { }
}

View File

@@ -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));
}
}

View File

@@ -5491,12 +5491,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;
@@ -5506,10 +5503,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();
}

View File

@@ -51,14 +51,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;
@@ -71,8 +69,6 @@ import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import java.util.ArrayList;
/**
* Tests for Size Compatibility mode.
*
@@ -477,52 +473,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.