Add host side CTS test for cross-app drag and drop

This is a host side replacement for android.dnd.cts.DragAndDropTest.
This should resolve the UIAutomator flakiness problem.

Bug: 28508640
Change-Id: I27b5a2724807e4805560cc0a9c69886129496e9f
diff --git a/OldCtsTestCaseList.mk b/OldCtsTestCaseList.mk
index 68130f3..aff9f3c 100644
--- a/OldCtsTestCaseList.mk
+++ b/OldCtsTestCaseList.mk
@@ -109,6 +109,8 @@
     CtsDeviceTaskSwitchingAppA \
     CtsDeviceTaskSwitchingAppB \
     CtsDeviceTaskSwitchingControl \
+    CtsDragAndDropSourceApp \
+    CtsDragAndDropTargetApp \
     CtsExternalServiceService \
     CtsHostsideNetworkTestsApp \
     CtsHostsideNetworkTestsApp2 \
@@ -266,6 +268,7 @@
     CtsAtraceHostTestCases \
     CtsCppToolsTestCases \
     CtsDevicePolicyManagerTestCases \
+    CtsDragAndDropHostTestCases \
     CtsDumpsysHostTestCases \
     CtsHostsideNetworkTests \
     CtsJdwpSecurityHostTestCases \
diff --git a/hostsidetests/services/windowmanager/Android.mk b/hostsidetests/services/windowmanager/Android.mk
new file mode 100644
index 0000000..eeac27b
--- /dev/null
+++ b/hostsidetests/services/windowmanager/Android.mk
@@ -0,0 +1,38 @@
+# Copyright (C) 2015 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.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+# Must match the package name in OldCtsTestCaseList.mk
+LOCAL_MODULE := CtsDragAndDropHostTestCases
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_JAVA_LIBRARIES := cts-tradefed tradefed-prebuilt
+
+LOCAL_CTS_TEST_PACKAGE := android.wm.cts
+
+LOCAL_CTS_MODULE_CONFIG := $(LOCAL_PATH)/Old$(CTS_MODULE_TEST_CONFIG)
+
+# Tag this module as a cts test artifact
+LOCAL_COMPATIBILITY_SUITE := cts
+
+include $(BUILD_CTS_HOST_JAVA_LIBRARY)
+
+# Build the test APKs using their own makefiles
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/hostsidetests/services/windowmanager/AndroidTest.xml b/hostsidetests/services/windowmanager/AndroidTest.xml
new file mode 100644
index 0000000..5e305cf
--- /dev/null
+++ b/hostsidetests/services/windowmanager/AndroidTest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<configuration description="Config for CTS drag and drop host test cases">
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.ApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="CtsDragAndDropSourceApp.apk" />
+        <option name="test-file-name" value="CtsDragAndDropTargetApp.apk" />
+    </target_preparer>
+    <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
+        <option name="jar" value="CtsDragAndDropHostTestCases.jar" />
+    </test>
+</configuration>
diff --git a/hostsidetests/services/windowmanager/OldAndroidTest.xml b/hostsidetests/services/windowmanager/OldAndroidTest.xml
new file mode 100644
index 0000000..844bca3
--- /dev/null
+++ b/hostsidetests/services/windowmanager/OldAndroidTest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="CTS package preparer for install/uninstall of the apk used as a test operation target">
+    <include name="common-config" />
+    <!-- This will tell tradefed to install the test apk. -->
+    <option name="cts-apk-installer:test-file-name" value="CtsDragAndDropSourceApp.apk" />
+    <option name="cts-apk-installer:test-file-name" value="CtsDragAndDropTargetApp.apk" />
+</configuration>
diff --git a/hostsidetests/services/windowmanager/dndsourceapp/Android.mk b/hostsidetests/services/windowmanager/dndsourceapp/Android.mk
new file mode 100644
index 0000000..1ec751c
--- /dev/null
+++ b/hostsidetests/services/windowmanager/dndsourceapp/Android.mk
@@ -0,0 +1,31 @@
+# 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+# Don't include this package in any target.
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_SDK_VERSION := current
+
+# Tag this module as a cts test artifact
+LOCAL_COMPATIBILITY_SUITE := cts
+
+LOCAL_PACKAGE_NAME := CtsDragAndDropSourceApp
+
+include $(BUILD_CTS_SUPPORT_PACKAGE)
diff --git a/hostsidetests/services/windowmanager/dndsourceapp/AndroidManifest.xml b/hostsidetests/services/windowmanager/dndsourceapp/AndroidManifest.xml
new file mode 100644
index 0000000..296a979
--- /dev/null
+++ b/hostsidetests/services/windowmanager/dndsourceapp/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android.wm.cts.dndsourceapp">
+    <application android:label="CtsDnDSource">
+        <activity android:name="android.wm.cts.dndsourceapp.DragSource">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+
+        <provider android:name="android.wm.cts.dndsourceapp.DragSourceContentProvider"
+                  android:authorities="android.wm.cts.dndsource.contentprovider"
+                  android:grantUriPermissions="true"/>
+    </application>
+</manifest>
diff --git a/hostsidetests/services/windowmanager/dndsourceapp/res/layout/source_activity.xml b/hostsidetests/services/windowmanager/dndsourceapp/res/layout/source_activity.xml
new file mode 100644
index 0000000..673994c
--- /dev/null
+++ b/hostsidetests/services/windowmanager/dndsourceapp/res/layout/source_activity.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+    <TextView
+            android:id="@+id/drag_source"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:gravity="center">
+    </TextView>
+</LinearLayout>
\ No newline at end of file
diff --git a/hostsidetests/services/windowmanager/dndsourceapp/src/android/wm/cts/dndsourceapp/DragSource.java b/hostsidetests/services/windowmanager/dndsourceapp/src/android/wm/cts/dndsourceapp/DragSource.java
new file mode 100644
index 0000000..ff92656
--- /dev/null
+++ b/hostsidetests/services/windowmanager/dndsourceapp/src/android/wm/cts/dndsourceapp/DragSource.java
@@ -0,0 +1,102 @@
+/*
+ * 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.wm.cts.dndsourceapp;
+
+import android.app.Activity;
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.PersistableBundle;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.TextView;
+
+public class DragSource extends Activity{
+    private static final String URI_PREFIX =
+            "content://" + DragSourceContentProvider.AUTHORITY + "/data";
+
+    private static final String MAGIC_VALUE = "42";
+    private static final long TIMEOUT_CANCEL = 150;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        View view = getLayoutInflater().inflate(R.layout.source_activity, null);
+        setContentView(view);
+
+        final Uri plainUri = Uri.parse(URI_PREFIX + "/" + MAGIC_VALUE);
+
+        setUpDragSource("disallow_global", plainUri, 0);
+        setUpDragSource("cancel_soon", plainUri, View.DRAG_FLAG_GLOBAL);
+
+        setUpDragSource("grant_none", plainUri, View.DRAG_FLAG_GLOBAL);
+        setUpDragSource("grant_read", plainUri,
+                View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ);
+        setUpDragSource("grant_write", plainUri,
+                View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_WRITE);
+        setUpDragSource("grant_read_persistable", plainUri,
+                View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ |
+                        View.DRAG_FLAG_GLOBAL_PERSISTABLE_URI_PERMISSION);
+
+        final Uri prefixUri = Uri.parse(URI_PREFIX);
+
+        setUpDragSource("grant_read_prefix", prefixUri,
+                View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ |
+                        View.DRAG_FLAG_GLOBAL_PREFIX_URI_PERMISSION);
+        setUpDragSource("grant_read_noprefix", prefixUri,
+                View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ);
+    }
+
+    private void setUpDragSource(String mode, final Uri uri, final int flags) {
+        if (!mode.equals(getIntent().getStringExtra("mode"))) {
+            return;
+        }
+        final View source = findViewById(R.id.drag_source);
+        ((TextView) source).setText(mode);
+        source.setOnTouchListener(new View.OnTouchListener() {
+            @Override
+            public boolean onTouch(View v, MotionEvent event) {
+                if (event.getAction() != MotionEvent.ACTION_DOWN) {
+                    return false;
+                }
+                final ClipDescription clipDescription = new ClipDescription("", new String[] {
+                        ClipDescription.MIMETYPE_TEXT_URILIST });
+                PersistableBundle extras = new PersistableBundle(1);
+                extras.putString("extraKey", "extraValue");
+                clipDescription.setExtras(extras);
+                final ClipData clipData = new ClipData(clipDescription, new ClipData.Item(uri));
+                v.startDragAndDrop(
+                        clipData,
+                        new View.DragShadowBuilder(v),
+                        null,
+                        flags);
+                if (mode.equals("cancel_soon")) {
+                    new Handler().postDelayed(new Runnable() {
+                        @Override
+                        public void run() {
+                            v.cancelDragAndDrop();
+                        }
+                    }, TIMEOUT_CANCEL);
+                }
+                return true;
+            }
+        });
+    }
+}
diff --git a/hostsidetests/services/windowmanager/dndsourceapp/src/android/wm/cts/dndsourceapp/DragSourceContentProvider.java b/hostsidetests/services/windowmanager/dndsourceapp/src/android/wm/cts/dndsourceapp/DragSourceContentProvider.java
new file mode 100644
index 0000000..06a94aa
--- /dev/null
+++ b/hostsidetests/services/windowmanager/dndsourceapp/src/android/wm/cts/dndsourceapp/DragSourceContentProvider.java
@@ -0,0 +1,71 @@
+/*
+ * 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.wm.cts.dndsourceapp;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.net.Uri;
+
+public class DragSourceContentProvider extends ContentProvider {
+
+    public static final String AUTHORITY = "android.wm.cts.dndsource.contentprovider";
+
+    private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+    private static final int URI_DATA = 1;
+
+    static {
+        sMatcher.addURI(AUTHORITY, "data/#", URI_DATA);
+    }
+
+    @Override
+    public boolean onCreate() {
+        return false;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+                        String sortOrder) {
+        switch (sMatcher.match(uri)) {
+            case URI_DATA:
+                return new DragSourceCursor(uri.getLastPathSegment());
+        }
+        return null;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return null;
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        return null;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        return 0;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        return 0;
+    }
+}
diff --git a/hostsidetests/services/windowmanager/dndsourceapp/src/android/wm/cts/dndsourceapp/DragSourceCursor.java b/hostsidetests/services/windowmanager/dndsourceapp/src/android/wm/cts/dndsourceapp/DragSourceCursor.java
new file mode 100644
index 0000000..c54df65
--- /dev/null
+++ b/hostsidetests/services/windowmanager/dndsourceapp/src/android/wm/cts/dndsourceapp/DragSourceCursor.java
@@ -0,0 +1,80 @@
+/*
+ * 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.wm.cts.dndsourceapp;
+
+import android.database.AbstractCursor;
+
+public class DragSourceCursor extends AbstractCursor {
+    private static final String COLUMN_KEY = "key";
+
+    private final String mValue;
+
+    public DragSourceCursor(String value) {
+        mValue = value;
+    }
+
+    @Override
+    public int getCount() {
+        return 1;
+    }
+
+    @Override
+    public String[] getColumnNames() {
+        return new String[] {COLUMN_KEY};
+    }
+
+    @Override
+    public String getString(int column) {
+        if (getPosition() != 0) {
+            throw new IllegalArgumentException("Incorrect position: " + getPosition());
+        }
+        if (column != 0) {
+            throw new IllegalArgumentException("Incorrect column: " + column);
+        }
+        return mValue;
+    }
+
+    @Override
+    public short getShort(int column) {
+        return 0;
+    }
+
+    @Override
+    public int getInt(int column) {
+        return 0;
+    }
+
+    @Override
+    public long getLong(int column) {
+        return 0;
+    }
+
+    @Override
+    public float getFloat(int column) {
+        return 0;
+    }
+
+    @Override
+    public double getDouble(int column) {
+        return 0;
+    }
+
+    @Override
+    public boolean isNull(int column) {
+        return false;
+    }
+}
diff --git a/hostsidetests/services/windowmanager/dndtargetapp/Android.mk b/hostsidetests/services/windowmanager/dndtargetapp/Android.mk
new file mode 100644
index 0000000..7dc512c
--- /dev/null
+++ b/hostsidetests/services/windowmanager/dndtargetapp/Android.mk
@@ -0,0 +1,31 @@
+# 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+# Don't include this package in any target.
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_SDK_VERSION := current
+
+# Tag this module as a cts test artifact
+LOCAL_COMPATIBILITY_SUITE := cts
+
+LOCAL_PACKAGE_NAME := CtsDragAndDropTargetApp
+
+include $(BUILD_CTS_SUPPORT_PACKAGE)
diff --git a/hostsidetests/services/windowmanager/dndtargetapp/AndroidManifest.xml b/hostsidetests/services/windowmanager/dndtargetapp/AndroidManifest.xml
new file mode 100644
index 0000000..ed7b9c2
--- /dev/null
+++ b/hostsidetests/services/windowmanager/dndtargetapp/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="android.wm.cts.dndtargetapp">
+    <application android:label="CtsDnDTarget">
+        <activity android:name="android.wm.cts.dndtargetapp.DropTarget">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/hostsidetests/services/windowmanager/dndtargetapp/res/layout/target_activity.xml b/hostsidetests/services/windowmanager/dndtargetapp/res/layout/target_activity.xml
new file mode 100644
index 0000000..ddcdf33
--- /dev/null
+++ b/hostsidetests/services/windowmanager/dndtargetapp/res/layout/target_activity.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+    <TextView
+            android:id="@+id/drag_target"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:gravity="center">
+    </TextView>
+</LinearLayout>
\ No newline at end of file
diff --git a/hostsidetests/services/windowmanager/dndtargetapp/src/android/wm/cts/dndtargetapp/DropTarget.java b/hostsidetests/services/windowmanager/dndtargetapp/src/android/wm/cts/dndtargetapp/DropTarget.java
new file mode 100644
index 0000000..904a422
--- /dev/null
+++ b/hostsidetests/services/windowmanager/dndtargetapp/src/android/wm/cts/dndtargetapp/DropTarget.java
@@ -0,0 +1,249 @@
+/*
+ * 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.wm.cts.dndtargetapp;
+
+import android.app.Activity;
+import android.content.ClipData;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.PersistableBundle;
+import android.util.Log;
+import android.view.DragAndDropPermissions;
+import android.view.DragEvent;
+import android.view.View;
+import android.widget.TextView;
+
+public class DropTarget extends Activity {
+    public static final String LOG_TAG = "DropTarget";
+
+    private static final String RESULT_KEY_DRAG_STARTED = "DRAG_STARTED";
+    private static final String RESULT_KEY_EXTRAS = "EXTRAS";
+    private static final String RESULT_KEY_DROP_RESULT = "DROP";
+    private static final String RESULT_KEY_DETAILS = "DETAILS";
+
+    public static final String RESULT_OK = "OK";
+    public static final String RESULT_EXCEPTION = "Exception";
+
+    protected static final String MAGIC_VALUE = "42";
+
+    private TextView mTextView;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        View view = getLayoutInflater().inflate(R.layout.target_activity, null);
+        setContentView(view);
+
+        setUpDropTarget("request_none", new OnDragUriReadListener(false));
+        setUpDropTarget("request_read", new OnDragUriReadListener());
+        setUpDropTarget("request_write", new OnDragUriWriteListener());
+        setUpDropTarget("request_read_nested", new OnDragUriReadPrefixListener());
+        setUpDropTarget("request_take_persistable", new OnDragUriTakePersistableListener());
+    }
+
+    private void setUpDropTarget(String mode, OnDragUriListener listener) {
+        if (!mode.equals(getIntent().getStringExtra("mode"))) {
+            return;
+        }
+        mTextView = (TextView)findViewById(R.id.drag_target);
+        mTextView.setText(mode);
+        mTextView.setOnDragListener(listener);
+    }
+
+    private String checkExtraValue(DragEvent event) {
+        PersistableBundle extras = event.getClipDescription().getExtras();
+        if (extras == null) {
+            return "Null";
+        }
+
+        final String value = extras.getString("extraKey");
+        if ("extraValue".equals(value)) {
+            return RESULT_OK;
+        }
+        return value;
+    }
+
+    private void logResult(String key, String value) {
+        Log.i(LOG_TAG, key + "=" + value);
+        mTextView.setText(mTextView.getText() + "\n" + key + "=" + value);
+    }
+
+    private abstract class OnDragUriListener implements View.OnDragListener {
+        private final boolean requestPermissions;
+
+        public OnDragUriListener(boolean requestPermissions) {
+            this.requestPermissions = requestPermissions;
+        }
+
+        @Override
+        public boolean onDrag(View v, DragEvent event) {
+            switch (event.getAction()) {
+                case DragEvent.ACTION_DRAG_STARTED:
+                    logResult(RESULT_KEY_DRAG_STARTED, RESULT_OK);
+                    logResult(RESULT_KEY_EXTRAS, checkExtraValue(event));
+                    return true;
+
+                case DragEvent.ACTION_DRAG_ENTERED:
+                    return true;
+
+                case DragEvent.ACTION_DRAG_LOCATION:
+                    return true;
+
+                case DragEvent.ACTION_DRAG_EXITED:
+                    return true;
+
+                case DragEvent.ACTION_DROP:
+                    String result;
+                    try {
+                        result = processDrop(event, requestPermissions);
+                    } catch (Exception e) {
+                        result = RESULT_EXCEPTION;
+                        logResult(RESULT_KEY_DETAILS, e.getMessage());
+                    }
+                    logResult(RESULT_KEY_DROP_RESULT, result);
+                    return true;
+
+                case DragEvent.ACTION_DRAG_ENDED:
+                    return true;
+
+                default:
+                    return false;
+            }
+        }
+
+        private String processDrop(DragEvent event, boolean requestPermissions) {
+            final ClipData clipData = event.getClipData();
+            if (clipData == null) {
+                return "Null ClipData";
+            }
+            if (clipData.getItemCount() == 0) {
+                return "Empty ClipData";
+            }
+            ClipData.Item item = clipData.getItemAt(0);
+            if (item == null) {
+                return "Null ClipData.Item";
+            }
+            Uri uri = item.getUri();
+            if (uri == null) {
+                return "Null Uri";
+            }
+
+            DragAndDropPermissions permissions = null;
+            if (requestPermissions) {
+                permissions = requestDragAndDropPermissions(event);
+                if (permissions == null) {
+                    return "Null DragAndDropPermissions";
+                }
+            }
+
+            try {
+                return processUri(uri);
+            } finally {
+                if (permissions != null) {
+                    permissions.release();
+                }
+            }
+        }
+
+        abstract protected String processUri(Uri uri);
+    }
+
+    private class OnDragUriReadListener extends OnDragUriListener {
+        OnDragUriReadListener(boolean requestPermissions) {
+            super(requestPermissions);
+        }
+
+        OnDragUriReadListener() {
+            super(true);
+        }
+
+        protected String processUri(Uri uri) {
+            return checkQueryResult(uri, MAGIC_VALUE);
+        }
+
+        protected String checkQueryResult(Uri uri, String expectedValue) {
+            Cursor cursor = null;
+            try {
+                cursor = getContentResolver().query(uri, null, null, null, null);
+                if (cursor == null) {
+                    return "Null Cursor";
+                }
+                cursor.moveToPosition(0);
+                String value = cursor.getString(0);
+                if (!expectedValue.equals(value)) {
+                    return "Wrong value: " + value;
+                }
+                return RESULT_OK;
+            } finally {
+                if (cursor != null) {
+                    cursor.close();
+                }
+            }
+        }
+    }
+
+    private class OnDragUriWriteListener extends OnDragUriListener {
+        OnDragUriWriteListener() {
+            super(true);
+        }
+
+        protected String processUri(Uri uri) {
+            ContentValues values = new ContentValues();
+            values.put("key", 100);
+            getContentResolver().update(uri, values, null, null);
+            return RESULT_OK;
+        }
+    }
+
+    private class OnDragUriReadPrefixListener extends OnDragUriReadListener {
+        @Override
+        protected String processUri(Uri uri) {
+            final String result1 = queryPrefixed(uri, "1");
+            if (!result1.equals(RESULT_OK)) {
+                return result1;
+            }
+            final String result2 = queryPrefixed(uri, "2");
+            if (!result2.equals(RESULT_OK)) {
+                return result2;
+            }
+            return queryPrefixed(uri, "3");
+        }
+
+        private String queryPrefixed(Uri uri, String selector) {
+            final Uri prefixedUri = Uri.parse(uri.toString() + "/" + selector);
+            return checkQueryResult(prefixedUri, selector);
+        }
+    }
+
+    private class OnDragUriTakePersistableListener extends OnDragUriListener {
+        OnDragUriTakePersistableListener() {
+            super(true);
+        }
+
+        @Override
+        protected String processUri(Uri uri) {
+            getContentResolver().takePersistableUriPermission(
+                    uri, View.DRAG_FLAG_GLOBAL_URI_READ);
+            getContentResolver().releasePersistableUriPermission(
+                    uri, View.DRAG_FLAG_GLOBAL_URI_READ);
+            return RESULT_OK;
+        }
+    }
+}
diff --git a/hostsidetests/services/windowmanager/src/android/wm/cts/CrossAppDragAndDropTests.java b/hostsidetests/services/windowmanager/src/android/wm/cts/CrossAppDragAndDropTests.java
new file mode 100644
index 0000000..5dc789a
--- /dev/null
+++ b/hostsidetests/services/windowmanager/src/android/wm/cts/CrossAppDragAndDropTests.java
@@ -0,0 +1,334 @@
+/*
+ * 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.wm.cts;
+
+import com.android.tradefed.device.CollectingOutputReceiver;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.testtype.DeviceTestCase;
+
+import java.util.HashMap;
+import java.util.Map;
+
+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;
+
+    private static final String AM_FORCE_STOP = "am force-stop ";
+    private static final String AM_MOVE_TASK = "am stack movetask ";
+    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";
+
+    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 SOURCE_ACTIVITY_NAME = "DragSource";
+    private static final String TARGET_ACTIVITY_NAME = "DropTarget";
+
+    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 TARGET_LOG_TAG = "DropTarget";
+
+    private static final String RESULT_KEY_DRAG_STARTED = "DRAG_STARTED";
+    private static final String RESULT_KEY_EXTRAS = "EXTRAS";
+    private static final String RESULT_KEY_DROP_RESULT = "DROP";
+
+    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 ITestDevice mDevice;
+
+    private Map<String, String> mResults;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mDevice = getDevice();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        mDevice.executeShellCommand(AM_FORCE_STOP + SOURCE_PACKAGE_NAME);
+        mDevice.executeShellCommand(AM_FORCE_STOP + TARGET_PACKAGE_NAME);
+    }
+
+    private String adbShell(String command) throws DeviceNotAvailableException {
+        return mDevice.executeShellCommand(command);
+    }
+
+    private void clearLogs() throws DeviceNotAvailableException {
+        adbShell("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 getComponentName(String packageName, String activityName) {
+        return packageName + "/" + packageName + "." + activityName;
+    }
+
+    private void launchDockedActivity(String packageName, String activityName, String mode)
+            throws Exception {
+        clearLogs();
+        final String componentName = getComponentName(packageName, activityName);
+        adbShell(getStartCommand(componentName, mode));
+        final int taskId = getActivityTaskId(componentName);
+        adbShell(getMoveTaskCommand(taskId, DOCKED_STACK_ID));
+        waitForResume(packageName, activityName);
+    }
+
+    private void launchFreeformActivity(String packageName, String activityName, String mode)
+            throws Exception {
+        clearLogs();
+        final String componentName = getComponentName(packageName, activityName);
+        adbShell(getStartCommand(componentName, mode) + " --stack " + FREEFORM_WORKSPACE_STACK_ID);
+        waitForResume(packageName, activityName);
+    }
+
+    private void waitForResume(String packageName, String activityName) throws Exception {
+        final String fullActivityName = packageName + "." + activityName;
+        int retryCount = 3;
+        do {
+            String logs = adbShell("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 {
+        adbShell(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();
+        for (String line : output.split("\\n")) {
+            if (line.contains(name)) {
+                return line;
+            }
+        }
+        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 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 Map<String, String> getLogResults(String className) throws Exception {
+        int retryCount = 3;
+        Map<String, String> output = new HashMap<String, String>();
+        do {
+
+            String logs = adbShell("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(RESULT_KEY_DROP_RESULT)) {
+                return output;
+            }
+        } while (retryCount-- > 0);
+        return output;
+    }
+
+    private void doTestDragAndDrop(String sourceMode, String targetMode, String expectedDropResult)
+            throws Exception {
+
+        launchDockedActivity(SOURCE_PACKAGE_NAME, SOURCE_ACTIVITY_NAME, sourceMode);
+        launchFreeformActivity(TARGET_PACKAGE_NAME, TARGET_ACTIVITY_NAME, targetMode);
+
+        clearLogs();
+
+        injectInput(
+                getWindowCenter(getComponentName(SOURCE_PACKAGE_NAME, SOURCE_ACTIVITY_NAME)),
+                getWindowCenter(getComponentName(TARGET_PACKAGE_NAME, TARGET_ACTIVITY_NAME)),
+                SWIPE_DURATION_MS);
+
+        mResults = getLogResults(TARGET_LOG_TAG);
+        assertResult(RESULT_KEY_DROP_RESULT, expectedDropResult);
+    }
+
+
+    private void assertResult(String resultKey, String expectedResult) {
+        if (expectedResult == null) {
+            if (mResults.containsKey(resultKey)) {
+                fail("Unexpected " + resultKey + "=" + mResults.get(resultKey));
+            }
+        } else {
+            assertTrue("Missing " + resultKey, mResults.containsKey(resultKey));
+            assertEquals(expectedResult, mResults.get(resultKey));
+        }
+    }
+
+    public void testCancelSoon() throws Exception {
+        doTestDragAndDrop(CANCEL_SOON, REQUEST_NONE, null);
+        assertResult(RESULT_KEY_DRAG_STARTED, RESULT_OK);
+        assertResult(RESULT_KEY_EXTRAS, RESULT_OK);
+    }
+
+    public void testDisallowGlobal() throws Exception {
+        doTestDragAndDrop(DISALLOW_GLOBAL, REQUEST_NONE, null);
+        assertResult(RESULT_KEY_DRAG_STARTED, null);
+    }
+
+    public void testGrantNoneRequestNone() throws Exception {
+        doTestDragAndDrop(GRANT_NONE, REQUEST_NONE, RESULT_EXCEPTION);
+        assertResult(RESULT_KEY_DRAG_STARTED, RESULT_OK);
+        assertResult(RESULT_KEY_EXTRAS, RESULT_OK);
+    }
+
+    public void testGrantNoneRequestRead() throws Exception {
+        doTestDragAndDrop(GRANT_NONE, REQUEST_READ, RESULT_NULL_DROP_PERMISSIONS);
+    }
+
+    public void testGrantNoneRequestWrite() throws Exception {
+        doTestDragAndDrop(GRANT_NONE, REQUEST_WRITE, RESULT_NULL_DROP_PERMISSIONS);
+    }
+
+    public void testGrantReadRequestNone() throws Exception {
+        doTestDragAndDrop(GRANT_READ, REQUEST_NONE, RESULT_EXCEPTION);
+    }
+
+    public void testGrantReadRequestRead() throws Exception {
+        doTestDragAndDrop(GRANT_READ, REQUEST_READ, RESULT_OK);
+    }
+
+    public void testGrantReadRequestWrite() throws Exception {
+        doTestDragAndDrop(GRANT_READ, REQUEST_WRITE, RESULT_EXCEPTION);
+    }
+
+    public void testGrantReadNoPrefixRequestReadNested() throws Exception {
+        doTestDragAndDrop(GRANT_READ_NOPREFIX, REQUEST_READ_NESTED, RESULT_EXCEPTION);
+    }
+
+    public void testGrantReadPrefixRequestReadNested() throws Exception {
+        doTestDragAndDrop(GRANT_READ_PREFIX, REQUEST_READ_NESTED, RESULT_OK);
+    }
+
+    public void testGrantPersistableRequestTakePersistable() throws Exception {
+        doTestDragAndDrop(GRANT_READ_PERSISTABLE, REQUEST_TAKE_PERSISTABLE, RESULT_OK);
+    }
+
+    public void testGrantReadRequestTakePersistable() throws Exception {
+        doTestDragAndDrop(GRANT_READ, REQUEST_TAKE_PERSISTABLE, RESULT_EXCEPTION);
+    }
+
+    public void testGrantWriteRequestNone() throws Exception {
+        doTestDragAndDrop(GRANT_WRITE, REQUEST_NONE, RESULT_EXCEPTION);
+    }
+
+    public void testGrantWriteRequestRead() throws Exception {
+        doTestDragAndDrop(GRANT_WRITE, REQUEST_READ, RESULT_EXCEPTION);
+    }
+
+    public void testGrantWriteRequestWrite() throws Exception {
+        doTestDragAndDrop(GRANT_WRITE, REQUEST_WRITE, RESULT_OK);
+    }
+}