blob: 32cca47b991c0eb8973e8689f681e2ef480de6bb [file] [log] [blame]
/*
* Copyright (C) 2017 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.server.wm;
import static android.Manifest.permission.START_TASKS_FROM_RECENTS;
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 android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.view.DragEvent.ACTION_DRAG_STARTED;
import static android.view.DragEvent.ACTION_DROP;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP;
import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import android.app.PendingIntent;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.Intent;
import android.content.pm.ShortcutServiceInternal;
import android.graphics.PixelFormat;
import android.os.Binder;
import android.os.IBinder;
import android.os.Looper;
import android.os.Parcelable;
import android.os.UserHandle;
import android.platform.test.annotations.Presubmit;
import android.view.DragEvent;
import android.view.IWindowSessionCallback;
import android.view.InputChannel;
import android.view.SurfaceControl;
import android.view.SurfaceSession;
import android.view.View;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager;
import androidx.test.filters.SmallTest;
import com.android.server.LocalServices;
import com.android.server.pm.UserManagerInternal;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* Tests for the {@link DragDropController} class.
*
* Build/Install/Run:
* atest WmTests:DragDropControllerTests
*/
@SmallTest
@Presubmit
@RunWith(WindowTestRunner.class)
public class DragDropControllerTests extends WindowTestsBase {
private static final int TIMEOUT_MS = 3000;
private static final int TEST_UID = 12345;
private static final int TEST_PROFILE_UID = 12345 * UserHandle.PER_USER_RANGE;
private static final int TEST_PID = 67890;
private static final String TEST_PACKAGE = "com.test.package";
private TestDragDropController mTarget;
private WindowState mWindow;
private IBinder mToken;
static class TestDragDropController extends DragDropController {
private Runnable mCloseCallback;
boolean mDeferDragStateClosed;
boolean mIsAccessibilityDrag;
TestDragDropController(WindowManagerService service, Looper looper) {
super(service, looper);
}
void setOnClosedCallbackLocked(Runnable runnable) {
if (mIsAccessibilityDrag) {
// Accessibility does not use animation
assertTrue(!dragDropActiveLocked());
} else {
assertTrue(dragDropActiveLocked());
mCloseCallback = runnable;
}
}
@Override
void onDragStateClosedLocked(DragState dragState) {
if (mDeferDragStateClosed) {
return;
}
super.onDragStateClosedLocked(dragState);
if (mCloseCallback != null) {
mCloseCallback.run();
mCloseCallback = null;
}
}
}
/**
* Creates a window state which can be used as a drop target.
*/
private WindowState createDropTargetWindow(String name, int ownerId) {
final ActivityRecord activity = createNonAttachedActivityRecord(mDisplayContent);
final Task rootTask = createTask(mDisplayContent);
final Task task = createTaskInRootTask(rootTask, ownerId);
task.addChild(activity, 0);
// Use a new TestIWindow so we don't collect events for other windows
final WindowState window = createWindow(
null, TYPE_BASE_APPLICATION, activity, name, ownerId, false, new TestIWindow());
window.mInputChannel = new InputChannel();
window.mHasSurface = true;
mWm.mInputToWindowMap.put(window.mInputChannelToken, window);
return window;
}
@BeforeClass
public static void setUpOnce() {
final UserManagerInternal userManager = mock(UserManagerInternal.class);
LocalServices.addService(UserManagerInternal.class, userManager);
}
@AfterClass
public static void tearDownOnce() {
LocalServices.removeServiceForTest(UserManagerInternal.class);
}
@Before
public void setUp() throws Exception {
mTarget = new TestDragDropController(mWm, mWm.mH.getLooper());
mWindow = createDropTargetWindow("Drag test window", 0);
doReturn(mWindow).when(mDisplayContent).getTouchableWinAtPointLocked(0, 0);
when(mWm.mInputManager.transferTouchFocus(any(InputChannel.class),
any(InputChannel.class), any(boolean.class))).thenReturn(true);
mWm.mWindowMap.put(mWindow.mClient.asBinder(), mWindow);
}
@After
public void tearDown() throws Exception {
final CountDownLatch latch;
if (!mTarget.dragDropActiveLocked()) {
return;
}
if (mToken != null) {
mTarget.cancelDragAndDrop(mToken, false);
}
latch = new CountDownLatch(1);
mTarget.setOnClosedCallbackLocked(latch::countDown);
if (mTarget.mIsAccessibilityDrag) {
mTarget.mIsAccessibilityDrag = false;
return;
}
assertTrue(awaitInWmLock(() -> latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)));
}
@Test
public void testDragFlow() {
doDragAndDrop(0, ClipData.newPlainText("label", "Test"), 0, 0);
}
@Test
public void testA11yDragFlow() {
mTarget.mIsAccessibilityDrag = true;
doA11yDragAndDrop(0, ClipData.newPlainText("label", "Test"), 0, 0);
}
@Test
public void testPerformDrag_NullDataWithGrantUri() {
doDragAndDrop(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, null, 0, 0);
}
@Test
public void testPerformDrag_NullDataToOtherUser() {
final WindowState otherUsersWindow =
createDropTargetWindow("Other user's window", 1 * UserHandle.PER_USER_RANGE);
doReturn(otherUsersWindow).when(mDisplayContent).getTouchableWinAtPointLocked(10, 10);
doDragAndDrop(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, null, 10, 10);
mToken = otherUsersWindow.mClient.asBinder();
}
@Test
public void testPrivateInterceptGlobalDragDropFlagChecksPermission() {
spyOn(mWm.mContext);
DisplayPolicy policy = mDisplayContent.getDisplayPolicy();
WindowManager.LayoutParams attrs = new WindowManager.LayoutParams();
attrs.privateFlags |= PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP;
policy.validateAddingWindowLw(attrs, Binder.getCallingPid(), Binder.getCallingUid());
verify(mWm.mAtmService).enforceTaskPermission(any());
}
@Test
public void testPrivateInterceptGlobalDragDropFlagBehaviour() {
mWindow.mAttrs.privateFlags |= PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP;
mWindow.setViewVisibility(View.GONE);
// Necessary for now since DragState.sendDragStartedLocked() will recycle drag events
// immediately after dispatching, which is a problem when using mockito arguments captor
// because it returns and modifies the same drag event
TestIWindow iwindow = (TestIWindow) mWindow.mClient;
final ArrayList<DragEvent> dragEvents = new ArrayList<>();
iwindow.setDragEventJournal(dragEvents);
startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ,
ClipData.newPlainText("label", "text"), () -> {
// Verify the start-drag event is sent for invisible windows
final DragEvent dragEvent = dragEvents.get(0);
assertTrue(dragEvent.getAction() == ACTION_DRAG_STARTED);
// Verify after consuming that the drag surface is relinquished
try {
mTarget.mDeferDragStateClosed = true;
mTarget.reportDropWindow(mWindow.mInputChannelToken, 0, 0);
// Verify the drop event includes the drag surface
mTarget.handleMotionEvent(false, 0, 0);
final DragEvent dropEvent = dragEvents.get(dragEvents.size() - 1);
assertTrue(dropEvent.getDragSurface() != null);
mTarget.reportDropResult(iwindow, true);
} finally {
mTarget.mDeferDragStateClosed = false;
}
assertTrue(mTarget.dragSurfaceRelinquished());
});
}
@Test
public void testPrivateInterceptGlobalDragDropIgnoresNonLocalWindows() {
WindowState nonLocalWindow = createDropTargetWindow("App drag test window", 0);
WindowState globalInterceptWindow = createDropTargetWindow("Global drag test window", 0);
globalInterceptWindow.mAttrs.privateFlags |= PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP;
// Necessary for now since DragState.sendDragStartedLocked() will recycle drag events
// immediately after dispatching, which is a problem when using mockito arguments captor
// because it returns and modifies the same drag event
TestIWindow localIWindow = (TestIWindow) mWindow.mClient;
final ArrayList<DragEvent> localWindowDragEvents = new ArrayList<>();
localIWindow.setDragEventJournal(localWindowDragEvents);
TestIWindow nonLocalIWindow = (TestIWindow) nonLocalWindow.mClient;
final ArrayList<DragEvent> nonLocalWindowDragEvents = new ArrayList<>();
nonLocalIWindow.setDragEventJournal(nonLocalWindowDragEvents);
TestIWindow globalInterceptIWindow = (TestIWindow) globalInterceptWindow.mClient;
final ArrayList<DragEvent> globalInterceptWindowDragEvents = new ArrayList<>();
globalInterceptIWindow.setDragEventJournal(globalInterceptWindowDragEvents);
startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ,
createClipDataForActivity(null, mock(UserHandle.class)), () -> {
// Verify the start-drag event is sent for the local and global intercept window
// but not the other window
assertTrue(nonLocalWindowDragEvents.isEmpty());
assertTrue(localWindowDragEvents.get(0).getAction()
== ACTION_DRAG_STARTED);
assertTrue(globalInterceptWindowDragEvents.get(0).getAction()
== ACTION_DRAG_STARTED);
// Verify that only the global intercept window receives the clip data with the
// resolved activity info for the drag
assertNull(localWindowDragEvents.get(0).getClipData());
assertTrue(globalInterceptWindowDragEvents.get(0).getClipData()
.willParcelWithActivityInfo());
mTarget.reportDropWindow(globalInterceptWindow.mInputChannelToken, 0, 0);
mTarget.handleMotionEvent(false, 0, 0);
mToken = globalInterceptWindow.mClient.asBinder();
// Verify the drop event is only sent for the global intercept window
assertTrue(nonLocalWindowDragEvents.isEmpty());
assertTrue(last(localWindowDragEvents).getAction() != ACTION_DROP);
assertTrue(last(globalInterceptWindowDragEvents).getAction() == ACTION_DROP);
// Verify that item extras were not sent with the drop event
assertNull(last(localWindowDragEvents).getClipData());
assertFalse(last(globalInterceptWindowDragEvents).getClipData()
.willParcelWithActivityInfo());
});
}
private DragEvent last(ArrayList<DragEvent> list) {
return list.get(list.size() - 1);
}
@Test
public void testValidateAppActivityArguments() {
final Session session = new Session(mWm, new IWindowSessionCallback.Stub() {
@Override
public void onAnimatorScaleChanged(float scale) {}
});
try {
session.validateAndResolveDragMimeTypeExtras(
createClipDataForActivity(mock(PendingIntent.class), null), TEST_UID, TEST_PID,
TEST_PACKAGE);
fail("Expected failure without user");
} catch (IllegalArgumentException e) {
// Expected failure
}
try {
session.validateAndResolveDragMimeTypeExtras(
createClipDataForActivity(null, mock(UserHandle.class)), TEST_UID, TEST_PID,
TEST_PACKAGE);
fail("Expected failure without pending intent");
} catch (IllegalArgumentException e) {
// Expected failure
}
}
private ClipData createClipDataForActivity(PendingIntent pi, UserHandle user) {
final Intent data = new Intent();
if (pi != null) {
data.putExtra(ClipDescription.EXTRA_PENDING_INTENT, (Parcelable) pi);
}
if (user != null) {
data.putExtra(Intent.EXTRA_USER, user);
}
final ClipData clipData = new ClipData(
new ClipDescription("drag", new String[] {
MIMETYPE_APPLICATION_ACTIVITY}),
new ClipData.Item(data));
return clipData;
}
@Test
public void testValidateAppShortcutArguments() {
doReturn(PERMISSION_GRANTED).when(mWm.mContext)
.checkCallingOrSelfPermission(eq(START_TASKS_FROM_RECENTS));
final Session session = new Session(mWm, new IWindowSessionCallback.Stub() {
@Override
public void onAnimatorScaleChanged(float scale) {}
});
try {
session.validateAndResolveDragMimeTypeExtras(
createClipDataForShortcut(null, "test_shortcut_id", mock(UserHandle.class)),
TEST_UID, TEST_PID, TEST_PACKAGE);
fail("Expected failure without package name");
} catch (IllegalArgumentException e) {
// Expected failure
}
try {
session.validateAndResolveDragMimeTypeExtras(
createClipDataForShortcut("test_package", null, mock(UserHandle.class)),
TEST_UID, TEST_PID, TEST_PACKAGE);
fail("Expected failure without shortcut id");
} catch (IllegalArgumentException e) {
// Expected failure
}
try {
session.validateAndResolveDragMimeTypeExtras(
createClipDataForShortcut("test_package", "test_shortcut_id", null),
TEST_UID, TEST_PID, TEST_PACKAGE);
fail("Expected failure without package name");
} catch (IllegalArgumentException e) {
// Expected failure
}
}
@Test
public void testValidateProfileAppShortcutArguments_notCallingUid() {
doReturn(PERMISSION_GRANTED).when(mWm.mContext)
.checkCallingOrSelfPermission(eq(START_TASKS_FROM_RECENTS));
final Session session = Mockito.spy(new Session(mWm, new IWindowSessionCallback.Stub() {
@Override
public void onAnimatorScaleChanged(float scale) {}
}));
final ShortcutServiceInternal shortcutService = mock(ShortcutServiceInternal.class);
final Intent[] shortcutIntents = new Intent[1];
shortcutIntents[0] = new Intent();
doReturn(shortcutIntents).when(shortcutService).createShortcutIntents(anyInt(), any(),
any(), any(), anyInt(), anyInt(), anyInt());
LocalServices.removeServiceForTest(ShortcutServiceInternal.class);
LocalServices.addService(ShortcutServiceInternal.class, shortcutService);
ArgumentCaptor<Integer> callingUser = ArgumentCaptor.forClass(Integer.class);
session.validateAndResolveDragMimeTypeExtras(
createClipDataForShortcut("test_package", "test_shortcut_id",
mock(UserHandle.class)),
TEST_PROFILE_UID, TEST_PID, TEST_PACKAGE);
verify(shortcutService).createShortcutIntents(callingUser.capture(), any(),
any(), any(), anyInt(), anyInt(), anyInt());
assertTrue(callingUser.getValue() == UserHandle.getUserId(TEST_PROFILE_UID));
}
private ClipData createClipDataForShortcut(String packageName, String shortcutId,
UserHandle user) {
final Intent data = new Intent();
if (packageName != null) {
data.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName);
}
if (shortcutId != null) {
data.putExtra(Intent.EXTRA_SHORTCUT_ID, shortcutId);
}
if (user != null) {
data.putExtra(Intent.EXTRA_USER, user);
}
final ClipData clipData = new ClipData(
new ClipDescription("drag", new String[] {
MIMETYPE_APPLICATION_SHORTCUT}),
new ClipData.Item(data));
return clipData;
}
@Test
public void testValidateAppTaskArguments() {
doReturn(PERMISSION_GRANTED).when(mWm.mContext)
.checkCallingOrSelfPermission(eq(START_TASKS_FROM_RECENTS));
final Session session = new Session(mWm, new IWindowSessionCallback.Stub() {
@Override
public void onAnimatorScaleChanged(float scale) {}
});
try {
final ClipData clipData = new ClipData(
new ClipDescription("drag", new String[] { MIMETYPE_APPLICATION_TASK }),
new ClipData.Item(new Intent()));
session.validateAndResolveDragMimeTypeExtras(clipData, TEST_UID, TEST_PID,
TEST_PACKAGE);
fail("Expected failure without task id");
} catch (IllegalArgumentException e) {
// Expected failure
}
}
private void doDragAndDrop(int flags, ClipData data, float dropX, float dropY) {
startDrag(flags, data, () -> {
mTarget.reportDropWindow(mWindow.mInputChannelToken, dropX, dropY);
mTarget.handleMotionEvent(false, dropX, dropY);
mToken = mWindow.mClient.asBinder();
});
}
private void startDrag(int flag, ClipData data, Runnable r) {
final SurfaceSession appSession = new SurfaceSession();
try {
final SurfaceControl surface = new SurfaceControl.Builder(appSession)
.setName("drag surface")
.setBufferSize(100, 100)
.setFormat(PixelFormat.TRANSLUCENT)
.build();
assertTrue(mWm.mInputManager.transferTouchFocus(new InputChannel(),
new InputChannel(), true /* isDragDrop */));
mToken = mTarget.performDrag(0, 0, mWindow.mClient, flag, surface, 0, 0, 0, 0, 0, data);
assertNotNull(mToken);
r.run();
} finally {
appSession.kill();
}
}
private void doA11yDragAndDrop(int flags, ClipData data, float dropX, float dropY) {
spyOn(mTarget);
AccessibilityManager accessibilityManager = Mockito.mock(AccessibilityManager.class);
when(accessibilityManager.isEnabled()).thenReturn(true);
doReturn(accessibilityManager).when(mTarget).getAccessibilityManager();
startA11yDrag(flags, data, () -> {
boolean dropped = mTarget.dropForAccessibility(mWindow.mClient, dropX, dropY);
mToken = mWindow.mClient.asBinder();
});
}
private void startA11yDrag(int flags, ClipData data, Runnable r) {
mToken = mTarget.performDrag(0, 0, mWindow.mClient,
flags | View.DRAG_FLAG_ACCESSIBILITY_ACTION, null, 0, 0, 0, 0, 0, data);
assertNotNull(mToken);
r.run();
}
}