Merge "Tests for DocumentsProvider and DocumentsUI." into lmp-dev
diff --git a/CtsTestCaseList.mk b/CtsTestCaseList.mk
index 2215600..ce67d37 100644
--- a/CtsTestCaseList.mk
+++ b/CtsTestCaseList.mk
@@ -15,6 +15,8 @@
cts_security_apps_list := \
CtsAppAccessData \
CtsAppWithData \
+ CtsDocumentProvider \
+ CtsDocumentClient \
CtsExternalStorageApp \
CtsInstrumentationAppDiffCert \
CtsPermissionDeclareApp \
diff --git a/hostsidetests/appsecurity/src/com/android/cts/appsecurity/DocumentsTest.java b/hostsidetests/appsecurity/src/com/android/cts/appsecurity/DocumentsTest.java
new file mode 100644
index 0000000..fbde558
--- /dev/null
+++ b/hostsidetests/appsecurity/src/com/android/cts/appsecurity/DocumentsTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2014 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 com.android.cts.appsecurity;
+
+import com.android.cts.tradefed.build.CtsBuildHelper;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.testtype.IAbi;
+import com.android.tradefed.testtype.IAbiReceiver;
+import com.android.tradefed.testtype.IBuildReceiver;
+
+public class DocumentsTest extends DeviceTestCase implements IAbiReceiver, IBuildReceiver {
+ private static final String PROVIDER_PKG = "com.android.cts.documentprovider";
+ private static final String PROVIDER_APK = "CtsDocumentProvider.apk";
+
+ private static final String CLIENT_PKG = "com.android.cts.documentclient";
+ private static final String CLIENT_APK = "CtsDocumentClient.apk";
+
+ private IAbi mAbi;
+ private CtsBuildHelper mCtsBuild;
+
+ @Override
+ public void setAbi(IAbi abi) {
+ mAbi = abi;
+ }
+
+ @Override
+ public void setBuild(IBuildInfo buildInfo) {
+ mCtsBuild = CtsBuildHelper.createBuildHelper(buildInfo);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ assertNotNull(mAbi);
+ assertNotNull(mCtsBuild);
+
+ getDevice().uninstallPackage(PROVIDER_PKG);
+ getDevice().uninstallPackage(CLIENT_PKG);
+
+ assertNull(getDevice().installPackage(mCtsBuild.getTestApp(PROVIDER_APK), false));
+ assertNull(getDevice().installPackage(mCtsBuild.getTestApp(CLIENT_APK), false));
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+
+ getDevice().uninstallPackage(PROVIDER_PKG);
+ getDevice().uninstallPackage(CLIENT_PKG);
+ }
+
+ public void testOpenSimple() throws Exception {
+ runDeviceTests(CLIENT_PKG, ".DocumentsClientTest", "testOpenSimple");
+ }
+
+ public void testCreateNew() throws Exception {
+ runDeviceTests(CLIENT_PKG, ".DocumentsClientTest", "testCreateNew");
+ }
+
+ public void testCreateExisting() throws Exception {
+ runDeviceTests(CLIENT_PKG, ".DocumentsClientTest", "testCreateExisting");
+ }
+
+ public void testTree() throws Exception {
+ runDeviceTests(CLIENT_PKG, ".DocumentsClientTest", "testTree");
+ }
+
+ public void testGetContent() throws Exception {
+ runDeviceTests(CLIENT_PKG, ".DocumentsClientTest", "testGetContent");
+ }
+
+ public void runDeviceTests(String packageName, String testClassName, String testMethodName)
+ throws DeviceNotAvailableException {
+ Utils.runDeviceTests(getDevice(), packageName, testClassName, testMethodName);
+ }
+}
diff --git a/hostsidetests/appsecurity/src/com/android/cts/appsecurity/SplitTests.java b/hostsidetests/appsecurity/src/com/android/cts/appsecurity/SplitTests.java
index b9bc768..264c0b1 100644
--- a/hostsidetests/appsecurity/src/com/android/cts/appsecurity/SplitTests.java
+++ b/hostsidetests/appsecurity/src/com/android/cts/appsecurity/SplitTests.java
@@ -18,15 +18,9 @@
import com.android.cts.tradefed.build.CtsBuildHelper;
import com.android.cts.util.AbiUtils;
-import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
-import com.android.ddmlib.testrunner.TestIdentifier;
-import com.android.ddmlib.testrunner.TestResult;
-import com.android.ddmlib.testrunner.TestResult.TestStatus;
-import com.android.ddmlib.testrunner.TestRunResult;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.result.CollectingTestListener;
import com.android.tradefed.testtype.DeviceTestCase;
import com.android.tradefed.testtype.IAbi;
import com.android.tradefed.testtype.IAbiReceiver;
@@ -37,7 +31,6 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
-import java.util.Map;
/**
* Tests that verify installing of various split APKs from host side.
@@ -355,43 +348,8 @@
}
}
- public void runDeviceTests(String packageName) throws DeviceNotAvailableException {
- runDeviceTests(packageName, null, null);
- }
-
public void runDeviceTests(String packageName, String testClassName, String testMethodName)
throws DeviceNotAvailableException {
- if (testClassName != null && testClassName.startsWith(".")) {
- testClassName = packageName + testClassName;
- }
-
- RemoteAndroidTestRunner testRunner = new RemoteAndroidTestRunner(packageName,
- "android.support.test.runner.AndroidJUnitRunner", getDevice().getIDevice());
- if (testClassName != null && testMethodName != null) {
- testRunner.setMethodName(testClassName, testMethodName);
- }
-
- final CollectingTestListener listener = new CollectingTestListener();
- getDevice().runInstrumentationTests(testRunner, listener);
-
- final TestRunResult result = listener.getCurrentRunResults();
- if (result.isRunFailure()) {
- fail("Failed to successfully run device tests for " + result.getName() + ": "
- + result.getRunFailureMessage());
- }
-
- if (result.hasFailedTests()) {
- // build a meaningful error message
- StringBuilder errorBuilder = new StringBuilder("on-device tests failed:\n");
- for (Map.Entry<TestIdentifier, TestResult> resultEntry :
- result.getTestResults().entrySet()) {
- if (!resultEntry.getValue().getStatus().equals(TestStatus.PASSED)) {
- errorBuilder.append(resultEntry.getKey().toString());
- errorBuilder.append(":\n");
- errorBuilder.append(resultEntry.getValue().getStackTrace());
- }
- }
- fail(errorBuilder.toString());
- }
+ Utils.runDeviceTests(getDevice(), packageName, testClassName, testMethodName);
}
}
diff --git a/hostsidetests/appsecurity/src/com/android/cts/appsecurity/Utils.java b/hostsidetests/appsecurity/src/com/android/cts/appsecurity/Utils.java
new file mode 100644
index 0000000..c58d6bf
--- /dev/null
+++ b/hostsidetests/appsecurity/src/com/android/cts/appsecurity/Utils.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2014 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 com.android.cts.appsecurity;
+
+import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ddmlib.testrunner.TestResult;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.ddmlib.testrunner.TestRunResult;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.result.CollectingTestListener;
+
+import java.util.Map;
+
+public class Utils {
+ public static void runDeviceTests(ITestDevice device, String packageName)
+ throws DeviceNotAvailableException {
+ runDeviceTests(device, packageName, null, null);
+ }
+
+ public static void runDeviceTests(ITestDevice device, String packageName, String testClassName,
+ String testMethodName) throws DeviceNotAvailableException {
+ if (testClassName != null && testClassName.startsWith(".")) {
+ testClassName = packageName + testClassName;
+ }
+
+ RemoteAndroidTestRunner testRunner = new RemoteAndroidTestRunner(packageName,
+ "android.support.test.runner.AndroidJUnitRunner", device.getIDevice());
+ if (testClassName != null && testMethodName != null) {
+ testRunner.setMethodName(testClassName, testMethodName);
+ }
+
+ final CollectingTestListener listener = new CollectingTestListener();
+ device.runInstrumentationTests(testRunner, listener);
+
+ final TestRunResult result = listener.getCurrentRunResults();
+ if (result.isRunFailure()) {
+ throw new AssertionError("Failed to successfully run device tests for "
+ + result.getName() + ": " + result.getRunFailureMessage());
+ }
+
+ if (result.hasFailedTests()) {
+ // build a meaningful error message
+ StringBuilder errorBuilder = new StringBuilder("on-device tests failed:\n");
+ for (Map.Entry<TestIdentifier, TestResult> resultEntry :
+ result.getTestResults().entrySet()) {
+ if (!resultEntry.getValue().getStatus().equals(TestStatus.PASSED)) {
+ errorBuilder.append(resultEntry.getKey().toString());
+ errorBuilder.append(":\n");
+ errorBuilder.append(resultEntry.getValue().getStackTrace());
+ }
+ }
+ throw new AssertionError(errorBuilder.toString());
+ }
+ }
+}
diff --git a/hostsidetests/appsecurity/test-apps/DocumentClient/Android.mk b/hostsidetests/appsecurity/test-apps/DocumentClient/Android.mk
new file mode 100644
index 0000000..910e3cd
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/DocumentClient/Android.mk
@@ -0,0 +1,34 @@
+#
+# Copyright (C) 2014 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
+LOCAL_SDK_VERSION := current
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-test ctsdeviceutil ctstestrunner ub-uiautomator
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := CtsDocumentClient
+
+LOCAL_CERTIFICATE := cts/hostsidetests/appsecurity/certs/cts-testkey2
+
+LOCAL_PROGUARD_ENABLED := disabled
+LOCAL_DEX_PREOPT := false
+
+include $(BUILD_PACKAGE)
diff --git a/hostsidetests/appsecurity/test-apps/DocumentClient/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/DocumentClient/AndroidManifest.xml
new file mode 100644
index 0000000..0064e15
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/DocumentClient/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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="com.android.cts.documentclient">
+ <application>
+ <uses-library android:name="android.test.runner" />
+ <activity android:name=".MyActivity" />
+ </application>
+ <instrumentation
+ android:name="android.support.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.cts.documentclient" />
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/DocumentClient/src/com/android/cts/documentclient/DocumentsClientTest.java b/hostsidetests/appsecurity/test-apps/DocumentClient/src/com/android/cts/documentclient/DocumentsClientTest.java
new file mode 100644
index 0000000..83187c7
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/DocumentClient/src/com/android/cts/documentclient/DocumentsClientTest.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright (C) 2014 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 com.android.cts.documentclient;
+
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.SystemClock;
+import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.Document;
+import android.provider.DocumentsProvider;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject;
+import android.support.test.uiautomator.UiSelector;
+import android.test.InstrumentationTestCase;
+import android.test.MoreAsserts;
+
+import com.android.cts.documentclient.MyActivity.Result;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Tests for {@link DocumentsProvider} and interaction with platform intents
+ * like {@link Intent#ACTION_OPEN_DOCUMENT}.
+ */
+public class DocumentsClientTest extends InstrumentationTestCase {
+ private UiDevice mDevice;
+ private MyActivity mActivity;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mActivity = launchActivity(getInstrumentation().getTargetContext().getPackageName(),
+ MyActivity.class, null);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+ mActivity.finish();
+ }
+
+ public void testOpenSimple() throws Exception {
+ if (!supportedHardware()) return;
+
+ try {
+ // Opening without permission should fail
+ readFully(Uri.parse("content://com.android.cts.documentprovider/document/doc:file1"));
+ fail("Able to read data before opened!");
+ } catch (SecurityException expected) {
+ }
+
+ final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.setType("*/*");
+ mActivity.startActivityForResult(intent, 42);
+
+ // Ensure that we see both of our roots
+ assertTrue("CtsLocal root", new UiObject(new UiSelector().text("CtsLocal")).exists());
+ assertTrue("CtsCreate root", new UiObject(new UiSelector().text("CtsCreate")).exists());
+ assertFalse("CtsGetContent", new UiObject(new UiSelector().text("CtsGetContent")).exists());
+
+ // Pick a specific file from our test provider
+ mDevice.waitForIdle();
+ new UiObject(new UiSelector().text("CtsLocal")).click();
+
+ // make sure drawer is expanded?
+
+ mDevice.waitForIdle();
+ new UiObject(new UiSelector().text("FILE1")).click();
+
+ final Result result = mActivity.getResult();
+ final Uri uri = result.data.getData();
+
+ // We should now have permission to read/write
+ MoreAsserts.assertEquals("fileone".getBytes(), readFully(uri));
+
+ writeFully(uri, "replaced!".getBytes());
+ SystemClock.sleep(500);
+ MoreAsserts.assertEquals("replaced!".getBytes(), readFully(uri));
+ }
+
+ public void testCreateNew() throws Exception {
+ if (!supportedHardware()) return;
+
+ final String DISPLAY_NAME = "My New Awesome Document Title";
+ final String MIME_TYPE = "image/png";
+
+ final Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.putExtra(Intent.EXTRA_TITLE, DISPLAY_NAME);
+ intent.setType(MIME_TYPE);
+ mActivity.startActivityForResult(intent, 42);
+
+ mDevice.waitForIdle();
+ new UiObject(new UiSelector().text("CtsCreate")).click();
+ mDevice.waitForIdle();
+ new UiObject(new UiSelector().resourceId("com.android.documentsui:id/container_save")
+ .childSelector(new UiSelector().resourceId("android:id/button1"))).click();
+
+ final Result result = mActivity.getResult();
+ final Uri uri = result.data.getData();
+
+ writeFully(uri, "meow!".getBytes());
+
+ assertEquals(DISPLAY_NAME, getColumn(uri, Document.COLUMN_DISPLAY_NAME));
+ assertEquals(MIME_TYPE, getColumn(uri, Document.COLUMN_MIME_TYPE));
+ }
+
+ public void testCreateExisting() throws Exception {
+ if (!supportedHardware()) return;
+
+ final Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.putExtra(Intent.EXTRA_TITLE, "NEVERUSED");
+ intent.setType("mime2/file2");
+ mActivity.startActivityForResult(intent, 42);
+
+ mDevice.waitForIdle();
+ new UiObject(new UiSelector().text("CtsCreate")).click();
+
+ // Pick file2, which should be selected since MIME matches, then try
+ // picking a non-matching MIME, which should leave file2 selected.
+ mDevice.waitForIdle();
+ new UiObject(new UiSelector().text("FILE2")).click();
+ mDevice.waitForIdle();
+ new UiObject(new UiSelector().text("FILE1")).click();
+
+ new UiObject(new UiSelector().resourceId("com.android.documentsui:id/container_save")
+ .childSelector(new UiSelector().resourceId("android:id/button1"))).click();
+
+ final Result result = mActivity.getResult();
+ final Uri uri = result.data.getData();
+
+ MoreAsserts.assertEquals("filetwo".getBytes(), readFully(uri));
+ }
+
+ public void testTree() throws Exception {
+ if (!supportedHardware()) return;
+
+ final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
+ mActivity.startActivityForResult(intent, 42);
+
+ mDevice.waitForIdle();
+ new UiObject(new UiSelector().text("CtsCreate")).click();
+ mDevice.waitForIdle();
+ new UiObject(new UiSelector().text("DIR2")).click();
+ mDevice.waitForIdle();
+ new UiObject(new UiSelector().resourceId("com.android.documentsui:id/container_save")
+ .childSelector(new UiSelector().resourceId("android:id/button1"))).click();
+
+ final Result result = mActivity.getResult();
+ final Uri uri = result.data.getData();
+
+ // We should have selected DIR2
+ Uri doc = DocumentsContract.buildDocumentUriUsingTree(uri,
+ DocumentsContract.getTreeDocumentId(uri));
+ Uri children = DocumentsContract.buildChildDocumentsUriUsingTree(uri,
+ DocumentsContract.getTreeDocumentId(uri));
+
+ assertEquals("DIR2", getColumn(doc, Document.COLUMN_DISPLAY_NAME));
+
+ // Look around and make sure we can see children
+ final ContentResolver resolver = getInstrumentation().getContext().getContentResolver();
+ Cursor cursor = resolver.query(children, new String[] {
+ Document.COLUMN_DISPLAY_NAME }, null, null, null);
+ try {
+ assertEquals(1, cursor.getCount());
+ assertTrue(cursor.moveToFirst());
+ assertEquals("FILE4", cursor.getString(0));
+ } finally {
+ cursor.close();
+ }
+
+ // Create some documents
+ Uri pic = DocumentsContract.createDocument(resolver, doc, "image/png", "pic.png");
+ Uri dir = DocumentsContract.createDocument(resolver, doc, Document.MIME_TYPE_DIR, "my dir");
+ Uri dirPic = DocumentsContract.createDocument(resolver, dir, "image/png", "pic2.png");
+
+ writeFully(pic, "pic".getBytes());
+ writeFully(dirPic, "dirPic".getBytes());
+
+ // Read then delete existing doc
+ final Uri file4 = DocumentsContract.buildDocumentUriUsingTree(uri, "doc:file4");
+ MoreAsserts.assertEquals("filefour".getBytes(), readFully(file4));
+ assertTrue("delete", DocumentsContract.deleteDocument(resolver, file4));
+ try {
+ MoreAsserts.assertEquals("filefour".getBytes(), readFully(file4));
+ fail("Expected file to be gone");
+ } catch (FileNotFoundException expected) {
+ }
+
+ // And rename something
+ dirPic = DocumentsContract.renameDocument(resolver, dirPic, "wow");
+ assertNotNull("rename", dirPic);
+
+ // We should only see single child
+ assertEquals("wow", getColumn(dirPic, Document.COLUMN_DISPLAY_NAME));
+ MoreAsserts.assertEquals("dirPic".getBytes(), readFully(dirPic));
+
+ try {
+ // Make sure we can't see files outside selected dir
+ getColumn(DocumentsContract.buildDocumentUriUsingTree(uri, "doc:file1"),
+ Document.COLUMN_DISPLAY_NAME);
+ fail("Somehow read document outside tree!");
+ } catch (SecurityException expected) {
+ }
+ }
+
+ public void testGetContent() throws Exception {
+ if (!supportedHardware()) return;
+
+ final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.setType("*/*");
+ mActivity.startActivityForResult(intent, 42);
+
+ mDevice.waitForIdle();
+
+ // Look around, we should be able to see both DocumentsProviders and
+ // other GET_CONTENT sources.
+ assertTrue("CtsLocal root", new UiObject(new UiSelector().text("CtsLocal")).exists());
+ assertTrue("CtsCreate root", new UiObject(new UiSelector().text("CtsCreate")).exists());
+ assertTrue("CtsGetContent", new UiObject(new UiSelector().text("CtsGetContent")).exists());
+
+ new UiObject(new UiSelector().text("CtsGetContent")).click();
+ mDevice.waitForIdle();
+
+ final Result result = mActivity.getResult();
+ assertEquals("ReSuLt", result.data.getAction());
+ }
+
+ private String getColumn(Uri uri, String column) {
+ final ContentResolver resolver = getInstrumentation().getContext().getContentResolver();
+ final Cursor cursor = resolver.query(uri, new String[] { column }, null, null, null);
+ try {
+ assertTrue(cursor.moveToFirst());
+ return cursor.getString(0);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private byte[] readFully(Uri uri) throws IOException {
+ InputStream in = getInstrumentation().getContext().getContentResolver()
+ .openInputStream(uri);
+ try {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ byte[] buffer = new byte[1024];
+ int count;
+ while ((count = in.read(buffer)) != -1) {
+ bytes.write(buffer, 0, count);
+ }
+ return bytes.toByteArray();
+ } finally {
+ in.close();
+ }
+ }
+
+ private void writeFully(Uri uri, byte[] data) throws IOException {
+ OutputStream out = getInstrumentation().getContext().getContentResolver()
+ .openOutputStream(uri);
+ try {
+ out.write(data);
+ } finally {
+ out.close();
+ }
+ }
+
+ private boolean supportedHardware() {
+ final PackageManager pm = getInstrumentation().getContext().getPackageManager();
+ if (pm.hasSystemFeature("android.hardware.type.television")
+ || pm.hasSystemFeature("android.hardware.type.watch")) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/hostsidetests/appsecurity/test-apps/DocumentClient/src/com/android/cts/documentclient/MyActivity.java b/hostsidetests/appsecurity/test-apps/DocumentClient/src/com/android/cts/documentclient/MyActivity.java
new file mode 100644
index 0000000..a6cb28d
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/DocumentClient/src/com/android/cts/documentclient/MyActivity.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2014 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 com.android.cts.documentclient;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.WindowManager;
+
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.TimeUnit;
+
+public class MyActivity extends Activity {
+ private final SynchronousQueue<Result> mResult = new SynchronousQueue<>();
+
+ public static class Result {
+ public final int resultCode;
+ public final Intent data;
+
+ public Result(int resultCode, Intent data) {
+ this.resultCode = resultCode;
+ this.data = data;
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+ | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+ | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ try {
+ mResult.offer(new Result(resultCode, data), 5, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public Result getResult() {
+ try {
+ return mResult.take();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/hostsidetests/appsecurity/test-apps/DocumentProvider/Android.mk b/hostsidetests/appsecurity/test-apps/DocumentProvider/Android.mk
new file mode 100644
index 0000000..a886fb2
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/DocumentProvider/Android.mk
@@ -0,0 +1,34 @@
+#
+# Copyright (C) 2014 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
+LOCAL_SDK_VERSION := current
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-test ctsdeviceutil ctstestrunner ub-uiautomator
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := CtsDocumentProvider
+
+LOCAL_CERTIFICATE := cts/hostsidetests/appsecurity/certs/cts-testkey1
+
+LOCAL_PROGUARD_ENABLED := disabled
+LOCAL_DEX_PREOPT := false
+
+include $(BUILD_PACKAGE)
diff --git a/hostsidetests/appsecurity/test-apps/DocumentProvider/AndroidManifest.xml b/hostsidetests/appsecurity/test-apps/DocumentProvider/AndroidManifest.xml
new file mode 100644
index 0000000..c0fc6cc
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/DocumentProvider/AndroidManifest.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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="com.android.cts.documentprovider">
+ <application>
+ <provider android:name=".MyDocumentsProvider"
+ android:authorities="com.android.cts.documentprovider"
+ android:exported="true"
+ android:grantUriPermissions="true"
+ android:permission="android.permission.MANAGE_DOCUMENTS">
+ <intent-filter>
+ <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
+ </intent-filter>
+ </provider>
+
+ <activity android:name=".GetContentActivity"
+ android:label="CtsGetContent">
+ <intent-filter>
+ <action android:name="android.intent.action.GET_CONTENT" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.OPENABLE" />
+ <data android:mimeType="image/*" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/hostsidetests/appsecurity/test-apps/DocumentProvider/src/com/android/cts/documentprovider/GetContentActivity.java b/hostsidetests/appsecurity/test-apps/DocumentProvider/src/com/android/cts/documentprovider/GetContentActivity.java
new file mode 100644
index 0000000..1aba526
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/DocumentProvider/src/com/android/cts/documentprovider/GetContentActivity.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2014 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 com.android.cts.documentprovider;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+
+public class GetContentActivity extends Activity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setResult(Activity.RESULT_OK, new Intent("ReSuLt"));
+ finish();
+ }
+}
diff --git a/hostsidetests/appsecurity/test-apps/DocumentProvider/src/com/android/cts/documentprovider/MyDocumentsProvider.java b/hostsidetests/appsecurity/test-apps/DocumentProvider/src/com/android/cts/documentprovider/MyDocumentsProvider.java
new file mode 100644
index 0000000..fb8993c
--- /dev/null
+++ b/hostsidetests/appsecurity/test-apps/DocumentProvider/src/com/android/cts/documentprovider/MyDocumentsProvider.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2014 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 com.android.cts.documentprovider;
+
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.database.MatrixCursor.RowBuilder;
+import android.os.AsyncTask;
+import android.os.CancellationSignal;
+import android.os.ParcelFileDescriptor;
+import android.provider.DocumentsContract.Document;
+import android.provider.DocumentsContract.Root;
+import android.provider.DocumentsProvider;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class MyDocumentsProvider extends DocumentsProvider {
+ private static final String TAG = "TestDocumentsProvider";
+
+ private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
+ Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
+ Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES,
+ };
+
+ private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
+ Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
+ Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
+ };
+
+ private static String[] resolveRootProjection(String[] projection) {
+ return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
+ }
+
+ private static String[] resolveDocumentProjection(String[] projection) {
+ return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
+ }
+
+ @Override
+ public boolean onCreate() {
+ resetRoots();
+ return true;
+ }
+
+ @Override
+ public Cursor queryRoots(String[] projection) throws FileNotFoundException {
+ final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
+
+ RowBuilder row = result.newRow();
+ row.add(Root.COLUMN_ROOT_ID, "local");
+ row.add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY);
+ row.add(Root.COLUMN_TITLE, "CtsLocal");
+ row.add(Root.COLUMN_SUMMARY, "CtsLocalSummary");
+ row.add(Root.COLUMN_DOCUMENT_ID, "doc:local");
+
+ row = result.newRow();
+ row.add(Root.COLUMN_ROOT_ID, "create");
+ row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_IS_CHILD);
+ row.add(Root.COLUMN_TITLE, "CtsCreate");
+ row.add(Root.COLUMN_DOCUMENT_ID, "doc:create");
+
+ return result;
+ }
+
+ private Map<String, Doc> mDocs = new HashMap<>();
+
+ private Doc mLocalRoot;
+ private Doc mCreateRoot;
+
+ private Doc buildDoc(String docId, String displayName, String mimeType) {
+ final Doc doc = new Doc();
+ doc.docId = docId;
+ doc.displayName = displayName;
+ doc.mimeType = mimeType;
+ mDocs.put(doc.docId, doc);
+ return doc;
+ }
+
+ public void resetRoots() {
+ Log.d(TAG, "resetRoots()");
+
+ mDocs.clear();
+
+ mLocalRoot = buildDoc("doc:local", null, Document.MIME_TYPE_DIR);
+
+ mCreateRoot = buildDoc("doc:create", null, Document.MIME_TYPE_DIR);
+ mCreateRoot.flags = Document.FLAG_DIR_SUPPORTS_CREATE;
+
+ {
+ Doc file1 = buildDoc("doc:file1", "FILE1", "mime1/file1");
+ file1.contents = "fileone".getBytes();
+ file1.flags = Document.FLAG_SUPPORTS_WRITE;
+ mLocalRoot.children.add(file1);
+ mCreateRoot.children.add(file1);
+ }
+
+ {
+ Doc file2 = buildDoc("doc:file2", "FILE2", "mime2/file2");
+ file2.contents = "filetwo".getBytes();
+ file2.flags = Document.FLAG_SUPPORTS_WRITE;
+ mLocalRoot.children.add(file2);
+ mCreateRoot.children.add(file2);
+ }
+
+ Doc dir1 = buildDoc("doc:dir1", "DIR1", Document.MIME_TYPE_DIR);
+ mLocalRoot.children.add(dir1);
+
+ {
+ Doc file3 = buildDoc("doc:file3", "FILE3", "mime3/file3");
+ file3.contents = "filethree".getBytes();
+ file3.flags = Document.FLAG_SUPPORTS_WRITE;
+ dir1.children.add(file3);
+ }
+
+ Doc dir2 = buildDoc("doc:dir2", "DIR2", Document.MIME_TYPE_DIR);
+ mCreateRoot.children.add(dir2);
+
+ {
+ Doc file4 = buildDoc("doc:file4", "FILE4", "mime4/file4");
+ file4.contents = "filefour".getBytes();
+ file4.flags = Document.FLAG_SUPPORTS_WRITE;
+ dir2.children.add(file4);
+ }
+ }
+
+ private static class Doc {
+ public String docId;
+ public int flags;
+ public String displayName;
+ public long size;
+ public String mimeType;
+ public long lastModified;
+ public byte[] contents;
+ public List<Doc> children = new ArrayList<>();
+
+ public void include(MatrixCursor result) {
+ final RowBuilder row = result.newRow();
+ row.add(Document.COLUMN_DOCUMENT_ID, docId);
+ row.add(Document.COLUMN_DISPLAY_NAME, displayName);
+ row.add(Document.COLUMN_SIZE, size);
+ row.add(Document.COLUMN_MIME_TYPE, mimeType);
+ row.add(Document.COLUMN_FLAGS, flags);
+ row.add(Document.COLUMN_LAST_MODIFIED, lastModified);
+ }
+ }
+
+ @Override
+ public boolean isChildDocument(String parentDocumentId, String documentId) {
+ for (Doc doc : mDocs.get(parentDocumentId).children) {
+ if (doc.docId.equals(documentId)) {
+ return true;
+ }
+ if (Document.MIME_TYPE_DIR.equals(doc.mimeType)) {
+ return isChildDocument(doc.docId, documentId);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String createDocument(String parentDocumentId, String mimeType, String displayName)
+ throws FileNotFoundException {
+ final String docId = "doc:" + System.currentTimeMillis();
+ final Doc doc = buildDoc(docId, displayName, mimeType);
+ doc.flags = Document.FLAG_SUPPORTS_WRITE | Document.FLAG_SUPPORTS_RENAME;
+ mDocs.get(parentDocumentId).children.add(doc);
+ return docId;
+ }
+
+ @Override
+ public String renameDocument(String documentId, String displayName)
+ throws FileNotFoundException {
+ mDocs.get(documentId).displayName = displayName;
+ return null;
+ }
+
+ @Override
+ public void deleteDocument(String documentId) throws FileNotFoundException {
+ mDocs.remove(documentId);
+ for (Doc doc : mDocs.values()) {
+ doc.children.remove(documentId);
+ }
+ }
+
+ @Override
+ public Cursor queryDocument(String documentId, String[] projection)
+ throws FileNotFoundException {
+ final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
+ mDocs.get(documentId).include(result);
+ return result;
+ }
+
+ @Override
+ public Cursor queryChildDocuments(String parentDocumentId, String[] projection,
+ String sortOrder) throws FileNotFoundException {
+ final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
+ for (Doc doc : mDocs.get(parentDocumentId).children) {
+ doc.include(result);
+ }
+ return result;
+ }
+
+ @Override
+ public ParcelFileDescriptor openDocument(String documentId, String mode,
+ CancellationSignal signal) throws FileNotFoundException {
+ final Doc doc = mDocs.get(documentId);
+ if (doc == null) {
+ throw new FileNotFoundException();
+ }
+ final ParcelFileDescriptor[] pipe;
+ try {
+ pipe = ParcelFileDescriptor.createPipe();
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ if (mode.contains("w")) {
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ try {
+ final InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(
+ pipe[0]);
+ doc.contents = readFullyNoClose(is);
+ is.close();
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to stream", e);
+ }
+ return null;
+ }
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ return pipe[1];
+ } else {
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ try {
+ final OutputStream os = new ParcelFileDescriptor.AutoCloseOutputStream(
+ pipe[1]);
+ os.write(doc.contents);
+ os.close();
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to stream", e);
+ }
+ return null;
+ }
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ return pipe[0];
+ }
+ }
+
+ private static byte[] readFullyNoClose(InputStream in) throws IOException {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ byte[] buffer = new byte[1024];
+ int count;
+ while ((count = in.read(buffer)) != -1) {
+ bytes.write(buffer, 0, count);
+ }
+ return bytes.toByteArray();
+ }
+}