Make positioning in letterbox mode configurable.

Changes:
- Allow to specify in config_letterboxPortraitGravity and config_letterboxLandscapeGravity desired alignment for letterboxed apps.
- Add ADB commands to allow overriding these config values for testing
- Move positioning logic for letterbox mode from WM Core to WM Shell.

Test: atest LetterboxTaskListenerTest LetterboxConfigControllerTest
Test: go/wm-smoke
Test: manual with adb shell dumpsys activity service SystemUIService WMShell (get)set-letterbox-portrait-gravity / (get)set-letterbox-landscape-gravity
Fix: 170216257, 170215849
Change-Id: I8e6392faab05bec1bf61b9691f3bd9b8b09b9621
This commit is contained in:
Mariia Sandrikova
2020-10-31 14:29:36 +00:00
parent 660f7cd8f1
commit 49fc5569f0
23 changed files with 1173 additions and 366 deletions

View File

@@ -1,6 +1,12 @@
{
"version": "1.0.0",
"messages": {
"-1993693214": {
"message": "Letterbox Task Changed: #%d",
"level": "VERBOSE",
"group": "WM_SHELL_TASK_ORG",
"at": "com\/android\/wm\/shell\/letterbox\/LetterboxTaskListener.java"
},
"-1683614271": {
"message": "Existing task: id=%d component=%s",
"level": "VERBOSE",
@@ -67,12 +73,6 @@
"group": "WM_SHELL_TASK_ORG",
"at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java"
},
"-848099324": {
"message": "Letterbox Task Appeared: #%d",
"level": "VERBOSE",
"group": "WM_SHELL_TASK_ORG",
"at": "com\/android\/wm\/shell\/LetterboxTaskListener.java"
},
"-842742255": {
"message": "%s onTaskAppeared unknown taskId=%d winMode=%d",
"level": "VERBOSE",
@@ -97,6 +97,12 @@
"group": "WM_SHELL_TASK_ORG",
"at": "com\/android\/wm\/shell\/splitscreen\/SplitScreenTaskListener.java"
},
"-342975160": {
"message": "Letterbox Task Vanished: #%d",
"level": "VERBOSE",
"group": "WM_SHELL_TASK_ORG",
"at": "com\/android\/wm\/shell\/letterbox\/LetterboxTaskListener.java"
},
"-234284913": {
"message": "unpair taskId=%d pair=%s",
"level": "VERBOSE",
@@ -175,24 +181,12 @@
"group": "WM_SHELL_TASK_ORG",
"at": "com\/android\/wm\/shell\/apppairs\/AppPairsPool.java"
},
"1104702476": {
"message": "Letterbox Task Changed: #%d",
"level": "VERBOSE",
"group": "WM_SHELL_TASK_ORG",
"at": "com\/android\/wm\/shell\/LetterboxTaskListener.java"
},
"1184615936": {
"message": "Set drop target window visibility: displayId=%d visibility=%d",
"level": "VERBOSE",
"group": "WM_SHELL_DRAG_AND_DROP",
"at": "com\/android\/wm\/shell\/draganddrop\/DragAndDropController.java"
},
"1218010718": {
"message": "Letterbox Task Vanished: #%d",
"level": "VERBOSE",
"group": "WM_SHELL_TASK_ORG",
"at": "com\/android\/wm\/shell\/LetterboxTaskListener.java"
},
"1481772149": {
"message": "Current target: %s",
"level": "VERBOSE",
@@ -205,6 +199,12 @@
"group": "WM_SHELL_DRAG_AND_DROP",
"at": "com\/android\/wm\/shell\/draganddrop\/DragAndDropController.java"
},
"1885882094": {
"message": "Letterbox Task Appeared: #%d",
"level": "VERBOSE",
"group": "WM_SHELL_TASK_ORG",
"at": "com\/android\/wm\/shell\/letterbox\/LetterboxTaskListener.java"
},
"1891981945": {
"message": "release entry.taskId=%s listener=%s size=%d",
"level": "VERBOSE",

View File

@@ -36,4 +36,16 @@
<!-- Bounds [left top right bottom] on screen for picture-in-picture (PIP) windows,
when the PIP menu is shown in center. -->
<string translatable="false" name="pip_menu_bounds">"596 280 1324 690"</string>
<!-- Gravity of letterboxed apps in portrait screen orientation.
Can be Gravity.TOP, Gravity.CENTER or Gravity.BOTTOM.
Any other value will result in runtime exception for a letterboxed activity.
Default is Gravity.TOP. -->
<integer name="config_letterboxPortraitGravity">0x00000030</integer>
<!-- Gravity of letterboxed apps in landscape screen orientation.
Can be Gravity.LEFT, Gravity.CENTER or Gravity.RIGHT.
Any other value will result in runtime exception for a letterboxed activity.
Default is Gravity.CENTER. -->
<integer name="config_letterboxLandscapeGravity">0x00000011</integer>
</resources>

View File

@@ -32,14 +32,17 @@ import com.android.wm.shell.protolog.ShellProtoLogGroup;
import java.io.PrintWriter;
class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener {
/**
* Organizes tasks presented in {@link android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN}.
*/
public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener {
private static final String TAG = "FullscreenTaskListener";
private final SyncTransactionQueue mSyncQueue;
private final ArraySet<Integer> mTasks = new ArraySet<>();
FullscreenTaskListener(SyncTransactionQueue syncQueue) {
public FullscreenTaskListener(SyncTransactionQueue syncQueue) {
mSyncQueue = syncQueue;
}

View File

@@ -1,110 +0,0 @@
/*
* Copyright (C) 2020 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;
import android.app.ActivityManager;
import android.graphics.Point;
import android.graphics.Rect;
import android.util.Slog;
import android.util.SparseArray;
import android.view.SurfaceControl;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
/**
* Organizes a task in {@link android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN} when
* it's presented in the letterbox mode either because orientations of a top activity and a device
* don't match or because a top activity is in a size compat mode.
*/
final class LetterboxTaskListener implements ShellTaskOrganizer.TaskListener {
private static final String TAG = "LetterboxTaskListener";
private final SyncTransactionQueue mSyncQueue;
private final SparseArray<SurfaceControl> mLeashByTaskId = new SparseArray<>();
LetterboxTaskListener(SyncTransactionQueue syncQueue) {
mSyncQueue = syncQueue;
}
@Override
public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
synchronized (mLeashByTaskId) {
if (mLeashByTaskId.get(taskInfo.taskId) != null) {
throw new RuntimeException("Task appeared more than once: #" + taskInfo.taskId);
}
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Letterbox Task Appeared: #%d",
taskInfo.taskId);
mLeashByTaskId.put(taskInfo.taskId, leash);
final Rect taskBounds = taskInfo.getConfiguration().windowConfiguration.getBounds();
final Rect activtyBounds = taskInfo.letterboxActivityBounds;
final Point taskPositionInParent = taskInfo.positionInParent;
mSyncQueue.runInSync(t -> {
setPositionAndWindowCrop(
t, leash, activtyBounds, taskBounds, taskPositionInParent);
if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
t.setAlpha(leash, 1f);
t.setMatrix(leash, 1, 0, 0, 1);
t.show(leash);
}
});
}
}
@Override
public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
synchronized (mLeashByTaskId) {
if (mLeashByTaskId.get(taskInfo.taskId) == null) {
Slog.e(TAG, "Task already vanished: #" + taskInfo.taskId);
return;
}
mLeashByTaskId.remove(taskInfo.taskId);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Letterbox Task Vanished: #%d",
taskInfo.taskId);
}
}
@Override
public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
synchronized (mLeashByTaskId) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Letterbox Task Changed: #%d",
taskInfo.taskId);
final SurfaceControl leash = mLeashByTaskId.get(taskInfo.taskId);
final Rect taskBounds = taskInfo.getConfiguration().windowConfiguration.getBounds();
final Rect activtyBounds = taskInfo.letterboxActivityBounds;
final Point taskPositionInParent = taskInfo.positionInParent;
mSyncQueue.runInSync(t -> {
setPositionAndWindowCrop(
t, leash, activtyBounds, taskBounds, taskPositionInParent);
});
}
}
private static void setPositionAndWindowCrop(
SurfaceControl.Transaction transaction,
SurfaceControl leash,
final Rect activityBounds,
final Rect taskBounds,
final Point taskPositionInParent) {
Rect activtyInTaskCoordinates = new Rect(activityBounds);
activtyInTaskCoordinates.offset(-taskBounds.left, -taskBounds.top);
transaction.setPosition(leash, taskPositionInParent.x, taskPositionInParent.y);
transaction.setWindowCrop(leash, activtyInTaskCoordinates);
}
}

View File

@@ -0,0 +1,231 @@
/*
* 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.wm.shell;
import android.view.Gravity;
import com.android.wm.shell.apppairs.AppPairs;
import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout;
import com.android.wm.shell.letterbox.LetterboxConfigController;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.pip.Pip;
import com.android.wm.shell.splitscreen.SplitScreen;
import java.io.PrintWriter;
import java.util.Optional;
/**
* An entry point into the shell for dumping shell internal state and running adb commands.
*
* Use with {@code adb shell dumpsys activity service SystemUIService WMShell ...}.
*/
public final class ShellCommandHandler {
private final Optional<SplitScreen> mSplitScreenOptional;
private final Optional<Pip> mPipOptional;
private final Optional<OneHanded> mOneHandedOptional;
private final Optional<HideDisplayCutout> mHideDisplayCutout;
private final ShellTaskOrganizer mShellTaskOrganizer;
private final Optional<AppPairs> mAppPairsOptional;
private final LetterboxConfigController mLetterboxConfigController;
public ShellCommandHandler(
ShellTaskOrganizer shellTaskOrganizer,
Optional<SplitScreen> splitScreenOptional,
Optional<Pip> pipOptional,
Optional<OneHanded> oneHandedOptional,
Optional<HideDisplayCutout> hideDisplayCutout,
Optional<AppPairs> appPairsOptional,
LetterboxConfigController letterboxConfigController) {
mShellTaskOrganizer = shellTaskOrganizer;
mSplitScreenOptional = splitScreenOptional;
mPipOptional = pipOptional;
mOneHandedOptional = oneHandedOptional;
mHideDisplayCutout = hideDisplayCutout;
mAppPairsOptional = appPairsOptional;
mLetterboxConfigController = letterboxConfigController;
}
/** Dumps WM Shell internal state. */
public void dump(PrintWriter pw) {
mShellTaskOrganizer.dump(pw, "");
pw.println();
pw.println();
mPipOptional.ifPresent(pip -> pip.dump(pw));
mSplitScreenOptional.ifPresent(splitScreen -> splitScreen.dump(pw));
mOneHandedOptional.ifPresent(oneHanded -> oneHanded.dump(pw));
mHideDisplayCutout.ifPresent(hideDisplayCutout -> hideDisplayCutout.dump(pw));
pw.println();
pw.println();
mAppPairsOptional.ifPresent(appPairs -> appPairs.dump(pw, ""));
}
/** Returns {@code true} if command was found and executed. */
public boolean handleCommand(String[] args, PrintWriter pw) {
if (args.length < 2) {
// Argument at position 0 is "WMShell".
return false;
}
switch (args[1]) {
case "set-letterbox-portrait-gravity":
return runSetLetterboxPortraitGravity(args, pw);
case "get-letterbox-portrait-gravity":
return runGetLetterboxPortraitGravity(pw);
case "set-letterbox-landscape-gravity":
return runSetLetterboxLandscapeGravity(args, pw);
case "get-letterbox-landscape-gravity":
return runGetLetterboxLandscapeGravity(pw);
case "pair":
return runPair(args, pw);
case "unpair":
return runUnpair(args, pw);
case "help":
return runHelp(pw);
default:
return false;
}
}
private boolean runSetLetterboxPortraitGravity(String[] args, PrintWriter pw) {
if (args.length < 3) {
// First two arguments are "WMShell" and command name.
pw.println("Error: reset, TOP, CENTER or BOTTOM should be provided as an argument");
return true;
}
switch (args[2]) {
case "reset":
mLetterboxConfigController.resetPortraitGravity();
break;
case "TOP":
mLetterboxConfigController.setPortraitGravity(Gravity.TOP);
break;
case "CENTER":
mLetterboxConfigController.setPortraitGravity(Gravity.CENTER);
break;
case "BOTTOM":
mLetterboxConfigController.setPortraitGravity(Gravity.BOTTOM);
break;
default:
pw.println("Error: expected reset, TOP, CENTER or BOTTOM but got " + args[2]);
}
return true;
}
private boolean runGetLetterboxPortraitGravity(PrintWriter pw) {
final int gravity = mLetterboxConfigController.getPortraitGravity();
switch (gravity) {
case Gravity.TOP:
pw.println("TOP");
break;
case Gravity.CENTER:
pw.println("CENTER");
break;
case Gravity.BOTTOM:
pw.println("BOTTOM");
break;
default:
throw new AssertionError("Unexpected gravity: " + gravity);
}
return true;
}
private boolean runSetLetterboxLandscapeGravity(String[] args, PrintWriter pw) {
if (args.length < 3) {
// First two arguments are "WMShell" and command name.
pw.println("Error: reset, LEFT, CENTER or RIGHT should be provided as an argument");
return false;
}
switch (args[2]) {
case "reset":
mLetterboxConfigController.resetLandscapeGravity();
break;
case "LEFT":
mLetterboxConfigController.setLandscapeGravity(Gravity.LEFT);
break;
case "CENTER":
mLetterboxConfigController.setLandscapeGravity(Gravity.CENTER);
break;
case "RIGHT":
mLetterboxConfigController.setLandscapeGravity(Gravity.RIGHT);
break;
default:
pw.println(
"Error: expected reset, LEFT, CENTER or RIGHT but got " + args[2]);
}
return true;
}
private boolean runGetLetterboxLandscapeGravity(PrintWriter pw) {
final int gravity = mLetterboxConfigController.getLandscapeGravity();
switch (gravity) {
case Gravity.LEFT:
pw.println("LEFT");
break;
case Gravity.CENTER:
pw.println("CENTER");
break;
case Gravity.RIGHT:
pw.println("RIGHT");
break;
default:
throw new AssertionError("Unexpected gravity: " + gravity);
}
return true;
}
private boolean runPair(String[] args, PrintWriter pw) {
if (args.length < 4) {
// First two arguments are "WMShell" and command name.
pw.println("Error: two task ids should be provided as arguments");
return false;
}
final int taskId1 = new Integer(args[2]);
final int taskId2 = new Integer(args[3]);
mAppPairsOptional.ifPresent(appPairs -> appPairs.pair(taskId1, taskId2));
return true;
}
private boolean runUnpair(String[] args, PrintWriter pw) {
if (args.length < 3) {
// First two arguments are "WMShell" and command name.
pw.println("Error: task id should be provided as an argument");
return false;
}
final int taskId = new Integer(args[2]);
mAppPairsOptional.ifPresent(appPairs -> appPairs.unpair(taskId));
return true;
}
private boolean runHelp(PrintWriter pw) {
pw.println("Window Manager Shell commands:");
pw.println(" help");
pw.println(" Print this help text.");
pw.println(" <no arguments provided>");
pw.println(" Dump Window Manager Shell internal state");
pw.println(" set-letterbox-portrait-gravity [reset|TOP|CENTER|BOTTOM]");
pw.println(" get-letterbox-portrait-gravity");
pw.println(" Set, reset or print letterbox gravity for portrait screen mode.");
pw.println(" set-letterbox-landscape-gravity [reset|LEFT|CENTER|RIGHT]");
pw.println(" get-letterbox-landscape-gravity");
pw.println(" Set, reset or print letterbox gravity for landscape screen mode.");
pw.println(" pair <taskId1> <taskId2>");
pw.println(" unpair <taskId>");
pw.println(" Pairs/unpairs tasks with given ids.");
return true;
}
}

View File

@@ -1,66 +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.wm.shell;
import com.android.wm.shell.apppairs.AppPairs;
import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.pip.Pip;
import com.android.wm.shell.splitscreen.SplitScreen;
import java.io.PrintWriter;
import java.util.Optional;
/**
* An entry point into the shell for dumping shell internal state.
*/
public class ShellDump {
private final Optional<SplitScreen> mSplitScreenOptional;
private final Optional<Pip> mPipOptional;
private final Optional<OneHanded> mOneHandedOptional;
private final Optional<HideDisplayCutout> mHideDisplayCutout;
private final ShellTaskOrganizer mShellTaskOrganizer;
private final Optional<AppPairs> mAppPairsOptional;
public ShellDump(ShellTaskOrganizer shellTaskOrganizer,
Optional<SplitScreen> splitScreenOptional,
Optional<Pip> pipOptional,
Optional<OneHanded> oneHandedOptional,
Optional<HideDisplayCutout> hideDisplayCutout,
Optional<AppPairs> appPairsOptional) {
mShellTaskOrganizer = shellTaskOrganizer;
mSplitScreenOptional = splitScreenOptional;
mPipOptional = pipOptional;
mOneHandedOptional = oneHandedOptional;
mHideDisplayCutout = hideDisplayCutout;
mAppPairsOptional = appPairsOptional;
}
public void dump(PrintWriter pw) {
mShellTaskOrganizer.dump(pw, "");
pw.println();
pw.println();
mPipOptional.ifPresent(pip -> pip.dump(pw));
mSplitScreenOptional.ifPresent(splitScreen -> splitScreen.dump(pw));
mOneHandedOptional.ifPresent(oneHanded -> oneHanded.dump(pw));
mHideDisplayCutout.ifPresent(hideDisplayCutout -> hideDisplayCutout.dump(pw));
pw.println();
pw.println();
mAppPairsOptional.ifPresent(appPairs -> appPairs.dump(pw, ""));
}
}

View File

@@ -16,9 +16,13 @@
package com.android.wm.shell;
import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FULLSCREEN;
import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_LETTERBOX;
import com.android.wm.shell.apppairs.AppPairs;
import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.letterbox.LetterboxTaskListener;
import com.android.wm.shell.splitscreen.SplitScreen;
import java.util.Optional;
@@ -33,24 +37,36 @@ public class ShellInit {
private final ShellTaskOrganizer mShellTaskOrganizer;
private final Optional<SplitScreen> mSplitScreenOptional;
private final Optional<AppPairs> mAppPairsOptional;
private final LetterboxTaskListener mLetterboxTaskListener;
private final FullscreenTaskListener mFullscreenTaskListener;
public ShellInit(DisplayImeController displayImeController,
DragAndDropController dragAndDropController,
ShellTaskOrganizer shellTaskOrganizer,
Optional<SplitScreen> splitScreenOptional,
Optional<AppPairs> appPairsOptional) {
Optional<AppPairs> appPairsOptional,
LetterboxTaskListener letterboxTaskListener,
FullscreenTaskListener fullscreenTaskListener) {
mDisplayImeController = displayImeController;
mDragAndDropController = dragAndDropController;
mShellTaskOrganizer = shellTaskOrganizer;
mSplitScreenOptional = splitScreenOptional;
mAppPairsOptional = appPairsOptional;
mLetterboxTaskListener = letterboxTaskListener;
mFullscreenTaskListener = fullscreenTaskListener;
}
public void init() {
// Start listening for display changes
mDisplayImeController.startMonitorDisplays();
mShellTaskOrganizer.addListenerForType(
mLetterboxTaskListener, TASK_LISTENER_TYPE_LETTERBOX);
mShellTaskOrganizer.addListenerForType(
mFullscreenTaskListener, TASK_LISTENER_TYPE_FULLSCREEN);
// Register the shell organizer
mShellTaskOrganizer.registerOrganizer();
mAppPairsOptional.ifPresent(AppPairs::onOrganizerRegistered);
// Bind the splitscreen impl to the drag drop controller
mDragAndDropController.setSplitScreenController(mSplitScreenOptional);

View File

@@ -118,8 +118,6 @@ public class ShellTaskOrganizer extends TaskOrganizer {
SyncTransactionQueue syncQueue, TransactionPool transactionPool,
ShellExecutor mainExecutor, ShellExecutor animExecutor, Context context) {
super(taskOrganizerController, mainExecutor);
addListenerForType(new FullscreenTaskListener(syncQueue), TASK_LISTENER_TYPE_FULLSCREEN);
addListenerForType(new LetterboxTaskListener(syncQueue), TASK_LISTENER_TYPE_LETTERBOX);
mTransitions = new Transitions(this, transactionPool, mainExecutor, animExecutor);
if (Transitions.ENABLE_SHELL_TRANSITIONS) registerTransitionPlayer(mTransitions);
// TODO(b/131727939) temporarily live here, the starting surface drawer should be controlled

View File

@@ -0,0 +1,108 @@
/*
* Copyright (C) 2020 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.letterbox;
import android.content.Context;
import android.view.Gravity;
import com.android.wm.shell.R;
/**
* Controls access to and overrides of resource config values used by {@link
* LetterboxTaskOrganizer}.
*/
public final class LetterboxConfigController {
private final Context mContext;
/** {@link Gravity} of letterboxed apps in portrait screen orientation. */
private int mLetterboxPortraitGravity;
/** {@link Gravity} of letterboxed apps in landscape screen orientation. */
private int mLetterboxLandscapeGravity;
public LetterboxConfigController(Context context) {
mContext = context;
mLetterboxPortraitGravity =
mContext.getResources().getInteger(R.integer.config_letterboxPortraitGravity);
mLetterboxLandscapeGravity =
mContext.getResources().getInteger(R.integer.config_letterboxLandscapeGravity);
}
/**
* Overrides {@link Gravity} of letterboxed apps in portrait screen orientation.
*
* @throws IllegalArgumentException if gravity isn't equal to {@link Gravity#TOP}, {@link
* Gravity#CENTER} or {@link Gravity#BOTTOM}.
*/
public void setPortraitGravity(int gravity) {
if (gravity != Gravity.TOP && gravity != Gravity.CENTER && gravity != Gravity.BOTTOM) {
throw new IllegalArgumentException(
"Expected Gravity#TOP, Gravity#CENTER or Gravity#BOTTOM but got"
+ gravity);
}
mLetterboxPortraitGravity = gravity;
}
/**
* Resets {@link Gravity} of letterboxed apps in portrait screen orientation to {@link
* R.integer.config_letterboxPortraitGravity}.
*/
public void resetPortraitGravity() {
mLetterboxPortraitGravity =
mContext.getResources().getInteger(R.integer.config_letterboxPortraitGravity);
}
/**
* Gets {@link Gravity} of letterboxed apps in portrait screen orientation.
*/
public int getPortraitGravity() {
return mLetterboxPortraitGravity;
}
/**
* Overrides {@link Gravity} of letterboxed apps in landscape screen orientation.
*
* @throws IllegalArgumentException if gravity isn't equal to {@link Gravity#RIGHT}, {@link
* Gravity#CENTER} or {@link Gravity#LEFT}.
*/
public void setLandscapeGravity(int gravity) {
if (gravity != Gravity.LEFT && gravity != Gravity.CENTER && gravity != Gravity.RIGHT) {
throw new IllegalArgumentException(
"Expected Gravity#LEFT, Gravity#CENTER or Gravity#RIGHT but got"
+ gravity);
}
mLetterboxLandscapeGravity = gravity;
}
/**
* Resets {@link Gravity} of letterboxed apps in landscape screen orientation to {@link
* R.integer.config_letterboxLandscapeGravity}.
*/
public void resetLandscapeGravity() {
mLetterboxLandscapeGravity =
mContext.getResources().getInteger(R.integer.config_letterboxLandscapeGravity);
}
/**
* Gets {@link Gravity} of letterboxed apps in landscape screen orientation.
*/
public int getLandscapeGravity() {
return mLetterboxLandscapeGravity;
}
}

View File

@@ -0,0 +1,226 @@
/*
* Copyright (C) 2020 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.letterbox;
import android.app.ActivityManager;
import android.graphics.Insets;
import android.graphics.Point;
import android.graphics.Rect;
import android.util.Slog;
import android.util.SparseArray;
import android.view.Gravity;
import android.view.SurfaceControl;
import android.view.WindowInsets;
import android.view.WindowManager;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.Transitions;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
/**
* Organizes a task in {@link android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN} when
* it's presented in the letterbox mode either because orientations of a top activity and a device
* don't match or because a top activity is in a size compat mode.
*/
public class LetterboxTaskListener implements ShellTaskOrganizer.TaskListener {
private static final String TAG = "LetterboxTaskListener";
private final SyncTransactionQueue mSyncQueue;
private final LetterboxConfigController mLetterboxConfigController;
private final WindowManager mWindowManager;
private final SparseArray<SurfaceControl> mLeashByTaskId = new SparseArray<>();
public LetterboxTaskListener(
SyncTransactionQueue syncQueue,
LetterboxConfigController letterboxConfigController,
WindowManager windowManager) {
mSyncQueue = syncQueue;
mLetterboxConfigController = letterboxConfigController;
mWindowManager = windowManager;
}
@Override
public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
if (mLeashByTaskId.get(taskInfo.taskId) != null) {
throw new IllegalStateException("Task appeared more than once: #" + taskInfo.taskId);
}
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Letterbox Task Appeared: #%d",
taskInfo.taskId);
mLeashByTaskId.put(taskInfo.taskId, leash);
Point positionInParent = new Point();
Rect crop = new Rect();
resolveTaskPositionAndCrop(taskInfo, positionInParent, crop);
mSyncQueue.runInSync(t -> {
setPositionAndWindowCrop(t, leash, positionInParent, crop);
if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
t.setAlpha(leash, 1f);
t.setMatrix(leash, 1, 0, 0, 1);
t.show(leash);
}
});
}
@Override
public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
if (mLeashByTaskId.get(taskInfo.taskId) == null) {
Slog.e(TAG, "Task already vanished: #" + taskInfo.taskId);
return;
}
mLeashByTaskId.remove(taskInfo.taskId);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Letterbox Task Vanished: #%d",
taskInfo.taskId);
}
@Override
public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Letterbox Task Changed: #%d",
taskInfo.taskId);
final SurfaceControl leash = mLeashByTaskId.get(taskInfo.taskId);
Point positionInParent = new Point();
Rect crop = new Rect();
resolveTaskPositionAndCrop(taskInfo, positionInParent, crop);
mSyncQueue.runInSync(t -> setPositionAndWindowCrop(t, leash, positionInParent, crop));
}
private static void setPositionAndWindowCrop(
SurfaceControl.Transaction transaction,
SurfaceControl leash,
final Point positionInParent,
final Rect crop) {
transaction.setPosition(leash, positionInParent.x, positionInParent.y);
transaction.setWindowCrop(leash, crop);
}
private void resolveTaskPositionAndCrop(
ActivityManager.RunningTaskInfo taskInfo,
Point positionInParent,
Rect crop) {
// In screen coordinates
Rect parentBounds = new Rect(taskInfo.parentBounds);
// Intersect parent and max bounds. This is required for situations when parent bounds
// go beyond display bounds, for example, in One-handed mode.
final Rect maxBounds = taskInfo.getConfiguration().windowConfiguration.getMaxBounds();
if (!parentBounds.intersect(maxBounds)) {
Slog.w(TAG, "Task parent and max bounds don't intersect: #" + taskInfo.taskId);
}
// In screen coordinates
final Rect taskBounds = taskInfo.getConfiguration().windowConfiguration.getBounds();
final Rect activityBounds = taskInfo.letterboxActivityBounds;
Insets insets = getInsets();
Rect taskBoundsWithInsets = new Rect(taskBounds);
applyInsets(taskBoundsWithInsets, insets, taskInfo.parentBounds);
Rect activityBoundsWithInsets = new Rect(activityBounds);
applyInsets(activityBoundsWithInsets, insets, taskInfo.parentBounds);
Rect parentBoundsWithInsets = new Rect(parentBounds);
applyInsets(parentBoundsWithInsets, insets, parentBounds);
// Crop need to be in the task coordinates.
crop.set(activityBoundsWithInsets);
crop.offset(-taskBounds.left, -taskBounds.top);
// Account for insets since coordinates calculations below are done with them.
positionInParent.x = parentBoundsWithInsets.left - parentBounds.left
- (taskBoundsWithInsets.left - taskBounds.left);
positionInParent.y = parentBoundsWithInsets.top - parentBounds.top
- (taskBoundsWithInsets.top - taskBounds.top);
// Calculating a position of task bounds (without insets) in parent coordinates (without
// insets) to align activity bounds (without insets) as requested in config. Activity
// accounts for insets that overlap with its bounds (this overlap can be partial) so
// ignoring overlap with insets when computing the position. Also, cropping unwanted insets
// while keeping the top one if the activity is aligned at the top of the window to show
// status bar decor view.
if (parentBounds.height() >= parentBounds.width()) {
final int gravity = mLetterboxConfigController.getPortraitGravity();
// Center activity horizontally.
positionInParent.x +=
(parentBoundsWithInsets.width() - activityBoundsWithInsets.width()) / 2
+ taskBoundsWithInsets.left - activityBoundsWithInsets.left;
switch (gravity) {
case Gravity.TOP:
positionInParent.y += taskBoundsWithInsets.top - activityBoundsWithInsets.top;
// Showing status bar decor view.
crop.top -= activityBoundsWithInsets.top - activityBounds.top;
break;
case Gravity.CENTER:
positionInParent.y +=
taskBoundsWithInsets.top - activityBoundsWithInsets.top
+ (parentBoundsWithInsets.height()
- activityBoundsWithInsets.height()) / 2;
break;
case Gravity.BOTTOM:
positionInParent.y +=
parentBoundsWithInsets.height() - activityBoundsWithInsets.bottom
+ taskBoundsWithInsets.top;
break;
default:
throw new AssertionError(
"Unexpected portrait gravity " + gravity
+ " for task: #" + taskInfo.taskId);
}
} else {
final int gravity = mLetterboxConfigController.getLandscapeGravity();
// Align activity to the top.
positionInParent.y += taskBoundsWithInsets.top - activityBoundsWithInsets.top;
// Showing status bar decor view.
crop.top -= activityBoundsWithInsets.top - activityBounds.top;
switch (gravity) {
case Gravity.LEFT:
positionInParent.x += taskBoundsWithInsets.left - activityBoundsWithInsets.left;
break;
case Gravity.CENTER:
positionInParent.x +=
(parentBoundsWithInsets.width() - activityBoundsWithInsets.width()) / 2
+ taskBoundsWithInsets.left - activityBoundsWithInsets.left;
break;
case Gravity.RIGHT:
positionInParent.x +=
parentBoundsWithInsets.width()
- activityBoundsWithInsets.right + taskBoundsWithInsets.left;
break;
default:
throw new AssertionError(
"Unexpected landscape gravity " + gravity
+ " for task: #" + taskInfo.taskId);
}
}
}
private Insets getInsets() {
return mWindowManager
.getMaximumWindowMetrics()
.getWindowInsets()
.getInsets(
WindowInsets.Type.navigationBars()
| WindowInsets.Type.statusBars()
| WindowInsets.Type.displayCutout());
}
private void applyInsets(Rect innerBounds, Insets insets, Rect outerBounds) {
Rect outerBoundsWithInsets = new Rect(outerBounds);
outerBoundsWithInsets.inset(insets);
innerBounds.intersect(outerBoundsWithInsets);
}
}

View File

@@ -1,125 +0,0 @@
/*
* Copyright (C) 2020 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;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static org.mockito.ArgumentMatchers.eq;
import android.app.ActivityManager.RunningTaskInfo;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Looper;
import android.view.SurfaceControl;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TransactionPool;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/**
* Tests for {@link LetterboxTaskListener}.
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
public class LetterboxTaskListenerTest {
private static final Rect ACTIVITY_BOUNDS = new Rect(300, 200, 700, 400);
private static final Rect TASK_BOUNDS = new Rect(200, 100, 800, 500);
private static final Rect TASK_BOUNDS_2 = new Rect(300, 200, 800, 500);
private static final Point TASK_POSITION_IN_PARENT = new Point(100, 50);
private static final Point TASK_POSITION_IN_PARENT_2 = new Point(200, 100);
private static final Rect EXPECTED_WINDOW_CROP = new Rect(100, 100, 500, 300);
private static final Rect EXPECTED_WINDOW_CROP_2 = new Rect(0, 0, 400, 200);
private static final RunningTaskInfo TASK_INFO = createTaskInfo(
/* taskId */ 1, ACTIVITY_BOUNDS, TASK_BOUNDS, TASK_POSITION_IN_PARENT);
private static final RunningTaskInfo TASK_INFO_2 = createTaskInfo(
/* taskId */ 1, ACTIVITY_BOUNDS, TASK_BOUNDS_2, TASK_POSITION_IN_PARENT_2);
@Mock private SurfaceControl mLeash;
@Mock private SurfaceControl.Transaction mTransaction;
private LetterboxTaskListener mLetterboxTaskListener;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mLetterboxTaskListener = new LetterboxTaskListener(
new SyncTransactionQueue(
new TransactionPool() {
@Override
public SurfaceControl.Transaction acquire() {
return mTransaction;
}
@Override
public void release(SurfaceControl.Transaction t) {
}
},
new Handler(Looper.getMainLooper())));
}
@Test
public void testOnTaskAppearedAndonTaskInfoChanged_setCorrectPositionAndCrop() {
mLetterboxTaskListener.onTaskAppeared(TASK_INFO, mLeash);
verify(mTransaction).setPosition(
eq(mLeash),
eq((float) TASK_POSITION_IN_PARENT.x),
eq((float) TASK_POSITION_IN_PARENT.y));
// Should return activty coordinates offset by task coordinates
verify(mTransaction).setWindowCrop(eq(mLeash), eq(EXPECTED_WINDOW_CROP));
mLetterboxTaskListener.onTaskInfoChanged(TASK_INFO_2);
verify(mTransaction).setPosition(
eq(mLeash),
eq((float) TASK_POSITION_IN_PARENT_2.x),
eq((float) TASK_POSITION_IN_PARENT_2.y));
// Should return activty coordinates offset by task coordinates
verify(mTransaction).setWindowCrop(eq(mLeash), eq(EXPECTED_WINDOW_CROP_2));
}
@Test(expected = RuntimeException.class)
public void testOnTaskAppeared_calledSecondTimeWithSameTaskId_throwsException() {
mLetterboxTaskListener.onTaskAppeared(TASK_INFO, mLeash);
mLetterboxTaskListener.onTaskAppeared(TASK_INFO, mLeash);
}
private static RunningTaskInfo createTaskInfo(
int taskId,
final Rect activityBounds,
final Rect taskBounds,
final Point taskPositionInParent) {
RunningTaskInfo taskInfo = new RunningTaskInfo();
taskInfo.taskId = taskId;
taskInfo.configuration.windowConfiguration.setBounds(taskBounds);
taskInfo.letterboxActivityBounds = Rect.copyOrNull(activityBounds);
taskInfo.positionInParent = new Point(taskPositionInParent);
return taskInfo;
}
}

View File

@@ -0,0 +1,131 @@
/*
* Copyright (C) 2020 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.letterbox;
import static org.junit.Assert.assertEquals;
import android.view.Gravity;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTestCase;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Tests for {@link LetterboxConfigController}.
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
public final class LetterboxConfigControllerTest extends ShellTestCase {
private LetterboxConfigController mLetterboxConfigController;
@Before
public void setUp() {
mLetterboxConfigController = new LetterboxConfigController(getContext());
}
@Test
public void testGetPortraitGravity_noOverrides_returnConfigValue() {
assertEquals(
mLetterboxConfigController.getPortraitGravity(),
getContext().getResources().getInteger(R.integer.config_letterboxPortraitGravity));
}
@Test
public void testGetLandscapeGravity_noOverrides_returnConfigValue() {
assertEquals(
mLetterboxConfigController.getLandscapeGravity(),
getContext().getResources().getInteger(R.integer.config_letterboxLandscapeGravity));
}
@Test
public void testSetPortraitGravity_validValue_savesValue() {
mLetterboxConfigController.setPortraitGravity(Gravity.BOTTOM);
assertEquals(mLetterboxConfigController.getPortraitGravity(), Gravity.BOTTOM);
mLetterboxConfigController.setPortraitGravity(Gravity.CENTER);
assertEquals(mLetterboxConfigController.getPortraitGravity(), Gravity.CENTER);
mLetterboxConfigController.setPortraitGravity(Gravity.TOP);
assertEquals(mLetterboxConfigController.getPortraitGravity(), Gravity.TOP);
}
@Test
public void testSetLandscapeGravity_validValue_savesValue() {
mLetterboxConfigController.setLandscapeGravity(Gravity.LEFT);
assertEquals(mLetterboxConfigController.getLandscapeGravity(), Gravity.LEFT);
mLetterboxConfigController.setLandscapeGravity(Gravity.CENTER);
assertEquals(mLetterboxConfigController.getLandscapeGravity(), Gravity.CENTER);
mLetterboxConfigController.setLandscapeGravity(Gravity.RIGHT);
assertEquals(mLetterboxConfigController.getLandscapeGravity(), Gravity.RIGHT);
}
@Test(expected = IllegalArgumentException.class)
public void testSetPortraitGravity_invalidValue_throwsException() {
mLetterboxConfigController.setPortraitGravity(Gravity.RIGHT);
}
@Test(expected = IllegalArgumentException.class)
public void testSetLandscapeGravity_invalidValue_throwsException() {
mLetterboxConfigController.setLandscapeGravity(Gravity.TOP);
}
@Test
public void testResetPortraitGravity() {
int defaultGravity =
getContext().getResources().getInteger(R.integer.config_letterboxPortraitGravity);
mLetterboxConfigController.setPortraitGravity(Gravity.BOTTOM);
mLetterboxConfigController.resetPortraitGravity();
assertEquals(mLetterboxConfigController.getPortraitGravity(), defaultGravity);
mLetterboxConfigController.setPortraitGravity(Gravity.CENTER);
mLetterboxConfigController.resetPortraitGravity();
assertEquals(mLetterboxConfigController.getPortraitGravity(), defaultGravity);
mLetterboxConfigController.setPortraitGravity(Gravity.TOP);
mLetterboxConfigController.resetPortraitGravity();
assertEquals(mLetterboxConfigController.getPortraitGravity(), defaultGravity);
}
@Test
public void testResetLandscapeGravity() {
int defaultGravity =
getContext().getResources().getInteger(R.integer.config_letterboxLandscapeGravity);
mLetterboxConfigController.setLandscapeGravity(Gravity.RIGHT);
mLetterboxConfigController.resetLandscapeGravity();
assertEquals(mLetterboxConfigController.getLandscapeGravity(), defaultGravity);
mLetterboxConfigController.setLandscapeGravity(Gravity.CENTER);
mLetterboxConfigController.resetLandscapeGravity();
assertEquals(mLetterboxConfigController.getLandscapeGravity(), defaultGravity);
mLetterboxConfigController.setLandscapeGravity(Gravity.LEFT);
mLetterboxConfigController.resetLandscapeGravity();
assertEquals(mLetterboxConfigController.getLandscapeGravity(), defaultGravity);
}
}

View File

@@ -0,0 +1,320 @@
/*
* Copyright (C) 2020 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.letterbox;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import android.app.ActivityManager.RunningTaskInfo;
import android.graphics.Insets;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Looper;
import android.view.Gravity;
import android.view.SurfaceControl;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TransactionPool;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/**
* Tests for {@link LetterboxTaskListener}.
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
public final class LetterboxTaskListenerTest extends ShellTestCase {
@Mock private SurfaceControl mLeash;
@Mock private SurfaceControl.Transaction mTransaction;
@Mock private WindowManager mWindowManager;
@Mock private WindowMetrics mWindowMetrics;
@Mock private WindowInsets mWindowInsets;
private LetterboxTaskListener mLetterboxTaskListener;
private LetterboxConfigController mLetterboxConfigController;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mLetterboxConfigController = new LetterboxConfigController(getContext());
mLetterboxTaskListener = new LetterboxTaskListener(
new SyncTransactionQueue(
new TransactionPool() {
@Override
public SurfaceControl.Transaction acquire() {
return mTransaction;
}
@Override
public void release(SurfaceControl.Transaction t) {
}
},
new Handler(Looper.getMainLooper())),
mLetterboxConfigController,
mWindowManager);
when(mWindowManager.getMaximumWindowMetrics()).thenReturn(mWindowMetrics);
when(mWindowMetrics.getWindowInsets()).thenReturn(mWindowInsets);
}
@Test
public void testOnTaskInfoChanged_updatesPositionAndCrop() {
setWindowBoundsAndInsets(
/* windowBounds= */ new Rect(0, 0, 200, 100), // equal to parent bounds
Insets.NONE);
mLetterboxConfigController.setLandscapeGravity(Gravity.CENTER);
mLetterboxTaskListener.onTaskAppeared(
createTaskInfo(
/* taskId */ 1,
/* parentBounds */ new Rect(0, 0, 200, 100),
/* activityBounds */ new Rect(75, 0, 125, 75),
/* taskBounds */ new Rect(50, 0, 125, 100)),
mLeash);
// Task doesn't need to repositioned
verifySetPosition(50, 0);
// Should return activity coordinates offset by task coordinates
verifySetWindowCrop(new Rect(25, 0, 75, 75));
mLetterboxTaskListener.onTaskInfoChanged(
createTaskInfo(
/* taskId */ 1,
/* parentBounds */ new Rect(0, 0, 200, 100),
// Activity is offset by 25 to the left
/* activityBounds */ new Rect(50, 0, 100, 75),
/* taskBounds */ new Rect(50, 0, 125, 100)));
// Task needs to be repositioned by 25 to the left
verifySetPosition(75, 0);
// Should return activity coordinates offset by task coordinates
verifySetWindowCrop(new Rect(0, 0, 50, 75));
}
@Test
public void testOnTaskInfoAppeared_landscapeWithLeftGravity() {
mLetterboxConfigController.setLandscapeGravity(Gravity.LEFT);
setWindowBoundsAndInsets(
/* windowBounds= */ new Rect(0, 0, 200, 100), // equal to parent bounds
Insets.of(/* left= */ 10, /* top= */ 10, /* right= */ 10, /* bottom= */ 10));
mLetterboxTaskListener.onTaskAppeared(
createTaskInfo(
/* taskId */ 1,
/* parentBounds */ new Rect(0, 0, 200, 100),
/* activityBounds */ new Rect(150, 0, 200, 75),
/* taskBounds */ new Rect(125, 0, 200, 100)),
mLeash);
verifySetPosition(-15, 0);
// Should return activity coordinates offset by task coordinates minus unwanted right inset
verifySetWindowCrop(new Rect(25, 0, 65, 75));
}
@Test
public void testOnTaskInfoAppeared_landscapeWithCenterGravity() {
mLetterboxConfigController.setLandscapeGravity(Gravity.CENTER);
setWindowBoundsAndInsets(
/* windowBounds= */ new Rect(0, 0, 200, 100), // equal to parent bounds
Insets.of(/* left= */ 10, /* top= */ 10, /* right= */ 10, /* bottom= */ 10));
mLetterboxTaskListener.onTaskAppeared(
createTaskInfo(
/* taskId */ 1,
/* parentBounds */ new Rect(0, 0, 200, 100),
/* activityBounds */ new Rect(150, 0, 200, 75),
/* taskBounds */ new Rect(125, 0, 200, 100)),
mLeash);
verifySetPosition(55, 0);
// Should return activity coordinates offset by task coordinates minus unwanted right inset
verifySetWindowCrop(new Rect(25, 0, 65, 75));
}
@Test
public void testOnTaskInfoAppeared_landscapeWithRightGravity() {
mLetterboxConfigController.setLandscapeGravity(Gravity.RIGHT);
setWindowBoundsAndInsets(
/* windowBounds= */ new Rect(0, 0, 200, 100), // equal to parent bounds
Insets.of(/* left= */ 10, /* top= */ 10, /* right= */ 10, /* bottom= */ 10));
mLetterboxTaskListener.onTaskAppeared(
createTaskInfo(
/* taskId */ 1,
/* parentBounds */ new Rect(0, 0, 200, 100),
/* activityBounds */ new Rect(50, 0, 100, 75),
/* taskBounds */ new Rect(25, 0, 100, 100)),
mLeash);
verifySetPosition(115, 0);
// Should return activity coordinates offset by task coordinates
verifySetWindowCrop(new Rect(25, 0, 75, 75));
}
@Test
public void testOnTaskInfoAppeared_portraitWithTopGravity() {
mLetterboxConfigController.setPortraitGravity(Gravity.TOP);
setWindowBoundsAndInsets(
/* windowBounds= */ new Rect(0, 0, 100, 150), // equal to parent bounds
Insets.of(/* left= */ 10, /* top= */ 10, /* right= */ 10, /* bottom= */ 20));
mLetterboxTaskListener.onTaskAppeared(
createTaskInfo(
/* taskId */ 1,
/* parentBounds */ new Rect(0, 0, 100, 150),
/* activityBounds */ new Rect(0, 75, 50, 125),
/* taskBounds */ new Rect(0, 50, 100, 125)),
mLeash);
verifySetPosition(20, -15);
// Should return activity coordinates offset by task coordinates minus unwanted left inset
verifySetWindowCrop(new Rect(10, 25, 50, 75));
}
@Test
public void testOnTaskInfoAppeared_portraitWithCenterGravity() {
mLetterboxConfigController.setPortraitGravity(Gravity.CENTER);
setWindowBoundsAndInsets(
/* windowBounds= */ new Rect(0, 0, 100, 150), // equal to parent bounds
Insets.of(/* left= */ 10, /* top= */ 10, /* right= */ 10, /* bottom= */ 20));
mLetterboxTaskListener.onTaskAppeared(
createTaskInfo(
/* taskId */ 1,
/* parentBounds */ new Rect(0, 0, 100, 150),
/* activityBounds */ new Rect(0, 75, 50, 125),
/* taskBounds */ new Rect(0, 50, 100, 125)),
mLeash);
verifySetPosition(20, 20);
// Should return activity coordinates offset by task coordinates minus unwanted left inset
verifySetWindowCrop(new Rect(10, 25, 50, 75));
}
@Test
public void testOnTaskInfoAppeared_portraitWithBottomGravity() {
mLetterboxConfigController.setPortraitGravity(Gravity.BOTTOM);
setWindowBoundsAndInsets(
/* windowBounds= */ new Rect(0, 0, 100, 150), // equal to parent bounds
Insets.of(/* left= */ 10, /* top= */ 10, /* right= */ 10, /* bottom= */ 20));
mLetterboxTaskListener.onTaskAppeared(
createTaskInfo(
/* taskId */ 1,
/* parentBounds */ new Rect(0, 0, 100, 150),
/* activityBounds */ new Rect(0, 75, 50, 125),
/* taskBounds */ new Rect(0, 50, 100, 125)),
mLeash);
verifySetPosition(20, 55);
// Should return activity coordinates offset by task coordinates minus unwanted left inset
verifySetWindowCrop(new Rect(10, 25, 50, 75));
}
@Test
public void testOnTaskInfoAppeared_partlyOverlapsWithAllInsets() {
mLetterboxConfigController.setPortraitGravity(Gravity.TOP);
setWindowBoundsAndInsets(
/* windowBounds= */ new Rect(0, 0, 200, 125), // equal to parent bounds
Insets.of(/* left= */ 25, /* top= */ 25, /* right= */ 35, /* bottom= */ 15));
mLetterboxTaskListener.onTaskAppeared(
createTaskInfo(
/* taskId */ 1,
/* parentBounds */ new Rect(0, 0, 200, 125),
/* activityBounds */ new Rect(15, 0, 175, 120),
/* taskBounds */ new Rect(0, 0, 100, 125)), // equal to parent bounds
mLeash);
// Activity fully covers parent bounds with insets so doesn't need to be moved.
verifySetPosition(0, 0);
// Should return activity coordinates offset by task coordinates minus all insets
// except top one (keep status bar decor visible).
verifySetWindowCrop(new Rect(25, 0, 165, 110));
}
@Test
public void testOnTaskInfoAppeared_parentShiftedLikeInOneHandedMode() {
mLetterboxConfigController.setPortraitGravity(Gravity.TOP);
setWindowBoundsAndInsets(
/* windowBounds= */ new Rect(0, 0, 100, 150),
Insets.of(/* left= */ 0, /* top= */ 10, /* right= */ 0, /* bottom= */ 0));
mLetterboxTaskListener.onTaskAppeared(
createTaskInfo(
/* taskId */ 1,
/* parentBounds */ new Rect(0, 75, 100, 225),
/* activityBounds */ new Rect(25, 75, 75, 125),
/* taskBounds */ new Rect(0, 75, 100, 125)),
mLeash);
verifySetPosition(0, 0);
verifySetWindowCrop(new Rect(25, 0, 75, 50));
}
@Test(expected = IllegalStateException.class)
public void testOnTaskAppeared_calledSecondTimeWithSameTaskId_throwsException() {
setWindowBoundsAndInsets(new Rect(), Insets.NONE);
RunningTaskInfo taskInfo =
createTaskInfo(/* taskId */ 1, new Rect(), new Rect(), new Rect());
mLetterboxTaskListener.onTaskAppeared(taskInfo, mLeash);
mLetterboxTaskListener.onTaskAppeared(taskInfo, mLeash);
}
private void setWindowBoundsAndInsets(Rect windowBounds, Insets insets) {
when(mWindowMetrics.getBounds()).thenReturn(windowBounds);
when(mWindowInsets.getInsets(anyInt())).thenReturn(insets);
}
private void verifySetPosition(int x, int y) {
verify(mTransaction).setPosition(eq(mLeash), eq((float) x), eq((float) y));
}
private void verifySetWindowCrop(final Rect crop) {
// Should return activty coordinates offset by task coordinates
verify(mTransaction).setWindowCrop(eq(mLeash), eq(crop));
}
private static RunningTaskInfo createTaskInfo(
int taskId,
final Rect parentBounds,
final Rect activityBounds,
final Rect taskBounds) {
RunningTaskInfo taskInfo = new RunningTaskInfo();
taskInfo.taskId = taskId;
taskInfo.parentBounds = parentBounds;
taskInfo.configuration.windowConfiguration.setBounds(taskBounds);
taskInfo.letterboxActivityBounds = Rect.copyOrNull(activityBounds);
return taskInfo;
}
}