Loading src/com/android/documentsui/archives/Archive.java +29 −1 Original line number Diff line number Diff line Loading @@ -34,6 +34,10 @@ import android.support.annotation.Nullable; 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; Loading Loading @@ -71,6 +75,10 @@ import java.util.zip.ZipInputStream; 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, Loading Loading @@ -157,10 +165,25 @@ public class Archive implements Closeable { } } /** * 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. Loading @@ -170,8 +193,13 @@ public class Archive implements Closeable { 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; // TODO: Do not create a snapshot if a seekable file descriptor is passed. try { // Create a copy of the archive, as ZipFile doesn't operate on streams. // Moreover, ZipInputStream would be inefficient for large files on Loading tests/unit/com/android/documentsui/archives/ArchiveTest.java +94 −20 Original line number Diff line number Diff line Loading @@ -33,26 +33,50 @@ 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; private ArchiveId mArchiveId; @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); } public void loadArchive(int resource) { /** * 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 context = InstrumentationRegistry.getTargetContext(); final Context testContext = InstrumentationRegistry.getContext(); try { file = File.createTempFile("com.android.documentsui.archives.tests{", "}.zip", context.getCacheDir()); "}.zip", mContext.getCacheDir()); try ( final FileOutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream( Loading @@ -69,13 +93,10 @@ public class ArchiveTest extends AndroidTestCase { outputStream.flush(); } mArchive = Archive.createForParcelFileDescriptor( context, ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY), ARCHIVE_URI, Uri.parse(NOTIFICATION_URI)); 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. Loading @@ -85,15 +106,53 @@ public class ArchiveTest extends AndroidTestCase { } } /** * 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 tearDown() { if (mArchive != null) { mArchive.close(); 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(R.raw.archive); loadArchive(getNonSeekableDescriptor(R.raw.archive)); final Cursor cursor = mArchive.queryChildDocuments( new ArchiveId(ARCHIVE_URI, "/").toDocumentId(), null, null); Loading Loading @@ -151,7 +210,7 @@ public class ArchiveTest extends AndroidTestCase { } public void testQueryChildDocument_NoDirs() throws IOException { loadArchive(R.raw.no_dirs); loadArchive(getNonSeekableDescriptor(R.raw.no_dirs)); final Cursor cursor = mArchive.queryChildDocuments( new ArchiveId(ARCHIVE_URI, "/").toDocumentId(), null, null); Loading Loading @@ -198,7 +257,7 @@ public class ArchiveTest extends AndroidTestCase { } public void testQueryChildDocument_EmptyDirs() throws IOException { loadArchive(R.raw.empty_dirs); loadArchive(getNonSeekableDescriptor(R.raw.empty_dirs)); final Cursor cursor = mArchive.queryChildDocuments( new ArchiveId(ARCHIVE_URI, "/").toDocumentId(), null, null); Loading Loading @@ -258,7 +317,7 @@ public class ArchiveTest extends AndroidTestCase { } public void testGetDocumentType() throws IOException { loadArchive(R.raw.archive); loadArchive(getNonSeekableDescriptor(R.raw.archive)); assertEquals(Document.MIME_TYPE_DIR, mArchive.getDocumentType( new ArchiveId(ARCHIVE_URI, "dir1/").toDocumentId())); assertEquals("text/plain", mArchive.getDocumentType( Loading @@ -266,7 +325,7 @@ public class ArchiveTest extends AndroidTestCase { } public void testIsChildDocument() throws IOException { loadArchive(R.raw.archive); loadArchive(getNonSeekableDescriptor(R.raw.archive)); final String documentId = new ArchiveId(ARCHIVE_URI, "/").toDocumentId(); assertTrue(mArchive.isChildDocument(documentId, new ArchiveId(ARCHIVE_URI, "dir1/").toDocumentId())); Loading @@ -280,7 +339,7 @@ public class ArchiveTest extends AndroidTestCase { } public void testQueryDocument() throws IOException { loadArchive(R.raw.archive); loadArchive(getNonSeekableDescriptor(R.raw.archive)); final Cursor cursor = mArchive.queryDocument( new ArchiveId(ARCHIVE_URI, "dir2/strawberries.txt").toDocumentId(), null); Loading @@ -298,7 +357,17 @@ public class ArchiveTest extends AndroidTestCase { } public void testOpenDocument() throws IOException { loadArchive(R.raw.archive); 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 */); Loading @@ -307,4 +376,9 @@ public class ArchiveTest extends AndroidTestCase { 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))); } } Loading
src/com/android/documentsui/archives/Archive.java +29 −1 Original line number Diff line number Diff line Loading @@ -34,6 +34,10 @@ import android.support.annotation.Nullable; 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; Loading Loading @@ -71,6 +75,10 @@ import java.util.zip.ZipInputStream; 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, Loading Loading @@ -157,10 +165,25 @@ public class Archive implements Closeable { } } /** * 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. Loading @@ -170,8 +193,13 @@ public class Archive implements Closeable { 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; // TODO: Do not create a snapshot if a seekable file descriptor is passed. try { // Create a copy of the archive, as ZipFile doesn't operate on streams. // Moreover, ZipInputStream would be inefficient for large files on Loading
tests/unit/com/android/documentsui/archives/ArchiveTest.java +94 −20 Original line number Diff line number Diff line Loading @@ -33,26 +33,50 @@ 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; private ArchiveId mArchiveId; @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); } public void loadArchive(int resource) { /** * 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 context = InstrumentationRegistry.getTargetContext(); final Context testContext = InstrumentationRegistry.getContext(); try { file = File.createTempFile("com.android.documentsui.archives.tests{", "}.zip", context.getCacheDir()); "}.zip", mContext.getCacheDir()); try ( final FileOutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream( Loading @@ -69,13 +93,10 @@ public class ArchiveTest extends AndroidTestCase { outputStream.flush(); } mArchive = Archive.createForParcelFileDescriptor( context, ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY), ARCHIVE_URI, Uri.parse(NOTIFICATION_URI)); 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. Loading @@ -85,15 +106,53 @@ public class ArchiveTest extends AndroidTestCase { } } /** * 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 tearDown() { if (mArchive != null) { mArchive.close(); 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(R.raw.archive); loadArchive(getNonSeekableDescriptor(R.raw.archive)); final Cursor cursor = mArchive.queryChildDocuments( new ArchiveId(ARCHIVE_URI, "/").toDocumentId(), null, null); Loading Loading @@ -151,7 +210,7 @@ public class ArchiveTest extends AndroidTestCase { } public void testQueryChildDocument_NoDirs() throws IOException { loadArchive(R.raw.no_dirs); loadArchive(getNonSeekableDescriptor(R.raw.no_dirs)); final Cursor cursor = mArchive.queryChildDocuments( new ArchiveId(ARCHIVE_URI, "/").toDocumentId(), null, null); Loading Loading @@ -198,7 +257,7 @@ public class ArchiveTest extends AndroidTestCase { } public void testQueryChildDocument_EmptyDirs() throws IOException { loadArchive(R.raw.empty_dirs); loadArchive(getNonSeekableDescriptor(R.raw.empty_dirs)); final Cursor cursor = mArchive.queryChildDocuments( new ArchiveId(ARCHIVE_URI, "/").toDocumentId(), null, null); Loading Loading @@ -258,7 +317,7 @@ public class ArchiveTest extends AndroidTestCase { } public void testGetDocumentType() throws IOException { loadArchive(R.raw.archive); loadArchive(getNonSeekableDescriptor(R.raw.archive)); assertEquals(Document.MIME_TYPE_DIR, mArchive.getDocumentType( new ArchiveId(ARCHIVE_URI, "dir1/").toDocumentId())); assertEquals("text/plain", mArchive.getDocumentType( Loading @@ -266,7 +325,7 @@ public class ArchiveTest extends AndroidTestCase { } public void testIsChildDocument() throws IOException { loadArchive(R.raw.archive); loadArchive(getNonSeekableDescriptor(R.raw.archive)); final String documentId = new ArchiveId(ARCHIVE_URI, "/").toDocumentId(); assertTrue(mArchive.isChildDocument(documentId, new ArchiveId(ARCHIVE_URI, "dir1/").toDocumentId())); Loading @@ -280,7 +339,7 @@ public class ArchiveTest extends AndroidTestCase { } public void testQueryDocument() throws IOException { loadArchive(R.raw.archive); loadArchive(getNonSeekableDescriptor(R.raw.archive)); final Cursor cursor = mArchive.queryDocument( new ArchiveId(ARCHIVE_URI, "dir2/strawberries.txt").toDocumentId(), null); Loading @@ -298,7 +357,17 @@ public class ArchiveTest extends AndroidTestCase { } public void testOpenDocument() throws IOException { loadArchive(R.raw.archive); 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 */); Loading @@ -307,4 +376,9 @@ public class ArchiveTest extends AndroidTestCase { 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))); } }