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:
Jerry Chang
2020-11-12 19:21:09 +08:00
parent 2f5313a71d
commit 0545edfa3d
14 changed files with 484 additions and 24 deletions

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -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 + " ";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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