Merge "Add functional tests for FilesActivity."
diff --git a/packages/DocumentsUI/testing/TestDocumentsProvider/Android.mk b/packages/DocumentsUI/testing/TestDocumentsProvider/Android.mk
new file mode 100644
index 0000000..8baadba
--- /dev/null
+++ b/packages/DocumentsUI/testing/TestDocumentsProvider/Android.mk
@@ -0,0 +1,14 @@
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+
+LOCAL_PACKAGE_NAME := TestDocumentsProvider
+LOCAL_CERTIFICATE := platform
+LOCAL_MODULE_TAGS := tests
+#LOCAL_SDK_VERSION := current
+
+LOCAL_PROGUARD_ENABLED := disabled
+LOCAL_DEX_PREOPT := false
+
+include $(BUILD_PACKAGE)
diff --git a/packages/DocumentsUI/testing/TestDocumentsProvider/AndroidManifest.xml b/packages/DocumentsUI/testing/TestDocumentsProvider/AndroidManifest.xml
new file mode 100644
index 0000000..66988a1
--- /dev/null
+++ b/packages/DocumentsUI/testing/TestDocumentsProvider/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.documentsui.testing">
+ <application>
+ <provider android:name="TestDocumentsProvider"
+ android:authorities="com.android.documentsui.testing"
+ 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>
+ </application>
+</manifest>
diff --git a/packages/DocumentsUI/testing/TestDocumentsProvider/src/com/android/documentsui/testing/TestDocumentsProvider.java b/packages/DocumentsUI/testing/TestDocumentsProvider/src/com/android/documentsui/testing/TestDocumentsProvider.java
new file mode 100644
index 0000000..63ff0de
--- /dev/null
+++ b/packages/DocumentsUI/testing/TestDocumentsProvider/src/com/android/documentsui/testing/TestDocumentsProvider.java
@@ -0,0 +1,299 @@
+/*
+ * 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.
+ */
+
+package com.android.documentsui.testing;
+
+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 TestDocumentsProvider 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, "TEST-Local");
+ row.add(Root.COLUMN_SUMMARY, "TEST-LocalSummary");
+ 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, "TEST-Create");
+ 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) {
+ synchronized (doc) {
+ try {
+ final InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(
+ pipe[0]);
+ doc.contents = readFullyNoClose(is);
+ is.close();
+ doc.notifyAll();
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to stream", e);
+ }
+ }
+ return null;
+ }
+ }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
+ return pipe[1];
+ } else {
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ synchronized (doc) {
+ try {
+ final OutputStream os = new ParcelFileDescriptor.AutoCloseOutputStream(
+ pipe[1]);
+ while (doc.contents == null) {
+ doc.wait();
+ }
+ os.write(doc.contents);
+ os.close();
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to stream", e);
+ } catch (InterruptedException e) {
+ Log.w(TAG, "Interuppted", e);
+ }
+ }
+ return null;
+ }
+ }.executeOnExecutor(AsyncTask.SERIAL_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();
+ }
+}
diff --git a/packages/DocumentsUI/tests/Android.mk b/packages/DocumentsUI/tests/Android.mk
index 3f191a9..cf486b1 100644
--- a/packages/DocumentsUI/tests/Android.mk
+++ b/packages/DocumentsUI/tests/Android.mk
@@ -3,11 +3,12 @@
include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := tests
+#LOCAL_SDK_VERSION := current
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_JAVA_LIBRARIES := android.test.runner
-LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 mockito-target guava
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 mockito-target guava ub-uiautomator
LOCAL_PACKAGE_NAME := DocumentsUITests
LOCAL_INSTRUMENTATION_FOR := DocumentsUI
@@ -15,3 +16,5 @@
LOCAL_CERTIFICATE := platform
include $(BUILD_PACKAGE)
+
+include $(LOCAL_PATH)/../testing/TestDocumentsProvider/Android.mk
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/FilesActivityUiTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/FilesActivityUiTest.java
new file mode 100644
index 0000000..1f4b751
--- /dev/null
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/FilesActivityUiTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.
+ */
+
+package com.android.documentsui;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Configurator;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.view.MotionEvent;
+
+import java.util.concurrent.TimeoutException;
+
+public class FilesActivityUiTest extends InstrumentationTestCase {
+
+ private static final String TAG = "FilesActivityUiTest";
+ private static final String TARGET_PKG = "com.android.documentsui";
+ private static final String LAUNCHER_PKG = "com.android.launcher";
+ private static final int ONE_SECOND = 1000;
+ private static final int FIVE_SECONDS = 5 * ONE_SECOND;
+
+ private ActionBar mBar;
+ private UiDevice mDevice;
+ private Context mContext;
+
+ public void setUp() throws TimeoutException {
+ // Initialize UiDevice instance.
+ mDevice = UiDevice.getInstance(getInstrumentation());
+
+ Configurator.getInstance().setToolType(MotionEvent.TOOL_TYPE_MOUSE);
+
+ // Start from the home screen.
+ mDevice.pressHome();
+ mDevice.wait(Until.hasObject(By.pkg(LAUNCHER_PKG).depth(0)), FIVE_SECONDS);
+
+ // Launch app.
+ mContext = getInstrumentation().getContext();
+ Intent intent = mContext.getPackageManager().getLaunchIntentForPackage(TARGET_PKG);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ mContext.startActivity(intent);
+
+ // Wait for the app to appear.
+ mDevice.wait(Until.hasObject(By.pkg(TARGET_PKG).depth(0)), FIVE_SECONDS);
+ mDevice.waitForIdle();
+
+ mBar = new ActionBar();
+ }
+
+ public void testSwitchMode() throws Exception {
+ UiObject2 mode = mBar.gridMode(100);
+ if (mode != null) {
+ mode.click();
+ assertNotNull(mBar.listMode(ONE_SECOND));
+ } else {
+ mBar.listMode(100).click();
+ assertNotNull(mBar.gridMode(ONE_SECOND));
+ }
+ }
+
+ private class ActionBar {
+
+ public UiObject2 gridMode(int timeout) {
+ // Note that we're using By.desc rather than By.res, because of b/25285770
+ BySelector selector = By.desc("Grid view");
+ if (timeout > 0) {
+ mDevice.wait(Until.findObject(selector), timeout);
+ }
+ return mDevice.findObject(selector);
+ }
+
+ public UiObject2 listMode(int timeout) {
+ // Note that we're using By.desc rather than By.res, because of b/25285770
+ BySelector selector = By.desc("List view");
+ if (timeout > 0) {
+ mDevice.wait(Until.findObject(selector), timeout);
+ }
+ return mDevice.findObject(selector);
+ }
+ }
+}