DocumentArchive.java revision e7cddbdd108544a329500ccb02fdc3ef0fca94d7
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;
2009deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewskiimport android.content.res.AssetFileDescriptor;
2164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.database.Cursor;
2264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.database.MatrixCursor;
2309deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewskiimport android.graphics.Point;
2464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.net.Uri;
2564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.os.CancellationSignal;
2609deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewskiimport android.os.OperationCanceledException;
2764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.os.ParcelFileDescriptor;
2864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.provider.DocumentsContract.Document;
2964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.provider.DocumentsProvider;
30369746f1bd8e90fb04e11f181623413b7ae8e063Tomasz Mikolajewskiimport android.support.annotation.Nullable;
3164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.util.Log;
3264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.webkit.MimeTypeMap;
3364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
3464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.io.Closeable;
3564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.io.File;
3664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.io.FileNotFoundException;
3764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.io.FileOutputStream;
3864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.io.IOException;
3964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.io.InputStream;
4093307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewskiimport java.util.ArrayList;
4164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.lang.IllegalArgumentException;
4264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.lang.IllegalStateException;
4364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.lang.UnsupportedOperationException;
4464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.Collections;
4593307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewskiimport java.util.HashMap;
4664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.Iterator;
4793307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewskiimport java.util.List;
4893307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewskiimport java.util.Map;
4979ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewskiimport java.util.Stack;
5064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.concurrent.ExecutorService;
5164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.concurrent.Executors;
5264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.zip.ZipEntry;
5364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.zip.ZipFile;
5464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.zip.ZipInputStream;
5564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
5664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski/**
5764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * Provides basic implementation for creating, extracting and accessing
5864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * files within archives exposed by a document provider. The id delimiter
5964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * must be a character which is not used in document ids generated by the
6064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * document provider.
6164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski *
6264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * <p>This class is thread safe.
6364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski *
6464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * @hide
6564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski */
6664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskipublic class DocumentArchive implements Closeable {
6764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private static final String TAG = "DocumentArchive";
6864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
6964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private static final String[] DEFAULT_PROJECTION = new String[] {
7064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            Document.COLUMN_DOCUMENT_ID,
7164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            Document.COLUMN_DISPLAY_NAME,
7264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            Document.COLUMN_MIME_TYPE,
7309deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski            Document.COLUMN_SIZE,
7409deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski            Document.COLUMN_FLAGS
7564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    };
7664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
7764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private final Context mContext;
7864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private final String mDocumentId;
7964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private final char mIdDelimiter;
8064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private final Uri mNotificationUri;
8164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private final ZipFile mZipFile;
8264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private final ExecutorService mExecutor;
8379ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski    private final Map<String, ZipEntry> mEntries;
8493307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski    private final Map<String, List<ZipEntry>> mTree;
8564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
8664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private DocumentArchive(
8764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            Context context,
8864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            File file,
8964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            String documentId,
9064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            char idDelimiter,
91369746f1bd8e90fb04e11f181623413b7ae8e063Tomasz Mikolajewski            @Nullable Uri notificationUri)
9264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            throws IOException {
9364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        mContext = context;
9464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        mDocumentId = documentId;
9564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        mIdDelimiter = idDelimiter;
9664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        mNotificationUri = notificationUri;
9764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        mZipFile = new ZipFile(file);
9864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        mExecutor = Executors.newSingleThreadExecutor();
9993307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski
10093307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski        // Build the tree structure in memory.
10193307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski        mTree = new HashMap<String, List<ZipEntry>>();
10293307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski        mTree.put("/", new ArrayList<ZipEntry>());
10393307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski
10479ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        mEntries = new HashMap<String, ZipEntry>();
10579ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        ZipEntry entry;
10679ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        final List<? extends ZipEntry> entries = Collections.list(mZipFile.entries());
10779ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        final Stack<ZipEntry> stack = new Stack<>();
10879ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        for (int i = entries.size() - 1; i >= 0; i--) {
10979ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski            entry = entries.get(i);
110e7cddbdd108544a329500ccb02fdc3ef0fca94d7Tomasz Mikolajewski            if (entry.isDirectory() != entry.getName().endsWith("/")) {
111e7cddbdd108544a329500ccb02fdc3ef0fca94d7Tomasz Mikolajewski                throw new IOException(
112e7cddbdd108544a329500ccb02fdc3ef0fca94d7Tomasz Mikolajewski                        "Directories must have a trailing slash, and files must not.");
113e7cddbdd108544a329500ccb02fdc3ef0fca94d7Tomasz Mikolajewski            }
11479ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski            mEntries.put(entry.getName(), entry);
11593307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski            if (entry.isDirectory()) {
11693307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski                mTree.put(entry.getName(), new ArrayList<ZipEntry>());
11793307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski            }
11879ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski            stack.push(entry);
11993307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski        }
12093307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski
12193307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski        int delimiterIndex;
12293307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski        String parentPath;
12379ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        ZipEntry parentEntry;
12479ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        List<ZipEntry> parentList;
12579ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski
12679ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        while (stack.size() > 0) {
12779ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski            entry = stack.pop();
12879ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski
12993307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski            delimiterIndex = entry.getName().lastIndexOf('/', entry.isDirectory()
13093307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski                    ? entry.getName().length() - 2 : entry.getName().length() - 1);
13193307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski            parentPath =
13293307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski                    delimiterIndex != -1 ? entry.getName().substring(0, delimiterIndex) + "/" : "/";
13393307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski            parentList = mTree.get(parentPath);
13479ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski
13579ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski            if (parentList == null) {
13679ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski                parentEntry = mEntries.get(parentPath);
13779ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski                if (parentEntry == null) {
13879ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski                    // The ZIP file doesn't contain all directories leading to the entry.
13979ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski                    // It's rare, but can happen in a valid ZIP archive. In such case create a
14079ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski                    // fake ZipEntry and add it on top of the stack to process it next.
14179ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski                    parentEntry = new ZipEntry(parentPath);
14279ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski                    parentEntry.setSize(0);
14379ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski                    parentEntry.setTime(entry.getTime());
14479ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski                    mEntries.put(parentPath, parentEntry);
14579ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski                    stack.push(parentEntry);
14679ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski                }
14779ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski                parentList = new ArrayList<ZipEntry>();
14879ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski                mTree.put(parentPath, parentList);
14993307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski            }
15079ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski
15179ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski            parentList.add(entry);
15293307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski        }
15364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
15464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
15564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    /**
15664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * Creates a DocumentsArchive instance for opening, browsing and accessing
15764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * documents within the archive passed as a local file.
15864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *
15964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param context Context of the provider.
16064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param File Local file containing the archive.
16164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param documentId ID of the archive document.
16264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param idDelimiter Delimiter for constructing IDs of documents within the archive.
16364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *            The delimiter must never be used for IDs of other documents.
16464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param Uri notificationUri Uri for notifying that the archive file has changed.
16564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @see createForParcelFileDescriptor(DocumentsProvider, ParcelFileDescriptor, String, char,
16664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *          Uri)
16764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     */
16864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    public static DocumentArchive createForLocalFile(
169369746f1bd8e90fb04e11f181623413b7ae8e063Tomasz Mikolajewski            Context context, File file, String documentId, char idDelimiter,
170369746f1bd8e90fb04e11f181623413b7ae8e063Tomasz Mikolajewski            @Nullable Uri notificationUri)
17164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            throws IOException {
17264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        return new DocumentArchive(context, file, documentId, idDelimiter, notificationUri);
17364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
17464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
17564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    /**
17664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * Creates a DocumentsArchive instance for opening, browsing and accessing
17764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * documents within the archive passed as a file descriptor.
17864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *
17964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * <p>Note, that this method should be used only if the document does not exist
18064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * on the local storage. A snapshot file will be created, which may be slower
18164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * and consume significant resources, in contrast to using
18264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * {@see createForLocalFile(Context, File, String, char, Uri}.
18364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *
18464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param context Context of the provider.
18564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param descriptor File descriptor for the archive's contents.
18664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param documentId ID of the archive document.
18764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param idDelimiter Delimiter for constructing IDs of documents within the archive.
18864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *            The delimiter must never be used for IDs of other documents.
18964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param Uri notificationUri Uri for notifying that the archive file has changed.
19064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @see createForLocalFile(Context, File, String, char, Uri)
19164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     */
19264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    public static DocumentArchive createForParcelFileDescriptor(
19364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            Context context, ParcelFileDescriptor descriptor, String documentId,
194369746f1bd8e90fb04e11f181623413b7ae8e063Tomasz Mikolajewski            char idDelimiter, @Nullable Uri notificationUri)
19564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            throws IOException {
19664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        File snapshotFile = null;
19764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        try {
19864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            // Create a copy of the archive, as ZipFile doesn't operate on streams.
19964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            // Moreover, ZipInputStream would be inefficient for large files on
20064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            // pipes.
20164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            snapshotFile = File.createTempFile("android.support.provider.snapshot{",
20264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                    "}.zip", context.getCacheDir());
20364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
20464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            try (
20564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                final FileOutputStream outputStream =
20664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                        new ParcelFileDescriptor.AutoCloseOutputStream(
20764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                ParcelFileDescriptor.open(
20864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                        snapshotFile, ParcelFileDescriptor.MODE_WRITE_ONLY));
20964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                final ParcelFileDescriptor.AutoCloseInputStream inputStream =
21064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                        new ParcelFileDescriptor.AutoCloseInputStream(descriptor);
21164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            ) {
21264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                final byte[] buffer = new byte[32 * 1024];
21364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                int bytes;
21464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                while ((bytes = inputStream.read(buffer)) != -1) {
21564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                    outputStream.write(buffer, 0, bytes);
21664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                }
21764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                outputStream.flush();
21864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                return new DocumentArchive(context, snapshotFile, documentId, idDelimiter,
21964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                        notificationUri);
22064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            }
22164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        } finally {
22264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            // On UNIX the file will be still available for processes which opened it, even
22364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            // after deleting it. Remove it ASAP, as it won't be used by anyone else.
22464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            if (snapshotFile != null) {
22564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                snapshotFile.delete();
22664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            }
22764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
22864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
22964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
23064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    /**
23164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * Lists child documents of an archive or a directory within an
23264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * archive. Must be called only for archives with supported mime type,
23364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * or for documents within archives.
23464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *
23564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @see DocumentsProvider.queryChildDocuments(String, String[], String)
23664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     */
237369746f1bd8e90fb04e11f181623413b7ae8e063Tomasz Mikolajewski    public Cursor queryChildDocuments(String documentId, @Nullable String[] projection,
23830a5cbb8727113e2a640d4256d5c0c0435f3421aTomasz Mikolajewski            @Nullable String sortOrder) throws FileNotFoundException {
23964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ParsedDocumentId parsedParentId = ParsedDocumentId.fromDocumentId(
24064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                documentId, mIdDelimiter);
24164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentEquals(mDocumentId, parsedParentId.mArchiveId,
24264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                "Mismatching document ID. Expected: %s, actual: %s.");
24364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
24493307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski        final String parentPath = parsedParentId.mPath != null ? parsedParentId.mPath : "/";
24564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final MatrixCursor result = new MatrixCursor(
24664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                projection != null ? projection : DEFAULT_PROJECTION);
24764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (mNotificationUri != null) {
24864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            result.setNotificationUri(mContext.getContentResolver(), mNotificationUri);
24964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
25064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
25193307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski        final List<ZipEntry> parentList = mTree.get(parentPath);
25230a5cbb8727113e2a640d4256d5c0c0435f3421aTomasz Mikolajewski        if (parentList == null) {
25330a5cbb8727113e2a640d4256d5c0c0435f3421aTomasz Mikolajewski            throw new FileNotFoundException();
25430a5cbb8727113e2a640d4256d5c0c0435f3421aTomasz Mikolajewski        }
25593307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski        for (final ZipEntry entry : parentList) {
25693307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski            addCursorRow(result, entry);
25764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
25864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        return result;
25964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
26064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
26164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    /**
26264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * Returns a MIME type of a document within an archive.
26364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *
26464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @see DocumentsProvider.getDocumentType(String)
26564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     */
26664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    public String getDocumentType(String documentId) throws FileNotFoundException {
26764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ParsedDocumentId parsedId = ParsedDocumentId.fromDocumentId(
26864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                documentId, mIdDelimiter);
26964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentEquals(mDocumentId, parsedId.mArchiveId,
27064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                "Mismatching document ID. Expected: %s, actual: %s.");
27164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentNotNull(parsedId.mPath, "Not a document within an archive.");
27264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
27379ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        final ZipEntry entry = mEntries.get(parsedId.mPath);
27464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (entry == null) {
27564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            throw new FileNotFoundException();
27664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
27764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        return getMimeTypeForEntry(entry);
27864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
27964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
28064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    /**
28164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * Returns true if a document within an archive is a child or any descendant of the archive
28264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * document or another document within the archive.
28364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *
28464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @see DocumentsProvider.isChildDocument(String, String)
28564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     */
28664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    public boolean isChildDocument(String parentDocumentId, String documentId) {
28764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ParsedDocumentId parsedParentId = ParsedDocumentId.fromDocumentId(
28864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                parentDocumentId, mIdDelimiter);
28964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ParsedDocumentId parsedId = ParsedDocumentId.fromDocumentId(
29064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                documentId, mIdDelimiter);
29164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentEquals(mDocumentId, parsedParentId.mArchiveId,
29264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                "Mismatching document ID. Expected: %s, actual: %s.");
29364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentNotNull(parsedId.mPath,
29464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                "Not a document within an archive.");
29564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
29679ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        final ZipEntry entry = mEntries.get(parsedId.mPath);
29764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (entry == null) {
29864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            return false;
29964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
30064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
30164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (parsedParentId.mPath == null) {
30264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            // No need to compare paths. Every file in the archive is a child of the archive
30364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            // file.
30464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            return true;
30564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
30664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
30779ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        final ZipEntry parentEntry = mEntries.get(parsedParentId.mPath);
30864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (parentEntry == null || !parentEntry.isDirectory()) {
30964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            return false;
31064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
31164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
31293307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski        final String parentPath = entry.getName();
31364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
31464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        // Add a trailing slash even if it's not a directory, so it's easy to check if the
31564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        // entry is a descendant.
31693307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski        final String pathWithSlash = entry.isDirectory() ? entry.getName() : entry.getName() + "/";
31764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        return pathWithSlash.startsWith(parentPath) && !parentPath.equals(pathWithSlash);
31864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
31964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
32064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    /**
32164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * Returns metadata of a document within an archive.
32264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *
32364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @see DocumentsProvider.queryDocument(String, String[])
32464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     */
325369746f1bd8e90fb04e11f181623413b7ae8e063Tomasz Mikolajewski    public Cursor queryDocument(String documentId, @Nullable String[] projection)
32664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            throws FileNotFoundException {
32764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ParsedDocumentId parsedId = ParsedDocumentId.fromDocumentId(
32864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                documentId, mIdDelimiter);
32964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentEquals(mDocumentId, parsedId.mArchiveId,
33064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                "Mismatching document ID. Expected: %s, actual: %s.");
33164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentNotNull(parsedId.mPath, "Not a document within an archive.");
33264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
33379ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        final ZipEntry entry = mEntries.get(parsedId.mPath);
33464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (entry == null) {
33564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            throw new FileNotFoundException();
33664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
33764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
33864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final MatrixCursor result = new MatrixCursor(
33964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                projection != null ? projection : DEFAULT_PROJECTION);
34064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (mNotificationUri != null) {
34164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            result.setNotificationUri(mContext.getContentResolver(), mNotificationUri);
34264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
34364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        addCursorRow(result, entry);
34464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        return result;
34564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
34664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
34764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    /**
34864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * Opens a file within an archive.
34964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *
35064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @see DocumentsProvider.openDocument(String, String, CancellationSignal))
35164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     */
35264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    public ParcelFileDescriptor openDocument(
353369746f1bd8e90fb04e11f181623413b7ae8e063Tomasz Mikolajewski            String documentId, String mode, @Nullable final CancellationSignal signal)
35464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            throws FileNotFoundException {
35564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentEquals("r", mode,
35664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                "Invalid mode. Only reading \"r\" supported, but got: \"%s\".");
35764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ParsedDocumentId parsedId = ParsedDocumentId.fromDocumentId(
35864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                documentId, mIdDelimiter);
35964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentEquals(mDocumentId, parsedId.mArchiveId,
36064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                "Mismatching document ID. Expected: %s, actual: %s.");
36164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentNotNull(parsedId.mPath, "Not a document within an archive.");
36264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
36379ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        final ZipEntry entry = mEntries.get(parsedId.mPath);
36464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (entry == null) {
36564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            throw new FileNotFoundException();
36664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
36764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
36864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        ParcelFileDescriptor[] pipe;
36964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        InputStream inputStream = null;
37064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        try {
37164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            pipe = ParcelFileDescriptor.createReliablePipe();
37264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            inputStream = mZipFile.getInputStream(entry);
37364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        } catch (IOException e) {
37464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            if (inputStream != null) {
37564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                IoUtils.closeQuietly(inputStream);
37664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            }
37764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            // Ideally we'd simply throw IOException to the caller, but for consistency
37864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            // with DocumentsProvider::openDocument, converting it to IllegalStateException.
37964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            throw new IllegalStateException("Failed to open the document.", e);
38064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
38164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ParcelFileDescriptor outputPipe = pipe[1];
38264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final InputStream finalInputStream = inputStream;
38364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        mExecutor.execute(
38464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                new Runnable() {
38564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                    @Override
38664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                    public void run() {
38764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                        try (final ParcelFileDescriptor.AutoCloseOutputStream outputStream =
38864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                new ParcelFileDescriptor.AutoCloseOutputStream(outputPipe)) {
38964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                            try {
39064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                final byte buffer[] = new byte[32 * 1024];
39164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                int bytes;
39264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                while ((bytes = finalInputStream.read(buffer)) != -1) {
39364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                    if (Thread.interrupted()) {
39464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                        throw new InterruptedException();
39564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                    }
39664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                    if (signal != null) {
39764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                        signal.throwIfCanceled();
39864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                    }
39964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                    outputStream.write(buffer, 0, bytes);
40064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                }
40164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                            } catch (IOException | InterruptedException e) {
40264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                // Catch the exception before the outer try-with-resource closes the
40364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                // pipe with close() instead of closeWithError().
40464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                try {
40564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                    outputPipe.closeWithError(e.getMessage());
40664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                } catch (IOException e2) {
40764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                    Log.e(TAG, "Failed to close the pipe after an error.", e2);
40864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                }
40964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                            }
41009deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski                        } catch (OperationCanceledException e) {
41109deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski                            // Cancelled gracefully.
41264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                        } catch (IOException e) {
41364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                            Log.e(TAG, "Failed to close the output stream gracefully.", e);
41464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                        } finally {
41564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                            IoUtils.closeQuietly(finalInputStream);
41664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                        }
41764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                    }
41864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                });
41964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
42064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        return pipe[0];
42164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
42264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
42364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    /**
42409deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski     * Opens a thumbnail of a file within an archive.
42509deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski     *
42609deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski     * @see DocumentsProvider.openDocumentThumbnail(String, Point, CancellationSignal))
42709deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski     */
42809deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski    public AssetFileDescriptor openDocumentThumbnail(
42909deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski            String documentId, Point sizeHint, final CancellationSignal signal)
43009deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski            throws FileNotFoundException {
43109deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski        final ParsedDocumentId parsedId = ParsedDocumentId.fromDocumentId(documentId, mIdDelimiter);
43209deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski        Preconditions.checkArgumentEquals(mDocumentId, parsedId.mArchiveId,
43309deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski                "Mismatching document ID. Expected: %s, actual: %s.");
43409deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski        Preconditions.checkArgumentNotNull(parsedId.mPath, "Not a document within an archive.");
43509deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski        Preconditions.checkArgument(getDocumentType(documentId).startsWith("image/"),
43609deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski                "Thumbnails only supported for image/* MIME type.");
43709deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski
43809deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski        // TODO: Extract thumbnails from EXIF.
43979ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        final ZipEntry entry = mEntries.get(parsedId.mPath);
44009deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski        if (entry == null) {
44109deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski            throw new FileNotFoundException();
44209deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski        }
44309deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski
44409deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski        return new AssetFileDescriptor(
44509deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski                openDocument(documentId, "r", signal), 0, entry.getSize(), null);
44609deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski    }
44709deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski
44809deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski    /**
44964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * Schedules a gracefully close of the archive after any opened files are closed.
45064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *
45164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * <p>This method does not block until shutdown. Once called, other methods should not be
45264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * called.
45364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     */
45464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    @Override
45564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    public void close() {
45664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        mExecutor.execute(new Runnable() {
45764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            @Override
45864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            public void run() {
45964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                IoUtils.closeQuietly(mZipFile);
46064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            }
46164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        });
46264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        mExecutor.shutdown();
46364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
46464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
46564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private void addCursorRow(MatrixCursor cursor, ZipEntry entry) {
46664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final MatrixCursor.RowBuilder row = cursor.newRow();
46764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ParsedDocumentId parsedId = new ParsedDocumentId(mDocumentId, entry.getName());
46864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        row.add(Document.COLUMN_DOCUMENT_ID, parsedId.toDocumentId(mIdDelimiter));
46909deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski
47064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final File file = new File(entry.getName());
47164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        row.add(Document.COLUMN_DISPLAY_NAME, file.getName());
47264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        row.add(Document.COLUMN_SIZE, entry.getSize());
47309deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski
47409deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski        final String mimeType = getMimeTypeForEntry(entry);
47509deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski        row.add(Document.COLUMN_MIME_TYPE, mimeType);
47609deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski
47709deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski        final int flags = mimeType.startsWith("image/") ? Document.FLAG_SUPPORTS_THUMBNAIL : 0;
47809deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski        row.add(Document.COLUMN_FLAGS, flags);
47964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
48064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
48164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private String getMimeTypeForEntry(ZipEntry entry) {
48264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (entry.isDirectory()) {
48364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            return Document.MIME_TYPE_DIR;
48464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
48564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
48664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final int lastDot = entry.getName().lastIndexOf('.');
48764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (lastDot >= 0) {
48864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            final String extension = entry.getName().substring(lastDot + 1).toLowerCase();
48964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            final String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
49064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            if (mimeType != null) {
49164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                return mimeType;
49264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            }
49364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
49464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
49564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        return "application/octet-stream";
49664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
49764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski};
498