DocumentArchive.java revision 64ce8c2e2085a0d5ff3e69ba5520873d41c76af5
164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski/*
264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * Copyright (C) 2015 The Android Open Source Project
364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski *
464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * Licensed under the Apache License, Version 2.0 (the "License");
564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * you may not use this file except in compliance with the License.
664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * You may obtain a copy of the License at
764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski *
864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski *      http://www.apache.org/licenses/LICENSE-2.0
964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski *
1064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * Unless required by applicable law or agreed to in writing, software
1164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * distributed under the License is distributed on an "AS IS" BASIS,
1264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * See the License for the specific language governing permissions and
1464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * limitations under the License.
1564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski */
1664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
1764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskipackage android.support.provider;
1864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
1964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.content.Context;
2064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.database.Cursor;
2164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.database.MatrixCursor;
2264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.net.Uri;
2364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.os.CancellationSignal;
2464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.os.ParcelFileDescriptor;
2564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.provider.DocumentsContract.Document;
2664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.provider.DocumentsProvider;
2764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.util.Log;
2864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.webkit.MimeTypeMap;
2964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
3064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.io.Closeable;
3164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.io.File;
3264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.io.FileNotFoundException;
3364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.io.FileOutputStream;
3464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.io.IOException;
3564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.io.InputStream;
3664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.lang.IllegalArgumentException;
3764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.lang.IllegalStateException;
3864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.lang.UnsupportedOperationException;
3964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.Collections;
4064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.Iterator;
4164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.concurrent.ExecutorService;
4264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.concurrent.Executors;
4364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.zip.ZipEntry;
4464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.zip.ZipFile;
4564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.zip.ZipInputStream;
4664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
4764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski/**
4864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * Provides basic implementation for creating, extracting and accessing
4964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * files within archives exposed by a document provider. The id delimiter
5064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * must be a character which is not used in document ids generated by the
5164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * document provider.
5264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski *
5364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * <p>This class is thread safe.
5464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski *
5564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * @hide
5664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski */
5764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskipublic class DocumentArchive implements Closeable {
5864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private static final String TAG = "DocumentArchive";
5964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
6064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private static final String[] DEFAULT_PROJECTION = new String[] {
6164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            Document.COLUMN_DOCUMENT_ID,
6264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            Document.COLUMN_DISPLAY_NAME,
6364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            Document.COLUMN_MIME_TYPE,
6464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            Document.COLUMN_SIZE
6564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    };
6664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
6764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private final Context mContext;
6864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private final String mDocumentId;
6964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private final char mIdDelimiter;
7064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private final Uri mNotificationUri;
7164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private final ZipFile mZipFile;
7264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private final ExecutorService mExecutor;
7364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
7464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private DocumentArchive(
7564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            Context context,
7664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            File file,
7764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            String documentId,
7864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            char idDelimiter,
7964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            Uri notificationUri)
8064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            throws IOException {
8164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        mContext = context;
8264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        mDocumentId = documentId;
8364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        mIdDelimiter = idDelimiter;
8464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        mNotificationUri = notificationUri;
8564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        mZipFile = new ZipFile(file);
8664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        mExecutor = Executors.newSingleThreadExecutor();
8764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
8864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
8964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    /**
9064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * Creates a DocumentsArchive instance for opening, browsing and accessing
9164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * documents within the archive passed as a local file.
9264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *
9364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param context Context of the provider.
9464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param File Local file containing the archive.
9564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param documentId ID of the archive document.
9664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param idDelimiter Delimiter for constructing IDs of documents within the archive.
9764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *            The delimiter must never be used for IDs of other documents.
9864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param Uri notificationUri Uri for notifying that the archive file has changed.
9964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @see createForParcelFileDescriptor(DocumentsProvider, ParcelFileDescriptor, String, char,
10064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *          Uri)
10164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     */
10264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    public static DocumentArchive createForLocalFile(
10364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            Context context, File file, String documentId, char idDelimiter, Uri notificationUri)
10464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            throws IOException {
10564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        return new DocumentArchive(context, file, documentId, idDelimiter, notificationUri);
10664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
10764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
10864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    /**
10964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * Creates a DocumentsArchive instance for opening, browsing and accessing
11064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * documents within the archive passed as a file descriptor.
11164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *
11264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * <p>Note, that this method should be used only if the document does not exist
11364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * on the local storage. A snapshot file will be created, which may be slower
11464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * and consume significant resources, in contrast to using
11564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * {@see createForLocalFile(Context, File, String, char, Uri}.
11664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *
11764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param context Context of the provider.
11864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param descriptor File descriptor for the archive's contents.
11964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param documentId ID of the archive document.
12064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param idDelimiter Delimiter for constructing IDs of documents within the archive.
12164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *            The delimiter must never be used for IDs of other documents.
12264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param Uri notificationUri Uri for notifying that the archive file has changed.
12364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @see createForLocalFile(Context, File, String, char, Uri)
12464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     */
12564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    public static DocumentArchive createForParcelFileDescriptor(
12664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            Context context, ParcelFileDescriptor descriptor, String documentId,
12764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            char idDelimiter, Uri notificationUri)
12864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            throws IOException {
12964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        File snapshotFile = null;
13064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        try {
13164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            // Create a copy of the archive, as ZipFile doesn't operate on streams.
13264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            // Moreover, ZipInputStream would be inefficient for large files on
13364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            // pipes.
13464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            snapshotFile = File.createTempFile("android.support.provider.snapshot{",
13564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                    "}.zip", context.getCacheDir());
13664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
13764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            try (
13864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                final FileOutputStream outputStream =
13964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                        new ParcelFileDescriptor.AutoCloseOutputStream(
14064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                ParcelFileDescriptor.open(
14164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                        snapshotFile, ParcelFileDescriptor.MODE_WRITE_ONLY));
14264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                final ParcelFileDescriptor.AutoCloseInputStream inputStream =
14364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                        new ParcelFileDescriptor.AutoCloseInputStream(descriptor);
14464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            ) {
14564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                final byte[] buffer = new byte[32 * 1024];
14664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                int bytes;
14764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                while ((bytes = inputStream.read(buffer)) != -1) {
14864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                    outputStream.write(buffer, 0, bytes);
14964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                }
15064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                outputStream.flush();
15164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                return new DocumentArchive(context, snapshotFile, documentId, idDelimiter,
15264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                        notificationUri);
15364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            }
15464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        } finally {
15564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            // On UNIX the file will be still available for processes which opened it, even
15664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            // after deleting it. Remove it ASAP, as it won't be used by anyone else.
15764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            if (snapshotFile != null) {
15864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                snapshotFile.delete();
15964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            }
16064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
16164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
16264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
16364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    /**
16464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * Lists child documents of an archive or a directory within an
16564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * archive. Must be called only for archives with supported mime type,
16664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * or for documents within archives.
16764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *
16864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @see DocumentsProvider.queryChildDocuments(String, String[], String)
16964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     */
17064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    public Cursor queryChildDocuments(String documentId, String[] projection, String sortOrder) {
17164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ParsedDocumentId parsedParentId = ParsedDocumentId.fromDocumentId(
17264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                documentId, mIdDelimiter);
17364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentEquals(mDocumentId, parsedParentId.mArchiveId,
17464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                "Mismatching document ID. Expected: %s, actual: %s.");
17564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
17664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final String parentPath = parsedParentId.mPath != null ? normalizePath(
17764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                parsedParentId.mPath, true /* isDirectory */) : "/";
17864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final MatrixCursor result = new MatrixCursor(
17964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                projection != null ? projection : DEFAULT_PROJECTION);
18064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (mNotificationUri != null) {
18164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            result.setNotificationUri(mContext.getContentResolver(), mNotificationUri);
18264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
18364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
18464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        File file;
18564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        String maybeParentPath;
18664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        // TODO: Build an in-memory tree for storing the directory structure.
18764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        for (final ZipEntry entry : Collections.list(mZipFile.entries())) {
18864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            file = new File(getPathForEntry(entry));
18964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            maybeParentPath = normalizePath(file.getParent(), true /* isDirectory */);
19064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            if (maybeParentPath.equals(parentPath)) {
19164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                addCursorRow(result, entry);
19264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            }
19364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
19464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        return result;
19564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
19664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
19764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    /**
19864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * Returns a MIME type of a document within an archive.
19964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *
20064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @see DocumentsProvider.getDocumentType(String)
20164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     */
20264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    public String getDocumentType(String documentId) throws FileNotFoundException {
20364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ParsedDocumentId parsedId = ParsedDocumentId.fromDocumentId(
20464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                documentId, mIdDelimiter);
20564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentEquals(mDocumentId, parsedId.mArchiveId,
20664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                "Mismatching document ID. Expected: %s, actual: %s.");
20764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentNotNull(parsedId.mPath, "Not a document within an archive.");
20864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
20964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ZipEntry entry = mZipFile.getEntry(parsedId.mPath);
21064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (entry == null) {
21164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            throw new FileNotFoundException();
21264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
21364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        return getMimeTypeForEntry(entry);
21464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
21564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
21664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    /**
21764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * Returns true if a document within an archive is a child or any descendant of the archive
21864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * document or another document within the archive.
21964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *
22064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @see DocumentsProvider.isChildDocument(String, String)
22164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     */
22264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    public boolean isChildDocument(String parentDocumentId, String documentId) {
22364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ParsedDocumentId parsedParentId = ParsedDocumentId.fromDocumentId(
22464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                parentDocumentId, mIdDelimiter);
22564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ParsedDocumentId parsedId = ParsedDocumentId.fromDocumentId(
22664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                documentId, mIdDelimiter);
22764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentEquals(mDocumentId, parsedParentId.mArchiveId,
22864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                "Mismatching document ID. Expected: %s, actual: %s.");
22964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentNotNull(parsedId.mPath,
23064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                "Not a document within an archive.");
23164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
23264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ZipEntry entry = mZipFile.getEntry(parsedId.mPath);
23364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (entry == null) {
23464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            return false;
23564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
23664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
23764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (parsedParentId.mPath == null) {
23864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            // No need to compare paths. Every file in the archive is a child of the archive
23964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            // file.
24064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            return true;
24164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
24264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
24364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ZipEntry parentEntry = mZipFile.getEntry(parsedParentId.mPath);
24464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (parentEntry == null || !parentEntry.isDirectory()) {
24564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            return false;
24664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
24764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
24864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final String parentPath = getPathForEntry(parentEntry);
24964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
25064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        // Add a trailing slash even if it's not a directory, so it's easy to check if the
25164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        // entry is a descendant.
25264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final String pathWithSlash = normalizePath(getPathForEntry(entry), true /* isDirectory */);
25364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        return pathWithSlash.startsWith(parentPath) && !parentPath.equals(pathWithSlash);
25464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
25564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
25664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    /**
25764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * Returns metadata of a document within an archive.
25864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *
25964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @see DocumentsProvider.queryDocument(String, String[])
26064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     */
26164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    public Cursor queryDocument(String documentId, String[] projection)
26264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            throws FileNotFoundException {
26364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ParsedDocumentId parsedId = ParsedDocumentId.fromDocumentId(
26464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                documentId, mIdDelimiter);
26564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentEquals(mDocumentId, parsedId.mArchiveId,
26664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                "Mismatching document ID. Expected: %s, actual: %s.");
26764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentNotNull(parsedId.mPath, "Not a document within an archive.");
26864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
26964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ZipEntry entry = mZipFile.getEntry(parsedId.mPath);
27064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (entry == null) {
27164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            throw new FileNotFoundException();
27264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
27364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
27464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final MatrixCursor result = new MatrixCursor(
27564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                projection != null ? projection : DEFAULT_PROJECTION);
27664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (mNotificationUri != null) {
27764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            result.setNotificationUri(mContext.getContentResolver(), mNotificationUri);
27864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
27964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        addCursorRow(result, entry);
28064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        return result;
28164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
28264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
28364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    /**
28464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * Opens a file within an archive.
28564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *
28664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @see DocumentsProvider.openDocument(String, String, CancellationSignal))
28764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     */
28864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    public ParcelFileDescriptor openDocument(
28964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            String documentId, String mode, final CancellationSignal signal)
29064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            throws FileNotFoundException {
29164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentEquals("r", mode,
29264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                "Invalid mode. Only reading \"r\" supported, but got: \"%s\".");
29364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ParsedDocumentId parsedId = ParsedDocumentId.fromDocumentId(
29464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                documentId, mIdDelimiter);
29564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentEquals(mDocumentId, parsedId.mArchiveId,
29664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                "Mismatching document ID. Expected: %s, actual: %s.");
29764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentNotNull(parsedId.mPath, "Not a document within an archive.");
29864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
29964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ZipEntry entry = mZipFile.getEntry(parsedId.mPath);
30064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (entry == null) {
30164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            throw new FileNotFoundException();
30264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
30364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
30464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        ParcelFileDescriptor[] pipe;
30564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        InputStream inputStream = null;
30664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        try {
30764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            pipe = ParcelFileDescriptor.createReliablePipe();
30864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            inputStream = mZipFile.getInputStream(entry);
30964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        } catch (IOException e) {
31064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            if (inputStream != null) {
31164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                IoUtils.closeQuietly(inputStream);
31264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            }
31364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            // Ideally we'd simply throw IOException to the caller, but for consistency
31464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            // with DocumentsProvider::openDocument, converting it to IllegalStateException.
31564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            throw new IllegalStateException("Failed to open the document.", e);
31664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
31764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ParcelFileDescriptor outputPipe = pipe[1];
31864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final InputStream finalInputStream = inputStream;
31964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        mExecutor.execute(
32064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                new Runnable() {
32164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                    @Override
32264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                    public void run() {
32364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                        try (final ParcelFileDescriptor.AutoCloseOutputStream outputStream =
32464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                new ParcelFileDescriptor.AutoCloseOutputStream(outputPipe)) {
32564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                            try {
32664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                final byte buffer[] = new byte[32 * 1024];
32764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                int bytes;
32864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                while ((bytes = finalInputStream.read(buffer)) != -1) {
32964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                    if (Thread.interrupted()) {
33064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                        throw new InterruptedException();
33164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                    }
33264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                    if (signal != null) {
33364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                        signal.throwIfCanceled();
33464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                    }
33564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                    outputStream.write(buffer, 0, bytes);
33664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                }
33764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                            } catch (IOException | InterruptedException e) {
33864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                // Catch the exception before the outer try-with-resource closes the
33964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                // pipe with close() instead of closeWithError().
34064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                try {
34164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                    outputPipe.closeWithError(e.getMessage());
34264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                } catch (IOException e2) {
34364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                    Log.e(TAG, "Failed to close the pipe after an error.", e2);
34464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                }
34564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                            }
34664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                        } catch (IOException e) {
34764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                            Log.e(TAG, "Failed to close the output stream gracefully.", e);
34864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                        } finally {
34964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                            IoUtils.closeQuietly(finalInputStream);
35064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                        }
35164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                    }
35264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                });
35364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
35464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        return pipe[0];
35564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
35664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
35764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    /**
35864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * Schedules a gracefully close of the archive after any opened files are closed.
35964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *
36064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * <p>This method does not block until shutdown. Once called, other methods should not be
36164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * called.
36264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     */
36364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    @Override
36464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    public void close() {
36564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        mExecutor.execute(new Runnable() {
36664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            @Override
36764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            public void run() {
36864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                IoUtils.closeQuietly(mZipFile);
36964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            }
37064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        });
37164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        mExecutor.shutdown();
37264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
37364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
37464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private void addCursorRow(MatrixCursor cursor, ZipEntry entry) {
37564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final MatrixCursor.RowBuilder row = cursor.newRow();
37664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ParsedDocumentId parsedId = new ParsedDocumentId(mDocumentId, entry.getName());
37764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        row.add(Document.COLUMN_DOCUMENT_ID, parsedId.toDocumentId(mIdDelimiter));
37864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final File file = new File(entry.getName());
37964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        row.add(Document.COLUMN_DISPLAY_NAME, file.getName());
38064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        row.add(Document.COLUMN_SIZE, entry.getSize());
38164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        row.add(Document.COLUMN_MIME_TYPE, getMimeTypeForEntry(entry));
38264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
38364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
38464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private String getMimeTypeForEntry(ZipEntry entry) {
38564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (entry.isDirectory()) {
38664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            return Document.MIME_TYPE_DIR;
38764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
38864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
38964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final int lastDot = entry.getName().lastIndexOf('.');
39064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (lastDot >= 0) {
39164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            final String extension = entry.getName().substring(lastDot + 1).toLowerCase();
39264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            final String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
39364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            if (mimeType != null) {
39464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                return mimeType;
39564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            }
39664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
39764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
39864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        return "application/octet-stream";
39964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
40064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
40164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private static String normalizePath(String path, boolean isDirectory) {
40264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        // TODO: Add support for different path separators.
40364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final StringBuilder result = new StringBuilder();
40464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (!path.startsWith("/")) {
40564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            result.append("/");
40664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
40764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        result.append(path);
40864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (isDirectory && result.length() > 1 && !path.endsWith("/")) {
40964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            result.append("/");
41064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
41164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        return result.toString();
41264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
41364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
41464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private static String getPathForEntry(ZipEntry entry) {
41564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        return normalizePath(entry.getName(), entry.isDirectory());
41664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
41764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski};
418