4/ Add basic drag and drop policy & handler for the shell

- Add basic policy for handling drags over home/fullscreen tasks
  /split tasks. For now, default to only allowing split to non-docked
  root task
- Refactor drag layout to have same lifecycle as the window instead of
  being per-drag session
- Resolve activity info for tasks as well as activity drag items
  (shortcuts to be resolved in a follow up CL)
- Fix crash due to sending drag events to all displays

Bug: 169894807
Test: atest DragAndDropPolicyTest

Change-Id: I7d777c59296e9ca9e1d169c8a021f9263af94d65
This commit is contained in:
Winson Chung
2020-10-29 16:23:16 -07:00
parent dbf6f13897
commit 93fd63d021
13 changed files with 916 additions and 226 deletions

View File

@@ -0,0 +1,358 @@
/*
* 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.draganddrop;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
import static android.content.ClipDescription.MIMETYPE_APPLICATION_ACTIVITY;
import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT;
import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK;
import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_FULLSCREEN;
import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_BOTTOM;
import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import android.app.ActivityManager;
import android.app.IActivityTaskManager;
import android.app.PendingIntent;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Insets;
import android.graphics.Rect;
import android.os.RemoteException;
import android.view.DisplayInfo;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.draganddrop.DragAndDropPolicy.Target;
import com.android.wm.shell.splitscreen.DividerView;
import com.android.wm.shell.splitscreen.SplitScreen;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
/**
* Tests for the drag and drop policy.
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
public class DragAndDropPolicyTest {
@Mock
private Context mContext;
@Mock
private IActivityTaskManager mIActivityTaskManager;
@Mock
private SplitScreen mSplitScreen;
@Mock
private DragAndDropPolicy.Starter mStarter;
private DisplayLayout mDisplayLayout;
private Insets mInsets;
private DragAndDropPolicy mPolicy;
private ClipData mActivityClipData;
private ClipData mNonResizeableActivityClipData;
private ClipData mTaskClipData;
private ClipData mShortcutClipData;
private ActivityManager.RunningTaskInfo mHomeTask;
private ActivityManager.RunningTaskInfo mFullscreenAppTask;
private ActivityManager.RunningTaskInfo mNonResizeableFullscreenAppTask;
private ActivityManager.RunningTaskInfo mSplitPrimaryAppTask;
@Before
public void setUp() throws RemoteException {
MockitoAnnotations.initMocks(this);
Resources res = mock(Resources.class);
Configuration config = new Configuration();
doReturn(config).when(res).getConfiguration();
DisplayInfo info = new DisplayInfo();
info.logicalWidth = 100;
info.logicalHeight = 100;
mDisplayLayout = new DisplayLayout(info, res, false, false);
mInsets = Insets.of(0, 0, 0, 0);
DividerView divider = mock(DividerView.class);
doReturn(divider).when(mSplitScreen).getDividerView();
doReturn(new Rect(50, 0, 100, 100)).when(divider)
.getNonMinimizedSplitScreenSecondaryBounds();
mPolicy = new DragAndDropPolicy(mContext, mIActivityTaskManager, mSplitScreen, mStarter);
mActivityClipData = createClipData(MIMETYPE_APPLICATION_ACTIVITY);
mNonResizeableActivityClipData = createClipData(MIMETYPE_APPLICATION_ACTIVITY);
setClipDataResizeable(mNonResizeableActivityClipData, false);
mTaskClipData = createClipData(MIMETYPE_APPLICATION_TASK);
mShortcutClipData = createClipData(MIMETYPE_APPLICATION_SHORTCUT);
mHomeTask = createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_HOME);
mFullscreenAppTask = createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
mNonResizeableFullscreenAppTask =
createTaskInfo(WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
mNonResizeableFullscreenAppTask.isResizeable = false;
mSplitPrimaryAppTask = createTaskInfo(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY,
ACTIVITY_TYPE_STANDARD);
setIsPhone(false);
setInSplitScreen(false);
setRunningTask(mFullscreenAppTask);
}
/**
* Creates a clip data that is by default resizeable.
*/
private ClipData createClipData(String mimeType) {
ClipDescription clipDescription = new ClipDescription(mimeType, new String[] { mimeType });
Intent i = new Intent();
switch (mimeType) {
case MIMETYPE_APPLICATION_SHORTCUT:
i.putExtra(Intent.EXTRA_PACKAGE_NAME, "package");
i.putExtra(Intent.EXTRA_SHORTCUT_ID, "shortcut_id");
break;
case MIMETYPE_APPLICATION_TASK:
i.putExtra(Intent.EXTRA_TASK_ID, 12345);
break;
case MIMETYPE_APPLICATION_ACTIVITY:
i.putExtra(ClipDescription.EXTRA_PENDING_INTENT, mock(PendingIntent.class));
break;
}
i.putExtra(Intent.EXTRA_USER, android.os.Process.myUserHandle());
ClipData.Item item = new ClipData.Item(i);
item.setActivityInfo(new ActivityInfo());
ClipData data = new ClipData(clipDescription, item);
setClipDataResizeable(data, true);
return data;
}
private ActivityManager.RunningTaskInfo createTaskInfo(int winMode, int actType) {
ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo();
info.configuration.windowConfiguration.setActivityType(actType);
info.configuration.windowConfiguration.setWindowingMode(winMode);
info.isResizeable = true;
return info;
}
private void setRunningTask(ActivityManager.RunningTaskInfo task) throws RemoteException {
doReturn(Collections.singletonList(task)).when(mIActivityTaskManager)
.getFilteredTasks(anyInt(), anyBoolean());
}
private void setClipDataResizeable(ClipData data, boolean resizeable) {
data.getItemAt(0).getActivityInfo().resizeMode = resizeable
? ActivityInfo.RESIZE_MODE_RESIZEABLE
: ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
}
private void setIsPhone(boolean isPhone) {
Resources res = mock(Resources.class);
Configuration config = mock(Configuration.class);
config.smallestScreenWidthDp = isPhone ? 400 : 800;
doReturn(config).when(res).getConfiguration();
doReturn(res).when(mContext).getResources();
}
private void setInSplitScreen(boolean inSplitscreen) {
doReturn(inSplitscreen).when(mSplitScreen).isDividerVisible();
}
@Test
public void testDragAppOverFullscreenHome_expectOnlyFullscreenTarget() throws RemoteException {
setRunningTask(mHomeTask);
mPolicy.start(mDisplayLayout, mActivityClipData);
ArrayList<Target> targets = assertExactTargetTypes(
mPolicy.getTargets(mInsets), TYPE_FULLSCREEN);
mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData);
verify(mStarter).startIntent(any(), any());
}
@Test
public void testDragAppOverFullscreenApp_expectSplitScreenAndFullscreenTargets()
throws RemoteException {
setRunningTask(mFullscreenAppTask);
mPolicy.start(mDisplayLayout, mActivityClipData);
// TODO(b/169894807): For now, only allow splitting to the right/bottom until we have split
// pairs
ArrayList<Target> targets = assertExactTargetTypes(
mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_RIGHT);
mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData);
verify(mStarter).startIntent(any(), any());
reset(mStarter);
mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_RIGHT), mActivityClipData);
verify(mStarter).enterSplitScreen(anyInt(), eq(false));
verify(mStarter).startIntent(any(), any());
}
@Test
public void testDragAppOverFullscreenAppPhone_expectVerticalSplitScreenAndFullscreenTargets()
throws RemoteException {
setIsPhone(true);
setRunningTask(mFullscreenAppTask);
mPolicy.start(mDisplayLayout, mActivityClipData);
// TODO(b/169894807): For now, only allow splitting to the right/bottom until we have split
// pairs
ArrayList<Target> targets = assertExactTargetTypes(
mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_BOTTOM);
mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData);
verify(mStarter).startIntent(any(), any());
reset(mStarter);
mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_BOTTOM), mActivityClipData);
verify(mStarter).enterSplitScreen(anyInt(), eq(false));
verify(mStarter).startIntent(any(), any());
}
@Test
public void testDragAppOverFullscreenNonResizeableApp_expectOnlyFullscreenTargets()
throws RemoteException {
setRunningTask(mNonResizeableFullscreenAppTask);
mPolicy.start(mDisplayLayout, mActivityClipData);
ArrayList<Target> targets = assertExactTargetTypes(
mPolicy.getTargets(mInsets), TYPE_FULLSCREEN);
mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData);
verify(mStarter).startIntent(any(), any());
}
@Test
public void testDragNonResizeableAppOverFullscreenApp_expectOnlyFullscreenTargets()
throws RemoteException {
setRunningTask(mFullscreenAppTask);
mPolicy.start(mDisplayLayout, mNonResizeableActivityClipData);
ArrayList<Target> targets = assertExactTargetTypes(
mPolicy.getTargets(mInsets), TYPE_FULLSCREEN);
mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData);
verify(mStarter).startIntent(any(), any());
}
@Test
public void testDragAppOverSplitApp_expectFullscreenAndSplitTargets() throws RemoteException {
setInSplitScreen(true);
setRunningTask(mSplitPrimaryAppTask);
mPolicy.start(mDisplayLayout, mActivityClipData);
// TODO(b/169894807): For now, only allow splitting to the right/bottom until we have split
// pairs
ArrayList<Target> targets = assertExactTargetTypes(
mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_RIGHT);
mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData);
verify(mStarter).startIntent(any(), any());
reset(mStarter);
// TODO(b/169894807): Just verify starting for the non-docked task until we have app pairs
mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_RIGHT), mActivityClipData);
verify(mStarter).startIntent(any(), any());
}
@Test
public void testDragAppOverSplitAppPhone_expectFullscreenAndVerticalSplitTargets()
throws RemoteException {
setIsPhone(true);
setInSplitScreen(true);
setRunningTask(mSplitPrimaryAppTask);
mPolicy.start(mDisplayLayout, mActivityClipData);
// TODO(b/169894807): For now, only allow splitting to the right/bottom until we have split
// pairs
ArrayList<Target> targets = assertExactTargetTypes(
mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_BOTTOM);
mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData);
verify(mStarter).startIntent(any(), any());
reset(mStarter);
// TODO(b/169894807): Just verify starting for the non-docked task until we have app pairs
mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_BOTTOM), mActivityClipData);
verify(mStarter).startIntent(any(), any());
}
@Test
public void testTargetHitRects() throws RemoteException {
setRunningTask(mFullscreenAppTask);
mPolicy.start(mDisplayLayout, mActivityClipData);
ArrayList<Target> targets = mPolicy.getTargets(mInsets);
for (Target t : targets) {
assertTrue(mPolicy.getTargetAtLocation(t.hitRegion.left, t.hitRegion.top) == t);
assertTrue(mPolicy.getTargetAtLocation(t.hitRegion.right - 1, t.hitRegion.top) == t);
assertTrue(mPolicy.getTargetAtLocation(t.hitRegion.right - 1, t.hitRegion.bottom - 1)
== t);
assertTrue(mPolicy.getTargetAtLocation(t.hitRegion.left, t.hitRegion.bottom - 1)
== t);
}
}
private Target filterTargetByType(ArrayList<Target> targets, int type) {
for (Target t : targets) {
if (type == t.type) {
return t;
}
}
fail("Target with type: " + type + " not found");
return null;
}
private ArrayList<Target> assertExactTargetTypes(ArrayList<Target> targets,
int... expectedTargetTypes) {
HashSet<Integer> expected = new HashSet<>();
for (int t : expectedTargetTypes) {
expected.add(t);
}
for (Target t : targets) {
if (!expected.contains(t.type)) {
fail("Found unexpected target type: " + t.type);
}
expected.remove(t.type);
}
assertTrue(expected.isEmpty());
return targets;
}
}