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

Commit 5ed69834 authored by Tomasz Mikolajewski's avatar Tomasz Mikolajewski
Browse files

Use AppFuse for archives in DocumentsUI.

As a result, the quick viewer can seek on the FDs.

Test: Tested manually with Pico. Also, unit tests.
Bug: 33361622
Change-Id: I3ae3b9d0fc3a45ad8c7fb77b33b3694a7ab1c6c2
parent d7ed947c
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;
@@ -246,7 +248,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());
        }
    }