DocumentArchive.java revision 09deb05d980153aa6d1a3696aca74f49d46caa94
15f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes/*
25f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes * Copyright (C) 2015 The Android Open Source Project
35f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes *
45f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes * Licensed under the Apache License, Version 2.0 (the "License");
55f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes * you may not use this file except in compliance with the License.
65f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes * You may obtain a copy of the License at
75f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes *
85f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes *      http://www.apache.org/licenses/LICENSE-2.0
95f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes *
105f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes * Unless required by applicable law or agreed to in writing, software
115f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes * distributed under the License is distributed on an "AS IS" BASIS,
125f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
135f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes * See the License for the specific language governing permissions and
145f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes * limitations under the License.
155f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes */
165f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes
1754e7df1896a4066cbb9fe6f72249829f0b8c49c6Elliott Hughespackage android.support.provider;
185f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes
19f0dc8b5519102b3d3e738aed846975ae4239421eMathieu Chartierimport android.content.Context;
20f0dc8b5519102b3d3e738aed846975ae4239421eMathieu Chartierimport android.content.res.AssetFileDescriptor;
21f0dc8b5519102b3d3e738aed846975ae4239421eMathieu Chartierimport android.database.Cursor;
2208fc03ae5dded4adc9b45b7014a4b9dfedbe95a6Elliott Hughesimport android.database.MatrixCursor;
2308fc03ae5dded4adc9b45b7014a4b9dfedbe95a6Elliott Hughesimport android.graphics.Point;
24e401d146407d61eeb99f8d6176b2ac13c4df1e33Mathieu Chartierimport android.net.Uri;
2576b6167407c2b6f5d40ad895b2793a6b037f54b2Elliott Hughesimport android.os.CancellationSignal;
261aa246dec5abe212f699de1413a0c4a191ca364aElliott Hughesimport android.os.OperationCanceledException;
2780afd02024d20e60b197d3adfbb43cc303cf29e0Vladimir Markoimport android.os.ParcelFileDescriptor;
2833dc7717cd16592bcc825350bea6305be9eb2ea1jeffhaoimport android.provider.DocumentsContract.Document;
294f6ad8ab428038129b2d0d6c40b7fd625cca15e1Ian Rogersimport android.provider.DocumentsProvider;
3008fc03ae5dded4adc9b45b7014a4b9dfedbe95a6Elliott Hughesimport android.support.annotation.Nullable;
31d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogersimport android.util.Log;
324f6ad8ab428038129b2d0d6c40b7fd625cca15e1Ian Rogersimport android.webkit.MimeTypeMap;
3305f3057d6a4d23d712092ccd36a531590bff323bIan Rogers
342dd0e2cea360bc9206eb88ecc40d259e796c239dIan Rogersimport java.io.Closeable;
3500f7d0eaa6bd93d33bf0c1429bf4ba0b3f28abacIan Rogersimport java.io.File;
365f79133a435ebcb20000370d56046fe01201dd80Elliott Hughesimport java.io.FileNotFoundException;
378e4aac52962d54cb4be2078b9cd95685e067133aElliott Hughesimport java.io.FileOutputStream;
3808fc03ae5dded4adc9b45b7014a4b9dfedbe95a6Elliott Hughesimport java.io.IOException;
39044288fb8f69e154862763a6b3bfea2bac1b397eElliott Hughesimport java.io.InputStream;
405f79133a435ebcb20000370d56046fe01201dd80Elliott Hughesimport java.util.ArrayList;
415f79133a435ebcb20000370d56046fe01201dd80Elliott Hughesimport java.lang.IllegalArgumentException;
425f79133a435ebcb20000370d56046fe01201dd80Elliott Hughesimport java.lang.IllegalStateException;
43b9001abff3a45f1ae90536da7dd1ec28a6ae0174Mathieu Chartierimport java.lang.UnsupportedOperationException;
44b9001abff3a45f1ae90536da7dd1ec28a6ae0174Mathieu Chartierimport java.util.Collections;
455f79133a435ebcb20000370d56046fe01201dd80Elliott Hughesimport java.util.HashMap;
46d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogersimport java.util.Iterator;
47d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogersimport java.util.List;
48d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogersimport java.util.Map;
495f79133a435ebcb20000370d56046fe01201dd80Elliott Hughesimport java.util.concurrent.ExecutorService;
50d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogersimport java.util.concurrent.Executors;
51d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogersimport java.util.zip.ZipEntry;
52d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogersimport java.util.zip.ZipFile;
535f79133a435ebcb20000370d56046fe01201dd80Elliott Hughesimport java.util.zip.ZipInputStream;
54d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers
55d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers/**
56d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers * Provides basic implementation for creating, extracting and accessing
575f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes * files within archives exposed by a document provider. The id delimiter
58d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers * must be a character which is not used in document ids generated by the
59d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers * document provider.
6054e7df1896a4066cbb9fe6f72249829f0b8c49c6Elliott Hughes *
615f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes * <p>This class is thread safe.
625f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes *
635f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes * @hide
645f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes */
655f79133a435ebcb20000370d56046fe01201dd80Elliott Hughespublic class DocumentArchive implements Closeable {
665f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes    private static final String TAG = "DocumentArchive";
67d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers
68d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers    private static final String[] DEFAULT_PROJECTION = new String[] {
69d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers            Document.COLUMN_DOCUMENT_ID,
705f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes            Document.COLUMN_DISPLAY_NAME,
7154e7df1896a4066cbb9fe6f72249829f0b8c49c6Elliott Hughes            Document.COLUMN_MIME_TYPE,
722cebb24bfc3247d3e9be138a3350106737455918Mathieu Chartier            Document.COLUMN_SIZE,
73fc86162ce2a3467acb690e18cc8bd9b3daafc606Elliott Hughes            Document.COLUMN_FLAGS
7432d6e1e5654433d7eadede89e1c770b2c839aee9Elliott Hughes    };
75fc86162ce2a3467acb690e18cc8bd9b3daafc606Elliott Hughes
762cebb24bfc3247d3e9be138a3350106737455918Mathieu Chartier    private final Context mContext;
77fc86162ce2a3467acb690e18cc8bd9b3daafc606Elliott Hughes    private final String mDocumentId;
78fc86162ce2a3467acb690e18cc8bd9b3daafc606Elliott Hughes    private final char mIdDelimiter;
79fc86162ce2a3467acb690e18cc8bd9b3daafc606Elliott Hughes    private final Uri mNotificationUri;
80fc86162ce2a3467acb690e18cc8bd9b3daafc606Elliott Hughes    private final ZipFile mZipFile;
81fc86162ce2a3467acb690e18cc8bd9b3daafc606Elliott Hughes    private final ExecutorService mExecutor;
824dd9b4d95eec9db5338fb9bf132f9bb8facf6cf4Elliott Hughes    private final Map<String, List<ZipEntry>> mTree;
83fc86162ce2a3467acb690e18cc8bd9b3daafc606Elliott Hughes
84fc86162ce2a3467acb690e18cc8bd9b3daafc606Elliott Hughes    private DocumentArchive(
8532d6e1e5654433d7eadede89e1c770b2c839aee9Elliott Hughes            Context context,
8632d6e1e5654433d7eadede89e1c770b2c839aee9Elliott Hughes            File file,
87ef7d42fca18c16fbaf103822ad16f23246e2905dIan Rogers            String documentId,
8800f7d0eaa6bd93d33bf0c1429bf4ba0b3f28abacIan Rogers            char idDelimiter,
89d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers            @Nullable Uri notificationUri)
9046bc778f1feed02b20d25e3d03470c93ca2c0506Mathieu Chartier            throws IOException {
9100f7d0eaa6bd93d33bf0c1429bf4ba0b3f28abacIan Rogers        mContext = context;
925f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes        mDocumentId = documentId;
9394f7b49578b6aaa80de8ffed230648d601393905Hiroshi Yamauchi        mIdDelimiter = idDelimiter;
942cebb24bfc3247d3e9be138a3350106737455918Mathieu Chartier        mNotificationUri = notificationUri;
95ad2541a59c00c2c69e8973088891a2b5257c9780Mathieu Chartier        mZipFile = new ZipFile(file);
962cebb24bfc3247d3e9be138a3350106737455918Mathieu Chartier        mExecutor = Executors.newSingleThreadExecutor();
97ef7d42fca18c16fbaf103822ad16f23246e2905dIan Rogers
9874240819ae09e29b2753ef38f4eb4be1c2762e2eAndreas Gampe        // Build the tree structure in memory.
9974240819ae09e29b2753ef38f4eb4be1c2762e2eAndreas Gampe        mTree = new HashMap<String, List<ZipEntry>>();
10074240819ae09e29b2753ef38f4eb4be1c2762e2eAndreas Gampe        mTree.put("/", new ArrayList<ZipEntry>());
10174240819ae09e29b2753ef38f4eb4be1c2762e2eAndreas Gampe
10274240819ae09e29b2753ef38f4eb4be1c2762e2eAndreas Gampe        // Traversing entries via ZipFile.entries() is very slow, so copy the entries to a temporary
10374240819ae09e29b2753ef38f4eb4be1c2762e2eAndreas Gampe        // map for building the tree.
10474240819ae09e29b2753ef38f4eb4be1c2762e2eAndreas Gampe        final Map<String, ZipEntry> entries = new HashMap<String, ZipEntry>();
10574240819ae09e29b2753ef38f4eb4be1c2762e2eAndreas Gampe        for (final ZipEntry entry : Collections.list(mZipFile.entries())) {
10674240819ae09e29b2753ef38f4eb4be1c2762e2eAndreas Gampe            entries.put(entry.getName(), entry);
10774240819ae09e29b2753ef38f4eb4be1c2762e2eAndreas Gampe            if (entry.isDirectory()) {
10874240819ae09e29b2753ef38f4eb4be1c2762e2eAndreas Gampe                mTree.put(entry.getName(), new ArrayList<ZipEntry>());
10974240819ae09e29b2753ef38f4eb4be1c2762e2eAndreas Gampe            }
11074240819ae09e29b2753ef38f4eb4be1c2762e2eAndreas Gampe        }
11174240819ae09e29b2753ef38f4eb4be1c2762e2eAndreas Gampe
11274240819ae09e29b2753ef38f4eb4be1c2762e2eAndreas Gampe        int delimiterIndex;
11374240819ae09e29b2753ef38f4eb4be1c2762e2eAndreas Gampe        List<ZipEntry> parentList;
11474240819ae09e29b2753ef38f4eb4be1c2762e2eAndreas Gampe        String parentPath;
11574240819ae09e29b2753ef38f4eb4be1c2762e2eAndreas Gampe        for (final ZipEntry entry : entries.values()) {
11694f7b49578b6aaa80de8ffed230648d601393905Hiroshi Yamauchi            delimiterIndex = entry.getName().lastIndexOf('/', entry.isDirectory()
1172cebb24bfc3247d3e9be138a3350106737455918Mathieu Chartier                    ? entry.getName().length() - 2 : entry.getName().length() - 1);
11874240819ae09e29b2753ef38f4eb4be1c2762e2eAndreas Gampe            parentPath =
1192cebb24bfc3247d3e9be138a3350106737455918Mathieu Chartier                    delimiterIndex != -1 ? entry.getName().substring(0, delimiterIndex) + "/" : "/";
12074240819ae09e29b2753ef38f4eb4be1c2762e2eAndreas Gampe            parentList = mTree.get(parentPath);
12174240819ae09e29b2753ef38f4eb4be1c2762e2eAndreas Gampe            if (parentList != null) {
12274240819ae09e29b2753ef38f4eb4be1c2762e2eAndreas Gampe                parentList.add(entry);
12374240819ae09e29b2753ef38f4eb4be1c2762e2eAndreas Gampe            } else {
12474240819ae09e29b2753ef38f4eb4be1c2762e2eAndreas Gampe                Log.w(TAG, "Archived files without a parent are not supported: " + entry.getName());
125d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers            }
126d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers        }
127ef7d42fca18c16fbaf103822ad16f23246e2905dIan Rogers    }
128ad2541a59c00c2c69e8973088891a2b5257c9780Mathieu Chartier
129d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers    /**
130d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers     * Creates a DocumentsArchive instance for opening, browsing and accessing
1314e6a31eb97f22f4480827474b30b9e64f396eaceMathieu Chartier     * documents within the archive passed as a local file.
1324e6a31eb97f22f4480827474b30b9e64f396eaceMathieu Chartier     *
1333e5cf305db800b2989ad57b7cde8fb3cc9fa1b9eIan Rogers     * @param context Context of the provider.
1344e6a31eb97f22f4480827474b30b9e64f396eaceMathieu Chartier     * @param File Local file containing the archive.
1354e6a31eb97f22f4480827474b30b9e64f396eaceMathieu Chartier     * @param documentId ID of the archive document.
1364e6a31eb97f22f4480827474b30b9e64f396eaceMathieu Chartier     * @param idDelimiter Delimiter for constructing IDs of documents within the archive.
1374e6a31eb97f22f4480827474b30b9e64f396eaceMathieu Chartier     *            The delimiter must never be used for IDs of other documents.
1383e5cf305db800b2989ad57b7cde8fb3cc9fa1b9eIan Rogers     * @param Uri notificationUri Uri for notifying that the archive file has changed.
1394e6a31eb97f22f4480827474b30b9e64f396eaceMathieu Chartier     * @see createForParcelFileDescriptor(DocumentsProvider, ParcelFileDescriptor, String, char,
1404e6a31eb97f22f4480827474b30b9e64f396eaceMathieu Chartier     *          Uri)
141d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers     */
142d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers    public static DocumentArchive createForLocalFile(
143ad2541a59c00c2c69e8973088891a2b5257c9780Mathieu Chartier            Context context, File file, String documentId, char idDelimiter,
14400f7d0eaa6bd93d33bf0c1429bf4ba0b3f28abacIan Rogers            @Nullable Uri notificationUri)
1454cba0d979a11f955e6ec3c0f1bf61478af7aa810Hiroshi Yamauchi            throws IOException {
146ad2541a59c00c2c69e8973088891a2b5257c9780Mathieu Chartier        return new DocumentArchive(context, file, documentId, idDelimiter, notificationUri);
147ad2541a59c00c2c69e8973088891a2b5257c9780Mathieu Chartier    }
148ad2541a59c00c2c69e8973088891a2b5257c9780Mathieu Chartier
149ad2541a59c00c2c69e8973088891a2b5257c9780Mathieu Chartier    /**
150ad2541a59c00c2c69e8973088891a2b5257c9780Mathieu Chartier     * Creates a DocumentsArchive instance for opening, browsing and accessing
151ad2541a59c00c2c69e8973088891a2b5257c9780Mathieu Chartier     * documents within the archive passed as a file descriptor.
152ad2541a59c00c2c69e8973088891a2b5257c9780Mathieu Chartier     *
1533e5cf305db800b2989ad57b7cde8fb3cc9fa1b9eIan Rogers     * <p>Note, that this method should be used only if the document does not exist
154ad2541a59c00c2c69e8973088891a2b5257c9780Mathieu Chartier     * on the local storage. A snapshot file will be created, which may be slower
155ad2541a59c00c2c69e8973088891a2b5257c9780Mathieu Chartier     * and consume significant resources, in contrast to using
156ad2541a59c00c2c69e8973088891a2b5257c9780Mathieu Chartier     * {@see createForLocalFile(Context, File, String, char, Uri}.
157ad2541a59c00c2c69e8973088891a2b5257c9780Mathieu Chartier     *
158ad2541a59c00c2c69e8973088891a2b5257c9780Mathieu Chartier     * @param context Context of the provider.
159ad2541a59c00c2c69e8973088891a2b5257c9780Mathieu Chartier     * @param descriptor File descriptor for the archive's contents.
160ad2541a59c00c2c69e8973088891a2b5257c9780Mathieu Chartier     * @param documentId ID of the archive document.
161ad2541a59c00c2c69e8973088891a2b5257c9780Mathieu Chartier     * @param idDelimiter Delimiter for constructing IDs of documents within the archive.
162ad2541a59c00c2c69e8973088891a2b5257c9780Mathieu Chartier     *            The delimiter must never be used for IDs of other documents.
163ad2541a59c00c2c69e8973088891a2b5257c9780Mathieu Chartier     * @param Uri notificationUri Uri for notifying that the archive file has changed.
164590fee9e8972f872301c2d16a575d579ee564beeMathieu Chartier     * @see createForLocalFile(Context, File, String, char, Uri)
165590fee9e8972f872301c2d16a575d579ee564beeMathieu Chartier     */
166590fee9e8972f872301c2d16a575d579ee564beeMathieu Chartier    public static DocumentArchive createForParcelFileDescriptor(
167590fee9e8972f872301c2d16a575d579ee564beeMathieu Chartier            Context context, ParcelFileDescriptor descriptor, String documentId,
168d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers            char idDelimiter, @Nullable Uri notificationUri)
169e15ea086439b41a805d164d2beb07b4ba96aaa97Hiroshi Yamauchi            throws IOException {
170d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers        File snapshotFile = null;
171228602f562f1d130d06e60a98752d99c2d467d6aIan Rogers        try {
17200f7d0eaa6bd93d33bf0c1429bf4ba0b3f28abacIan Rogers            // Create a copy of the archive, as ZipFile doesn't operate on streams.
1739728f91a63016136261231ff5213bde703bd27b6Mathieu Chartier            // Moreover, ZipInputStream would be inefficient for large files on
1746ec8ebd178ed39aa09e4c7fad194900114c4121aAndreas Gampe            // pipes.
1756ec8ebd178ed39aa09e4c7fad194900114c4121aAndreas Gampe            snapshotFile = File.createTempFile("android.support.provider.snapshot{",
1766ec8ebd178ed39aa09e4c7fad194900114c4121aAndreas Gampe                    "}.zip", context.getCacheDir());
17700f7d0eaa6bd93d33bf0c1429bf4ba0b3f28abacIan Rogers
178d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers            try (
1795f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes                final FileOutputStream outputStream =
1805f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes                        new ParcelFileDescriptor.AutoCloseOutputStream(
1815f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes                                ParcelFileDescriptor.open(
182590fee9e8972f872301c2d16a575d579ee564beeMathieu Chartier                                        snapshotFile, ParcelFileDescriptor.MODE_WRITE_ONLY));
1835f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes                final ParcelFileDescriptor.AutoCloseInputStream inputStream =
1845f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes                        new ParcelFileDescriptor.AutoCloseInputStream(descriptor);
1855f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes            ) {
1865f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes                final byte[] buffer = new byte[32 * 1024];
1872cebb24bfc3247d3e9be138a3350106737455918Mathieu Chartier                int bytes;
188dd7624d2b9e599d57762d12031b10b89defc9807Ian Rogers                while ((bytes = inputStream.read(buffer)) != -1) {
1892cebb24bfc3247d3e9be138a3350106737455918Mathieu Chartier                    outputStream.write(buffer, 0, bytes);
1905f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes                }
1915f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes                outputStream.flush();
1925f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes                return new DocumentArchive(context, snapshotFile, documentId, idDelimiter,
1935f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes                        notificationUri);
1945f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes            }
1955f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes        } finally {
196dd7624d2b9e599d57762d12031b10b89defc9807Ian Rogers            // On UNIX the file will be still available for processes which opened it, even
197dd7624d2b9e599d57762d12031b10b89defc9807Ian Rogers            // after deleting it. Remove it ASAP, as it won't be used by anyone else.
1985f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes            if (snapshotFile != null) {
199dd7624d2b9e599d57762d12031b10b89defc9807Ian Rogers                snapshotFile.delete();
2005f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes            }
2015f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes        }
2025f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes    }
2035f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes
2042cebb24bfc3247d3e9be138a3350106737455918Mathieu Chartier    /**
2052cebb24bfc3247d3e9be138a3350106737455918Mathieu Chartier     * Lists child documents of an archive or a directory within an
2065f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes     * archive. Must be called only for archives with supported mime type,
2075f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes     * or for documents within archives.
2085f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes     *
209dd7624d2b9e599d57762d12031b10b89defc9807Ian Rogers     * @see DocumentsProvider.queryChildDocuments(String, String[], String)
210dd7624d2b9e599d57762d12031b10b89defc9807Ian Rogers     */
2115f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes    public Cursor queryChildDocuments(String documentId, @Nullable String[] projection,
2125f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes            @Nullable String sortOrder) {
2135f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes        final ParsedDocumentId parsedParentId = ParsedDocumentId.fromDocumentId(
2145f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes                documentId, mIdDelimiter);
2152cebb24bfc3247d3e9be138a3350106737455918Mathieu Chartier        Preconditions.checkArgumentEquals(mDocumentId, parsedParentId.mArchiveId,
216dd7624d2b9e599d57762d12031b10b89defc9807Ian Rogers                "Mismatching document ID. Expected: %s, actual: %s.");
217dd7624d2b9e599d57762d12031b10b89defc9807Ian Rogers
218dd7624d2b9e599d57762d12031b10b89defc9807Ian Rogers        final String parentPath = parsedParentId.mPath != null ? parsedParentId.mPath : "/";
2195f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes        final MatrixCursor result = new MatrixCursor(
2205f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes                projection != null ? projection : DEFAULT_PROJECTION);
221dd7624d2b9e599d57762d12031b10b89defc9807Ian Rogers        if (mNotificationUri != null) {
2225f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes            result.setNotificationUri(mContext.getContentResolver(), mNotificationUri);
2235f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes        }
2245f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes
2256aa3df965395566ed6a4fec4af37c2b7577992e9Mathieu Chartier        final List<ZipEntry> parentList = mTree.get(parentPath);
22694f7b49578b6aaa80de8ffed230648d601393905Hiroshi Yamauchi        Preconditions.checkArgumentNotNull(
2276aa3df965395566ed6a4fec4af37c2b7577992e9Mathieu Chartier                parentList, "The requested directory does not exist in the archive.");
2286aa3df965395566ed6a4fec4af37c2b7577992e9Mathieu Chartier        for (final ZipEntry entry : parentList) {
2295f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes            addCursorRow(result, entry);
230d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers        }
231d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers        return result;
232440e4ceb310349ee8eb569495bc04d3d7fbe71cbMathieu Chartier    }
233d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers
234d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers    /**
235d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers     * Returns a MIME type of a document within an archive.
236d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers     *
237d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers     * @see DocumentsProvider.getDocumentType(String)
238d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers     */
239fc86162ce2a3467acb690e18cc8bd9b3daafc606Elliott Hughes    public String getDocumentType(String documentId) throws FileNotFoundException {
240d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers        final ParsedDocumentId parsedId = ParsedDocumentId.fromDocumentId(
241d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers                documentId, mIdDelimiter);
242d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers        Preconditions.checkArgumentEquals(mDocumentId, parsedId.mArchiveId,
243d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers                "Mismatching document ID. Expected: %s, actual: %s.");
244fc86162ce2a3467acb690e18cc8bd9b3daafc606Elliott Hughes        Preconditions.checkArgumentNotNull(parsedId.mPath, "Not a document within an archive.");
245d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers
246d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers        final ZipEntry entry = mZipFile.getEntry(parsedId.mPath);
247b894a19dfd668b6779de939cf5265b7e409d8809Xin Guan        if (entry == null) {
248e401d146407d61eeb99f8d6176b2ac13c4df1e33Mathieu Chartier            throw new FileNotFoundException();
249d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers        }
250440e4ceb310349ee8eb569495bc04d3d7fbe71cbMathieu Chartier        return getMimeTypeForEntry(entry);
251b9001abff3a45f1ae90536da7dd1ec28a6ae0174Mathieu Chartier    }
252440e4ceb310349ee8eb569495bc04d3d7fbe71cbMathieu Chartier
253d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers    /**
254a6e7f0872c42009ecbee82d7fbe452deef9ae65bMathieu Chartier     * Returns true if a document within an archive is a child or any descendant of the archive
255d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers     * document or another document within the archive.
256d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers     *
2572cebb24bfc3247d3e9be138a3350106737455918Mathieu Chartier     * @see DocumentsProvider.isChildDocument(String, String)
2582cebb24bfc3247d3e9be138a3350106737455918Mathieu Chartier     */
259f0dc8b5519102b3d3e738aed846975ae4239421eMathieu Chartier    public boolean isChildDocument(String parentDocumentId, String documentId) {
260f0dc8b5519102b3d3e738aed846975ae4239421eMathieu Chartier        final ParsedDocumentId parsedParentId = ParsedDocumentId.fromDocumentId(
261f0dc8b5519102b3d3e738aed846975ae4239421eMathieu Chartier                parentDocumentId, mIdDelimiter);
262f0dc8b5519102b3d3e738aed846975ae4239421eMathieu Chartier        final ParsedDocumentId parsedId = ParsedDocumentId.fromDocumentId(
263f0dc8b5519102b3d3e738aed846975ae4239421eMathieu Chartier                documentId, mIdDelimiter);
264f0dc8b5519102b3d3e738aed846975ae4239421eMathieu Chartier        Preconditions.checkArgumentEquals(mDocumentId, parsedParentId.mArchiveId,
265d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers                "Mismatching document ID. Expected: %s, actual: %s.");
266d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers        Preconditions.checkArgumentNotNull(parsedId.mPath,
267d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers                "Not a document within an archive.");
268d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers
269d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers        final ZipEntry entry = mZipFile.getEntry(parsedId.mPath);
270d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers        if (entry == null) {
271d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers            return false;
272d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers        }
273d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers
274d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers        if (parsedParentId.mPath == null) {
275d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers            // No need to compare paths. Every file in the archive is a child of the archive
276d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers            // file.
277d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers            return true;
278d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers        }
279b9001abff3a45f1ae90536da7dd1ec28a6ae0174Mathieu Chartier
280b9001abff3a45f1ae90536da7dd1ec28a6ae0174Mathieu Chartier        final ZipEntry parentEntry = mZipFile.getEntry(parsedParentId.mPath);
281b9001abff3a45f1ae90536da7dd1ec28a6ae0174Mathieu Chartier        if (parentEntry == null || !parentEntry.isDirectory()) {
282b9001abff3a45f1ae90536da7dd1ec28a6ae0174Mathieu Chartier            return false;
283b9001abff3a45f1ae90536da7dd1ec28a6ae0174Mathieu Chartier        }
284b9001abff3a45f1ae90536da7dd1ec28a6ae0174Mathieu Chartier
285d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers        final String parentPath = entry.getName();
286d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers
287d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers        // Add a trailing slash even if it's not a directory, so it's easy to check if the
288f0dc8b5519102b3d3e738aed846975ae4239421eMathieu Chartier        // entry is a descendant.
289fc86162ce2a3467acb690e18cc8bd9b3daafc606Elliott Hughes        final String pathWithSlash = entry.isDirectory() ? entry.getName() : entry.getName() + "/";
2905f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes        return pathWithSlash.startsWith(parentPath) && !parentPath.equals(pathWithSlash);
291a6e7f0872c42009ecbee82d7fbe452deef9ae65bMathieu Chartier    }
292d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers
293440e4ceb310349ee8eb569495bc04d3d7fbe71cbMathieu Chartier    /**
294fc86162ce2a3467acb690e18cc8bd9b3daafc606Elliott Hughes     * Returns metadata of a document within an archive.
2955f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes     *
2965f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes     * @see DocumentsProvider.queryDocument(String, String[])
2976d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers     */
2986d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers    public Cursor queryDocument(String documentId, @Nullable String[] projection)
2996d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers            throws FileNotFoundException {
30000f7d0eaa6bd93d33bf0c1429bf4ba0b3f28abacIan Rogers        final ParsedDocumentId parsedId = ParsedDocumentId.fromDocumentId(
30190443477f9a0061581c420775ce3b7eeae7468bcMathieu Chartier                documentId, mIdDelimiter);
3026d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers        Preconditions.checkArgumentEquals(mDocumentId, parsedId.mArchiveId,
3036d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers                "Mismatching document ID. Expected: %s, actual: %s.");
30462d6c772205b8859f0ebf7ad105402ec4c3e2e01Ian Rogers        Preconditions.checkArgumentNotNull(parsedId.mPath, "Not a document within an archive.");
3050aa50ce2fb75bfc2e815a0c33adf9b049561923bNicolas Geoffray
306d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers        final ZipEntry entry = mZipFile.getEntry(parsedId.mPath);
30764277f38032208a0c1081ff9e466c04009b96383Brian Carlstrom        if (entry == null) {
30862d6c772205b8859f0ebf7ad105402ec4c3e2e01Ian Rogers            throw new FileNotFoundException();
309d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers        }
31014691c5e786e8c2c5734f687e4c96217340771beNicolas Geoffray
31164277f38032208a0c1081ff9e466c04009b96383Brian Carlstrom        final MatrixCursor result = new MatrixCursor(
3126d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers                projection != null ? projection : DEFAULT_PROJECTION);
3136d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers        if (mNotificationUri != null) {
3146d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers            result.setNotificationUri(mContext.getContentResolver(), mNotificationUri);
315d423741f91526cada9d081c64ee295ec9863032dElliott Hughes        }
3162cebb24bfc3247d3e9be138a3350106737455918Mathieu Chartier        addCursorRow(result, entry);
3172cebb24bfc3247d3e9be138a3350106737455918Mathieu Chartier        return result;
318d423741f91526cada9d081c64ee295ec9863032dElliott Hughes    }
319d423741f91526cada9d081c64ee295ec9863032dElliott Hughes
320d423741f91526cada9d081c64ee295ec9863032dElliott Hughes    /**
321d423741f91526cada9d081c64ee295ec9863032dElliott Hughes     * Opens a file within an archive.
322d423741f91526cada9d081c64ee295ec9863032dElliott Hughes     *
323d423741f91526cada9d081c64ee295ec9863032dElliott Hughes     * @see DocumentsProvider.openDocument(String, String, CancellationSignal))
324d423741f91526cada9d081c64ee295ec9863032dElliott Hughes     */
3252dd0e2cea360bc9206eb88ecc40d259e796c239dIan Rogers    public ParcelFileDescriptor openDocument(
326ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes            String documentId, String mode, @Nullable final CancellationSignal signal)
3272cebb24bfc3247d3e9be138a3350106737455918Mathieu Chartier            throws FileNotFoundException {
328ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes        Preconditions.checkArgumentEquals("r", mode,
329ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes                "Invalid mode. Only reading \"r\" supported, but got: \"%s\".");
330ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes        final ParsedDocumentId parsedId = ParsedDocumentId.fromDocumentId(
331ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes                documentId, mIdDelimiter);
332ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes        Preconditions.checkArgumentEquals(mDocumentId, parsedId.mArchiveId,
333ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes                "Mismatching document ID. Expected: %s, actual: %s.");
33450b35e2fd1a68cd1240e4a9d9f363e11764957d1Ian Rogers        Preconditions.checkArgumentNotNull(parsedId.mPath, "Not a document within an archive.");
335ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes
3362cebb24bfc3247d3e9be138a3350106737455918Mathieu Chartier        final ZipEntry entry = mZipFile.getEntry(parsedId.mPath);
337ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes        if (entry == null) {
338ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes            throw new FileNotFoundException();
339ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes        }
340ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes
341ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes        ParcelFileDescriptor[] pipe;
3422cebb24bfc3247d3e9be138a3350106737455918Mathieu Chartier        InputStream inputStream = null;
3432cebb24bfc3247d3e9be138a3350106737455918Mathieu Chartier        try {
3446d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers            pipe = ParcelFileDescriptor.createReliablePipe();
3456d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers            inputStream = mZipFile.getInputStream(entry);
346ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes        } catch (IOException e) {
347ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes            if (inputStream != null) {
3486d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers                IoUtils.closeQuietly(inputStream);
3496d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers            }
3506d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers            // Ideally we'd simply throw IOException to the caller, but for consistency
3516d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers            // with DocumentsProvider::openDocument, converting it to IllegalStateException.
352ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes            throw new IllegalStateException("Failed to open the document.", e);
353ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes        }
354ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes        final ParcelFileDescriptor outputPipe = pipe[1];
3556d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers        final InputStream finalInputStream = inputStream;
3566d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers        mExecutor.execute(
3572cebb24bfc3247d3e9be138a3350106737455918Mathieu Chartier                new Runnable() {
3586d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers                    @Override
3596d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers                    public void run() {
3606d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers                        try (final ParcelFileDescriptor.AutoCloseOutputStream outputStream =
361ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes                                new ParcelFileDescriptor.AutoCloseOutputStream(outputPipe)) {
362ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes                            try {
363ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes                                final byte buffer[] = new byte[32 * 1024];
3646d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers                                int bytes;
3656d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers                                while ((bytes = finalInputStream.read(buffer)) != -1) {
3666d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers                                    if (Thread.interrupted()) {
3676d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers                                        throw new InterruptedException();
3686d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers                                    }
369ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes                                    if (signal != null) {
370ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes                                        signal.throwIfCanceled();
371ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes                                    }
372ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes                                    outputStream.write(buffer, 0, bytes);
3736d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers                                }
3746d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers                            } catch (IOException | InterruptedException e) {
3756d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers                                // Catch the exception before the outer try-with-resource closes the
376ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes                                // pipe with close() instead of closeWithError().
377ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes                                try {
378ffb465f23d9549dd591e6aa62e9250523cb00233Elliott Hughes                                    outputPipe.closeWithError(e.getMessage());
3796d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers                                } catch (IOException e2) {
3806d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers                                    Log.e(TAG, "Failed to close the pipe after an error.", e2);
3816d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers                                }
3825f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes                            }
3835f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes                        } catch (OperationCanceledException e) {
384d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers                            // Cancelled gracefully.
3852cebb24bfc3247d3e9be138a3350106737455918Mathieu Chartier                        } catch (IOException e) {
386d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers                            Log.e(TAG, "Failed to close the output stream gracefully.", e);
3876d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers                        } finally {
3886d0b13e931fb73d5e964d14b0a7ecd1f3f5db547Ian Rogers                            IoUtils.closeQuietly(finalInputStream);
3895f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes                        }
3905f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes                    }
3912cebb24bfc3247d3e9be138a3350106737455918Mathieu Chartier                });
3922cebb24bfc3247d3e9be138a3350106737455918Mathieu Chartier
3930399dde18753aa9bd2bd0d7cf60beef154d164a4Ian Rogers        return pipe[0];
394d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers    }
395d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers
3965f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes    /**
3975f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes     * Opens a thumbnail of a file within an archive.
3985f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes     *
3995f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes     * @see DocumentsProvider.openDocumentThumbnail(String, Point, CancellationSignal))
4005f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes     */
4015f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes    public AssetFileDescriptor openDocumentThumbnail(
4025f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes            String documentId, Point sizeHint, final CancellationSignal signal)
4034cba0d979a11f955e6ec3c0f1bf61478af7aa810Hiroshi Yamauchi            throws FileNotFoundException {
4045f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes        final ParsedDocumentId parsedId = ParsedDocumentId.fromDocumentId(documentId, mIdDelimiter);
4055f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes        Preconditions.checkArgumentEquals(mDocumentId, parsedId.mArchiveId,
4065f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes                "Mismatching document ID. Expected: %s, actual: %s.");
4075f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes        Preconditions.checkArgumentNotNull(parsedId.mPath, "Not a document within an archive.");
4085f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes        Preconditions.checkArgument(getDocumentType(documentId).startsWith("image/"),
4094cd121ef0cb35fced70c7d9de378277be7a727d9Elliott Hughes                "Thumbnails only supported for image/* MIME type.");
4104cd121ef0cb35fced70c7d9de378277be7a727d9Elliott Hughes
4112cebb24bfc3247d3e9be138a3350106737455918Mathieu Chartier        // TODO: Extract thumbnails from EXIF.
4124cd121ef0cb35fced70c7d9de378277be7a727d9Elliott Hughes        final ZipEntry entry = mZipFile.getEntry(parsedId.mPath);
4135f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes        if (entry == null) {
414d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers            throw new FileNotFoundException();
415d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers        }
4165f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes
4175f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes        return new AssetFileDescriptor(
418d9c4fc94fa618617f94e1de9af5f034549100753Ian Rogers                openDocument(documentId, "r", signal), 0, entry.getSize(), null);
4191af6a1fa35ff7dc0a5c653f19dbc8a91c914aa42Elena Sayapina    }
4205f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes
4215f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes    /**
4224cd121ef0cb35fced70c7d9de378277be7a727d9Elliott Hughes     * Schedules a gracefully close of the archive after any opened files are closed.
423df42c4815c30b9df15aacb88070c1e94f41d0226Elliott Hughes     *
424df42c4815c30b9df15aacb88070c1e94f41d0226Elliott Hughes     * <p>This method does not block until shutdown. Once called, other methods should not be
425df42c4815c30b9df15aacb88070c1e94f41d0226Elliott Hughes     * called.
426df42c4815c30b9df15aacb88070c1e94f41d0226Elliott Hughes     */
427df42c4815c30b9df15aacb88070c1e94f41d0226Elliott Hughes    @Override
428df42c4815c30b9df15aacb88070c1e94f41d0226Elliott Hughes    public void close() {
4295f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes        mExecutor.execute(new Runnable() {
4305f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes            @Override
4311af6a1fa35ff7dc0a5c653f19dbc8a91c914aa42Elena Sayapina            public void run() {
4320aa50ce2fb75bfc2e815a0c33adf9b049561923bNicolas Geoffray                IoUtils.closeQuietly(mZipFile);
433ef7d42fca18c16fbaf103822ad16f23246e2905dIan Rogers            }
4345f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes        });
4355f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes        mExecutor.shutdown();
4365f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes    }
4375f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes
4385f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes    private void addCursorRow(MatrixCursor cursor, ZipEntry entry) {
4395f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes        final MatrixCursor.RowBuilder row = cursor.newRow();
4405f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes        final ParsedDocumentId parsedId = new ParsedDocumentId(mDocumentId, entry.getName());
4415f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes        row.add(Document.COLUMN_DOCUMENT_ID, parsedId.toDocumentId(mIdDelimiter));
4425f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes
4435f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes        final File file = new File(entry.getName());
4445f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes        row.add(Document.COLUMN_DISPLAY_NAME, file.getName());
4455f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes        row.add(Document.COLUMN_SIZE, entry.getSize());
4465f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes
4475f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes        final String mimeType = getMimeTypeForEntry(entry);
448440e4ceb310349ee8eb569495bc04d3d7fbe71cbMathieu Chartier        row.add(Document.COLUMN_MIME_TYPE, mimeType);
4490399dde18753aa9bd2bd0d7cf60beef154d164a4Ian Rogers
4505f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes        final int flags = mimeType.startsWith("image/") ? Document.FLAG_SUPPORTS_THUMBNAIL : 0;
4512cebb24bfc3247d3e9be138a3350106737455918Mathieu Chartier        row.add(Document.COLUMN_FLAGS, flags);
452e401d146407d61eeb99f8d6176b2ac13c4df1e33Mathieu Chartier    }
4532cebb24bfc3247d3e9be138a3350106737455918Mathieu Chartier
4540399dde18753aa9bd2bd0d7cf60beef154d164a4Ian Rogers    private String getMimeTypeForEntry(ZipEntry entry) {
4550399dde18753aa9bd2bd0d7cf60beef154d164a4Ian Rogers        if (entry.isDirectory()) {
4565f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes            return Document.MIME_TYPE_DIR;
457b4e94fd380a84c755264e8668a16052442c7ec32Elliott Hughes        }
45800f7d0eaa6bd93d33bf0c1429bf4ba0b3f28abacIan Rogers
459f1d666e1b48f8070ef1177fce156c08827f08eb8Mathieu Chartier        final int lastDot = entry.getName().lastIndexOf('.');
460f1d666e1b48f8070ef1177fce156c08827f08eb8Mathieu Chartier        if (lastDot >= 0) {
461f1d666e1b48f8070ef1177fce156c08827f08eb8Mathieu Chartier            final String extension = entry.getName().substring(lastDot + 1).toLowerCase();
462f1d666e1b48f8070ef1177fce156c08827f08eb8Mathieu Chartier            final String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
463f1d666e1b48f8070ef1177fce156c08827f08eb8Mathieu Chartier            if (mimeType != null) {
46400f7d0eaa6bd93d33bf0c1429bf4ba0b3f28abacIan Rogers                return mimeType;
465dd7624d2b9e599d57762d12031b10b89defc9807Ian Rogers            }
4665f79133a435ebcb20000370d56046fe01201dd80Elliott Hughes        }
46700f7d0eaa6bd93d33bf0c1429bf4ba0b3f28abacIan Rogers
4682cebb24bfc3247d3e9be138a3350106737455918Mathieu Chartier        return "application/octet-stream";
46900f7d0eaa6bd93d33bf0c1429bf4ba0b3f28abacIan Rogers    }
470dd7624d2b9e599d57762d12031b10b89defc9807Ian Rogers};
471dd7624d2b9e599d57762d12031b10b89defc9807Ian Rogers