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

Commit c8d3007d authored by Tomasz Mikolajewski's avatar Tomasz Mikolajewski
Browse files

Refactor archives to support creating archives.

Archive class used StrictJarFile, which only supports reading.
This CL makes Archive a base class which is going to have two
subclasses: ReadableArchive (current Archive) and WriteableArchive.

ReadableArchive will be used to open archives with StrictJarFile.
WriteableArchive will be used to create archives with ZipOutputStream.

Test: Unit tests.
Bug: 20822019
Change-Id: I40174c8d970bc3098929854231622e3006f6263e
(cherry picked from commit d683f975)
parent 188f2d3c
Loading
Loading
Loading
Loading
+3 −1
Original line number Original line Diff line number Diff line
@@ -22,6 +22,7 @@ import android.content.ContentResolver;
import android.content.Context;
import android.content.Context;
import android.database.Cursor;
import android.database.Cursor;
import android.net.Uri;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.RemoteException;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Path;
import android.provider.DocumentsContract.Path;
@@ -107,7 +108,8 @@ public interface DocumentsAccess {


        @Override
        @Override
        public DocumentInfo getArchiveDocument(Uri uri) {
        public DocumentInfo getArchiveDocument(Uri uri) {
            return getDocument(ArchivesProvider.buildUriForArchive(uri));
            return getDocument(ArchivesProvider.buildUriForArchive(uri,
                    ParcelFileDescriptor.MODE_READ_ONLY));
        }
        }


        @Override
        @Override
+25 −285
Original line number Original line Diff line number Diff line
@@ -21,44 +21,26 @@ import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MatrixCursor;
import android.graphics.Point;
import android.graphics.Point;
import android.media.ExifInterface;
import android.net.Uri;
import android.net.Uri;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.CancellationSignal;
import android.os.OperationCanceledException;
import android.os.ParcelFileDescriptor;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Document;
import android.support.annotation.Nullable;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.system.ErrnoException;
import android.system.ErrnoException;
import android.system.Os;
import android.system.Os;
import android.system.OsConstants;
import android.system.OsConstants;
import android.text.TextUtils;
import android.text.TextUtils;
import android.util.Log;
import android.util.jar.StrictJarFile;
import android.webkit.MimeTypeMap;
import android.webkit.MimeTypeMap;


import com.android.internal.util.Preconditions;
import com.android.internal.util.Preconditions;


import libcore.io.IoUtils;

import java.io.Closeable;
import java.io.Closeable;
import java.io.File;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.List;
import java.util.Locale;
import java.util.Locale;
import java.util.Map;
import java.util.Map;
import java.util.Stack;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeUnit;
@@ -70,7 +52,7 @@ import java.util.zip.ZipEntry;
 *
 *
 * <p>This class is thread safe.
 * <p>This class is thread safe.
 */
 */
public class Archive implements Closeable {
public abstract class Archive implements Closeable {
    private static final String TAG = "Archive";
    private static final String TAG = "Archive";


    public static final String[] DEFAULT_PROJECTION = new String[] {
    public static final String[] DEFAULT_PROJECTION = new String[] {
@@ -81,27 +63,23 @@ public class Archive implements Closeable {
            Document.COLUMN_FLAGS
            Document.COLUMN_FLAGS
    };
    };


    private final Context mContext;
    final Context mContext;
    private final Uri mArchiveUri;
    final Uri mArchiveUri;
    private final Uri mNotificationUri;
    final int mArchiveMode;
    private final StrictJarFile mZipFile;
    final Uri mNotificationUri;
    private final ThreadPoolExecutor mExecutor;
    final ThreadPoolExecutor mExecutor;
    private final Map<String, ZipEntry> mEntries;
    final Map<String, ZipEntry> mEntries;
    private final Map<String, List<ZipEntry>> mTree;
    final Map<String, List<ZipEntry>> mTree;


    private Archive(
    Archive(
            Context context,
            Context context,
            @Nullable File file,
            @Nullable FileDescriptor fd,
            Uri archiveUri,
            Uri archiveUri,
            @Nullable Uri notificationUri)
            int archiveMode,
            throws IOException {
            @Nullable Uri notificationUri) {
        mContext = context;
        mContext = context;
        mArchiveUri = archiveUri;
        mArchiveUri = archiveUri;
        mArchiveMode = archiveMode;
        mNotificationUri = notificationUri;
        mNotificationUri = notificationUri;
        mZipFile = file != null ?
                new StrictJarFile(file.getPath(), false /* verify */, false /* signatures */) :
                new StrictJarFile(fd, false /* verify */, false /* signatures */);


        // At most 8 active threads. All threads idling for more than a minute will
        // At most 8 active threads. All threads idling for more than a minute will
        // be closed.
        // be closed.
@@ -109,68 +87,8 @@ public class Archive implements Closeable {
                new LinkedBlockingQueue<Runnable>());
                new LinkedBlockingQueue<Runnable>());
        mExecutor.allowCoreThreadTimeOut(true);
        mExecutor.allowCoreThreadTimeOut(true);


        // Build the tree structure in memory.
        mTree = new HashMap<>();
        mTree = new HashMap<>();

        mEntries = new HashMap<>();
        mEntries = new HashMap<>();
        ZipEntry entry;
        String entryPath;
        final Iterator<ZipEntry> it = mZipFile.iterator();
        final Stack<ZipEntry> stack = new Stack<>();
        while (it.hasNext()) {
            entry = it.next();
            if (entry.isDirectory() != entry.getName().endsWith("/")) {
                throw new IOException(
                        "Directories must have a trailing slash, and files must not.");
            }
            entryPath = getEntryPath(entry);
            if (mEntries.containsKey(entryPath)) {
                throw new IOException("Multiple entries with the same name are not supported.");
            }
            mEntries.put(entryPath, entry);
            if (entry.isDirectory()) {
                mTree.put(entryPath, new ArrayList<ZipEntry>());
            }
            if (!"/".equals(entryPath)) { // Skip root, as it doesn't have a parent.
                stack.push(entry);
            }
        }

        int delimiterIndex;
        String parentPath;
        ZipEntry parentEntry;
        List<ZipEntry> parentList;

        // Go through all directories recursively and build a tree structure.
        while (stack.size() > 0) {
            entry = stack.pop();

            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) {
                // 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<>();
                mTree.put(parentPath, parentList);
            }

            parentList.add(entry);
        }
    }
    }


    /**
    /**
@@ -190,7 +108,6 @@ public class Archive implements Closeable {
     * Returns true if the file descriptor is seekable.
     * Returns true if the file descriptor is seekable.
     * @param descriptor File descriptor to check.
     * @param descriptor File descriptor to check.
     */
     */
    @VisibleForTesting
    public static boolean canSeek(ParcelFileDescriptor descriptor) {
    public static boolean canSeek(ParcelFileDescriptor descriptor) {
        try {
        try {
            return Os.lseek(descriptor.getFileDescriptor(), 0,
            return Os.lseek(descriptor.getFileDescriptor(), 0,
@@ -200,75 +117,6 @@ public class Archive implements Closeable {
        }
        }
    }
    }


    /**
     * 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.
     *
     * This method takes ownership for the passed descriptor. The caller must
     * not close it.
     *
     * @param context Context of the provider.
     * @param descriptor File descriptor for the archive's contents.
     * @param archiveUri Uri of the archive document.
     * @param Uri notificationUri Uri for notifying that the archive file has changed.
     */
    public static Archive createForParcelFileDescriptor(
            Context context, ParcelFileDescriptor descriptor, Uri archiveUri,
            @Nullable Uri notificationUri)
            throws IOException {
        FileDescriptor fd = null;
        try {
            if (canSeek(descriptor)) {
                fd = new FileDescriptor();
                fd.setInt$(descriptor.detachFd());
                return new Archive(context, null, fd, 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.
                // Moreover, ZipInputStream would be inefficient for large files on
                // pipes.
                snapshotFile = File.createTempFile("com.android.documentsui.snapshot{",
                        "}.zip", context.getCacheDir());

                try (
                    final FileOutputStream outputStream =
                            new ParcelFileDescriptor.AutoCloseOutputStream(
                                    ParcelFileDescriptor.open(
                                            snapshotFile, ParcelFileDescriptor.MODE_WRITE_ONLY));
                    final ParcelFileDescriptor.AutoCloseInputStream inputStream =
                            new ParcelFileDescriptor.AutoCloseInputStream(descriptor);
                ) {
                    final byte[] buffer = new byte[32 * 1024];
                    int bytes;
                    while ((bytes = inputStream.read(buffer)) != -1) {
                        outputStream.write(buffer, 0, bytes);
                    }
                    outputStream.flush();
                }
                return new Archive(context, snapshotFile, null, archiveUri,
                        notificationUri);
            } 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 (snapshotFile != null) {
                    snapshotFile.delete();
                }
            }
        } catch (Exception e) {
            // Since the method takes ownership of the passed descriptor, close it
            // on exception.
            IoUtils.closeQuietly(descriptor);
            IoUtils.closeQuietly(fd);
            throw e;
        }
    }

    /**
    /**
     * Lists child documents of an archive or a directory within an
     * Lists child documents of an archive or a directory within an
     * archive. Must be called only for archives with supported mime type,
     * archive. Must be called only for archives with supported mime type,
@@ -376,127 +224,24 @@ public class Archive implements Closeable {
     *
     *
     * @see DocumentsProvider.openDocument(String, String, CancellationSignal))
     * @see DocumentsProvider.openDocument(String, String, CancellationSignal))
     */
     */
    public ParcelFileDescriptor openDocument(
    abstract public ParcelFileDescriptor openDocument(
            String documentId, String mode, @Nullable final CancellationSignal signal)
            String documentId, String mode, @Nullable final CancellationSignal signal)
            throws FileNotFoundException {
            throws FileNotFoundException;
        MorePreconditions.checkArgumentEquals("r", mode,
                "Invalid mode. Only reading \"r\" supported, but got: \"%s\".");
        final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId);
        MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri,
                "Mismatching archive Uri. Expected: %s, actual: %s.");

        final ZipEntry entry = mEntries.get(parsedId.mPath);
        if (entry == null) {
            throw new FileNotFoundException();
        }

        ParcelFileDescriptor[] pipe;
        InputStream inputStream = null;
        try {
            pipe = ParcelFileDescriptor.createReliablePipe();
            inputStream = mZipFile.getInputStream(entry);
        } catch (IOException e) {
            if (inputStream != null) {
                IoUtils.closeQuietly(inputStream);
            }
            // 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);
        }
        final ParcelFileDescriptor outputPipe = pipe[1];
        final InputStream finalInputStream = inputStream;
        mExecutor.execute(
                new Runnable() {
                    @Override
                    public void run() {
                        try (final ParcelFileDescriptor.AutoCloseOutputStream outputStream =
                                new ParcelFileDescriptor.AutoCloseOutputStream(outputPipe)) {
                            try {
                                final byte buffer[] = new byte[32 * 1024];
                                int bytes;
                                while ((bytes = finalInputStream.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 {
                                    outputPipe.closeWithError(e.getMessage());
                                } 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(finalInputStream);
                        }
                    }
                });

        return pipe[0];
    }


    /**
    /**
     * Opens a thumbnail of a file within an archive.
     * Opens a thumbnail of a file within an archive.
     *
     *
     * @see DocumentsProvider.openDocumentThumbnail(String, Point, CancellationSignal))
     * @see DocumentsProvider.openDocumentThumbnail(String, Point, CancellationSignal))
     */
     */
    public AssetFileDescriptor openDocumentThumbnail(
    abstract public AssetFileDescriptor openDocumentThumbnail(
            String documentId, Point sizeHint, final CancellationSignal signal)
            String documentId, Point sizeHint, final CancellationSignal signal)
            throws FileNotFoundException {
            throws FileNotFoundException;
        final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId);
        MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri,
                "Mismatching archive Uri. Expected: %s, actual: %s.");
        Preconditions.checkArgument(getDocumentType(documentId).startsWith("image/"),
                "Thumbnails only supported for image/* MIME type.");

        final ZipEntry entry = mEntries.get(parsedId.mPath);
        if (entry == null) {
            throw new FileNotFoundException();
        }


        InputStream inputStream = null;
    /**
        try {
     * Creates an archive id for the passed path.
            inputStream = mZipFile.getInputStream(entry);
     */
            final ExifInterface exif = new ExifInterface(inputStream);
    public ArchiveId createArchiveId(String path) {
            if (exif.hasThumbnail()) {
        return new ArchiveId(mArchiveUri, mArchiveMode, path);
                Bundle extras = null;
                switch (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1)) {
                    case ExifInterface.ORIENTATION_ROTATE_90:
                        extras = new Bundle(1);
                        extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 90);
                        break;
                    case ExifInterface.ORIENTATION_ROTATE_180:
                        extras = new Bundle(1);
                        extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 180);
                        break;
                    case ExifInterface.ORIENTATION_ROTATE_270:
                        extras = new Bundle(1);
                        extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 270);
                        break;
                }
                final long[] range = exif.getThumbnailRange();
                return new AssetFileDescriptor(
                        openDocument(documentId, "r", signal), range[0], range[1], extras);
            }
        } catch (IOException e) {
            // Ignore the exception, as reading the EXIF may legally fail.
            Log.e(TAG, "Failed to obtain thumbnail from EXIF.", e);
        } finally {
            IoUtils.closeQuietly(inputStream);
        }

        return new AssetFileDescriptor(
                openDocument(documentId, "r", signal), 0, entry.getSize(), null);
    }
    }


    /**
    /**
@@ -508,16 +253,11 @@ public class Archive implements Closeable {
    @Override
    @Override
    public void close() {
    public void close() {
        mExecutor.shutdownNow();
        mExecutor.shutdownNow();
        try {
            mZipFile.close();
        } catch (IOException e) {
            // Silent close.
        }
    }
    }


    private void addCursorRow(MatrixCursor cursor, ZipEntry entry) {
    void addCursorRow(MatrixCursor cursor, ZipEntry entry) {
        final MatrixCursor.RowBuilder row = cursor.newRow();
        final MatrixCursor.RowBuilder row = cursor.newRow();
        final ArchiveId parsedId = new ArchiveId(mArchiveUri, getEntryPath(entry));
        final ArchiveId parsedId = createArchiveId(getEntryPath(entry));
        row.add(Document.COLUMN_DOCUMENT_ID, parsedId.toDocumentId());
        row.add(Document.COLUMN_DOCUMENT_ID, parsedId.toDocumentId());


        final File file = new File(entry.getName());
        final File file = new File(entry.getName());
@@ -531,7 +271,7 @@ public class Archive implements Closeable {
        row.add(Document.COLUMN_FLAGS, flags);
        row.add(Document.COLUMN_FLAGS, flags);
    }
    }


    private String getMimeTypeForEntry(ZipEntry entry) {
    static String getMimeTypeForEntry(ZipEntry entry) {
        if (entry.isDirectory()) {
        if (entry.isDirectory()) {
            return Document.MIME_TYPE_DIR;
            return Document.MIME_TYPE_DIR;
        }
        }
@@ -549,7 +289,7 @@ public class Archive implements Closeable {
    }
    }


    // TODO: Upstream to the Preconditions class.
    // TODO: Upstream to the Preconditions class.
    private static class MorePreconditions {
    static class MorePreconditions {
        static void checkArgumentEquals(String expected, @Nullable String actual,
        static void checkArgumentEquals(String expected, @Nullable String actual,
                String message) {
                String message) {
            if (!TextUtils.equals(expected, actual)) {
            if (!TextUtils.equals(expected, actual)) {
+19 −5
Original line number Original line Diff line number Diff line
@@ -22,22 +22,36 @@ public class ArchiveId {
    private final static char DELIMITER = '#';
    private final static char DELIMITER = '#';


    public final Uri mArchiveUri;
    public final Uri mArchiveUri;
    public final int mAccessMode;
    public final String mPath;
    public final String mPath;


    public ArchiveId(Uri archiveUri, String path) {
    public ArchiveId(Uri archiveUri, int accessMode, String path) {
        assert(archiveUri.toString().indexOf(DELIMITER) == -1);
        assert(!path.isEmpty());

        mArchiveUri = archiveUri;
        mArchiveUri = archiveUri;
        mAccessMode = accessMode;
        mPath = path;
        mPath = path;
        assert(!mPath.isEmpty());
    }
    }


    static public ArchiveId fromDocumentId(String documentId) {
    static public ArchiveId fromDocumentId(String documentId) {
        final int delimiterPosition = documentId.indexOf(DELIMITER);
        final int delimiterPosition = documentId.indexOf(DELIMITER);
        assert(delimiterPosition != -1);
        assert(delimiterPosition != -1);
        return new ArchiveId(Uri.parse(documentId.substring(0, delimiterPosition)),

                documentId.substring((delimiterPosition + 1)));
        final int secondDelimiterPosition = documentId.indexOf(DELIMITER, delimiterPosition + 1);
        assert(secondDelimiterPosition != -1);

        final String archiveUriPart = documentId.substring(0, delimiterPosition);
        final String accessModePart = documentId.substring(delimiterPosition + 1,
                secondDelimiterPosition);

        final String pathPart = documentId.substring(secondDelimiterPosition + 1);

        return new ArchiveId(Uri.parse(archiveUriPart), Integer.parseInt(accessModePart),
                pathPart);
    }
    }


    public String toDocumentId() {
    public String toDocumentId() {
        return mArchiveUri.toString() + DELIMITER + mPath;
        return mArchiveUri.toString() + DELIMITER + mAccessMode + DELIMITER + mPath;
    }
    }
};
};
+13 −7
Original line number Original line Diff line number Diff line
@@ -121,7 +121,7 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable {


            cursor.setExtras(bundle);
            cursor.setExtras(bundle);
            cursor.setNotificationUri(getContext().getContentResolver(),
            cursor.setNotificationUri(getContext().getContentResolver(),
                    buildUriForArchive(archiveId.mArchiveUri));
                    buildUriForArchive(archiveId.mArchiveUri, archiveId.mAccessMode));
            return cursor;
            return cursor;
        } finally {
        } finally {
            releaseInstance(loader);
            releaseInstance(loader);
@@ -231,9 +231,15 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable {
        return false;
        return false;
    }
    }


    public static Uri buildUriForArchive(Uri archiveUri) {
    /**
        return DocumentsContract.buildDocumentUri(
     * Creates a Uri for accessing an archive with the specified access mode.
                AUTHORITY, new ArchiveId(archiveUri, "/").toDocumentId());
     *
     * @see ParcelFileDescriptor#MODE_READ
     * @see ParcelFileDescriptor#MODE_WRITE
     */
    public static Uri buildUriForArchive(Uri archiveUri, int accessMode) {
        return DocumentsContract.buildDocumentUri(AUTHORITY,
                new ArchiveId(archiveUri, accessMode, "/").toDocumentId());
    }
    }


    /**
    /**
@@ -263,8 +269,7 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable {
        }
        }
    }
    }


    private Loader getInstanceUncheckedLocked(String documentId)
    private Loader getInstanceUncheckedLocked(String documentId) throws FileNotFoundException {
            throws FileNotFoundException {
        final ArchiveId id = ArchiveId.fromDocumentId(documentId);
        final ArchiveId id = ArchiveId.fromDocumentId(documentId);
        if (mArchives.get(id.mArchiveUri) != null) {
        if (mArchives.get(id.mArchiveUri) != null) {
            return mArchives.get(id.mArchiveUri);
            return mArchives.get(id.mArchiveUri);
@@ -281,7 +286,8 @@ public class ArchivesProvider extends DocumentsProvider implements Closeable {
                Document.COLUMN_MIME_TYPE));
                Document.COLUMN_MIME_TYPE));
        Preconditions.checkArgument(isSupportedArchiveType(mimeType));
        Preconditions.checkArgument(isSupportedArchiveType(mimeType));
        final Uri notificationUri = cursor.getNotificationUri();
        final Uri notificationUri = cursor.getNotificationUri();
        final Loader loader = new Loader(getContext(), id.mArchiveUri, notificationUri);
        final Loader loader = new Loader(getContext(), id.mArchiveUri, id.mAccessMode,
                notificationUri);


        // Remove the instance from mArchives collection once the archive file changes.
        // Remove the instance from mArchives collection once the archive file changes.
        if (notificationUri != null) {
        if (notificationUri != null) {
+15 −7
Original line number Original line Diff line number Diff line
@@ -42,6 +42,7 @@ public class Loader {


    private final Context mContext;
    private final Context mContext;
    private final Uri mArchiveUri;
    private final Uri mArchiveUri;
    private final int mAccessMode;
    private final Uri mNotificationUri;
    private final Uri mNotificationUri;
    private final ReentrantReadWriteLock mLock = new ReentrantReadWriteLock();
    private final ReentrantReadWriteLock mLock = new ReentrantReadWriteLock();
    private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
    private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
@@ -50,9 +51,10 @@ public class Loader {
    private int mStatus = STATUS_OPENING;
    private int mStatus = STATUS_OPENING;
    private Archive mArchive = null;
    private Archive mArchive = null;


    Loader(Context context, Uri archiveUri, Uri notificationUri) {
    Loader(Context context, Uri archiveUri, int accessMode, Uri notificationUri) {
        this.mContext = context;
        this.mContext = context;
        this.mArchiveUri = archiveUri;
        this.mArchiveUri = archiveUri;
        this.mAccessMode = accessMode;
        this.mNotificationUri = notificationUri;
        this.mNotificationUri = notificationUri;


        // Start loading the archive immediately in the background.
        // Start loading the archive immediately in the background.
@@ -77,11 +79,17 @@ public class Loader {
        }
        }


        try {
        try {
            mArchive = Archive.createForParcelFileDescriptor(
            if (ReadableArchive.supportsAccessMode(mAccessMode)) {
                mArchive = ReadableArchive.createForParcelFileDescriptor(
                        mContext,
                        mContext,
                        mContext.getContentResolver().openFileDescriptor(
                        mContext.getContentResolver().openFileDescriptor(
                                mArchiveUri, "r", null /* signal */),
                                mArchiveUri, "r", null /* signal */),
                    mArchiveUri, mNotificationUri);
                        mArchiveUri, mAccessMode, mNotificationUri);
            // TODO:
            // } else if (WriteableArchive.supportsAccessMode(mAccessMode)) {
            } else {
                throw new IllegalStateException("Access mode not supported.");
            }
            synchronized (mStatusLock) {
            synchronized (mStatusLock) {
                mStatus = STATUS_OPENED;
                mStatus = STATUS_OPENED;
            }
            }
@@ -95,7 +103,7 @@ public class Loader {
            // Notify observers that the root directory is loaded (or failed)
            // Notify observers that the root directory is loaded (or failed)
            // so clients reload it.
            // so clients reload it.
            mContext.getContentResolver().notifyChange(
            mContext.getContentResolver().notifyChange(
                    ArchivesProvider.buildUriForArchive(mArchiveUri),
                    ArchivesProvider.buildUriForArchive(mArchiveUri, mAccessMode),
                    null /* observer */, false /* syncToNetwork */);
                    null /* observer */, false /* syncToNetwork */);
        }
        }


Loading