| /* |
| * Copyright (C) 2016 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 android.server.cts; |
| |
| import com.android.tradefed.device.CollectingOutputReceiver; |
| import com.android.tradefed.device.DeviceNotAvailableException; |
| import com.android.tradefed.device.ITestDevice; |
| import com.android.tradefed.log.LogUtil.CLog; |
| import com.android.tradefed.testtype.DeviceTestCase; |
| |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.regex.Pattern; |
| |
| public class CrossAppDragAndDropTests extends DeviceTestCase { |
| // Constants copied from ActivityManager.StackId. If they are changed there, these must be |
| // updated. |
| /** ID of stack where fullscreen activities are normally launched into. */ |
| private static final int FULLSCREEN_WORKSPACE_STACK_ID = 1; |
| |
| /** ID of stack where freeform/resized activities are normally launched into. */ |
| private static final int FREEFORM_WORKSPACE_STACK_ID = FULLSCREEN_WORKSPACE_STACK_ID + 1; |
| |
| /** ID of stack that occupies a dedicated region of the screen. */ |
| private static final int DOCKED_STACK_ID = FREEFORM_WORKSPACE_STACK_ID + 1; |
| |
| /** ID of stack that always on top (always visible) when it exists. */ |
| private static final int PINNED_STACK_ID = DOCKED_STACK_ID + 1; |
| |
| private static final String AM_FORCE_STOP = "am force-stop "; |
| private static final String AM_MOVE_TASK = "am stack move-task "; |
| private static final String AM_RESIZE_TASK = "am task resize "; |
| private static final String AM_REMOVE_STACK = "am stack remove "; |
| private static final String AM_START_N = "am start -n "; |
| private static final String AM_STACK_LIST = "am stack list"; |
| private static final String INPUT_MOUSE_SWIPE = "input mouse swipe "; |
| private static final String TASK_ID_PREFIX = "taskId"; |
| |
| // Regex pattern to match adb shell am stack list output of the form: |
| // taskId=<TASK_ID>: <componentName> bounds=[LEFT,TOP][RIGHT,BOTTOM] |
| private static final String TASK_REGEX_PATTERN_STRING = |
| "taskId=[0-9]+: %s bounds=\\[[0-9]+,[0-9]+\\]\\[[0-9]+,[0-9]+\\]"; |
| |
| private static final int SWIPE_DURATION_MS = 500; |
| |
| private static final String SOURCE_PACKAGE_NAME = "android.wm.cts.dndsourceapp"; |
| private static final String TARGET_PACKAGE_NAME = "android.wm.cts.dndtargetapp"; |
| private static final String TARGET_23_PACKAGE_NAME = "android.wm.cts.dndtargetappsdk23"; |
| |
| |
| private static final String SOURCE_ACTIVITY_NAME = "DragSource"; |
| private static final String TARGET_ACTIVITY_NAME = "DropTarget"; |
| |
| private static final String FILE_GLOBAL = "file_global"; |
| private static final String FILE_LOCAL = "file_local"; |
| private static final String DISALLOW_GLOBAL = "disallow_global"; |
| private static final String CANCEL_SOON = "cancel_soon"; |
| private static final String GRANT_NONE = "grant_none"; |
| private static final String GRANT_READ = "grant_read"; |
| private static final String GRANT_WRITE = "grant_write"; |
| private static final String GRANT_READ_PREFIX = "grant_read_prefix"; |
| private static final String GRANT_READ_NOPREFIX = "grant_read_noprefix"; |
| private static final String GRANT_READ_PERSISTABLE = "grant_read_persistable"; |
| |
| private static final String REQUEST_NONE = "request_none"; |
| private static final String REQUEST_READ = "request_read"; |
| private static final String REQUEST_READ_NESTED = "request_read_nested"; |
| private static final String REQUEST_TAKE_PERSISTABLE = "request_take_persistable"; |
| private static final String REQUEST_WRITE = "request_write"; |
| |
| private static final String SOURCE_LOG_TAG = "DragSource"; |
| private static final String TARGET_LOG_TAG = "DropTarget"; |
| |
| private static final String RESULT_KEY_START_DRAG = "START_DRAG"; |
| private static final String RESULT_KEY_DRAG_STARTED = "DRAG_STARTED"; |
| private static final String RESULT_KEY_DRAG_ENDED = "DRAG_ENDED"; |
| private static final String RESULT_KEY_EXTRAS = "EXTRAS"; |
| private static final String RESULT_KEY_DROP_RESULT = "DROP"; |
| private static final String RESULT_KEY_ACCESS_BEFORE = "BEFORE"; |
| private static final String RESULT_KEY_ACCESS_AFTER = "AFTER"; |
| private static final String RESULT_KEY_CLIP_DATA_ERROR = "CLIP_DATA_ERROR"; |
| private static final String RESULT_KEY_CLIP_DESCR_ERROR = "CLIP_DESCR_ERROR"; |
| private static final String RESULT_KEY_LOCAL_STATE_ERROR = "LOCAL_STATE_ERROR"; |
| |
| private static final String RESULT_MISSING = "Missing"; |
| private static final String RESULT_OK = "OK"; |
| private static final String RESULT_EXCEPTION = "Exception"; |
| private static final String RESULT_NULL_DROP_PERMISSIONS = "Null DragAndDropPermissions"; |
| |
| private static final String AM_SUPPORTS_SPLIT_SCREEN_MULTIWINDOW = |
| "am supports-split-screen-multiwindow"; |
| |
| private ITestDevice mDevice; |
| |
| private Map<String, String> mSourceResults; |
| private Map<String, String> mTargetResults; |
| |
| private String mSourcePackageName; |
| private String mTargetPackageName; |
| |
| @Override |
| protected void setUp() throws Exception { |
| super.setUp(); |
| |
| mDevice = getDevice(); |
| |
| if (!supportsDragAndDrop()) { |
| return; |
| } |
| |
| mSourcePackageName = SOURCE_PACKAGE_NAME; |
| mTargetPackageName = TARGET_PACKAGE_NAME; |
| cleanupState(); |
| } |
| |
| @Override |
| protected void tearDown() throws Exception { |
| super.tearDown(); |
| |
| if (!supportsDragAndDrop()) { |
| return; |
| } |
| |
| mDevice.executeShellCommand(AM_FORCE_STOP + mSourcePackageName); |
| mDevice.executeShellCommand(AM_FORCE_STOP + mTargetPackageName); |
| } |
| |
| private String executeShellCommand(String command) throws DeviceNotAvailableException { |
| return mDevice.executeShellCommand(command); |
| } |
| |
| private void clearLogs() throws DeviceNotAvailableException { |
| executeShellCommand("logcat -c"); |
| } |
| |
| private String getStartCommand(String componentName, String modeExtra) { |
| return AM_START_N + componentName + " -e mode " + modeExtra; |
| } |
| |
| private String getMoveTaskCommand(int taskId, int stackId) throws Exception { |
| return AM_MOVE_TASK + taskId + " " + stackId + " true"; |
| } |
| |
| private String getResizeTaskCommand(int taskId, Point topLeft, Point bottomRight) |
| throws Exception { |
| return AM_RESIZE_TASK + taskId + " " + topLeft.x + " " + topLeft.y + " " + bottomRight.x |
| + " " + bottomRight.y; |
| } |
| |
| private String getComponentName(String packageName, String activityName) { |
| return packageName + "/" + packageName + "." + activityName; |
| } |
| |
| /** |
| * Make sure that the special activity stacks are removed and the ActivityManager/WindowManager |
| * is in a good state. |
| */ |
| private void cleanupState() throws Exception { |
| executeShellCommand(AM_FORCE_STOP + SOURCE_PACKAGE_NAME); |
| executeShellCommand(AM_FORCE_STOP + TARGET_PACKAGE_NAME); |
| executeShellCommand(AM_FORCE_STOP + TARGET_23_PACKAGE_NAME); |
| unlockDevice(); |
| |
| // Reinitialize the docked stack to force the window manager to reset its default bounds. |
| // See b/29068935. |
| clearLogs(); |
| final String componentName = getComponentName(mSourcePackageName, SOURCE_ACTIVITY_NAME); |
| executeShellCommand(getStartCommand(componentName, null) + " --stack " + |
| FULLSCREEN_WORKSPACE_STACK_ID); |
| final int taskId = getActivityTaskId(componentName); |
| // Moving a task from the full screen stack to the docked stack resets |
| // WindowManagerService#mDockedStackCreateBounds. |
| executeShellCommand(getMoveTaskCommand(taskId, DOCKED_STACK_ID)); |
| waitForResume(mSourcePackageName, SOURCE_ACTIVITY_NAME); |
| executeShellCommand(AM_FORCE_STOP + SOURCE_PACKAGE_NAME); |
| |
| // Remove special stacks. |
| executeShellCommand(AM_REMOVE_STACK + PINNED_STACK_ID); |
| executeShellCommand(AM_REMOVE_STACK + DOCKED_STACK_ID); |
| executeShellCommand(AM_REMOVE_STACK + FREEFORM_WORKSPACE_STACK_ID); |
| } |
| |
| private void launchDockedActivity(String packageName, String activityName, String mode) |
| throws Exception { |
| clearLogs(); |
| final String componentName = getComponentName(packageName, activityName); |
| executeShellCommand(getStartCommand(componentName, mode) + " --stack " + DOCKED_STACK_ID); |
| waitForResume(packageName, activityName); |
| } |
| |
| private void launchFullscreenActivity(String packageName, String activityName, String mode) |
| throws Exception { |
| clearLogs(); |
| final String componentName = getComponentName(packageName, activityName); |
| executeShellCommand(getStartCommand(componentName, mode) + " --stack " |
| + FULLSCREEN_WORKSPACE_STACK_ID); |
| waitForResume(packageName, activityName); |
| } |
| |
| /** |
| * @param displaySize size of the display |
| * @param leftSide {@code true} to launch the app taking up the left half of the display, |
| * {@code false} to launch the app taking up the right half of the display. |
| */ |
| private void launchFreeformActivity(String packageName, String activityName, String mode, |
| Point displaySize, boolean leftSide) throws Exception{ |
| clearLogs(); |
| final String componentName = getComponentName(packageName, activityName); |
| executeShellCommand(getStartCommand(componentName, mode) + " --stack " |
| + FREEFORM_WORKSPACE_STACK_ID); |
| waitForResume(packageName, activityName); |
| Point topLeft = new Point(leftSide ? 0 : displaySize.x / 2, 0); |
| Point bottomRight = new Point(leftSide ? displaySize.x / 2 : displaySize.x, displaySize.y); |
| executeShellCommand(getResizeTaskCommand(getActivityTaskId(componentName), topLeft, |
| bottomRight)); |
| } |
| |
| private void waitForResume(String packageName, String activityName) throws Exception { |
| final String fullActivityName = packageName + "." + activityName; |
| int retryCount = 3; |
| do { |
| Thread.sleep(500); |
| String logs = executeShellCommand("logcat -d -b events"); |
| for (String line : logs.split("\\n")) { |
| if(line.contains("am_on_resume_called") && line.contains(fullActivityName)) { |
| return; |
| } |
| } |
| } while (retryCount-- > 0); |
| |
| throw new Exception(fullActivityName + " has failed to start"); |
| } |
| |
| private void injectInput(Point from, Point to, int durationMs) throws Exception { |
| executeShellCommand( |
| INPUT_MOUSE_SWIPE + from.x + " " + from.y + " " + to.x + " " + to.y + " " + |
| durationMs); |
| } |
| |
| static class Point { |
| public int x, y; |
| |
| public Point(int _x, int _y) { |
| x=_x; |
| y=_y; |
| } |
| |
| public Point() {} |
| } |
| |
| private String findTaskInfo(String name) throws Exception { |
| CollectingOutputReceiver outputReceiver = new CollectingOutputReceiver(); |
| mDevice.executeShellCommand(AM_STACK_LIST, outputReceiver); |
| final String output = outputReceiver.getOutput(); |
| final StringBuilder builder = new StringBuilder(); |
| builder.append("Finding task info for task: "); |
| builder.append(name); |
| builder.append("\nParsing adb shell am output: " ); |
| builder.append(output); |
| CLog.i(builder.toString()); |
| final Pattern pattern = Pattern.compile(String.format(TASK_REGEX_PATTERN_STRING, name)); |
| for (String line : output.split("\\n")) { |
| final String truncatedLine; |
| // Only look for the activity name before the "topActivity" string. |
| final int pos = line.indexOf("topActivity"); |
| if (pos > 0) { |
| truncatedLine = line.substring(0, pos); |
| } else { |
| truncatedLine = line; |
| } |
| if (pattern.matcher(truncatedLine).find()) { |
| return truncatedLine; |
| } |
| } |
| return ""; |
| } |
| |
| private boolean getWindowBounds(String name, Point from, Point to) throws Exception { |
| final String taskInfo = findTaskInfo(name); |
| final String[] sections = taskInfo.split("\\["); |
| if (sections.length > 2) { |
| try { |
| parsePoint(sections[1], from); |
| parsePoint(sections[2], to); |
| return true; |
| } catch (Exception e) { |
| return false; |
| } |
| } |
| return false; |
| } |
| |
| private int getActivityTaskId(String name) throws Exception { |
| final String taskInfo = findTaskInfo(name); |
| for (String word : taskInfo.split("\\s+")) { |
| if (word.startsWith(TASK_ID_PREFIX)) { |
| final String withColon = word.split("=")[1]; |
| return Integer.parseInt(withColon.substring(0, withColon.length() - 1)); |
| } |
| } |
| return -1; |
| } |
| |
| private Point getDisplaySize() throws Exception { |
| final String output = executeShellCommand("wm size"); |
| final String[] sizes = output.split(" ")[2].split("x"); |
| return new Point(Integer.valueOf(sizes[0].trim()), Integer.valueOf(sizes[1].trim())); |
| } |
| |
| private Point getWindowCenter(String name) throws Exception { |
| Point p1 = new Point(); |
| Point p2 = new Point(); |
| if (getWindowBounds(name, p1, p2)) { |
| return new Point((p1.x + p2.x) / 2, (p1.y + p2.y) / 2); |
| } |
| return null; |
| } |
| |
| private void parsePoint(String string, Point point) { |
| final String[] parts = string.split("[,|\\]]"); |
| point.x = Integer.parseInt(parts[0]); |
| point.y = Integer.parseInt(parts[1]); |
| } |
| |
| private void unlockDevice() throws DeviceNotAvailableException { |
| // Wake up the device, if necessary. |
| executeShellCommand("input keyevent 224"); |
| // Unlock the screen. |
| executeShellCommand("input keyevent 82"); |
| } |
| |
| private Map<String, String> getLogResults(String className, String lastResultKey) |
| throws Exception { |
| int retryCount = 10; |
| Map<String, String> output = new HashMap<String, String>(); |
| do { |
| |
| String logs = executeShellCommand("logcat -v brief -d " + className + ":I" + " *:S"); |
| for (String line : logs.split("\\n")) { |
| if (line.startsWith("I/" + className)) { |
| String payload = line.split(":")[1].trim(); |
| final String[] split = payload.split("="); |
| if (split.length > 1) { |
| output.put(split[0], split[1]); |
| } |
| } |
| } |
| if (output.containsKey(lastResultKey)) { |
| return output; |
| } |
| } while (retryCount-- > 0); |
| return output; |
| } |
| |
| private void assertDropResult(String sourceMode, String targetMode, String expectedDropResult) |
| throws Exception { |
| assertDragAndDropResults(sourceMode, targetMode, RESULT_OK, expectedDropResult, RESULT_OK); |
| } |
| |
| private void assertNoGlobalDragEvents(String sourceMode, String expectedStartDragResult) |
| throws Exception { |
| assertDragAndDropResults( |
| sourceMode, REQUEST_NONE, expectedStartDragResult, RESULT_MISSING, RESULT_MISSING); |
| } |
| |
| private void assertDragAndDropResults(String sourceMode, String targetMode, |
| String expectedStartDragResult, String expectedDropResult, |
| String expectedListenerResults) throws Exception { |
| if (!supportsDragAndDrop()) { |
| return; |
| } |
| |
| if (supportsSplitScreenMultiWindow()) { |
| launchDockedActivity(mSourcePackageName, SOURCE_ACTIVITY_NAME, sourceMode); |
| launchFullscreenActivity(mTargetPackageName, TARGET_ACTIVITY_NAME, targetMode); |
| } else if (supportsFreeformMultiWindow()) { |
| // Fallback to try to launch two freeform windows side by side. |
| Point displaySize = getDisplaySize(); |
| launchFreeformActivity(mSourcePackageName, SOURCE_ACTIVITY_NAME, sourceMode, |
| displaySize, true /* leftSide */); |
| launchFreeformActivity(mTargetPackageName, TARGET_ACTIVITY_NAME, targetMode, |
| displaySize, false /* leftSide */); |
| } else { |
| return; |
| } |
| |
| clearLogs(); |
| |
| injectInput( |
| getWindowCenter(getComponentName(mSourcePackageName, SOURCE_ACTIVITY_NAME)), |
| getWindowCenter(getComponentName(mTargetPackageName, TARGET_ACTIVITY_NAME)), |
| SWIPE_DURATION_MS); |
| |
| mSourceResults = getLogResults(SOURCE_LOG_TAG, RESULT_KEY_START_DRAG); |
| assertSourceResult(RESULT_KEY_START_DRAG, expectedStartDragResult); |
| |
| mTargetResults = getLogResults(TARGET_LOG_TAG, RESULT_KEY_DRAG_ENDED); |
| assertTargetResult(RESULT_KEY_DROP_RESULT, expectedDropResult); |
| if (!RESULT_MISSING.equals(expectedDropResult)) { |
| assertTargetResult(RESULT_KEY_ACCESS_BEFORE, RESULT_EXCEPTION); |
| assertTargetResult(RESULT_KEY_ACCESS_AFTER, RESULT_EXCEPTION); |
| } |
| assertListenerResults(expectedListenerResults); |
| } |
| |
| private void assertListenerResults(String expectedResult) throws Exception { |
| assertTargetResult(RESULT_KEY_DRAG_STARTED, expectedResult); |
| assertTargetResult(RESULT_KEY_DRAG_ENDED, expectedResult); |
| assertTargetResult(RESULT_KEY_EXTRAS, expectedResult); |
| |
| assertTargetResult(RESULT_KEY_CLIP_DATA_ERROR, RESULT_MISSING); |
| assertTargetResult(RESULT_KEY_CLIP_DESCR_ERROR, RESULT_MISSING); |
| assertTargetResult(RESULT_KEY_LOCAL_STATE_ERROR, RESULT_MISSING); |
| } |
| |
| private void assertSourceResult(String resultKey, String expectedResult) throws Exception { |
| assertResult(mSourceResults, resultKey, expectedResult); |
| } |
| |
| private void assertTargetResult(String resultKey, String expectedResult) throws Exception { |
| assertResult(mTargetResults, resultKey, expectedResult); |
| } |
| |
| private void assertResult(Map<String, String> results, String resultKey, String expectedResult) |
| throws Exception { |
| if (!supportsDragAndDrop()) { |
| return; |
| } |
| |
| if (RESULT_MISSING.equals(expectedResult)) { |
| if (results.containsKey(resultKey)) { |
| fail("Unexpected " + resultKey + "=" + results.get(resultKey)); |
| } |
| } else { |
| assertTrue("Missing " + resultKey, results.containsKey(resultKey)); |
| assertEquals(resultKey + " result mismatch,", expectedResult, |
| results.get(resultKey)); |
| } |
| } |
| |
| private boolean supportsDragAndDrop() throws Exception { |
| String supportsMultiwindow = mDevice.executeShellCommand("am supports-multiwindow").trim(); |
| if ("true".equals(supportsMultiwindow)) { |
| return true; |
| } else if ("false".equals(supportsMultiwindow)) { |
| return false; |
| } else { |
| throw new Exception( |
| "device does not support \"am supports-multiwindow\" shell command."); |
| } |
| } |
| |
| private boolean supportsSplitScreenMultiWindow() throws DeviceNotAvailableException { |
| return !executeShellCommand(AM_SUPPORTS_SPLIT_SCREEN_MULTIWINDOW).startsWith("false"); |
| } |
| |
| private boolean supportsFreeformMultiWindow() throws DeviceNotAvailableException { |
| return mDevice.hasFeature("feature:android.software.freeform_window_management"); |
| } |
| |
| public void testCancelSoon() throws Exception { |
| assertDropResult(CANCEL_SOON, REQUEST_NONE, RESULT_MISSING); |
| } |
| |
| public void testDisallowGlobal() throws Exception { |
| assertNoGlobalDragEvents(DISALLOW_GLOBAL, RESULT_OK); |
| } |
| |
| public void testDisallowGlobalBelowSdk24() throws Exception { |
| mTargetPackageName = TARGET_23_PACKAGE_NAME; |
| assertNoGlobalDragEvents(GRANT_NONE, RESULT_OK); |
| } |
| |
| public void testFileUriLocal() throws Exception { |
| assertNoGlobalDragEvents(FILE_LOCAL, RESULT_OK); |
| } |
| |
| public void testFileUriGlobal() throws Exception { |
| assertNoGlobalDragEvents(FILE_GLOBAL, RESULT_EXCEPTION); |
| } |
| |
| public void testGrantNoneRequestNone() throws Exception { |
| assertDropResult(GRANT_NONE, REQUEST_NONE, RESULT_EXCEPTION); |
| } |
| |
| public void testGrantNoneRequestRead() throws Exception { |
| assertDropResult(GRANT_NONE, REQUEST_READ, RESULT_NULL_DROP_PERMISSIONS); |
| } |
| |
| public void testGrantNoneRequestWrite() throws Exception { |
| assertDropResult(GRANT_NONE, REQUEST_WRITE, RESULT_NULL_DROP_PERMISSIONS); |
| } |
| |
| public void testGrantReadRequestNone() throws Exception { |
| assertDropResult(GRANT_READ, REQUEST_NONE, RESULT_EXCEPTION); |
| } |
| |
| public void testGrantReadRequestRead() throws Exception { |
| assertDropResult(GRANT_READ, REQUEST_READ, RESULT_OK); |
| } |
| |
| public void testGrantReadRequestWrite() throws Exception { |
| assertDropResult(GRANT_READ, REQUEST_WRITE, RESULT_EXCEPTION); |
| } |
| |
| public void testGrantReadNoPrefixRequestReadNested() throws Exception { |
| assertDropResult(GRANT_READ_NOPREFIX, REQUEST_READ_NESTED, RESULT_EXCEPTION); |
| } |
| |
| public void testGrantReadPrefixRequestReadNested() throws Exception { |
| assertDropResult(GRANT_READ_PREFIX, REQUEST_READ_NESTED, RESULT_OK); |
| } |
| |
| public void testGrantPersistableRequestTakePersistable() throws Exception { |
| assertDropResult(GRANT_READ_PERSISTABLE, REQUEST_TAKE_PERSISTABLE, RESULT_OK); |
| } |
| |
| public void testGrantReadRequestTakePersistable() throws Exception { |
| assertDropResult(GRANT_READ, REQUEST_TAKE_PERSISTABLE, RESULT_EXCEPTION); |
| } |
| |
| public void testGrantWriteRequestNone() throws Exception { |
| assertDropResult(GRANT_WRITE, REQUEST_NONE, RESULT_EXCEPTION); |
| } |
| |
| public void testGrantWriteRequestRead() throws Exception { |
| assertDropResult(GRANT_WRITE, REQUEST_READ, RESULT_EXCEPTION); |
| } |
| |
| public void testGrantWriteRequestWrite() throws Exception { |
| assertDropResult(GRANT_WRITE, REQUEST_WRITE, RESULT_OK); |
| } |
| } |