Merge "Features around findPath API." into nyc-andromeda-dev
diff --git a/src/com/android/documentsui/archives/Archive.java b/src/com/android/documentsui/archives/Archive.java
index 35ecb54..4db6b4a 100644
--- a/src/com/android/documentsui/archives/Archive.java
+++ b/src/com/android/documentsui/archives/Archive.java
@@ -34,6 +34,10 @@
import android.util.Log;
import android.webkit.MimeTypeMap;
+import android.system.Os;
+import android.system.OsConstants;
+import android.system.ErrnoException;
+
import libcore.io.IoUtils;
import com.android.internal.util.Preconditions;
@@ -71,6 +75,10 @@
public class Archive implements Closeable {
private static final String TAG = "Archive";
+ // Stores file representations of file descriptors. Used to open pipes
+ // by path.
+ private static final String PROC_FD_PATH = "/proc/self/fd/";
+
public static final String[] DEFAULT_PROJECTION = new String[] {
Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_DISPLAY_NAME,
@@ -101,26 +109,29 @@
// Build the tree structure in memory.
mTree = new HashMap<String, List<ZipEntry>>();
- mTree.put("/", new ArrayList<ZipEntry>());
mEntries = new HashMap<String, ZipEntry>();
ZipEntry entry;
final List<? extends ZipEntry> entries = Collections.list(mZipFile.entries());
final Stack<ZipEntry> stack = new Stack<>();
+ String entryPath;
for (int i = entries.size() - 1; i >= 0; i--) {
entry = entries.get(i);
if (entry.isDirectory() != entry.getName().endsWith("/")) {
throw new IOException(
"Directories must have a trailing slash, and files must not.");
}
- if (mEntries.containsKey(entry.getName())) {
+ entryPath = getEntryPath(entry);
+ if (mEntries.containsKey(entryPath)) {
throw new IOException("Multiple entries with the same name are not supported.");
}
- mEntries.put(entry.getName(), entry);
+ mEntries.put(entryPath, entry);
if (entry.isDirectory()) {
- mTree.put(entry.getName(), new ArrayList<ZipEntry>());
+ mTree.put(entryPath, new ArrayList<ZipEntry>());
}
- stack.push(entry);
+ if (!"/".equals(entryPath)) { // Skip root, as it doesn't have a parent.
+ stack.push(entry);
+ }
}
int delimiterIndex;
@@ -128,27 +139,30 @@
ZipEntry parentEntry;
List<ZipEntry> parentList;
+ // Go through all directories recursively and build a tree structure.
while (stack.size() > 0) {
entry = stack.pop();
- delimiterIndex = entry.getName().lastIndexOf('/', entry.isDirectory()
- ? entry.getName().length() - 2 : entry.getName().length() - 1);
- parentPath =
- delimiterIndex != -1 ? entry.getName().substring(0, delimiterIndex) + "/" : "/";
+ entryPath = getEntryPath(entry);
+ delimiterIndex = entryPath.lastIndexOf('/', entry.isDirectory()
+ ? entryPath.length() - 2 : entryPath.length() - 1);
+ parentPath = entryPath.substring(0, delimiterIndex) + "/";
+
parentList = mTree.get(parentPath);
if (parentList == null) {
- parentEntry = mEntries.get(parentPath);
- if (parentEntry == null) {
- // The ZIP file doesn't contain all directories leading to the entry.
- // It's rare, but can happen in a valid ZIP archive. In such case create a
- // fake ZipEntry and add it on top of the stack to process it next.
- parentEntry = new ZipEntry(parentPath);
- parentEntry.setSize(0);
- parentEntry.setTime(entry.getTime());
- mEntries.put(parentPath, parentEntry);
+ // The ZIP file doesn't contain all directories leading to the entry.
+ // It's rare, but can happen in a valid ZIP archive. In such case create a
+ // fake ZipEntry and add it on top of the stack to process it next.
+ parentEntry = new ZipEntry(parentPath);
+ parentEntry.setSize(0);
+ parentEntry.setTime(entry.getTime());
+ mEntries.put(parentPath, parentEntry);
+
+ if (!"/".equals(parentPath)) {
stack.push(parentEntry);
}
+
parentList = new ArrayList<ZipEntry>();
mTree.put(parentPath, parentList);
}
@@ -158,9 +172,37 @@
}
/**
+ * Returns a valid, normalized path for an entry.
+ */
+ public static String getEntryPath(ZipEntry entry) {
+ Preconditions.checkArgument(entry.isDirectory() == entry.getName().endsWith("/"),
+ "Ill-formated ZIP-file.");
+ if (entry.getName().startsWith("/")) {
+ return entry.getName();
+ } else {
+ return "/" + entry.getName();
+ }
+ }
+
+ /**
+ * Returns true if the file descriptor is seekable.
+ * @param descriptor File descriptor to check.
+ */
+ public static boolean canSeek(ParcelFileDescriptor descriptor) {
+ try {
+ return Os.lseek(descriptor.getFileDescriptor(), 0,
+ OsConstants.SEEK_SET) == 0;
+ } catch (ErrnoException e) {
+ return false;
+ }
+ }
+
+ /**
* Creates a DocumentsArchive instance for opening, browsing and accessing
* documents within the archive passed as a file descriptor.
*
+ * If the file descriptor is not seekable, then a snapshot will be created.
+ *
* @param context Context of the provider.
* @param descriptor File descriptor for the archive's contents.
* @param archiveUri Uri of the archive document.
@@ -170,6 +212,12 @@
Context context, ParcelFileDescriptor descriptor, Uri archiveUri,
@Nullable Uri notificationUri)
throws IOException {
+ if (canSeek(descriptor)) {
+ return new Archive(context, new File(PROC_FD_PATH + descriptor.getFd()),
+ archiveUri, notificationUri);
+ }
+
+ // Fallback for non-seekable file descriptors.
File snapshotFile = null;
try {
// Create a copy of the archive, as ZipFile doesn't operate on streams.
@@ -272,12 +320,13 @@
return false;
}
- final String parentPath = entry.getName();
-
// Add a trailing slash even if it's not a directory, so it's easy to check if the
// entry is a descendant.
- final String pathWithSlash = entry.isDirectory() ? entry.getName() : entry.getName() + "/";
- return pathWithSlash.startsWith(parentPath) && !parentPath.equals(pathWithSlash);
+ String pathWithSlash = entry.isDirectory() ? getEntryPath(entry)
+ : getEntryPath(entry) + "/";
+
+ return pathWithSlash.startsWith(parsedParentId.mPath) &&
+ !parsedParentId.mPath.equals(pathWithSlash);
}
/**
@@ -452,7 +501,7 @@
private void addCursorRow(MatrixCursor cursor, ZipEntry entry) {
final MatrixCursor.RowBuilder row = cursor.newRow();
- final ArchiveId parsedId = new ArchiveId(mArchiveUri, entry.getName());
+ final ArchiveId parsedId = new ArchiveId(mArchiveUri, getEntryPath(entry));
row.add(Document.COLUMN_DOCUMENT_ID, parsedId.toDocumentId());
final File file = new File(entry.getName());
diff --git a/src/com/android/documentsui/files/ActionHandler.java b/src/com/android/documentsui/files/ActionHandler.java
index 403be64..09f90d4 100644
--- a/src/com/android/documentsui/files/ActionHandler.java
+++ b/src/com/android/documentsui/files/ActionHandler.java
@@ -357,6 +357,7 @@
}
Intent intent = Intent.createChooser(buildViewIntent(doc), null);
+ intent.putExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, false);
try {
mActivity.startActivity(intent);
} catch (ActivityNotFoundException e) {
diff --git a/src/com/android/documentsui/files/ActivityInputHandler.java b/src/com/android/documentsui/files/ActivityInputHandler.java
new file mode 100644
index 0000000..a7b0e2c
--- /dev/null
+++ b/src/com/android/documentsui/files/ActivityInputHandler.java
@@ -0,0 +1,49 @@
+/*
+ * 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 com.android.documentsui.files;
+
+import android.view.KeyEvent;
+
+import com.android.documentsui.selection.SelectionManager;
+import com.android.documentsui.ActionHandler;
+
+/**
+ * Used by {@link FilesActivity} to manage global keyboard shortcuts tied to file actions
+ */
+final class ActivityInputHandler {
+
+ private final SelectionManager mSelectionMgr;
+ private final ActionHandler mActions;
+
+ ActivityInputHandler(SelectionManager selectionMgr, ActionHandler actionHandler) {
+ mSelectionMgr = selectionMgr;
+ mActions = actionHandler;
+ }
+
+ boolean onKeyDown(int keyCode, KeyEvent event) {
+ if ((keyCode == KeyEvent.KEYCODE_DEL && event.isAltPressed())
+ || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
+ if (mSelectionMgr.hasSelection()) {
+ mActions.deleteSelectedDocuments();
+ return true;
+ } else {
+ return false;
+ }
+ }
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/documentsui/files/FilesActivity.java b/src/com/android/documentsui/files/FilesActivity.java
index e934d48..5777f9c 100644
--- a/src/com/android/documentsui/files/FilesActivity.java
+++ b/src/com/android/documentsui/files/FilesActivity.java
@@ -78,6 +78,7 @@
private DialogController mDialogs;
private DocumentClipper mClipper;
private ActionModeController mActionModeController;
+ private ActivityInputHandler mActivityInputHandler;
public FilesActivity() {
super(R.layout.files_activity, TAG);
@@ -122,6 +123,8 @@
mClipper,
DocumentsApplication.getClipStore(this));
+ mActivityInputHandler = new ActivityInputHandler(mSelectionMgr, mActions);
+
RootsFragment.show(getFragmentManager(), null);
final Intent intent = getIntent();
@@ -289,16 +292,8 @@
@CallSuper
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
- if ((keyCode == KeyEvent.KEYCODE_DEL && event.isAltPressed())
- || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
- if (mSelectionMgr.hasSelection()) {
- mActions.deleteSelectedDocuments();
- return true;
- } else {
- return false;
- }
- }
- return super.onKeyDown(keyCode, event);
+ return mActivityInputHandler.onKeyDown(keyCode, event) ? true
+ : super.onKeyDown(keyCode, event);
}
@Override
diff --git a/tests/Android.mk b/tests/Android.mk
index 8c2896a..9d3b64e 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -1,6 +1,4 @@
-
LOCAL_PATH := $(call my-dir)
-
include $(CLEAR_VARS)
# unittests
@@ -8,6 +6,10 @@
LOCAL_SRC_FILES := $(call all-java-files-under, common) \
$(call all-java-files-under, unit) \
$(call all-java-files-under, functional)
+
+# For testing ZIP files.
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+
LOCAL_JAVA_LIBRARIES := android.test.runner
LOCAL_STATIC_JAVA_LIBRARIES := mockito-target ub-uiautomator espresso-core guava
LOCAL_JARJAR_RULES := $(LOCAL_PATH)/jarjar-rules.txt
@@ -16,4 +18,3 @@
LOCAL_CERTIFICATE := platform
include $(BUILD_PACKAGE)
-
diff --git a/tests/common/com/android/documentsui/testing/TestActionHandler.java b/tests/common/com/android/documentsui/testing/TestActionHandler.java
index bc4295a..5ad0563 100644
--- a/tests/common/com/android/documentsui/testing/TestActionHandler.java
+++ b/tests/common/com/android/documentsui/testing/TestActionHandler.java
@@ -28,6 +28,7 @@
public final TestEventHandler<DocumentDetails> open = new TestEventHandler<>();
public final TestEventHandler<DocumentDetails> view = new TestEventHandler<>();
public final TestEventHandler<DocumentDetails> preview = new TestEventHandler<>();
+ public boolean mDeleteHappened;
public TestActionHandler() {
this(TestEnv.create());
@@ -61,6 +62,11 @@
}
@Override
+ public void deleteSelectedDocuments() {
+ mDeleteHappened = true;
+ }
+
+ @Override
public void openRoot(RootInfo root) {
throw new UnsupportedOperationException();
}
diff --git a/tests/res/raw/archive.zip b/tests/res/raw/archive.zip
new file mode 100644
index 0000000..c3b8d22
--- /dev/null
+++ b/tests/res/raw/archive.zip
Binary files differ
diff --git a/tests/res/raw/empty_dirs.zip b/tests/res/raw/empty_dirs.zip
new file mode 100644
index 0000000..1dd2251
--- /dev/null
+++ b/tests/res/raw/empty_dirs.zip
Binary files differ
diff --git a/tests/res/raw/no_dirs.zip b/tests/res/raw/no_dirs.zip
new file mode 100644
index 0000000..e178ae1
--- /dev/null
+++ b/tests/res/raw/no_dirs.zip
Binary files differ
diff --git a/tests/unit/com/android/documentsui/archives/ArchiveTest.java b/tests/unit/com/android/documentsui/archives/ArchiveTest.java
new file mode 100644
index 0000000..4ea3a01
--- /dev/null
+++ b/tests/unit/com/android/documentsui/archives/ArchiveTest.java
@@ -0,0 +1,384 @@
+/*
+ * 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.archives;
+
+import com.android.documentsui.archives.Archive;
+import com.android.documentsui.tests.R;
+
+import android.database.Cursor;
+import android.content.Context;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.provider.DocumentsContract.Document;
+import android.support.test.InstrumentationRegistry;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Scanner;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+@MediumTest
+public class ArchiveTest extends AndroidTestCase {
+ private static final Uri ARCHIVE_URI = Uri.parse("content://i/love/strawberries");
+ private static final String NOTIFICATION_URI = "content://notification-uri";
+ private ExecutorService mExecutor = null;
+ private Context mContext = null;
+ private Archive mArchive = null;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mContext = InstrumentationRegistry.getTargetContext();
+ mExecutor = Executors.newSingleThreadExecutor();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mExecutor.shutdown();
+ assertTrue(mExecutor.awaitTermination(3 /* timeout */, TimeUnit.SECONDS));
+ if (mArchive != null) {
+ mArchive.close();
+ }
+ super.tearDown();
+ }
+
+ public static ArchiveId createArchiveId(String path) {
+ return new ArchiveId(ARCHIVE_URI, path);
+ }
+
+ /**
+ * Opens a resource and returns the contents via file descriptor to a local
+ * snapshot file.
+ */
+ public ParcelFileDescriptor getSeekableDescriptor(int resource) {
+ // Extract the file from resources.
+ File file = null;
+ final Context testContext = InstrumentationRegistry.getContext();
+ try {
+ file = File.createTempFile("com.android.documentsui.archives.tests{",
+ "}.zip", mContext.getCacheDir());
+ try (
+ final FileOutputStream outputStream =
+ new ParcelFileDescriptor.AutoCloseOutputStream(
+ ParcelFileDescriptor.open(
+ file, ParcelFileDescriptor.MODE_WRITE_ONLY));
+ final InputStream inputStream =
+ testContext.getResources().openRawResource(resource);
+ ) {
+ final byte[] buffer = new byte[32 * 1024];
+ int bytes;
+ while ((bytes = inputStream.read(buffer)) != -1) {
+ outputStream.write(buffer, 0, bytes);
+ }
+ outputStream.flush();
+
+ }
+ return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
+ } catch (IOException e) {
+ fail(String.valueOf(e));
+ return null;
+ } finally {
+ // On UNIX the file will be still available for processes which opened it, even
+ // after deleting it. Remove it ASAP, as it won't be used by anyone else.
+ if (file != null) {
+ file.delete();
+ }
+ }
+ }
+
+ /**
+ * Opens a resource and returns the contents via a pipe.
+ */
+ public ParcelFileDescriptor getNonSeekableDescriptor(int resource) {
+ ParcelFileDescriptor[] pipe = null;
+ final Context testContext = InstrumentationRegistry.getContext();
+ try {
+ pipe = ParcelFileDescriptor.createPipe();
+ final ParcelFileDescriptor finalOutputPipe = pipe[1];
+ mExecutor.execute(
+ new Runnable() {
+ @Override
+ public void run() {
+ try (
+ final ParcelFileDescriptor.AutoCloseOutputStream outputStream =
+ new ParcelFileDescriptor.
+ AutoCloseOutputStream(finalOutputPipe);
+ final InputStream inputStream =
+ testContext.getResources().openRawResource(resource);
+ ) {
+ final byte[] buffer = new byte[32 * 1024];
+ int bytes;
+ while ((bytes = inputStream.read(buffer)) != -1) {
+ outputStream.write(buffer, 0, bytes);
+ }
+ } catch (IOException e) {
+ fail(String.valueOf(e));
+ }
+ }
+ });
+ return pipe[0];
+ } catch (IOException e) {
+ fail(String.valueOf(e));
+ return null;
+ }
+ }
+
+ public void loadArchive(ParcelFileDescriptor descriptor) throws IOException {
+ mArchive = Archive.createForParcelFileDescriptor(
+ mContext,
+ descriptor,
+ ARCHIVE_URI,
+ Uri.parse(NOTIFICATION_URI));
+ }
+
+ public void testQueryChildDocument() throws IOException {
+ loadArchive(getNonSeekableDescriptor(R.raw.archive));
+ final Cursor cursor = mArchive.queryChildDocuments(
+ new ArchiveId(ARCHIVE_URI, "/").toDocumentId(), null, null);
+
+ assertTrue(cursor.moveToFirst());
+ assertEquals(new ArchiveId(ARCHIVE_URI, "/dir1/").toDocumentId(),
+ cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)));
+ assertEquals("dir1",
+ cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
+ assertEquals(Document.MIME_TYPE_DIR,
+ cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)));
+ assertEquals(0,
+ cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
+
+ assertTrue(cursor.moveToNext());
+ assertEquals(
+ new ArchiveId(ARCHIVE_URI, "/dir2/").toDocumentId(),
+ cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)));
+ assertEquals("dir2",
+ cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
+ assertEquals(Document.MIME_TYPE_DIR,
+ cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)));
+ assertEquals(0,
+ cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
+
+ assertTrue(cursor.moveToNext());
+ assertEquals(
+ new ArchiveId(ARCHIVE_URI, "/file1.txt").toDocumentId(),
+ cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)));
+ assertEquals("file1.txt",
+ cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
+ assertEquals("text/plain",
+ cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)));
+ assertEquals(13,
+ cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
+
+ assertFalse(cursor.moveToNext());
+
+ // Check if querying children works too.
+ final Cursor childCursor = mArchive.queryChildDocuments(
+ new ArchiveId(ARCHIVE_URI, "/dir1/").toDocumentId(), null, null);
+
+ assertTrue(childCursor.moveToFirst());
+ assertEquals(
+ new ArchiveId(ARCHIVE_URI, "/dir1/cherries.txt").toDocumentId(),
+ childCursor.getString(childCursor.getColumnIndexOrThrow(
+ Document.COLUMN_DOCUMENT_ID)));
+ assertEquals("cherries.txt",
+ childCursor.getString(childCursor.getColumnIndexOrThrow(
+ Document.COLUMN_DISPLAY_NAME)));
+ assertEquals("text/plain",
+ childCursor.getString(childCursor.getColumnIndexOrThrow(
+ Document.COLUMN_MIME_TYPE)));
+ assertEquals(17,
+ childCursor.getInt(childCursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
+ }
+
+ public void testQueryChildDocument_NoDirs() throws IOException {
+ loadArchive(getNonSeekableDescriptor(R.raw.no_dirs));
+ final Cursor cursor = mArchive.queryChildDocuments(
+ new ArchiveId(ARCHIVE_URI, "/").toDocumentId(), null, null);
+
+ assertTrue(cursor.moveToFirst());
+ assertEquals(
+ new ArchiveId(ARCHIVE_URI, "/dir1/").toDocumentId(),
+ cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)));
+ assertEquals("dir1",
+ cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
+ assertEquals(Document.MIME_TYPE_DIR,
+ cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)));
+ assertEquals(0,
+ cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
+ assertFalse(cursor.moveToNext());
+
+ final Cursor childCursor = mArchive.queryChildDocuments(
+ new ArchiveId(ARCHIVE_URI, "/dir1/").toDocumentId(), null, null);
+
+ assertTrue(childCursor.moveToFirst());
+ assertEquals(
+ new ArchiveId(ARCHIVE_URI, "/dir1/dir2/").toDocumentId(),
+ childCursor.getString(childCursor.getColumnIndexOrThrow(
+ Document.COLUMN_DOCUMENT_ID)));
+ assertEquals("dir2",
+ childCursor.getString(childCursor.getColumnIndexOrThrow(
+ Document.COLUMN_DISPLAY_NAME)));
+ assertEquals(Document.MIME_TYPE_DIR,
+ childCursor.getString(childCursor.getColumnIndexOrThrow(
+ Document.COLUMN_MIME_TYPE)));
+ assertEquals(0,
+ childCursor.getInt(childCursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
+ assertFalse(childCursor.moveToNext());
+
+ final Cursor childCursor2 = mArchive.queryChildDocuments(
+ new ArchiveId(ARCHIVE_URI, "/dir1/dir2/").toDocumentId(),
+ null, null);
+
+ assertTrue(childCursor2.moveToFirst());
+ assertEquals(
+ new ArchiveId(ARCHIVE_URI, "/dir1/dir2/cherries.txt").toDocumentId(),
+ childCursor2.getString(childCursor.getColumnIndexOrThrow(
+ Document.COLUMN_DOCUMENT_ID)));
+ assertFalse(childCursor2.moveToNext());
+ }
+
+ public void testQueryChildDocument_EmptyDirs() throws IOException {
+ loadArchive(getNonSeekableDescriptor(R.raw.empty_dirs));
+ final Cursor cursor = mArchive.queryChildDocuments(
+ new ArchiveId(ARCHIVE_URI, "/").toDocumentId(), null, null);
+
+ assertTrue(cursor.moveToFirst());
+ assertEquals(
+ new ArchiveId(ARCHIVE_URI, "/dir1/").toDocumentId(),
+ cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)));
+ assertEquals("dir1",
+ cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
+ assertEquals(Document.MIME_TYPE_DIR,
+ cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)));
+ assertEquals(0,
+ cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
+ assertFalse(cursor.moveToNext());
+
+ final Cursor childCursor = mArchive.queryChildDocuments(
+ new ArchiveId(ARCHIVE_URI, "/dir1/").toDocumentId(), null, null);
+
+ assertTrue(childCursor.moveToFirst());
+ assertEquals(
+ new ArchiveId(ARCHIVE_URI, "/dir1/dir2/").toDocumentId(),
+ childCursor.getString(childCursor.getColumnIndexOrThrow(
+ Document.COLUMN_DOCUMENT_ID)));
+ assertEquals("dir2",
+ childCursor.getString(childCursor.getColumnIndexOrThrow(
+ Document.COLUMN_DISPLAY_NAME)));
+ assertEquals(Document.MIME_TYPE_DIR,
+ childCursor.getString(childCursor.getColumnIndexOrThrow(
+ Document.COLUMN_MIME_TYPE)));
+ assertEquals(0,
+ childCursor.getInt(childCursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
+
+ assertTrue(childCursor.moveToNext());
+ assertEquals(
+ new ArchiveId(ARCHIVE_URI, "/dir1/dir3/").toDocumentId(),
+ childCursor.getString(childCursor.getColumnIndexOrThrow(
+ Document.COLUMN_DOCUMENT_ID)));
+ assertEquals("dir3",
+ childCursor.getString(childCursor.getColumnIndexOrThrow(
+ Document.COLUMN_DISPLAY_NAME)));
+ assertEquals(Document.MIME_TYPE_DIR,
+ childCursor.getString(childCursor.getColumnIndexOrThrow(
+ Document.COLUMN_MIME_TYPE)));
+ assertEquals(0,
+ childCursor.getInt(childCursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
+ assertFalse(cursor.moveToNext());
+
+ final Cursor childCursor2 = mArchive.queryChildDocuments(
+ new ArchiveId(ARCHIVE_URI, "/dir1/dir2/").toDocumentId(),
+ null, null);
+ assertFalse(childCursor2.moveToFirst());
+
+ final Cursor childCursor3 = mArchive.queryChildDocuments(
+ new ArchiveId(ARCHIVE_URI, "/dir1/dir3/").toDocumentId(),
+ null, null);
+ assertFalse(childCursor3.moveToFirst());
+ }
+
+ public void testGetDocumentType() throws IOException {
+ loadArchive(getNonSeekableDescriptor(R.raw.archive));
+ assertEquals(Document.MIME_TYPE_DIR, mArchive.getDocumentType(
+ new ArchiveId(ARCHIVE_URI, "/dir1/").toDocumentId()));
+ assertEquals("text/plain", mArchive.getDocumentType(
+ new ArchiveId(ARCHIVE_URI, "/file1.txt").toDocumentId()));
+ }
+
+ public void testIsChildDocument() throws IOException {
+ loadArchive(getNonSeekableDescriptor(R.raw.archive));
+ final String documentId = new ArchiveId(ARCHIVE_URI, "/").toDocumentId();
+ assertTrue(mArchive.isChildDocument(documentId,
+ new ArchiveId(ARCHIVE_URI, "/dir1/").toDocumentId()));
+ assertFalse(mArchive.isChildDocument(documentId,
+ new ArchiveId(ARCHIVE_URI, "/this-does-not-exist").toDocumentId()));
+ assertTrue(mArchive.isChildDocument(
+ new ArchiveId(ARCHIVE_URI, "/dir1/").toDocumentId(),
+ new ArchiveId(ARCHIVE_URI, "/dir1/cherries.txt").toDocumentId()));
+ assertTrue(mArchive.isChildDocument(documentId,
+ new ArchiveId(ARCHIVE_URI, "/dir1/cherries.txt").toDocumentId()));
+ }
+
+ public void testQueryDocument() throws IOException {
+ loadArchive(getNonSeekableDescriptor(R.raw.archive));
+ final Cursor cursor = mArchive.queryDocument(
+ new ArchiveId(ARCHIVE_URI, "/dir2/strawberries.txt").toDocumentId(),
+ null);
+
+ assertTrue(cursor.moveToFirst());
+ assertEquals(
+ new ArchiveId(ARCHIVE_URI, "/dir2/strawberries.txt").toDocumentId(),
+ cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)));
+ assertEquals("strawberries.txt",
+ cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
+ assertEquals("text/plain",
+ cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)));
+ assertEquals(21,
+ cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
+ }
+
+ public void testOpenDocument() throws IOException {
+ loadArchive(getSeekableDescriptor(R.raw.archive));
+ commonTestOpenDocument();
+ }
+
+ public void testOpenDocument_NonSeekable() throws IOException {
+ loadArchive(getNonSeekableDescriptor(R.raw.archive));
+ commonTestOpenDocument();
+ }
+
+ // Common part of testOpenDocument and testOpenDocument_NonSeekable.
+ void commonTestOpenDocument() throws IOException {
+ final ParcelFileDescriptor descriptor = mArchive.openDocument(
+ new ArchiveId(ARCHIVE_URI, "/dir2/strawberries.txt").toDocumentId(),
+ "r", null /* signal */);
+ try (final ParcelFileDescriptor.AutoCloseInputStream inputStream =
+ new ParcelFileDescriptor.AutoCloseInputStream(descriptor)) {
+ assertEquals("I love strawberries!", new Scanner(inputStream).nextLine());
+ }
+ }
+
+ public void testCanSeek() throws IOException {
+ assertTrue(Archive.canSeek(getSeekableDescriptor(R.raw.archive)));
+ assertFalse(Archive.canSeek(getNonSeekableDescriptor(R.raw.archive)));
+ }
+}
diff --git a/tests/unit/com/android/documentsui/dirlist/DragStartListenerTest.java b/tests/unit/com/android/documentsui/dirlist/DragStartListenerTest.java
index f94e10f..f2e044b 100644
--- a/tests/unit/com/android/documentsui/dirlist/DragStartListenerTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/DragStartListenerTest.java
@@ -92,11 +92,6 @@
.primary();
}
- @Override
- protected void tearDown() throws Exception {
- mMultiSelectManager.clearSelection();
- }
-
public void testDragStarted_OnMouseMove() {
assertTrue(mListener.onMouseDragEvent(mEvent.build()));
assertTrue(mDragStarted);
diff --git a/tests/unit/com/android/documentsui/files/ActivityInputHandlerTest.java b/tests/unit/com/android/documentsui/files/ActivityInputHandlerTest.java
new file mode 100644
index 0000000..37a696f
--- /dev/null
+++ b/tests/unit/com/android/documentsui/files/ActivityInputHandlerTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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 com.android.documentsui.files;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import com.android.documentsui.dirlist.TestData;
+import com.android.documentsui.selection.SelectionManager;
+import com.android.documentsui.selection.SelectionProbe;
+import com.android.documentsui.testing.SelectionManagers;
+import com.android.documentsui.testing.TestActionHandler;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class ActivityInputHandlerTest {
+
+ private static final List<String> ITEMS = TestData.create(100);
+
+ private SelectionProbe mSelection;
+ private TestActionHandler mActionHandler;
+ private ActivityInputHandler mActivityInputHandler;
+
+ @Before
+ public void setUp() {
+ SelectionManager selectionMgr = SelectionManagers.createTestInstance(ITEMS);
+ mSelection = new SelectionProbe(selectionMgr);
+ mActionHandler = new TestActionHandler();
+ mActivityInputHandler = new ActivityInputHandler(selectionMgr, mActionHandler);
+ }
+
+ @Test
+ public void testDelete_noSelection() {
+ KeyEvent event = new KeyEvent(0, 0, MotionEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL, 0,
+ KeyEvent.META_ALT_ON);
+ assertFalse(mActivityInputHandler.onKeyDown(event.getKeyCode(), event));
+ assertFalse(mActionHandler.mDeleteHappened);
+ }
+
+ @Test
+ public void testDelete_hasSelection() {
+ mSelection.select(1);
+ KeyEvent event = new KeyEvent(0, 0, MotionEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL, 0,
+ KeyEvent.META_ALT_ON);
+ assertTrue(mActivityInputHandler.onKeyDown(event.getKeyCode(), event));
+ assertTrue(mActionHandler.mDeleteHappened);
+ }
+}