Add divider view support for app-pairs
Add divider bar for app-pairs. AppPairLayout and divide policy records and handles the layout in pair. Bug: 172704238 Bug: 172704672 Test: manul check the behavior of the splits and divider bar. Test: AppPairTests, AppPairsPoolTests, AppPairsControllerTests Change-Id: I688c7001d056fe8dd9a192885e1f9fd5f004dc11
This commit is contained in:
@@ -207,12 +207,19 @@ public class WindowlessWindowManager implements IWindowSession {
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
@Nullable
|
||||
protected SurfaceControl getSurfaceControl(View rootView) {
|
||||
final ViewRootImpl root = rootView.getViewRootImpl();
|
||||
if (root == null) {
|
||||
return null;
|
||||
}
|
||||
final State s = mStateForWindow.get(root.mWindow.asBinder());
|
||||
return getSurfaceControl(root.mWindow);
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
@Nullable
|
||||
protected SurfaceControl getSurfaceControl(IWindow window) {
|
||||
final State s = mStateForWindow.get(window.asBinder());
|
||||
if (s == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
27
libs/WindowManager/Shell/res/layout/split_divider.xml
Normal file
27
libs/WindowManager/Shell/res/layout/split_divider.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<com.android.wm.shell.apppairs.DividerView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent">
|
||||
|
||||
<View
|
||||
style="@style/DockedDividerBackground"
|
||||
android:id="@+id/docked_divider_background"
|
||||
android:background="@color/docked_divider_background"/>
|
||||
|
||||
</com.android.wm.shell.apppairs.DividerView>
|
||||
@@ -32,6 +32,7 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.internal.protolog.common.ProtoLog;
|
||||
import com.android.wm.shell.ShellTaskOrganizer;
|
||||
import com.android.wm.shell.common.DisplayController;
|
||||
import com.android.wm.shell.common.SyncTransactionQueue;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
@@ -41,8 +42,6 @@ import java.io.PrintWriter;
|
||||
* {@link #mTaskInfo1} and {@link #mTaskInfo2} in the pair.
|
||||
* Also includes all UI for managing the pair like the divider.
|
||||
*/
|
||||
// TODO: Add divider
|
||||
// TODO: Handle display rotation
|
||||
class AppPair implements ShellTaskOrganizer.TaskListener {
|
||||
private static final String TAG = AppPair.class.getSimpleName();
|
||||
|
||||
@@ -55,10 +54,13 @@ class AppPair implements ShellTaskOrganizer.TaskListener {
|
||||
|
||||
private final AppPairsController mController;
|
||||
private final SyncTransactionQueue mSyncQueue;
|
||||
private final DisplayController mDisplayController;
|
||||
private AppPairLayout mAppPairLayout;
|
||||
|
||||
AppPair(AppPairsController controller) {
|
||||
mController = controller;
|
||||
mSyncQueue = controller.getSyncTransactionQueue();
|
||||
mDisplayController = controller.getDisplayController();
|
||||
}
|
||||
|
||||
int getRootTaskId() {
|
||||
@@ -90,13 +92,12 @@ class AppPair implements ShellTaskOrganizer.TaskListener {
|
||||
|
||||
mTaskInfo1 = task1;
|
||||
mTaskInfo2 = task2;
|
||||
mAppPairLayout = new AppPairLayout(
|
||||
mDisplayController.getDisplayContext(mRootTaskInfo.displayId),
|
||||
mDisplayController.getDisplay(mRootTaskInfo.displayId),
|
||||
mRootTaskInfo.configuration,
|
||||
mRootTaskLeash);
|
||||
|
||||
// TODO: properly calculate bounds for pairs.
|
||||
final Rect rootBounds = mRootTaskInfo.configuration.windowConfiguration.getBounds();
|
||||
final Rect bounds1 = new Rect(
|
||||
rootBounds.left, rootBounds.top, rootBounds.right / 2, rootBounds.bottom / 2);
|
||||
final Rect bounds2 = new Rect(
|
||||
bounds1.right, bounds1.bottom, rootBounds.right, rootBounds.bottom);
|
||||
final WindowContainerToken token1 = task1.token;
|
||||
final WindowContainerToken token2 = task2.token;
|
||||
final WindowContainerTransaction wct = new WindowContainerTransaction();
|
||||
@@ -106,8 +107,8 @@ class AppPair implements ShellTaskOrganizer.TaskListener {
|
||||
.reparent(token2, mRootTaskInfo.token, true /* onTop */)
|
||||
.setWindowingMode(token1, WINDOWING_MODE_MULTI_WINDOW)
|
||||
.setWindowingMode(token2, WINDOWING_MODE_MULTI_WINDOW)
|
||||
.setBounds(token1, bounds1)
|
||||
.setBounds(token2, bounds2)
|
||||
.setBounds(token1, mAppPairLayout.getBounds1())
|
||||
.setBounds(token2, mAppPairLayout.getBounds2())
|
||||
// Moving the root task to top after the child tasks were repareted , or the root
|
||||
// task cannot be visible and focused.
|
||||
.reorder(mRootTaskInfo.token, true);
|
||||
@@ -131,6 +132,15 @@ class AppPair implements ShellTaskOrganizer.TaskListener {
|
||||
|
||||
mTaskInfo1 = null;
|
||||
mTaskInfo2 = null;
|
||||
mAppPairLayout.release();
|
||||
mAppPairLayout = null;
|
||||
}
|
||||
|
||||
void setVisible(boolean visible) {
|
||||
if (mAppPairLayout == null) {
|
||||
return;
|
||||
}
|
||||
mAppPairLayout.setDividerVisibility(visible);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -150,13 +160,20 @@ class AppPair implements ShellTaskOrganizer.TaskListener {
|
||||
|
||||
if (mTaskLeash1 == null || mTaskLeash2 == null) return;
|
||||
|
||||
setVisible(true);
|
||||
final SurfaceControl dividerLeash = mAppPairLayout.getDividerLeash();
|
||||
final Rect dividerBounds = mAppPairLayout.getDividerBounds();
|
||||
|
||||
// TODO: Is there more we need to do here?
|
||||
mSyncQueue.runInSync(t -> t
|
||||
.setPosition(mTaskLeash1, mTaskInfo1.positionInParent.x,
|
||||
mTaskInfo1.positionInParent.y)
|
||||
.setPosition(mTaskLeash2, mTaskInfo2.positionInParent.x,
|
||||
mTaskInfo2.positionInParent.y)
|
||||
.setLayer(dividerLeash, Integer.MAX_VALUE)
|
||||
.setPosition(dividerLeash, dividerBounds.left, dividerBounds.top)
|
||||
.show(mRootTaskLeash)
|
||||
.show(dividerLeash)
|
||||
.show(mTaskLeash1)
|
||||
.show(mTaskLeash2));
|
||||
}
|
||||
@@ -165,6 +182,24 @@ class AppPair implements ShellTaskOrganizer.TaskListener {
|
||||
public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
|
||||
if (taskInfo.taskId == getRootTaskId()) {
|
||||
mRootTaskInfo = taskInfo;
|
||||
|
||||
if (mAppPairLayout != null
|
||||
&& mAppPairLayout.updateConfiguration(mRootTaskInfo.configuration)) {
|
||||
// Update bounds when there is root bounds or orientation changed.
|
||||
final WindowContainerTransaction wct = new WindowContainerTransaction();
|
||||
final SurfaceControl dividerLeash = mAppPairLayout.getDividerLeash();
|
||||
final Rect dividerBounds = mAppPairLayout.getDividerBounds();
|
||||
final Rect bounds1 = mAppPairLayout.getBounds1();
|
||||
final Rect bounds2 = mAppPairLayout.getBounds2();
|
||||
|
||||
wct.setBounds(mTaskInfo1.token, bounds1)
|
||||
.setBounds(mTaskInfo2.token, bounds2);
|
||||
mController.getTaskOrganizer().applyTransaction(wct);
|
||||
mSyncQueue.runInSync(t -> t
|
||||
.setPosition(mTaskLeash1, bounds1.left, bounds1.top)
|
||||
.setPosition(mTaskLeash2, bounds2.left, bounds2.top)
|
||||
.setPosition(dividerLeash, dividerBounds.left, dividerBounds.top));
|
||||
}
|
||||
} else if (taskInfo.taskId == getTaskId1()) {
|
||||
mTaskInfo1 = taskInfo;
|
||||
} else if (taskInfo.taskId == getTaskId2()) {
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* 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.apppairs;
|
||||
|
||||
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
|
||||
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
|
||||
import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
|
||||
import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY;
|
||||
import static android.view.WindowManager.LayoutParams.FLAG_SPLIT_TOUCH;
|
||||
import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
|
||||
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
|
||||
import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Region;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.view.Display;
|
||||
import android.view.IWindow;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.SurfaceControl;
|
||||
import android.view.SurfaceControlViewHost;
|
||||
import android.view.WindowManager;
|
||||
import android.view.WindowlessWindowManager;
|
||||
|
||||
import com.android.wm.shell.R;
|
||||
|
||||
/**
|
||||
* Records and handles layout of a pair of apps.
|
||||
*/
|
||||
// TODO(172704238): add tests
|
||||
final class AppPairLayout {
|
||||
private static final String DIVIDER_WINDOW_TITLE = "AppPairDivider";
|
||||
private final Context mContext;
|
||||
private final AppPairWindowManager mAppPairWindowManager;
|
||||
private final SurfaceControlViewHost mViewHost;
|
||||
|
||||
private final int mDividerWindowWidth;
|
||||
private final int mDividerWindowInsets;
|
||||
|
||||
private boolean mIsLandscape;
|
||||
private Rect mRootBounds;
|
||||
private DIVIDE_POLICY mDividePolicy;
|
||||
|
||||
private DividerView mDividerView;
|
||||
private SurfaceControl mDividerLeash;
|
||||
|
||||
AppPairLayout(
|
||||
Context context,
|
||||
Display display,
|
||||
Configuration configuration,
|
||||
SurfaceControl rootLeash) {
|
||||
mContext = context.createConfigurationContext(configuration);
|
||||
mIsLandscape = isLandscape(configuration);
|
||||
mRootBounds = configuration.windowConfiguration.getBounds();
|
||||
mDividerWindowWidth = mContext.getResources().getDimensionPixelSize(
|
||||
com.android.internal.R.dimen.docked_stack_divider_thickness);
|
||||
mDividerWindowInsets = mContext.getResources().getDimensionPixelSize(
|
||||
com.android.internal.R.dimen.docked_stack_divider_insets);
|
||||
|
||||
mAppPairWindowManager = new AppPairWindowManager(configuration, rootLeash);
|
||||
mViewHost = new SurfaceControlViewHost(mContext, display, mAppPairWindowManager);
|
||||
mDividePolicy = DIVIDE_POLICY.MIDDLE;
|
||||
mDividePolicy.update(mIsLandscape, mRootBounds, mDividerWindowWidth, mDividerWindowInsets);
|
||||
}
|
||||
|
||||
boolean updateConfiguration(Configuration configuration) {
|
||||
mAppPairWindowManager.setConfiguration(configuration);
|
||||
final Rect rootBounds = configuration.windowConfiguration.getBounds();
|
||||
final boolean isLandscape = isLandscape(configuration);
|
||||
if (mIsLandscape == isLandscape && isIdenticalBounds(mRootBounds, rootBounds)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mIsLandscape = isLandscape;
|
||||
mRootBounds = rootBounds;
|
||||
mDividePolicy.update(mIsLandscape, mRootBounds, mDividerWindowWidth, mDividerWindowInsets);
|
||||
mViewHost.relayout(
|
||||
mDividePolicy.mDividerBounds.width(),
|
||||
mDividePolicy.mDividerBounds.height());
|
||||
// TODO(172704238): handle divider bar rotation.
|
||||
return true;
|
||||
}
|
||||
|
||||
Rect getBounds1() {
|
||||
return mDividePolicy.mBounds1;
|
||||
}
|
||||
|
||||
Rect getBounds2() {
|
||||
return mDividePolicy.mBounds2;
|
||||
}
|
||||
|
||||
Rect getDividerBounds() {
|
||||
return mDividePolicy.mDividerBounds;
|
||||
}
|
||||
|
||||
SurfaceControl getDividerLeash() {
|
||||
return mDividerLeash;
|
||||
}
|
||||
|
||||
void release() {
|
||||
if (mViewHost == null) return;
|
||||
mViewHost.release();
|
||||
}
|
||||
|
||||
void setDividerVisibility(boolean visible) {
|
||||
if (mDividerView == null) {
|
||||
initDivider();
|
||||
}
|
||||
if (visible) {
|
||||
mDividerView.show();
|
||||
} else {
|
||||
mDividerView.hide();
|
||||
}
|
||||
}
|
||||
|
||||
private void initDivider() {
|
||||
final DividerView dividerView = (DividerView) LayoutInflater.from(mContext)
|
||||
.inflate(R.layout.split_divider, null);
|
||||
|
||||
WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
|
||||
mDividePolicy.mDividerBounds.width(),
|
||||
mDividePolicy.mDividerBounds.height(),
|
||||
TYPE_DOCK_DIVIDER,
|
||||
FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_WATCH_OUTSIDE_TOUCH
|
||||
| FLAG_SPLIT_TOUCH | FLAG_SLIPPERY,
|
||||
PixelFormat.TRANSLUCENT);
|
||||
lp.token = new Binder();
|
||||
lp.setTitle(DIVIDER_WINDOW_TITLE);
|
||||
lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION;
|
||||
|
||||
mViewHost.setView(dividerView, lp);
|
||||
mDividerView = dividerView;
|
||||
mDividerLeash = mAppPairWindowManager.getSurfaceControl(mViewHost.getWindowToken());
|
||||
}
|
||||
|
||||
private static boolean isLandscape(Configuration configuration) {
|
||||
return configuration.orientation == ORIENTATION_LANDSCAPE;
|
||||
}
|
||||
|
||||
private static boolean isIdenticalBounds(Rect bounds1, Rect bounds2) {
|
||||
return bounds1.left == bounds2.left && bounds1.top == bounds2.top
|
||||
&& bounds1.right == bounds2.right && bounds1.bottom == bounds2.bottom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates the policy of placing divider bar and corresponding split-screens.
|
||||
*/
|
||||
// TODO(172704238): add more divide policy and provide snap to resize feature for divider bar.
|
||||
enum DIVIDE_POLICY {
|
||||
MIDDLE;
|
||||
|
||||
void update(boolean isLandscape, Rect rootBounds, int dividerWindowWidth,
|
||||
int dividerWindowInsets) {
|
||||
final int dividerOffset = dividerWindowWidth / 2;
|
||||
final int boundsOffset = dividerOffset - dividerWindowInsets;
|
||||
|
||||
mDividerBounds = new Rect(rootBounds);
|
||||
mBounds1 = new Rect(rootBounds);
|
||||
mBounds2 = new Rect(rootBounds);
|
||||
|
||||
switch (this) {
|
||||
case MIDDLE:
|
||||
default:
|
||||
if (isLandscape) {
|
||||
mDividerBounds.left = rootBounds.width() / 2 - dividerOffset;
|
||||
mDividerBounds.right = rootBounds.width() / 2 + dividerOffset;
|
||||
mBounds1.left = rootBounds.width() / 2 + boundsOffset;
|
||||
mBounds2.right = rootBounds.width() / 2 - boundsOffset;
|
||||
} else {
|
||||
mDividerBounds.top = rootBounds.height() / 2 - dividerOffset;
|
||||
mDividerBounds.bottom = rootBounds.height() / 2 + dividerOffset;
|
||||
mBounds1.bottom = rootBounds.height() / 2 - boundsOffset;
|
||||
mBounds2.top = rootBounds.height() / 2 + boundsOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rect mDividerBounds;
|
||||
Rect mBounds1;
|
||||
Rect mBounds2;
|
||||
}
|
||||
|
||||
/**
|
||||
* WindowManger for app pair. Holds view hierarchy for the root task.
|
||||
*/
|
||||
private static final class AppPairWindowManager extends WindowlessWindowManager {
|
||||
AppPairWindowManager(Configuration config, SurfaceControl rootSurface) {
|
||||
super(config, rootSurface, null /* hostInputToken */);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTouchRegion(IBinder window, Region region) {
|
||||
super.setTouchRegion(window, region);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SurfaceControl getSurfaceControl(IWindow window) {
|
||||
return super.getSurfaceControl(window);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setConfiguration(Configuration configuration) {
|
||||
super.setConfiguration(configuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,4 +36,6 @@ public interface AppPairs {
|
||||
void dump(@NonNull PrintWriter pw, String prefix);
|
||||
/** Called when the shell organizer has been registered. */
|
||||
void onOrganizerRegistered();
|
||||
/** Called when the visibility of the keyguard changes. */
|
||||
void onKeyguardVisibilityChanged(boolean showing);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package com.android.wm.shell.apppairs;
|
||||
|
||||
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
|
||||
|
||||
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
@@ -26,14 +28,17 @@ import androidx.annotation.NonNull;
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.protolog.common.ProtoLog;
|
||||
import com.android.wm.shell.ShellTaskOrganizer;
|
||||
import com.android.wm.shell.common.DisplayController;
|
||||
import com.android.wm.shell.common.SyncTransactionQueue;
|
||||
import com.android.wm.shell.common.TaskStackListenerCallback;
|
||||
import com.android.wm.shell.common.TaskStackListenerImpl;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
|
||||
/**
|
||||
* Class manages app-pairs multitasking mode and implements the main interface {@link AppPairs}.
|
||||
*/
|
||||
public class AppPairsController implements AppPairs {
|
||||
public class AppPairsController implements AppPairs, TaskStackListenerCallback {
|
||||
private static final String TAG = AppPairsController.class.getSimpleName();
|
||||
|
||||
private final ShellTaskOrganizer mTaskOrganizer;
|
||||
@@ -42,10 +47,15 @@ public class AppPairsController implements AppPairs {
|
||||
private AppPairsPool mPairsPool;
|
||||
// Active app-pairs mapped by root task id key.
|
||||
private final SparseArray<AppPair> mActiveAppPairs = new SparseArray<>();
|
||||
private final DisplayController mDisplayController;
|
||||
private int mForegroundTaskId = INVALID_TASK_ID;
|
||||
|
||||
public AppPairsController(ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue) {
|
||||
public AppPairsController(ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue,
|
||||
DisplayController displayController, TaskStackListenerImpl taskStackListener) {
|
||||
mTaskOrganizer = organizer;
|
||||
mSyncQueue = syncQueue;
|
||||
mDisplayController = displayController;
|
||||
taskStackListener.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -60,6 +70,27 @@ public class AppPairsController implements AppPairs {
|
||||
mPairsPool = pool;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskMovedToFront(int taskId) {
|
||||
mForegroundTaskId = INVALID_TASK_ID;
|
||||
for (int i = mActiveAppPairs.size() - 1; i >= 0; --i) {
|
||||
final AppPair candidate = mActiveAppPairs.valueAt(i);
|
||||
final boolean containForegroundTask = candidate.contains(taskId);
|
||||
candidate.setVisible(containForegroundTask);
|
||||
if (containForegroundTask) {
|
||||
mForegroundTaskId = candidate.getRootTaskId();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKeyguardVisibilityChanged(boolean showing) {
|
||||
if (mForegroundTaskId == INVALID_TASK_ID) {
|
||||
return;
|
||||
}
|
||||
mActiveAppPairs.get(mForegroundTaskId).setVisible(!showing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean pair(int taskId1, int taskId2) {
|
||||
final ActivityManager.RunningTaskInfo task1 = mTaskOrganizer.getRunningTaskInfo(taskId1);
|
||||
@@ -127,6 +158,10 @@ public class AppPairsController implements AppPairs {
|
||||
return mSyncQueue;
|
||||
}
|
||||
|
||||
DisplayController getDisplayController() {
|
||||
return mDisplayController;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dump(@NonNull PrintWriter pw, String prefix) {
|
||||
final String innerPrefix = prefix + " ";
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.apppairs;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Stack divider for app pair.
|
||||
*/
|
||||
public class DividerView extends FrameLayout {
|
||||
public DividerView(@NonNull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public DividerView(@NonNull Context context,
|
||||
@Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public DividerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public DividerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
|
||||
int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
void show() {
|
||||
post(() -> setVisibility(View.VISIBLE));
|
||||
}
|
||||
|
||||
void hide() {
|
||||
post(() -> setVisibility(View.INVISIBLE));
|
||||
}
|
||||
}
|
||||
@@ -16,15 +16,25 @@
|
||||
|
||||
package com.android.wm.shell.apppairs;
|
||||
|
||||
import static android.view.Display.DEFAULT_DISPLAY;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.hardware.display.DisplayManager;
|
||||
|
||||
import androidx.test.annotation.UiThreadTest;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.wm.shell.ShellTaskOrganizer;
|
||||
import com.android.wm.shell.ShellTestCase;
|
||||
import com.android.wm.shell.common.DisplayController;
|
||||
import com.android.wm.shell.common.SyncTransactionQueue;
|
||||
import com.android.wm.shell.common.TaskStackListenerImpl;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
@@ -36,22 +46,32 @@ import org.mockito.MockitoAnnotations;
|
||||
/** Tests for {@link AppPair} */
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class AppPairTests {
|
||||
public class AppPairTests extends ShellTestCase {
|
||||
|
||||
private AppPairsController mController;
|
||||
@Mock private SyncTransactionQueue mSyncQueue;
|
||||
@Mock private ShellTaskOrganizer mTaskOrganizer;
|
||||
@Mock private DisplayController mDisplayController;
|
||||
@Mock private TaskStackListenerImpl mTaskStackListener;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mController = new TestAppPairsController(mTaskOrganizer, mSyncQueue);
|
||||
mController = new TestAppPairsController(
|
||||
mTaskOrganizer,
|
||||
mSyncQueue,
|
||||
mDisplayController,
|
||||
mTaskStackListener);
|
||||
when(mDisplayController.getDisplayContext(anyInt())).thenReturn(mContext);
|
||||
when(mDisplayController.getDisplay(anyInt())).thenReturn(
|
||||
mContext.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY));
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void testContains() {
|
||||
final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build();
|
||||
final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build();
|
||||
@@ -66,6 +86,7 @@ public class AppPairTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void testVanishUnpairs() {
|
||||
final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build();
|
||||
final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build();
|
||||
|
||||
@@ -16,15 +16,25 @@
|
||||
|
||||
package com.android.wm.shell.apppairs;
|
||||
|
||||
import static android.view.Display.DEFAULT_DISPLAY;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.hardware.display.DisplayManager;
|
||||
|
||||
import androidx.test.annotation.UiThreadTest;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.wm.shell.ShellTaskOrganizer;
|
||||
import com.android.wm.shell.ShellTestCase;
|
||||
import com.android.wm.shell.common.DisplayController;
|
||||
import com.android.wm.shell.common.SyncTransactionQueue;
|
||||
import com.android.wm.shell.common.TaskStackListenerImpl;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
@@ -36,23 +46,33 @@ import org.mockito.MockitoAnnotations;
|
||||
/** Tests for {@link AppPairsController} */
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class AppPairsControllerTests {
|
||||
public class AppPairsControllerTests extends ShellTestCase {
|
||||
private TestAppPairsController mController;
|
||||
private TestAppPairsPool mPool;
|
||||
@Mock private SyncTransactionQueue mSyncQueue;
|
||||
@Mock private ShellTaskOrganizer mTaskOrganizer;
|
||||
@Mock private DisplayController mDisplayController;
|
||||
@Mock private TaskStackListenerImpl mTaskStackListener;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mController = new TestAppPairsController(mTaskOrganizer, mSyncQueue);
|
||||
mController = new TestAppPairsController(
|
||||
mTaskOrganizer,
|
||||
mSyncQueue,
|
||||
mDisplayController,
|
||||
mTaskStackListener);
|
||||
mPool = mController.getPool();
|
||||
when(mDisplayController.getDisplayContext(anyInt())).thenReturn(mContext);
|
||||
when(mDisplayController.getDisplay(anyInt())).thenReturn(
|
||||
mContext.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY));
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void testPairUnpair() {
|
||||
final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build();
|
||||
final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build();
|
||||
@@ -69,6 +89,7 @@ public class AppPairsControllerTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
public void testUnpair_DontReleaseToPool() {
|
||||
final ActivityManager.RunningTaskInfo task1 = new TestRunningTaskInfoBuilder().build();
|
||||
final ActivityManager.RunningTaskInfo task2 = new TestRunningTaskInfoBuilder().build();
|
||||
|
||||
@@ -22,7 +22,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.wm.shell.ShellTaskOrganizer;
|
||||
import com.android.wm.shell.common.DisplayController;
|
||||
import com.android.wm.shell.common.SyncTransactionQueue;
|
||||
import com.android.wm.shell.common.TaskStackListenerImpl;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
@@ -39,11 +41,17 @@ public class AppPairsPoolTests {
|
||||
private TestAppPairsPool mPool;
|
||||
@Mock private SyncTransactionQueue mSyncQueue;
|
||||
@Mock private ShellTaskOrganizer mTaskOrganizer;
|
||||
@Mock private DisplayController mDisplayController;
|
||||
@Mock private TaskStackListenerImpl mTaskStackListener;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mController = new TestAppPairsController(mTaskOrganizer, mSyncQueue);
|
||||
mController = new TestAppPairsController(
|
||||
mTaskOrganizer,
|
||||
mSyncQueue,
|
||||
mDisplayController,
|
||||
mTaskStackListener);
|
||||
mPool = mController.getPool();
|
||||
}
|
||||
|
||||
|
||||
@@ -17,13 +17,16 @@
|
||||
package com.android.wm.shell.apppairs;
|
||||
|
||||
import com.android.wm.shell.ShellTaskOrganizer;
|
||||
import com.android.wm.shell.common.DisplayController;
|
||||
import com.android.wm.shell.common.SyncTransactionQueue;
|
||||
import com.android.wm.shell.common.TaskStackListenerImpl;
|
||||
|
||||
public class TestAppPairsController extends AppPairsController {
|
||||
TestAppPairsPool mPool;
|
||||
|
||||
public TestAppPairsController(ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue) {
|
||||
super(organizer, syncQueue);
|
||||
public TestAppPairsController(ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue,
|
||||
DisplayController displayController, TaskStackListenerImpl taskStackListener) {
|
||||
super(organizer, syncQueue, displayController, taskStackListener);
|
||||
mPool = new TestAppPairsPool(this);
|
||||
setPairsPool(mPool);
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ public final class WMShell extends SystemUI
|
||||
private KeyguardUpdateMonitorCallback mSplitScreenKeyguardCallback;
|
||||
private KeyguardUpdateMonitorCallback mPipKeyguardCallback;
|
||||
private KeyguardUpdateMonitorCallback mOneHandedKeyguardCallback;
|
||||
private KeyguardUpdateMonitorCallback mAppPairsKeyguardCallback;
|
||||
|
||||
@Inject
|
||||
public WMShell(Context context, CommandQueue commandQueue,
|
||||
@@ -144,6 +145,7 @@ public final class WMShell extends SystemUI
|
||||
mSplitScreenOptional.ifPresent(this::initSplitScreen);
|
||||
mOneHandedOptional.ifPresent(this::initOneHanded);
|
||||
mHideDisplayCutoutOptional.ifPresent(this::initHideDisplayCutout);
|
||||
mAppPairsOptional.ifPresent(this::initAppPairs);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@@ -292,6 +294,16 @@ public final class WMShell extends SystemUI
|
||||
});
|
||||
}
|
||||
|
||||
void initAppPairs(AppPairs appPairs) {
|
||||
mAppPairsKeyguardCallback = new KeyguardUpdateMonitorCallback() {
|
||||
@Override
|
||||
public void onKeyguardVisibilityChanged(boolean showing) {
|
||||
appPairs.onKeyguardVisibilityChanged(showing);
|
||||
}
|
||||
};
|
||||
mKeyguardUpdateMonitor.registerCallback(mAppPairsKeyguardCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToProto(SystemUiTraceProto proto) {
|
||||
if (proto.wmShell == null) {
|
||||
|
||||
@@ -84,8 +84,10 @@ public class WMShellModule {
|
||||
@WMSingleton
|
||||
@Provides
|
||||
static AppPairs provideAppPairs(ShellTaskOrganizer shellTaskOrganizer,
|
||||
SyncTransactionQueue syncQueue) {
|
||||
return new AppPairsController(shellTaskOrganizer, syncQueue);
|
||||
SyncTransactionQueue syncQueue, DisplayController displayController,
|
||||
TaskStackListenerImpl taskStackListener) {
|
||||
return new AppPairsController(shellTaskOrganizer, syncQueue, displayController,
|
||||
taskStackListener);
|
||||
}
|
||||
|
||||
@WMSingleton
|
||||
|
||||
@@ -657,6 +657,13 @@ final class TaskDisplayArea extends DisplayArea<Task> {
|
||||
}
|
||||
}
|
||||
return SCREEN_ORIENTATION_UNSPECIFIED;
|
||||
} else {
|
||||
// Apps and their containers are not allowed to specify an orientation of full screen
|
||||
// tasks created by organizer. The organizer handles the orientation instead.
|
||||
final Task task = getTopStackInWindowingMode(WINDOWING_MODE_FULLSCREEN);
|
||||
if (task != null && task.isVisible() && task.mCreatedByOrganizer) {
|
||||
return SCREEN_ORIENTATION_UNSPECIFIED;
|
||||
}
|
||||
}
|
||||
|
||||
final int orientation = super.getOrientation(candidate);
|
||||
|
||||
Reference in New Issue
Block a user