Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b903b52f authored by Tomasz Mikolajewski's avatar Tomasz Mikolajewski Committed by Android (Google) Code Review
Browse files

Merge "Use AppFuse for archives in DocumentsUI."

parents bf87a5ce 5ed69834
Loading
Loading
Loading
Loading
+3 −1
Original line number Diff line number Diff line
@@ -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;
@@ -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.");
    }

    /**
+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;
    }
}
+6 −84
Original line number Diff line number Diff line
@@ -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;
@@ -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;

@@ -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(
@@ -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 */,
@@ -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
@@ -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) {
+11 −4
Original line number Diff line number Diff line
@@ -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;

@@ -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());
        }
    }