Loading src/com/android/documentsui/archives/Archive.java +3 −1 Original line number Diff line number Diff line Loading @@ -24,6 +24,8 @@ import android.graphics.Point; import android.net.Uri; import android.os.CancellationSignal; import android.os.ParcelFileDescriptor; import android.os.storage.StorageManager; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.support.annotation.Nullable; import android.system.ErrnoException; Loading Loading @@ -243,7 +245,7 @@ public abstract class Archive implements Closeable { public ParcelFileDescriptor openDocument( String documentId, String mode, @Nullable final CancellationSignal signal) throws FileNotFoundException { throw new UnsupportedOperationException("Thumbnails not supported."); throw new UnsupportedOperationException("Opening not supported."); } /** Loading src/com/android/documentsui/archives/Proxy.java 0 → 100644 +96 −0 Original line number Diff line number Diff line /* * 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.archives; import android.os.ProxyFileDescriptorCallback; import android.system.ErrnoException; import android.system.OsConstants; import android.util.Log; import android.util.jar.StrictJarFile; import java.io.IOException; import java.io.InputStream; import java.util.zip.ZipEntry; import libcore.io.IoUtils; /** * Provides a backend for a seekable file descriptors for files in archives. */ public class Proxy extends ProxyFileDescriptorCallback { private final StrictJarFile mFile; private final ZipEntry mEntry; private InputStream mInputStream = null; private long mOffset = 0; Proxy(StrictJarFile file, ZipEntry entry) throws IOException { mFile = file; mEntry = entry; recreateInputStream(); } @Override public long onGetSize() throws ErrnoException { return mEntry.getSize(); } @Override public int onRead(long offset, int size, byte[] data) throws ErrnoException { // TODO: Add a ring buffer to prevent expensive seeks. if (offset < mOffset) { try { recreateInputStream(); } catch (IOException e) { throw new ErrnoException("onRead", OsConstants.EIO); } } while (mOffset < offset) { try { mOffset += mInputStream.skip(offset - mOffset); } catch (IOException e) { throw new ErrnoException("onRead", OsConstants.EIO); } } int remainingSize = size; while (remainingSize > 0) { try { int bytes = mInputStream.read(data, size - remainingSize, remainingSize); if (bytes <= 0) { return size - remainingSize; } remainingSize -= bytes; mOffset += bytes; } catch (IOException e) { throw new ErrnoException("onRead", OsConstants.EIO); } } return size - remainingSize; } @Override public void onRelease() { IoUtils.closeQuietly(mInputStream); } private void recreateInputStream() throws IOException { IoUtils.closeQuietly(mInputStream); mInputStream = mFile.getInputStream(mEntry); mOffset = 0; } } src/com/android/documentsui/archives/ReadableArchive.java +6 −84 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ import android.os.Bundle; import android.os.CancellationSignal; import android.os.OperationCanceledException; import android.os.ParcelFileDescriptor; import android.os.storage.StorageManager; import android.provider.DocumentsContract; import android.support.annotation.Nullable; import android.util.Log; Loading @@ -47,9 +48,6 @@ import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.Stack; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.zip.ZipEntry; Loading @@ -62,9 +60,7 @@ import java.util.zip.ZipEntry; public class ReadableArchive extends Archive { private static final String TAG = "ReadableArchive"; @GuardedBy("mEnqueuedOutputPipes") private final Set<ParcelFileDescriptor> mEnqueuedOutputPipes = new HashSet<>(); private final ThreadPoolExecutor mExecutor; private final StorageManager mStorageManager; private final StrictJarFile mZipFile; private ReadableArchive( Loading @@ -80,11 +76,7 @@ public class ReadableArchive extends Archive { throw new IllegalStateException("Unsupported access mode."); } // At most 8 active threads. All threads idling for more than a minute will // be closed. mExecutor = new ThreadPoolExecutor(8, 8, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); mExecutor.allowCoreThreadTimeOut(true); mStorageManager = mContext.getSystemService(StorageManager.class); mZipFile = file != null ? new StrictJarFile(file.getPath(), false /* verify */, Loading Loading @@ -243,72 +235,12 @@ public class ReadableArchive extends Archive { throw new FileNotFoundException(); } ParcelFileDescriptor[] pipe; try { pipe = ParcelFileDescriptor.createReliablePipe(); return mStorageManager.openProxyFileDescriptor( ParcelFileDescriptor.MODE_READ_ONLY, new Proxy(mZipFile, entry)); } catch (IOException e) { // Ideally we'd simply throw IOException to the caller, but for consistency // with DocumentsProvider::openDocument, converting it to IllegalStateException. throw new IllegalStateException("Failed to open the document.", e); throw new IllegalStateException(e); } final InputStream inputStream = mZipFile.getInputStream(entry); final ParcelFileDescriptor outputPipe = pipe[1]; synchronized (mEnqueuedOutputPipes) { mEnqueuedOutputPipes.add(outputPipe); } try { mExecutor.execute( new Runnable() { @Override public void run() { synchronized (mEnqueuedOutputPipes) { mEnqueuedOutputPipes.remove(outputPipe); } try (final ParcelFileDescriptor.AutoCloseOutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(outputPipe)) { try { final byte buffer[] = new byte[32 * 1024]; int bytes; while ((bytes = inputStream.read(buffer)) != -1) { if (Thread.interrupted()) { throw new InterruptedException(); } if (signal != null) { signal.throwIfCanceled(); } outputStream.write(buffer, 0, bytes); } } catch (IOException | InterruptedException e) { // Catch the exception before the outer try-with-resource closes // the pipe with close() instead of closeWithError(). try { Log.e(TAG, "Failed while reading a file.", e); outputPipe.closeWithError("Reading failure."); } catch (IOException e2) { Log.e(TAG, "Failed to close the pipe after an error.", e2); } } } catch (OperationCanceledException e) { // Cancelled gracefully. } catch (IOException e) { Log.e(TAG, "Failed to close the output stream gracefully.", e); } finally { IoUtils.closeQuietly(inputStream); } } }); } catch (RejectedExecutionException e) { IoUtils.closeQuietly(pipe[0]); IoUtils.closeQuietly(pipe[1]); synchronized (mEnqueuedOutputPipes) { mEnqueuedOutputPipes.remove(outputPipe); } throw new IllegalStateException("Failed to initialize pipe."); } return pipe[0]; } @Override Loading Loading @@ -369,16 +301,6 @@ public class ReadableArchive extends Archive { */ @Override public void close() { mExecutor.shutdownNow(); synchronized (mEnqueuedOutputPipes) { for (ParcelFileDescriptor outputPipe : mEnqueuedOutputPipes) { try { outputPipe.closeWithError("Archive closed."); } catch (IOException e2) { // Silent close. } } } try { mZipFile.close(); } catch (IOException e) { Loading tests/unit/com/android/documentsui/archives/ReadableArchiveTest.java +11 −4 Original line number Diff line number Diff line Loading @@ -19,12 +19,15 @@ package com.android.documentsui.archives; import com.android.documentsui.archives.ReadableArchive; import com.android.documentsui.tests.R; import android.database.Cursor; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.ParcelFileDescriptor; import android.provider.DocumentsContract.Document; import android.support.test.InstrumentationRegistry; import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.MediumTest; Loading Loading @@ -281,23 +284,27 @@ public class ReadableArchiveTest extends AndroidTestCase { cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE))); } public void testOpenDocument() throws IOException { public void testOpenDocument() throws IOException, ErrnoException { loadArchive(mTestUtils.getSeekableDescriptor(R.raw.archive)); commonTestOpenDocument(); } public void testOpenDocument_NonSeekable() throws IOException { public void testOpenDocument_NonSeekable() throws IOException, ErrnoException { loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive)); commonTestOpenDocument(); } // Common part of testOpenDocument and testOpenDocument_NonSeekable. void commonTestOpenDocument() throws IOException { void commonTestOpenDocument() throws IOException, ErrnoException { final ParcelFileDescriptor descriptor = mArchive.openDocument( createArchiveId("/dir2/strawberries.txt").toDocumentId(), "r", null /* signal */); assertTrue(Archive.canSeek(descriptor)); try (final ParcelFileDescriptor.AutoCloseInputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(descriptor)) { Os.lseek(descriptor.getFileDescriptor(), "I love ".length(), OsConstants.SEEK_SET); assertEquals("strawberries!", new Scanner(inputStream).nextLine()); Os.lseek(descriptor.getFileDescriptor(), 0, OsConstants.SEEK_SET); assertEquals("I love strawberries!", new Scanner(inputStream).nextLine()); } } Loading Loading
src/com/android/documentsui/archives/Archive.java +3 −1 Original line number Diff line number Diff line Loading @@ -24,6 +24,8 @@ import android.graphics.Point; import android.net.Uri; import android.os.CancellationSignal; import android.os.ParcelFileDescriptor; import android.os.storage.StorageManager; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.support.annotation.Nullable; import android.system.ErrnoException; Loading Loading @@ -243,7 +245,7 @@ public abstract class Archive implements Closeable { public ParcelFileDescriptor openDocument( String documentId, String mode, @Nullable final CancellationSignal signal) throws FileNotFoundException { throw new UnsupportedOperationException("Thumbnails not supported."); throw new UnsupportedOperationException("Opening not supported."); } /** Loading
src/com/android/documentsui/archives/Proxy.java 0 → 100644 +96 −0 Original line number Diff line number Diff line /* * 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.archives; import android.os.ProxyFileDescriptorCallback; import android.system.ErrnoException; import android.system.OsConstants; import android.util.Log; import android.util.jar.StrictJarFile; import java.io.IOException; import java.io.InputStream; import java.util.zip.ZipEntry; import libcore.io.IoUtils; /** * Provides a backend for a seekable file descriptors for files in archives. */ public class Proxy extends ProxyFileDescriptorCallback { private final StrictJarFile mFile; private final ZipEntry mEntry; private InputStream mInputStream = null; private long mOffset = 0; Proxy(StrictJarFile file, ZipEntry entry) throws IOException { mFile = file; mEntry = entry; recreateInputStream(); } @Override public long onGetSize() throws ErrnoException { return mEntry.getSize(); } @Override public int onRead(long offset, int size, byte[] data) throws ErrnoException { // TODO: Add a ring buffer to prevent expensive seeks. if (offset < mOffset) { try { recreateInputStream(); } catch (IOException e) { throw new ErrnoException("onRead", OsConstants.EIO); } } while (mOffset < offset) { try { mOffset += mInputStream.skip(offset - mOffset); } catch (IOException e) { throw new ErrnoException("onRead", OsConstants.EIO); } } int remainingSize = size; while (remainingSize > 0) { try { int bytes = mInputStream.read(data, size - remainingSize, remainingSize); if (bytes <= 0) { return size - remainingSize; } remainingSize -= bytes; mOffset += bytes; } catch (IOException e) { throw new ErrnoException("onRead", OsConstants.EIO); } } return size - remainingSize; } @Override public void onRelease() { IoUtils.closeQuietly(mInputStream); } private void recreateInputStream() throws IOException { IoUtils.closeQuietly(mInputStream); mInputStream = mFile.getInputStream(mEntry); mOffset = 0; } }
src/com/android/documentsui/archives/ReadableArchive.java +6 −84 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ import android.os.Bundle; import android.os.CancellationSignal; import android.os.OperationCanceledException; import android.os.ParcelFileDescriptor; import android.os.storage.StorageManager; import android.provider.DocumentsContract; import android.support.annotation.Nullable; import android.util.Log; Loading @@ -47,9 +48,6 @@ import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.Stack; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.zip.ZipEntry; Loading @@ -62,9 +60,7 @@ import java.util.zip.ZipEntry; public class ReadableArchive extends Archive { private static final String TAG = "ReadableArchive"; @GuardedBy("mEnqueuedOutputPipes") private final Set<ParcelFileDescriptor> mEnqueuedOutputPipes = new HashSet<>(); private final ThreadPoolExecutor mExecutor; private final StorageManager mStorageManager; private final StrictJarFile mZipFile; private ReadableArchive( Loading @@ -80,11 +76,7 @@ public class ReadableArchive extends Archive { throw new IllegalStateException("Unsupported access mode."); } // At most 8 active threads. All threads idling for more than a minute will // be closed. mExecutor = new ThreadPoolExecutor(8, 8, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); mExecutor.allowCoreThreadTimeOut(true); mStorageManager = mContext.getSystemService(StorageManager.class); mZipFile = file != null ? new StrictJarFile(file.getPath(), false /* verify */, Loading Loading @@ -243,72 +235,12 @@ public class ReadableArchive extends Archive { throw new FileNotFoundException(); } ParcelFileDescriptor[] pipe; try { pipe = ParcelFileDescriptor.createReliablePipe(); return mStorageManager.openProxyFileDescriptor( ParcelFileDescriptor.MODE_READ_ONLY, new Proxy(mZipFile, entry)); } catch (IOException e) { // Ideally we'd simply throw IOException to the caller, but for consistency // with DocumentsProvider::openDocument, converting it to IllegalStateException. throw new IllegalStateException("Failed to open the document.", e); throw new IllegalStateException(e); } final InputStream inputStream = mZipFile.getInputStream(entry); final ParcelFileDescriptor outputPipe = pipe[1]; synchronized (mEnqueuedOutputPipes) { mEnqueuedOutputPipes.add(outputPipe); } try { mExecutor.execute( new Runnable() { @Override public void run() { synchronized (mEnqueuedOutputPipes) { mEnqueuedOutputPipes.remove(outputPipe); } try (final ParcelFileDescriptor.AutoCloseOutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(outputPipe)) { try { final byte buffer[] = new byte[32 * 1024]; int bytes; while ((bytes = inputStream.read(buffer)) != -1) { if (Thread.interrupted()) { throw new InterruptedException(); } if (signal != null) { signal.throwIfCanceled(); } outputStream.write(buffer, 0, bytes); } } catch (IOException | InterruptedException e) { // Catch the exception before the outer try-with-resource closes // the pipe with close() instead of closeWithError(). try { Log.e(TAG, "Failed while reading a file.", e); outputPipe.closeWithError("Reading failure."); } catch (IOException e2) { Log.e(TAG, "Failed to close the pipe after an error.", e2); } } } catch (OperationCanceledException e) { // Cancelled gracefully. } catch (IOException e) { Log.e(TAG, "Failed to close the output stream gracefully.", e); } finally { IoUtils.closeQuietly(inputStream); } } }); } catch (RejectedExecutionException e) { IoUtils.closeQuietly(pipe[0]); IoUtils.closeQuietly(pipe[1]); synchronized (mEnqueuedOutputPipes) { mEnqueuedOutputPipes.remove(outputPipe); } throw new IllegalStateException("Failed to initialize pipe."); } return pipe[0]; } @Override Loading Loading @@ -369,16 +301,6 @@ public class ReadableArchive extends Archive { */ @Override public void close() { mExecutor.shutdownNow(); synchronized (mEnqueuedOutputPipes) { for (ParcelFileDescriptor outputPipe : mEnqueuedOutputPipes) { try { outputPipe.closeWithError("Archive closed."); } catch (IOException e2) { // Silent close. } } } try { mZipFile.close(); } catch (IOException e) { Loading
tests/unit/com/android/documentsui/archives/ReadableArchiveTest.java +11 −4 Original line number Diff line number Diff line Loading @@ -19,12 +19,15 @@ package com.android.documentsui.archives; import com.android.documentsui.archives.ReadableArchive; import com.android.documentsui.tests.R; import android.database.Cursor; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.ParcelFileDescriptor; import android.provider.DocumentsContract.Document; import android.support.test.InstrumentationRegistry; import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.MediumTest; Loading Loading @@ -281,23 +284,27 @@ public class ReadableArchiveTest extends AndroidTestCase { cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE))); } public void testOpenDocument() throws IOException { public void testOpenDocument() throws IOException, ErrnoException { loadArchive(mTestUtils.getSeekableDescriptor(R.raw.archive)); commonTestOpenDocument(); } public void testOpenDocument_NonSeekable() throws IOException { public void testOpenDocument_NonSeekable() throws IOException, ErrnoException { loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive)); commonTestOpenDocument(); } // Common part of testOpenDocument and testOpenDocument_NonSeekable. void commonTestOpenDocument() throws IOException { void commonTestOpenDocument() throws IOException, ErrnoException { final ParcelFileDescriptor descriptor = mArchive.openDocument( createArchiveId("/dir2/strawberries.txt").toDocumentId(), "r", null /* signal */); assertTrue(Archive.canSeek(descriptor)); try (final ParcelFileDescriptor.AutoCloseInputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(descriptor)) { Os.lseek(descriptor.getFileDescriptor(), "I love ".length(), OsConstants.SEEK_SET); assertEquals("strawberries!", new Scanner(inputStream).nextLine()); Os.lseek(descriptor.getFileDescriptor(), 0, OsConstants.SEEK_SET); assertEquals("I love strawberries!", new Scanner(inputStream).nextLine()); } } Loading