Home | History | Annotate | Download | only in wm
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.server.wm;
     18 
     19 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
     20 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY;
     21 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
     22 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
     23 import static android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT;
     24 import static android.server.am.ActivityManagerTestBase.executeShellCommand;
     25 import static android.server.am.StateLogger.log;
     26 import static android.server.am.UiDeviceUtils.dragPointer;
     27 import static android.server.am.UiDeviceUtils.pressMenuButton;
     28 import static android.server.am.UiDeviceUtils.wakeUpDevice;
     29 
     30 import static org.junit.Assert.assertEquals;
     31 import static org.junit.Assert.assertNotNull;
     32 import static org.junit.Assert.assertTrue;
     33 import static org.junit.Assert.fail;
     34 import static org.junit.Assume.assumeTrue;
     35 
     36 import android.app.ActivityManager;
     37 import android.content.Context;
     38 import android.graphics.Point;
     39 import android.os.RemoteException;
     40 import android.platform.test.annotations.AppModeFull;
     41 import android.os.SystemClock;
     42 import android.platform.test.annotations.Presubmit;
     43 import android.support.test.InstrumentationRegistry;
     44 import android.util.Log;
     45 
     46 import org.junit.After;
     47 import org.junit.Before;
     48 import org.junit.Test;
     49 
     50 import java.util.Map;
     51 import java.util.regex.Pattern;
     52 
     53 /**
     54  * Build: mmma -j32 cts/tests/framework/base
     55  * Run: cts/tests/framework/base/activitymanager/util/run-test CtsWindowManagerDeviceTestCases android.server.wm.CrossAppDragAndDropTests
     56  */
     57 @Presubmit
     58 @AppModeFull(reason = "Requires android.permission.MANAGE_ACTIVITY_STACKS")
     59 public class CrossAppDragAndDropTests {
     60     private static final String TAG = "CrossAppDragAndDrop";
     61 
     62     private static final String AM_FORCE_STOP = "am force-stop ";
     63     private static final String AM_MOVE_TASK = "am stack move-task ";
     64     private static final String AM_RESIZE_TASK = "am task resize ";
     65     private static final String AM_REMOVE_STACK = "am stack remove ";
     66     private static final String AM_START_N = "am start -n ";
     67     private static final String AM_STACK_LIST = "am stack list";
     68     private static final String TASK_ID_PREFIX = "taskId";
     69 
     70     // Regex pattern to match adb shell am stack list output of the form:
     71     // taskId=<TASK_ID>: <componentName> bounds=[LEFT,TOP][RIGHT,BOTTOM]
     72     private static final String TASK_REGEX_PATTERN_STRING =
     73             "taskId=[0-9]+: %s bounds=\\[[0-9]+,[0-9]+\\]\\[[0-9]+,[0-9]+\\]";
     74 
     75     private static final int SWIPE_STEPS = 100;
     76 
     77     private static final String SOURCE_PACKAGE_NAME = "android.server.wm.dndsourceapp";
     78     private static final String TARGET_PACKAGE_NAME = "android.server.wm.dndtargetapp";
     79     private static final String TARGET_23_PACKAGE_NAME = "android.server.wm.dndtargetappsdk23";
     80 
     81 
     82     private static final String SOURCE_ACTIVITY_NAME = "DragSource";
     83     private static final String TARGET_ACTIVITY_NAME = "DropTarget";
     84 
     85     private static final String FILE_GLOBAL = "file_global";
     86     private static final String FILE_LOCAL = "file_local";
     87     private static final String DISALLOW_GLOBAL = "disallow_global";
     88     private static final String CANCEL_SOON = "cancel_soon";
     89     private static final String GRANT_NONE = "grant_none";
     90     private static final String GRANT_READ = "grant_read";
     91     private static final String GRANT_WRITE = "grant_write";
     92     private static final String GRANT_READ_PREFIX = "grant_read_prefix";
     93     private static final String GRANT_READ_NOPREFIX = "grant_read_noprefix";
     94     private static final String GRANT_READ_PERSISTABLE = "grant_read_persistable";
     95 
     96     private static final String REQUEST_NONE = "request_none";
     97     private static final String REQUEST_READ = "request_read";
     98     private static final String REQUEST_READ_NESTED = "request_read_nested";
     99     private static final String REQUEST_TAKE_PERSISTABLE = "request_take_persistable";
    100     private static final String REQUEST_WRITE = "request_write";
    101 
    102     private static final String SOURCE_LOG_TAG = "DragSource";
    103     private static final String TARGET_LOG_TAG = "DropTarget";
    104 
    105     private static final String RESULT_KEY_START_DRAG = "START_DRAG";
    106     private static final String RESULT_KEY_DRAG_STARTED = "DRAG_STARTED";
    107     private static final String RESULT_KEY_DRAG_ENDED = "DRAG_ENDED";
    108     private static final String RESULT_KEY_EXTRAS = "EXTRAS";
    109     private static final String RESULT_KEY_DROP_RESULT = "DROP";
    110     private static final String RESULT_KEY_ACCESS_BEFORE = "BEFORE";
    111     private static final String RESULT_KEY_ACCESS_AFTER = "AFTER";
    112     private static final String RESULT_KEY_CLIP_DATA_ERROR = "CLIP_DATA_ERROR";
    113     private static final String RESULT_KEY_CLIP_DESCR_ERROR = "CLIP_DESCR_ERROR";
    114     private static final String RESULT_KEY_LOCAL_STATE_ERROR = "LOCAL_STATE_ERROR";
    115 
    116     private static final String RESULT_MISSING = "Missing";
    117     private static final String RESULT_OK = "OK";
    118     private static final String RESULT_EXCEPTION = "Exception";
    119     private static final String RESULT_NULL_DROP_PERMISSIONS = "Null DragAndDropPermissions";
    120 
    121     protected Context mContext;
    122     protected ActivityManager mAm;
    123 
    124     private Map<String, String> mSourceResults;
    125     private Map<String, String> mTargetResults;
    126 
    127     private String mSourcePackageName;
    128     private String mTargetPackageName;
    129 
    130     private String mSessionId;
    131     private String mSourceLogTag;
    132     private String mTargetLogTag;
    133 
    134     @Before
    135     public void setUp() throws Exception {
    136         assumeTrue(supportsDragAndDrop());
    137 
    138         // Use uptime in seconds as unique test invocation id.
    139         mSessionId = Long.toString(SystemClock.uptimeMillis() / 1000);
    140         mSourceLogTag = SOURCE_LOG_TAG + mSessionId;
    141         mTargetLogTag = TARGET_LOG_TAG + mSessionId;
    142 
    143         mContext = InstrumentationRegistry.getContext();
    144         mAm = mContext.getSystemService(ActivityManager.class);
    145 
    146         mSourcePackageName = SOURCE_PACKAGE_NAME;
    147         mTargetPackageName = TARGET_PACKAGE_NAME;
    148         cleanupState();
    149     }
    150 
    151     @After
    152     public void tearDown() throws Exception {
    153         if (!supportsDragAndDrop()) {
    154           return;
    155         }
    156 
    157         executeShellCommand(AM_FORCE_STOP + mSourcePackageName);
    158         executeShellCommand(AM_FORCE_STOP + mTargetPackageName);
    159     }
    160 
    161     private void clearLogs() {
    162         executeShellCommand("logcat -c");
    163     }
    164 
    165     private String getStartCommand(String componentName, String modeExtra, String logtag) {
    166         return AM_START_N + componentName + " -e mode " + modeExtra + " -e logtag " + logtag;
    167     }
    168 
    169     private String getMoveTaskCommand(int taskId, int stackId) {
    170         return AM_MOVE_TASK + taskId + " " + stackId + " true";
    171     }
    172 
    173     private String getResizeTaskCommand(int taskId, Point topLeft, Point bottomRight)
    174             throws Exception {
    175         return AM_RESIZE_TASK + taskId + " " + topLeft.x + " " + topLeft.y + " " + bottomRight.x
    176                 + " " + bottomRight.y;
    177     }
    178 
    179     private String getComponentName(String packageName, String activityName) {
    180         return packageName + "/" + packageName + "." + activityName;
    181     }
    182 
    183     /**
    184      * Make sure that the special activity stacks are removed and the ActivityManager/WindowManager
    185      * is in a good state.
    186      */
    187     private void cleanupState() throws Exception {
    188         executeShellCommand(AM_FORCE_STOP + SOURCE_PACKAGE_NAME);
    189         executeShellCommand(AM_FORCE_STOP + TARGET_PACKAGE_NAME);
    190         executeShellCommand(AM_FORCE_STOP + TARGET_23_PACKAGE_NAME);
    191         unlockDevice();
    192         clearLogs();
    193 
    194         // Remove special stacks.
    195         mAm.removeStacksInWindowingModes(new int[] {
    196                 WINDOWING_MODE_PINNED,
    197                 WINDOWING_MODE_SPLIT_SCREEN_PRIMARY,
    198                 WINDOWING_MODE_FREEFORM
    199         });
    200     }
    201 
    202     private void launchDockedActivity(String packageName, String activityName, String mode,
    203             String logtag) throws Exception {
    204         clearLogs();
    205         final String componentName = getComponentName(packageName, activityName);
    206         executeShellCommand(getStartCommand(componentName, mode, logtag) + " --windowingMode "
    207                 + WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
    208         waitForResume(packageName, activityName);
    209     }
    210 
    211     private void launchFullscreenActivity(String packageName, String activityName, String mode,
    212             String logtag) throws Exception {
    213         clearLogs();
    214         final String componentName = getComponentName(packageName, activityName);
    215         executeShellCommand(getStartCommand(componentName, mode, logtag) + " --windowingMode "
    216                 + WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY);
    217         waitForResume(packageName, activityName);
    218     }
    219 
    220     /**
    221      * @param displaySize size of the display
    222      * @param leftSide {@code true} to launch the app taking up the left half of the display,
    223      *         {@code false} to launch the app taking up the right half of the display.
    224      */
    225     private void launchFreeformActivity(String packageName, String activityName, String mode,
    226             String logtag, Point displaySize, boolean leftSide) throws Exception {
    227         clearLogs();
    228         final String componentName = getComponentName(packageName, activityName);
    229         executeShellCommand(getStartCommand(componentName, mode, logtag) + " --windowingMode "
    230                 + WINDOWING_MODE_FREEFORM);
    231         waitForResume(packageName, activityName);
    232         Point topLeft = new Point(leftSide ? 0 : displaySize.x / 2, 0);
    233         Point bottomRight = new Point(leftSide ? displaySize.x / 2 : displaySize.x, displaySize.y);
    234         executeShellCommand(getResizeTaskCommand(getActivityTaskId(componentName), topLeft,
    235                 bottomRight));
    236     }
    237 
    238     private void waitForResume(String packageName, String activityName) throws Exception {
    239         final String fullActivityName = packageName + "." + activityName;
    240         int retryCount = 3;
    241         do {
    242             Thread.sleep(500);
    243             String logs = executeShellCommand("logcat -d -b events");
    244             for (String line : logs.split("\\n")) {
    245                 if (line.contains("am_on_resume_called") && line.contains(fullActivityName)) {
    246                     return;
    247                 }
    248             }
    249         } while (retryCount-- > 0);
    250 
    251         throw new Exception(fullActivityName + " has failed to start");
    252     }
    253 
    254     private void injectInput(Point from, Point to, int steps) throws Exception {
    255         dragPointer(from, to, steps);
    256     }
    257 
    258     private String findTaskInfo(String name) {
    259         final String output = executeShellCommand(AM_STACK_LIST);
    260         final StringBuilder builder = new StringBuilder();
    261         builder.append("Finding task info for task: ");
    262         builder.append(name);
    263         builder.append("\nParsing adb shell am output: ");
    264         builder.append(output);
    265         log(builder.toString());
    266         final Pattern pattern = Pattern.compile(String.format(TASK_REGEX_PATTERN_STRING, name));
    267         for (String line : output.split("\\n")) {
    268             final String truncatedLine;
    269             // Only look for the activity name before the "topActivity" string.
    270             final int pos = line.indexOf("topActivity");
    271             if (pos > 0) {
    272                 truncatedLine = line.substring(0, pos);
    273             } else {
    274                 truncatedLine = line;
    275             }
    276             if (pattern.matcher(truncatedLine).find()) {
    277                 return truncatedLine;
    278             }
    279         }
    280         return "";
    281     }
    282 
    283     private boolean getWindowBounds(String name, Point from, Point to) throws Exception {
    284         final String taskInfo = findTaskInfo(name);
    285         final String[] sections = taskInfo.split("\\[");
    286         if (sections.length > 2) {
    287             try {
    288                 parsePoint(sections[1], from);
    289                 parsePoint(sections[2], to);
    290                 return true;
    291             } catch (Exception e) {
    292                 return false;
    293             }
    294         }
    295         return false;
    296     }
    297 
    298     private int getActivityTaskId(String name) {
    299         final String taskInfo = findTaskInfo(name);
    300         for (String word : taskInfo.split("\\s+")) {
    301             if (word.startsWith(TASK_ID_PREFIX)) {
    302                 final String withColon = word.split("=")[1];
    303                 return Integer.parseInt(withColon.substring(0, withColon.length() - 1));
    304             }
    305         }
    306         return -1;
    307     }
    308 
    309     private Point getDisplaySize() throws Exception {
    310         final String output = executeShellCommand("wm size");
    311         final String[] sizes = output.split(" ")[2].split("x");
    312         return new Point(Integer.valueOf(sizes[0].trim()), Integer.valueOf(sizes[1].trim()));
    313     }
    314 
    315     private Point getWindowCenter(String name) throws Exception {
    316         Point p1 = new Point();
    317         Point p2 = new Point();
    318         if (getWindowBounds(name, p1, p2)) {
    319             return new Point((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
    320         }
    321         return null;
    322     }
    323 
    324     private void parsePoint(String string, Point point) {
    325         final String[] parts = string.split("[,|\\]]");
    326         point.x = Integer.parseInt(parts[0]);
    327         point.y = Integer.parseInt(parts[1]);
    328     }
    329 
    330     private void unlockDevice() {
    331         // Wake up the device, if necessary.
    332         try {
    333             wakeUpDevice();
    334         } catch (RemoteException e) {
    335             throw new RuntimeException(e);
    336         }
    337         // Unlock the screen.
    338         pressMenuButton();
    339     }
    340 
    341     private void assertDropResult(String sourceMode, String targetMode, String expectedDropResult)
    342             throws Exception {
    343         assertDragAndDropResults(sourceMode, targetMode, RESULT_OK, expectedDropResult, RESULT_OK);
    344     }
    345 
    346     private void assertNoGlobalDragEvents(String sourceMode, String expectedStartDragResult)
    347             throws Exception {
    348         assertDragAndDropResults(
    349                 sourceMode, REQUEST_NONE, expectedStartDragResult, RESULT_MISSING, RESULT_MISSING);
    350     }
    351 
    352     private void assertDragAndDropResults(String sourceMode, String targetMode,
    353             String expectedStartDragResult, String expectedDropResult,
    354             String expectedListenerResults) throws Exception {
    355         Log.e(TAG, "session: " + mSessionId + ", source: " + sourceMode
    356                 + ", target: " + targetMode);
    357 
    358         if (supportsSplitScreenMultiWindow()) {
    359             launchDockedActivity(
    360                     mSourcePackageName, SOURCE_ACTIVITY_NAME, sourceMode, mSourceLogTag);
    361             launchFullscreenActivity(
    362                     mTargetPackageName, TARGET_ACTIVITY_NAME, targetMode, mTargetLogTag);
    363         } else if (supportsFreeformMultiWindow()) {
    364             // Fallback to try to launch two freeform windows side by side.
    365             Point displaySize = getDisplaySize();
    366             launchFreeformActivity(
    367                     mSourcePackageName, SOURCE_ACTIVITY_NAME, sourceMode, mSourceLogTag,
    368                     displaySize, true /* leftSide */);
    369             launchFreeformActivity(
    370                     mTargetPackageName, TARGET_ACTIVITY_NAME, targetMode, mTargetLogTag,
    371                     displaySize, false /* leftSide */);
    372         } else {
    373             return;
    374         }
    375 
    376         Point p1 = getWindowCenter(getComponentName(mSourcePackageName, SOURCE_ACTIVITY_NAME));
    377         assertNotNull(p1);
    378         Point p2 = getWindowCenter(getComponentName(mTargetPackageName, TARGET_ACTIVITY_NAME));
    379         assertNotNull(p2);
    380 
    381         TestLogService.registerClient(mSourceLogTag, RESULT_KEY_START_DRAG);
    382         TestLogService.registerClient(mTargetLogTag, RESULT_KEY_DRAG_ENDED);
    383 
    384         injectInput(p1, p2, SWIPE_STEPS);
    385 
    386         mSourceResults = TestLogService.getResultsForClient(mSourceLogTag, 1000);
    387         assertSourceResult(RESULT_KEY_START_DRAG, expectedStartDragResult);
    388 
    389         mTargetResults = TestLogService.getResultsForClient(mTargetLogTag, 1000);
    390         assertTargetResult(RESULT_KEY_DROP_RESULT, expectedDropResult);
    391         if (!RESULT_MISSING.equals(expectedDropResult)) {
    392             assertTargetResult(RESULT_KEY_ACCESS_BEFORE, RESULT_EXCEPTION);
    393             assertTargetResult(RESULT_KEY_ACCESS_AFTER, RESULT_EXCEPTION);
    394         }
    395         assertListenerResults(expectedListenerResults);
    396     }
    397 
    398     private void assertListenerResults(String expectedResult) throws Exception {
    399         assertTargetResult(RESULT_KEY_DRAG_STARTED, expectedResult);
    400         assertTargetResult(RESULT_KEY_DRAG_ENDED, expectedResult);
    401         assertTargetResult(RESULT_KEY_EXTRAS, expectedResult);
    402 
    403         assertTargetResult(RESULT_KEY_CLIP_DATA_ERROR, RESULT_MISSING);
    404         assertTargetResult(RESULT_KEY_CLIP_DESCR_ERROR, RESULT_MISSING);
    405         assertTargetResult(RESULT_KEY_LOCAL_STATE_ERROR, RESULT_MISSING);
    406     }
    407 
    408     private void assertSourceResult(String resultKey, String expectedResult) throws Exception {
    409         assertResult(mSourceResults, resultKey, expectedResult);
    410     }
    411 
    412     private void assertTargetResult(String resultKey, String expectedResult) throws Exception {
    413         assertResult(mTargetResults, resultKey, expectedResult);
    414     }
    415 
    416     private void assertResult(Map<String, String> results, String resultKey, String expectedResult)
    417             throws Exception {
    418         if (RESULT_MISSING.equals(expectedResult)) {
    419             if (results.containsKey(resultKey)) {
    420                 fail("Unexpected " + resultKey + "=" + results.get(resultKey));
    421             }
    422         } else {
    423             assertTrue("Missing " + resultKey, results.containsKey(resultKey));
    424             assertEquals(resultKey + " result mismatch,", expectedResult,
    425                     results.get(resultKey));
    426         }
    427     }
    428 
    429     private static boolean supportsDragAndDrop() {
    430         return ActivityManager.supportsMultiWindow(InstrumentationRegistry.getContext());
    431     }
    432 
    433     private static boolean supportsSplitScreenMultiWindow() {
    434         return ActivityManager.supportsSplitScreenMultiWindow(InstrumentationRegistry.getContext());
    435     }
    436 
    437     private static boolean supportsFreeformMultiWindow() {
    438         return InstrumentationRegistry.getContext()
    439                 .getPackageManager()
    440                 .hasSystemFeature(FEATURE_FREEFORM_WINDOW_MANAGEMENT);
    441     }
    442 
    443     @Test
    444     public void testCancelSoon() throws Exception {
    445         assertDropResult(CANCEL_SOON, REQUEST_NONE, RESULT_MISSING);
    446     }
    447 
    448     @Test
    449     public void testDisallowGlobal() throws Exception {
    450         assertNoGlobalDragEvents(DISALLOW_GLOBAL, RESULT_OK);
    451     }
    452 
    453     @Test
    454     public void testDisallowGlobalBelowSdk24() throws Exception {
    455         mTargetPackageName = TARGET_23_PACKAGE_NAME;
    456         assertNoGlobalDragEvents(GRANT_NONE, RESULT_OK);
    457     }
    458 
    459     @Test
    460     public void testFileUriLocal() throws Exception {
    461         assertNoGlobalDragEvents(FILE_LOCAL, RESULT_OK);
    462     }
    463 
    464     @Test
    465     public void testFileUriGlobal() throws Exception {
    466         assertNoGlobalDragEvents(FILE_GLOBAL, RESULT_EXCEPTION);
    467     }
    468 
    469     @Test
    470     public void testGrantNoneRequestNone() throws Exception {
    471         assertDropResult(GRANT_NONE, REQUEST_NONE, RESULT_EXCEPTION);
    472     }
    473 
    474     @Test
    475     public void testGrantNoneRequestRead() throws Exception {
    476         assertDropResult(GRANT_NONE, REQUEST_READ, RESULT_NULL_DROP_PERMISSIONS);
    477     }
    478 
    479     @Test
    480     public void testGrantNoneRequestWrite() throws Exception {
    481         assertDropResult(GRANT_NONE, REQUEST_WRITE, RESULT_NULL_DROP_PERMISSIONS);
    482     }
    483 
    484     @Test
    485     public void testGrantReadRequestNone() throws Exception {
    486         assertDropResult(GRANT_READ, REQUEST_NONE, RESULT_EXCEPTION);
    487     }
    488 
    489     @Test
    490     public void testGrantReadRequestRead() throws Exception {
    491         assertDropResult(GRANT_READ, REQUEST_READ, RESULT_OK);
    492     }
    493 
    494     @Test
    495     public void testGrantReadRequestWrite() throws Exception {
    496         assertDropResult(GRANT_READ, REQUEST_WRITE, RESULT_EXCEPTION);
    497     }
    498 
    499     @Test
    500     public void testGrantReadNoPrefixRequestReadNested() throws Exception {
    501         assertDropResult(GRANT_READ_NOPREFIX, REQUEST_READ_NESTED, RESULT_EXCEPTION);
    502     }
    503 
    504     @Test
    505     public void testGrantReadPrefixRequestReadNested() throws Exception {
    506         assertDropResult(GRANT_READ_PREFIX, REQUEST_READ_NESTED, RESULT_OK);
    507     }
    508 
    509     @Test
    510     public void testGrantPersistableRequestTakePersistable() throws Exception {
    511         assertDropResult(GRANT_READ_PERSISTABLE, REQUEST_TAKE_PERSISTABLE, RESULT_OK);
    512     }
    513 
    514     @Test
    515     public void testGrantReadRequestTakePersistable() throws Exception {
    516         assertDropResult(GRANT_READ, REQUEST_TAKE_PERSISTABLE, RESULT_EXCEPTION);
    517     }
    518 
    519     @Test
    520     public void testGrantWriteRequestNone() throws Exception {
    521         assertDropResult(GRANT_WRITE, REQUEST_NONE, RESULT_EXCEPTION);
    522     }
    523 
    524     @Test
    525     public void testGrantWriteRequestRead() throws Exception {
    526         assertDropResult(GRANT_WRITE, REQUEST_READ, RESULT_EXCEPTION);
    527     }
    528 
    529     @Test
    530     public void testGrantWriteRequestWrite() throws Exception {
    531         assertDropResult(GRANT_WRITE, REQUEST_WRITE, RESULT_OK);
    532     }
    533 }
    534