Add accessibility tests for drag and drop

Bug: 26871588
Test: atest AccessibilityDragAndDropTest
Change-Id: I8587811e8920c9a063111a64fc5f589c42568f4f
diff --git a/tests/accessibilityservice/AndroidManifest.xml b/tests/accessibilityservice/AndroidManifest.xml
index adf2f4f..84981cd 100644
--- a/tests/accessibilityservice/AndroidManifest.xml
+++ b/tests/accessibilityservice/AndroidManifest.xml
@@ -74,6 +74,10 @@
              android:theme="@android:style/Theme.Dialog"
              android:screenOrientation="locked"/>
 
+        <activity android:label="@string/accessibility_drag_and_drop_test_activity"
+                  android:name=".activities.AccessibilityDragAndDropActivity"
+                  android:screenOrientation="locked"/>
+
         <service android:name=".StubSystemActionsAccessibilityService"
              android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
              android:exported="true">
diff --git a/tests/accessibilityservice/res/layout/accessibility_drag_and_drop.xml b/tests/accessibilityservice/res/layout/accessibility_drag_and_drop.xml
new file mode 100644
index 0000000..93d52a3
--- /dev/null
+++ b/tests/accessibilityservice/res/layout/accessibility_drag_and_drop.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+     Copyright 2021 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">
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="center"
+            android:orientation="horizontal">
+            <TextView
+                android:id="@+id/source"
+                android:text="@string/drag_and_drop_text"
+                android:layout_width="60dp"
+                android:layout_height="60dp"
+                android:layout_margin="60dp"/>
+            <TextView
+                android:id="@+id/target"
+                android:layout_width="60dp"
+                android:layout_height="60dp"
+                android:layout_margin="60dp"/>
+        </LinearLayout>
+</LinearLayout>
diff --git a/tests/accessibilityservice/res/values/strings.xml b/tests/accessibilityservice/res/values/strings.xml
index dd59c0b..fde3d2e 100644
--- a/tests/accessibilityservice/res/values/strings.xml
+++ b/tests/accessibilityservice/res/values/strings.xml
@@ -195,4 +195,8 @@
     <!-- String title of embedded display activity -->
     <string name="non_default_display_activity">Non default display activity</string>
 
+    <!-- String title of the accessibility drag & drop activity -->
+    <string name="accessibility_drag_and_drop_test_activity">Drag and drop</string>
+    <string name="drag_and_drop_text">Dragged text</string>
+
 </resources>
diff --git a/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityDragAndDropTest.java b/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityDragAndDropTest.java
new file mode 100644
index 0000000..b876dc0
--- /dev/null
+++ b/tests/accessibilityservice/src/android/accessibilityservice/cts/AccessibilityDragAndDropTest.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2021 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.accessibilityservice.cts;
+
+import static android.accessibilityservice.cts.utils.ActivityLaunchUtils.launchActivityAndWaitForItToBeOnscreen;
+import static android.accessibilityservice.cts.utils.AsyncUtils.DEFAULT_TIMEOUT_MS;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.accessibility.cts.common.AccessibilityDumpOnFailureRule;
+import android.accessibilityservice.cts.activities.AccessibilityDragAndDropActivity;
+import android.accessibilityservice.cts.utils.AccessibilityEventFilterUtils;
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.TextView;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.rule.ActivityTestRule;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+
+public class AccessibilityDragAndDropTest {
+    private static Instrumentation sInstrumentation;
+    private static UiAutomation sUiAutomation;
+
+    private AccessibilityDragAndDropActivity mActivity;
+    private TextView mSourceView;
+    private UiAutomation.AccessibilityEventFilter mDragStartedFilter =
+            AccessibilityEventFilterUtils.filterWindowContentChangedWithChangeTypes(
+                    AccessibilityEvent.CONTENT_CHANGE_TYPE_DRAG_STARTED);
+
+    private ActivityTestRule<AccessibilityDragAndDropActivity> mActivityRule =
+            new ActivityTestRule<>(AccessibilityDragAndDropActivity.class, false, false);
+
+    private AccessibilityDumpOnFailureRule mDumpOnFailureRule =
+            new AccessibilityDumpOnFailureRule();
+
+    @Rule
+    public final RuleChain mRuleChain = RuleChain
+            .outerRule(mActivityRule)
+            .around(mDumpOnFailureRule);
+
+    @BeforeClass
+    public static void oneTimeSetup() throws Exception {
+        sInstrumentation = InstrumentationRegistry.getInstrumentation();
+        sUiAutomation = sInstrumentation.getUiAutomation();
+    }
+
+    @AfterClass
+    public static void postTestTearDown() {
+        sUiAutomation.destroy();
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        mActivity = launchActivityAndWaitForItToBeOnscreen(
+                sInstrumentation, sUiAutomation, mActivityRule);
+        mSourceView = mActivity.findViewById(R.id.source);
+    }
+
+    @After
+    public void tearDown() {
+        // Reset system drag state
+        mSourceView.cancelDragAndDrop();
+    }
+
+    @Test
+    public void testStartDrag_eventSentAndActionsUpdated() throws Throwable {
+        AccessibilityEvent startEvent = performActionAndWaitForEvent(mSourceView,
+                AccessibilityNodeInfo.AccessibilityAction.ACTION_DRAG_START, mDragStartedFilter);
+        assertNotNull("Did not receive CONTENT_CHANGE_TYPE_DRAG_STARTED", startEvent);
+
+        final AccessibilityNodeInfo sourceNode = getSourceNode();
+        assertNodeAction(sourceNode, AccessibilityNodeInfo.AccessibilityAction.ACTION_DRAG_CANCEL);
+
+        final AccessibilityNodeInfo targetNode = getTargetNode();
+        assertNodeAction(targetNode, AccessibilityNodeInfo.AccessibilityAction.ACTION_DRAG_DROP);
+    }
+
+    @Test
+    public void testCancelDrag_eventSentAndActionsUpdated() throws Throwable {
+        performActionAndWaitForEvent(mSourceView,
+                AccessibilityNodeInfo.AccessibilityAction.ACTION_DRAG_START,
+                mDragStartedFilter);
+
+        AccessibilityEvent cancelEvent = performActionAndWaitForEvent(mSourceView,
+                AccessibilityNodeInfo.AccessibilityAction.ACTION_DRAG_CANCEL,
+                AccessibilityEventFilterUtils.filterWindowContentChangedWithChangeTypes(
+                        AccessibilityEvent.CONTENT_CHANGE_TYPE_DRAG_CANCELLED));
+
+        assertNotNull("Did not receive CONTENT_CHANGE_TYPE_DRAG_CANCELLED",
+                cancelEvent);
+
+        final AccessibilityNodeInfo sourceNode = getSourceNode();
+        assertNoNodeAction(sourceNode,
+                AccessibilityNodeInfo.AccessibilityAction.ACTION_DRAG_CANCEL);
+        assertNodeAction(sourceNode, AccessibilityNodeInfo.AccessibilityAction.ACTION_DRAG_START);
+
+        final AccessibilityNodeInfo targetNode = getTargetNode();
+        assertNoNodeAction(targetNode, AccessibilityNodeInfo.AccessibilityAction.ACTION_DRAG_DROP);
+    }
+
+    @Test
+    public void testDrop_eventSentAndActionsUpdated() throws Throwable {
+        performActionAndWaitForEvent(mSourceView,
+                AccessibilityNodeInfo.AccessibilityAction.ACTION_DRAG_START, mDragStartedFilter);
+        final TextView target = mActivity.findViewById(R.id.target);
+        AccessibilityEvent dropEvent = performActionAndWaitForEvent(target,
+                AccessibilityNodeInfo.AccessibilityAction.ACTION_DRAG_DROP,
+                AccessibilityEventFilterUtils.filterWindowContentChangedWithChangeTypes(
+                        AccessibilityEvent.CONTENT_CHANGE_TYPE_DRAG_DROPPED));
+
+        assertNotNull("Did not receive CONTENT_CHANGE_TYPE_DRAG_DROPPED",
+                dropEvent);
+
+        final AccessibilityNodeInfo targetNode = getTargetNode();
+        assertEquals("Target text was: " + targetNode.getText(), mSourceView.getText(),
+                targetNode.getText());
+        assertNoNodeAction(targetNode, AccessibilityNodeInfo.AccessibilityAction.ACTION_DRAG_DROP);
+
+        final AccessibilityNodeInfo sourceNode = getSourceNode();
+        assertNoNodeAction(sourceNode,
+                AccessibilityNodeInfo.AccessibilityAction.ACTION_DRAG_CANCEL);
+        assertNodeAction(sourceNode, AccessibilityNodeInfo.AccessibilityAction.ACTION_DRAG_START);
+    }
+
+    private AccessibilityEvent performActionAndWaitForEvent(View view,
+            AccessibilityNodeInfo.AccessibilityAction action,
+            UiAutomation.AccessibilityEventFilter filter) throws Throwable {
+        AccessibilityEvent awaitedEvent =
+                sUiAutomation.executeAndWaitForEvent(
+                        () -> {
+                            mActivity.runOnUiThread(new Runnable() {
+                                @Override
+                                public void run() {
+                                    view.performAccessibilityAction(action.getId(), null);
+                                }
+                            });
+                        },
+                        filter,
+                        DEFAULT_TIMEOUT_MS);
+        return awaitedEvent;
+    }
+
+    private AccessibilityNodeInfo getSourceNode() {
+        return sUiAutomation.getRootInActiveWindow()
+                .findAccessibilityNodeInfosByViewId(
+                        "android.accessibilityservice.cts:id/source").get(0);
+    }
+
+    private AccessibilityNodeInfo getTargetNode() {
+        return sUiAutomation.getRootInActiveWindow()
+                .findAccessibilityNodeInfosByViewId(
+                        "android.accessibilityservice.cts:id/target").get(0);
+    }
+
+    private void assertNoNodeAction(
+            AccessibilityNodeInfo info, AccessibilityNodeInfo.AccessibilityAction action) {
+        assertFalse("Node has action: " + action.toString(),
+                info.getActionList().contains(action));
+    }
+
+    private void assertNodeAction(
+            AccessibilityNodeInfo info, AccessibilityNodeInfo.AccessibilityAction action) {
+        assertTrue("Node does not have action: " + action.toString(),
+                info.getActionList().contains(action));
+    }
+}
diff --git a/tests/accessibilityservice/src/android/accessibilityservice/cts/activities/AccessibilityDragAndDropActivity.java b/tests/accessibilityservice/src/android/accessibilityservice/cts/activities/AccessibilityDragAndDropActivity.java
new file mode 100644
index 0000000..ed5cad1
--- /dev/null
+++ b/tests/accessibilityservice/src/android/accessibilityservice/cts/activities/AccessibilityDragAndDropActivity.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2021 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.accessibilityservice.cts.activities;
+
+import android.accessibilityservice.cts.R;
+import android.content.ClipData;
+import android.os.Bundle;
+import android.view.DragEvent;
+import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.TextView;
+
+public class AccessibilityDragAndDropActivity extends AccessibilityTestActivity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.accessibility_drag_and_drop);
+        final TextView text = findViewById(R.id.source);
+
+        View.AccessibilityDelegate delegate = new View.AccessibilityDelegate() {
+            @Override
+            public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+                info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_DRAG_START);
+                super.onInitializeAccessibilityNodeInfo(host, info);
+            }
+
+            @Override
+            public boolean performAccessibilityAction(View host, int action, Bundle args) {
+                boolean result = super.performAccessibilityAction(host, action, args);
+                if (action == android.R.id.accessibilityActionDragStart) {
+                    final ClipData clipData = ClipData.newPlainText("Text", text.getText());
+                    View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder();
+                    return host.startDragAndDrop(clipData, shadowBuilder, null,
+                            View.DRAG_FLAG_ACCESSIBILITY_ACTION);
+                }
+                return result;
+            }
+        };
+
+        text.setAccessibilityDelegate(delegate);
+        final TextView target = findViewById(R.id.target);
+
+        View.OnDragListener dragListener = new View.OnDragListener() {
+            @Override
+            public boolean onDrag(View v, DragEvent event) {
+                switch (event.getAction()) {
+                    case DragEvent.ACTION_DRAG_STARTED:
+                    case DragEvent.ACTION_DRAG_ENTERED:
+                    case DragEvent.ACTION_DRAG_LOCATION:
+                    case DragEvent.ACTION_DRAG_EXITED:
+                    case DragEvent.ACTION_DRAG_ENDED:
+                        return true;
+                    case DragEvent.ACTION_DROP:
+                        ((TextView) v).setText(event.getClipData().getItemAt(0).getText());
+                        return true;
+
+                    default:
+                        break;
+                }
+                return false;
+            }
+        };
+        target.setOnDragListener(dragListener);
+    }
+}
diff --git a/tests/accessibilityservice/src/android/accessibilityservice/cts/utils/AccessibilityEventFilterUtils.java b/tests/accessibilityservice/src/android/accessibilityservice/cts/utils/AccessibilityEventFilterUtils.java
index 7f1cd37..57d1103 100644
--- a/tests/accessibilityservice/src/android/accessibilityservice/cts/utils/AccessibilityEventFilterUtils.java
+++ b/tests/accessibilityservice/src/android/accessibilityservice/cts/utils/AccessibilityEventFilterUtils.java
@@ -38,6 +38,12 @@
         return (new AccessibilityEventTypeMatcher(eventType))::matches;
     }
 
+    public static AccessibilityEventFilter filterWindowContentChangedWithChangeTypes(int changes) {
+        return (both(new AccessibilityEventTypeMatcher(
+                AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED)).and(
+                        new ContentChangesMatcher(changes)))::matches;
+    }
+
     public static AccessibilityEventFilter filterWindowsChangedWithChangeTypes(int changes) {
         return (both(new AccessibilityEventTypeMatcher(AccessibilityEvent.TYPE_WINDOWS_CHANGED))
                         .and(new WindowChangesMatcher(changes)))::matches;