| /* |
| * 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.wm; |
| |
| import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; |
| import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY; |
| import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; |
| import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; |
| import static android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT; |
| import static android.server.am.ActivityManagerTestBase.executeShellCommand; |
| import static android.server.am.StateLogger.log; |
| import static android.server.am.UiDeviceUtils.dragPointer; |
| import static android.server.am.UiDeviceUtils.pressMenuButton; |
| import static android.server.am.UiDeviceUtils.wakeUpDevice; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| import static org.junit.Assume.assumeTrue; |
| |
| import android.app.ActivityManager; |
| import android.content.Context; |
| import android.graphics.Point; |
| import android.hardware.display.DisplayManager; |
| import android.os.RemoteException; |
| import android.os.SystemClock; |
| import android.platform.test.annotations.AppModeFull; |
| import android.platform.test.annotations.Presubmit; |
| import android.util.Log; |
| import android.view.Display; |
| |
| import androidx.test.InstrumentationRegistry; |
| |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Test; |
| |
| import java.util.Map; |
| import java.util.regex.Pattern; |
| |
| /** |
| * Build: mmma -j32 cts/tests/framework/base |
| * Run: cts/tests/framework/base/activitymanager/util/run-test CtsWindowManagerDeviceTestCases android.server.wm.CrossAppDragAndDropTests |
| */ |
| @Presubmit |
| @AppModeFull(reason = "Requires android.permission.MANAGE_ACTIVITY_STACKS") |
| public class CrossAppDragAndDropTests { |
| private static final String TAG = "CrossAppDragAndDrop"; |
| |
| 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 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_STEPS = 100; |
| |
| private static final String SOURCE_PACKAGE_NAME = "android.server.wm.dndsourceapp"; |
| private static final String TARGET_PACKAGE_NAME = "android.server.wm.dndtargetapp"; |
| private static final String TARGET_23_PACKAGE_NAME = "android.server.wm.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"; |
| |
| protected Context mContext; |
| protected ActivityManager mAm; |
| protected DisplayManager mDm; |
| |
| private Map<String, String> mSourceResults; |
| private Map<String, String> mTargetResults; |
| |
| private String mSourcePackageName; |
| private String mTargetPackageName; |
| |
| private String mSessionId; |
| private String mSourceLogTag; |
| private String mTargetLogTag; |
| |
| @Before |
| public void setUp() throws Exception { |
| assumeTrue(supportsDragAndDrop()); |
| |
| // Use uptime in seconds as unique test invocation id. |
| mSessionId = Long.toString(SystemClock.uptimeMillis() / 1000); |
| mSourceLogTag = SOURCE_LOG_TAG + mSessionId; |
| mTargetLogTag = TARGET_LOG_TAG + mSessionId; |
| |
| mContext = InstrumentationRegistry.getContext(); |
| mAm = mContext.getSystemService(ActivityManager.class); |
| mDm = mContext.getSystemService(DisplayManager.class); |
| |
| mSourcePackageName = SOURCE_PACKAGE_NAME; |
| mTargetPackageName = TARGET_PACKAGE_NAME; |
| cleanupState(); |
| } |
| |
| @After |
| public void tearDown() throws Exception { |
| if (!supportsDragAndDrop()) { |
| return; |
| } |
| |
| executeShellCommand(AM_FORCE_STOP + mSourcePackageName); |
| executeShellCommand(AM_FORCE_STOP + mTargetPackageName); |
| } |
| |
| private void clearLogs() { |
| executeShellCommand("logcat -c"); |
| } |
| |
| private String getStartCommand(String componentName, String modeExtra, String logtag) { |
| return AM_START_N + componentName + " -e mode " + modeExtra + " -e logtag " + logtag; |
| } |
| |
| private String getMoveTaskCommand(int taskId, int stackId) { |
| 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(); |
| clearLogs(); |
| |
| // Remove special stacks. |
| mAm.removeStacksInWindowingModes(new int[] { |
| WINDOWING_MODE_PINNED, |
| WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, |
| WINDOWING_MODE_FREEFORM |
| }); |
| } |
| |
| private void launchDockedActivity(String packageName, String activityName, String mode, |
| String logtag) throws Exception { |
| clearLogs(); |
| final String componentName = getComponentName(packageName, activityName); |
| executeShellCommand(getStartCommand(componentName, mode, logtag) + " --windowingMode " |
| + WINDOWING_MODE_SPLIT_SCREEN_PRIMARY); |
| waitForResume(packageName, activityName); |
| } |
| |
| private void launchFullscreenActivity(String packageName, String activityName, String mode, |
| String logtag) throws Exception { |
| clearLogs(); |
| final String componentName = getComponentName(packageName, activityName); |
| executeShellCommand(getStartCommand(componentName, mode, logtag) + " --windowingMode " |
| + WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY); |
| 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, |
| String logtag, Point displaySize, boolean leftSide) throws Exception { |
| clearLogs(); |
| final String componentName = getComponentName(packageName, activityName); |
| executeShellCommand(getStartCommand(componentName, mode, logtag) + " --windowingMode " |
| + WINDOWING_MODE_FREEFORM); |
| 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 steps) throws Exception { |
| dragPointer(from, to, steps); |
| } |
| |
| private String findTaskInfo(String name) { |
| final String output = executeShellCommand(AM_STACK_LIST); |
| 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); |
| log(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) { |
| 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 Point displaySize = new Point(); |
| mDm.getDisplay(Display.DEFAULT_DISPLAY).getRealSize(displaySize); |
| return displaySize; |
| } |
| |
| 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() { |
| // Wake up the device, if necessary. |
| try { |
| wakeUpDevice(); |
| } catch (RemoteException e) { |
| throw new RuntimeException(e); |
| } |
| // Unlock the screen. |
| pressMenuButton(); |
| } |
| |
| 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 { |
| Log.e(TAG, "session: " + mSessionId + ", source: " + sourceMode |
| + ", target: " + targetMode); |
| |
| if (supportsSplitScreenMultiWindow()) { |
| launchDockedActivity( |
| mSourcePackageName, SOURCE_ACTIVITY_NAME, sourceMode, mSourceLogTag); |
| launchFullscreenActivity( |
| mTargetPackageName, TARGET_ACTIVITY_NAME, targetMode, mTargetLogTag); |
| } else if (supportsFreeformMultiWindow()) { |
| // Fallback to try to launch two freeform windows side by side. |
| Point displaySize = getDisplaySize(); |
| launchFreeformActivity( |
| mSourcePackageName, SOURCE_ACTIVITY_NAME, sourceMode, mSourceLogTag, |
| displaySize, true /* leftSide */); |
| launchFreeformActivity( |
| mTargetPackageName, TARGET_ACTIVITY_NAME, targetMode, mTargetLogTag, |
| displaySize, false /* leftSide */); |
| } else { |
| return; |
| } |
| |
| Point p1 = getWindowCenter(getComponentName(mSourcePackageName, SOURCE_ACTIVITY_NAME)); |
| assertNotNull(p1); |
| Point p2 = getWindowCenter(getComponentName(mTargetPackageName, TARGET_ACTIVITY_NAME)); |
| assertNotNull(p2); |
| |
| TestLogService.registerClient(mSourceLogTag, RESULT_KEY_START_DRAG); |
| TestLogService.registerClient(mTargetLogTag, RESULT_KEY_DRAG_ENDED); |
| |
| injectInput(p1, p2, SWIPE_STEPS); |
| |
| mSourceResults = TestLogService.getResultsForClient(mSourceLogTag, 1000); |
| assertSourceResult(RESULT_KEY_START_DRAG, expectedStartDragResult); |
| |
| mTargetResults = TestLogService.getResultsForClient(mTargetLogTag, 1000); |
| 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 (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 static boolean supportsDragAndDrop() { |
| return ActivityManager.supportsMultiWindow(InstrumentationRegistry.getContext()); |
| } |
| |
| private static boolean supportsSplitScreenMultiWindow() { |
| return ActivityManager.supportsSplitScreenMultiWindow(InstrumentationRegistry.getContext()); |
| } |
| |
| private static boolean supportsFreeformMultiWindow() { |
| return InstrumentationRegistry.getContext() |
| .getPackageManager() |
| .hasSystemFeature(FEATURE_FREEFORM_WINDOW_MANAGEMENT); |
| } |
| |
| @Test |
| public void testCancelSoon() throws Exception { |
| assertDropResult(CANCEL_SOON, REQUEST_NONE, RESULT_MISSING); |
| } |
| |
| @Test |
| public void testDisallowGlobal() throws Exception { |
| assertNoGlobalDragEvents(DISALLOW_GLOBAL, RESULT_OK); |
| } |
| |
| @Test |
| public void testDisallowGlobalBelowSdk24() throws Exception { |
| mTargetPackageName = TARGET_23_PACKAGE_NAME; |
| assertNoGlobalDragEvents(GRANT_NONE, RESULT_OK); |
| } |
| |
| @Test |
| public void testFileUriLocal() throws Exception { |
| assertNoGlobalDragEvents(FILE_LOCAL, RESULT_OK); |
| } |
| |
| @Test |
| public void testFileUriGlobal() throws Exception { |
| assertNoGlobalDragEvents(FILE_GLOBAL, RESULT_EXCEPTION); |
| } |
| |
| @Test |
| public void testGrantNoneRequestNone() throws Exception { |
| assertDropResult(GRANT_NONE, REQUEST_NONE, RESULT_EXCEPTION); |
| } |
| |
| @Test |
| public void testGrantNoneRequestRead() throws Exception { |
| assertDropResult(GRANT_NONE, REQUEST_READ, RESULT_NULL_DROP_PERMISSIONS); |
| } |
| |
| @Test |
| public void testGrantNoneRequestWrite() throws Exception { |
| assertDropResult(GRANT_NONE, REQUEST_WRITE, RESULT_NULL_DROP_PERMISSIONS); |
| } |
| |
| @Test |
| public void testGrantReadRequestNone() throws Exception { |
| assertDropResult(GRANT_READ, REQUEST_NONE, RESULT_EXCEPTION); |
| } |
| |
| @Test |
| public void testGrantReadRequestRead() throws Exception { |
| assertDropResult(GRANT_READ, REQUEST_READ, RESULT_OK); |
| } |
| |
| @Test |
| public void testGrantReadRequestWrite() throws Exception { |
| assertDropResult(GRANT_READ, REQUEST_WRITE, RESULT_EXCEPTION); |
| } |
| |
| @Test |
| public void testGrantReadNoPrefixRequestReadNested() throws Exception { |
| assertDropResult(GRANT_READ_NOPREFIX, REQUEST_READ_NESTED, RESULT_EXCEPTION); |
| } |
| |
| @Test |
| public void testGrantReadPrefixRequestReadNested() throws Exception { |
| assertDropResult(GRANT_READ_PREFIX, REQUEST_READ_NESTED, RESULT_OK); |
| } |
| |
| @Test |
| public void testGrantPersistableRequestTakePersistable() throws Exception { |
| assertDropResult(GRANT_READ_PERSISTABLE, REQUEST_TAKE_PERSISTABLE, RESULT_OK); |
| } |
| |
| @Test |
| public void testGrantReadRequestTakePersistable() throws Exception { |
| assertDropResult(GRANT_READ, REQUEST_TAKE_PERSISTABLE, RESULT_EXCEPTION); |
| } |
| |
| @Test |
| public void testGrantWriteRequestNone() throws Exception { |
| assertDropResult(GRANT_WRITE, REQUEST_NONE, RESULT_EXCEPTION); |
| } |
| |
| @Test |
| public void testGrantWriteRequestRead() throws Exception { |
| assertDropResult(GRANT_WRITE, REQUEST_READ, RESULT_EXCEPTION); |
| } |
| |
| @Test |
| public void testGrantWriteRequestWrite() throws Exception { |
| assertDropResult(GRANT_WRITE, REQUEST_WRITE, RESULT_OK); |
| } |
| } |