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;
2474f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewskiimport android.media.ExifInterface;
2564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.net.Uri;
2674f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewskiimport android.os.Bundle;
2764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.os.CancellationSignal;
2809deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewskiimport android.os.OperationCanceledException;
2964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.os.ParcelFileDescriptor;
3074f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewskiimport android.provider.DocumentsContract;
3164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.provider.DocumentsContract.Document;
3264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.provider.DocumentsProvider;
33369746f1bd8e90fb04e11f181623413b7ae8e063Tomasz Mikolajewskiimport android.support.annotation.Nullable;
3464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.util.Log;
3564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.webkit.MimeTypeMap;
3664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
3764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.io.Closeable;
3864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.io.File;
3964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.io.FileNotFoundException;
4064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.io.FileOutputStream;
4164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.io.IOException;
4264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.io.InputStream;
4393307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewskiimport java.util.ArrayList;
4464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.lang.IllegalArgumentException;
4564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.lang.IllegalStateException;
4664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.lang.UnsupportedOperationException;
4764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.Collections;
4893307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewskiimport java.util.HashMap;
4964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.Iterator;
5093307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewskiimport java.util.List;
51aaf68419351d65048712c636cbe107c42cdb6844Tomasz Mikolajewskiimport java.util.Locale;
5293307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewskiimport java.util.Map;
5379ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewskiimport java.util.Stack;
5464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.concurrent.ExecutorService;
5564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.concurrent.Executors;
5664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.zip.ZipEntry;
5764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.zip.ZipFile;
5864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.zip.ZipInputStream;
5964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
6064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski/**
6164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * Provides basic implementation for creating, extracting and accessing
6264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * files within archives exposed by a document provider. The id delimiter
6364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * must be a character which is not used in document ids generated by the
6464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * document provider.
6564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski *
6664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * <p>This class is thread safe.
6764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski *
6864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * @hide
6964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski */
7064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskipublic class DocumentArchive implements Closeable {
7164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private static final String TAG = "DocumentArchive";
7264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
7364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private static final String[] DEFAULT_PROJECTION = new String[] {
7464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            Document.COLUMN_DOCUMENT_ID,
7564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            Document.COLUMN_DISPLAY_NAME,
7664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            Document.COLUMN_MIME_TYPE,
7709deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski            Document.COLUMN_SIZE,
7809deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski            Document.COLUMN_FLAGS
7964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    };
8064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
8164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private final Context mContext;
8264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private final String mDocumentId;
8364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private final char mIdDelimiter;
8464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private final Uri mNotificationUri;
8564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private final ZipFile mZipFile;
8664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private final ExecutorService mExecutor;
8779ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski    private final Map<String, ZipEntry> mEntries;
8893307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski    private final Map<String, List<ZipEntry>> mTree;
8964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
9064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private DocumentArchive(
9164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            Context context,
9264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            File file,
9364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            String documentId,
9464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            char idDelimiter,
95369746f1bd8e90fb04e11f181623413b7ae8e063Tomasz Mikolajewski            @Nullable Uri notificationUri)
9664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            throws IOException {
9764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        mContext = context;
9864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        mDocumentId = documentId;
9964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        mIdDelimiter = idDelimiter;
10064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        mNotificationUri = notificationUri;
10164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        mZipFile = new ZipFile(file);
10264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        mExecutor = Executors.newSingleThreadExecutor();
10393307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski
10493307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski        // Build the tree structure in memory.
10593307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski        mTree = new HashMap<String, List<ZipEntry>>();
10693307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski        mTree.put("/", new ArrayList<ZipEntry>());
10793307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski
10879ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        mEntries = new HashMap<String, ZipEntry>();
10979ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        ZipEntry entry;
11079ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        final List<? extends ZipEntry> entries = Collections.list(mZipFile.entries());
11179ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        final Stack<ZipEntry> stack = new Stack<>();
11279ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        for (int i = entries.size() - 1; i >= 0; i--) {
11379ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski            entry = entries.get(i);
114e7cddbdd108544a329500ccb02fdc3ef0fca94d7Tomasz Mikolajewski            if (entry.isDirectory() != entry.getName().endsWith("/")) {
115e7cddbdd108544a329500ccb02fdc3ef0fca94d7Tomasz Mikolajewski                throw new IOException(
116e7cddbdd108544a329500ccb02fdc3ef0fca94d7Tomasz Mikolajewski                        "Directories must have a trailing slash, and files must not.");
117e7cddbdd108544a329500ccb02fdc3ef0fca94d7Tomasz Mikolajewski            }
1183bf877d757a981d73fb85d46c7f77231ea03a801Tomasz Mikolajewski            if (mEntries.containsKey(entry.getName())) {
1193bf877d757a981d73fb85d46c7f77231ea03a801Tomasz Mikolajewski                throw new IOException("Multiple entries with the same name are not supported.");
1203bf877d757a981d73fb85d46c7f77231ea03a801Tomasz Mikolajewski            }
12179ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski            mEntries.put(entry.getName(), entry);
12293307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski            if (entry.isDirectory()) {
12393307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski                mTree.put(entry.getName(), new ArrayList<ZipEntry>());
12493307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski            }
12579ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski            stack.push(entry);
12693307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski        }
12793307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski
12893307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski        int delimiterIndex;
12993307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski        String parentPath;
13079ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        ZipEntry parentEntry;
13179ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        List<ZipEntry> parentList;
13279ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski
13379ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        while (stack.size() > 0) {
13479ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski            entry = stack.pop();
13579ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski
13693307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski            delimiterIndex = entry.getName().lastIndexOf('/', entry.isDirectory()
13793307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski                    ? entry.getName().length() - 2 : entry.getName().length() - 1);
13893307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski            parentPath =
13993307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski                    delimiterIndex != -1 ? entry.getName().substring(0, delimiterIndex) + "/" : "/";
14093307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski            parentList = mTree.get(parentPath);
14179ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski
14279ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski            if (parentList == null) {
14379ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski                parentEntry = mEntries.get(parentPath);
14479ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski                if (parentEntry == null) {
14579ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski                    // The ZIP file doesn't contain all directories leading to the entry.
14679ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski                    // It's rare, but can happen in a valid ZIP archive. In such case create a
14779ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski                    // fake ZipEntry and add it on top of the stack to process it next.
14879ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski                    parentEntry = new ZipEntry(parentPath);
14979ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski                    parentEntry.setSize(0);
15079ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski                    parentEntry.setTime(entry.getTime());
15179ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski                    mEntries.put(parentPath, parentEntry);
15279ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski                    stack.push(parentEntry);
15379ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski                }
15479ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski                parentList = new ArrayList<ZipEntry>();
15579ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski                mTree.put(parentPath, parentList);
15693307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski            }
15779ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski
15879ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski            parentList.add(entry);
15993307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski        }
16064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
16164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
16264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    /**
16364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * Creates a DocumentsArchive instance for opening, browsing and accessing
16464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * documents within the archive passed as a local file.
16564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *
16664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param context Context of the provider.
16764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param File Local file containing the archive.
16864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param documentId ID of the archive document.
16964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param idDelimiter Delimiter for constructing IDs of documents within the archive.
17064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *            The delimiter must never be used for IDs of other documents.
17164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param Uri notificationUri Uri for notifying that the archive file has changed.
17264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @see createForParcelFileDescriptor(DocumentsProvider, ParcelFileDescriptor, String, char,
17364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *          Uri)
17464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     */
17564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    public static DocumentArchive createForLocalFile(
176369746f1bd8e90fb04e11f181623413b7ae8e063Tomasz Mikolajewski            Context context, File file, String documentId, char idDelimiter,
177369746f1bd8e90fb04e11f181623413b7ae8e063Tomasz Mikolajewski            @Nullable Uri notificationUri)
17864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            throws IOException {
17964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        return new DocumentArchive(context, file, documentId, idDelimiter, notificationUri);
18064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
18164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
18264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    /**
18364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * Creates a DocumentsArchive instance for opening, browsing and accessing
18464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * documents within the archive passed as a file descriptor.
18564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *
18664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * <p>Note, that this method should be used only if the document does not exist
18764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * on the local storage. A snapshot file will be created, which may be slower
18864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * and consume significant resources, in contrast to using
18964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * {@see createForLocalFile(Context, File, String, char, Uri}.
19064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *
19164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param context Context of the provider.
19264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param descriptor File descriptor for the archive's contents.
19364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param documentId ID of the archive document.
19464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param idDelimiter Delimiter for constructing IDs of documents within the archive.
19564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *            The delimiter must never be used for IDs of other documents.
19664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @param Uri notificationUri Uri for notifying that the archive file has changed.
19764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @see createForLocalFile(Context, File, String, char, Uri)
19864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     */
19964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    public static DocumentArchive createForParcelFileDescriptor(
20064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            Context context, ParcelFileDescriptor descriptor, String documentId,
201369746f1bd8e90fb04e11f181623413b7ae8e063Tomasz Mikolajewski            char idDelimiter, @Nullable Uri notificationUri)
20264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            throws IOException {
20364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        File snapshotFile = null;
20464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        try {
20564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            // Create a copy of the archive, as ZipFile doesn't operate on streams.
20664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            // Moreover, ZipInputStream would be inefficient for large files on
20764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            // pipes.
20864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            snapshotFile = File.createTempFile("android.support.provider.snapshot{",
20964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                    "}.zip", context.getCacheDir());
21064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
21164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            try (
21264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                final FileOutputStream outputStream =
21364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                        new ParcelFileDescriptor.AutoCloseOutputStream(
21464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                ParcelFileDescriptor.open(
21564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                        snapshotFile, ParcelFileDescriptor.MODE_WRITE_ONLY));
21664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                final ParcelFileDescriptor.AutoCloseInputStream inputStream =
21764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                        new ParcelFileDescriptor.AutoCloseInputStream(descriptor);
21864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            ) {
21964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                final byte[] buffer = new byte[32 * 1024];
22064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                int bytes;
22164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                while ((bytes = inputStream.read(buffer)) != -1) {
22264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                    outputStream.write(buffer, 0, bytes);
22364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                }
22464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                outputStream.flush();
22564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                return new DocumentArchive(context, snapshotFile, documentId, idDelimiter,
22664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                        notificationUri);
22764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            }
22864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        } finally {
22964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            // On UNIX the file will be still available for processes which opened it, even
23064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            // after deleting it. Remove it ASAP, as it won't be used by anyone else.
23164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            if (snapshotFile != null) {
23264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                snapshotFile.delete();
23364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            }
23464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
23564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
23664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
23764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    /**
23864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * Lists child documents of an archive or a directory within an
23964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * archive. Must be called only for archives with supported mime type,
24064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * or for documents within archives.
24164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *
24264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @see DocumentsProvider.queryChildDocuments(String, String[], String)
24364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     */
244369746f1bd8e90fb04e11f181623413b7ae8e063Tomasz Mikolajewski    public Cursor queryChildDocuments(String documentId, @Nullable String[] projection,
24530a5cbb8727113e2a640d4256d5c0c0435f3421aTomasz Mikolajewski            @Nullable String sortOrder) throws FileNotFoundException {
24664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ParsedDocumentId parsedParentId = ParsedDocumentId.fromDocumentId(
24764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                documentId, mIdDelimiter);
24864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentEquals(mDocumentId, parsedParentId.mArchiveId,
24964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                "Mismatching document ID. Expected: %s, actual: %s.");
25064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
25193307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski        final String parentPath = parsedParentId.mPath != null ? parsedParentId.mPath : "/";
25264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final MatrixCursor result = new MatrixCursor(
25364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                projection != null ? projection : DEFAULT_PROJECTION);
25464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (mNotificationUri != null) {
25564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            result.setNotificationUri(mContext.getContentResolver(), mNotificationUri);
25664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
25764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
25893307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski        final List<ZipEntry> parentList = mTree.get(parentPath);
25930a5cbb8727113e2a640d4256d5c0c0435f3421aTomasz Mikolajewski        if (parentList == null) {
26030a5cbb8727113e2a640d4256d5c0c0435f3421aTomasz Mikolajewski            throw new FileNotFoundException();
26130a5cbb8727113e2a640d4256d5c0c0435f3421aTomasz Mikolajewski        }
26293307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski        for (final ZipEntry entry : parentList) {
26393307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski            addCursorRow(result, entry);
26464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
26564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        return result;
26664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
26764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
26864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    /**
26964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * Returns a MIME type of a document within an archive.
27064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *
27164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @see DocumentsProvider.getDocumentType(String)
27264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     */
27364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    public String getDocumentType(String documentId) throws FileNotFoundException {
27464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ParsedDocumentId parsedId = ParsedDocumentId.fromDocumentId(
27564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                documentId, mIdDelimiter);
27664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentEquals(mDocumentId, parsedId.mArchiveId,
27764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                "Mismatching document ID. Expected: %s, actual: %s.");
27864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentNotNull(parsedId.mPath, "Not a document within an archive.");
27964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
28079ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        final ZipEntry entry = mEntries.get(parsedId.mPath);
28164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (entry == null) {
28264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            throw new FileNotFoundException();
28364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
28464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        return getMimeTypeForEntry(entry);
28564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
28664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
28764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    /**
28864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * Returns true if a document within an archive is a child or any descendant of the archive
28964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * document or another document within the archive.
29064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *
29164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @see DocumentsProvider.isChildDocument(String, String)
29264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     */
29364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    public boolean isChildDocument(String parentDocumentId, String documentId) {
29464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ParsedDocumentId parsedParentId = ParsedDocumentId.fromDocumentId(
29564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                parentDocumentId, mIdDelimiter);
29664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ParsedDocumentId parsedId = ParsedDocumentId.fromDocumentId(
29764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                documentId, mIdDelimiter);
29864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentEquals(mDocumentId, parsedParentId.mArchiveId,
29964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                "Mismatching document ID. Expected: %s, actual: %s.");
30064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentNotNull(parsedId.mPath,
30164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                "Not a document within an archive.");
30264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
30379ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        final ZipEntry entry = mEntries.get(parsedId.mPath);
30464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (entry == null) {
30564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            return false;
30664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
30764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
30864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (parsedParentId.mPath == null) {
30964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            // No need to compare paths. Every file in the archive is a child of the archive
31064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            // file.
31164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            return true;
31264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
31364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
31479ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        final ZipEntry parentEntry = mEntries.get(parsedParentId.mPath);
31564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (parentEntry == null || !parentEntry.isDirectory()) {
31664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            return false;
31764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
31864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
31993307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski        final String parentPath = entry.getName();
32064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
32164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        // Add a trailing slash even if it's not a directory, so it's easy to check if the
32264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        // entry is a descendant.
32393307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski        final String pathWithSlash = entry.isDirectory() ? entry.getName() : entry.getName() + "/";
32464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        return pathWithSlash.startsWith(parentPath) && !parentPath.equals(pathWithSlash);
32564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
32664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
32764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    /**
32864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * Returns metadata of a document within an archive.
32964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *
33064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @see DocumentsProvider.queryDocument(String, String[])
33164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     */
332369746f1bd8e90fb04e11f181623413b7ae8e063Tomasz Mikolajewski    public Cursor queryDocument(String documentId, @Nullable String[] projection)
33364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            throws FileNotFoundException {
33464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ParsedDocumentId parsedId = ParsedDocumentId.fromDocumentId(
33564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                documentId, mIdDelimiter);
33664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentEquals(mDocumentId, parsedId.mArchiveId,
33764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                "Mismatching document ID. Expected: %s, actual: %s.");
33864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentNotNull(parsedId.mPath, "Not a document within an archive.");
33964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
34079ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        final ZipEntry entry = mEntries.get(parsedId.mPath);
34164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (entry == null) {
34264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            throw new FileNotFoundException();
34364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
34464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
34564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final MatrixCursor result = new MatrixCursor(
34664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                projection != null ? projection : DEFAULT_PROJECTION);
34764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (mNotificationUri != null) {
34864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            result.setNotificationUri(mContext.getContentResolver(), mNotificationUri);
34964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
35064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        addCursorRow(result, entry);
35164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        return result;
35264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
35364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
35464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    /**
35564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * Opens a file within an archive.
35664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *
35764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * @see DocumentsProvider.openDocument(String, String, CancellationSignal))
35864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     */
35964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    public ParcelFileDescriptor openDocument(
360369746f1bd8e90fb04e11f181623413b7ae8e063Tomasz Mikolajewski            String documentId, String mode, @Nullable final CancellationSignal signal)
36164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            throws FileNotFoundException {
36264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentEquals("r", mode,
36364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                "Invalid mode. Only reading \"r\" supported, but got: \"%s\".");
36464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ParsedDocumentId parsedId = ParsedDocumentId.fromDocumentId(
36564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                documentId, mIdDelimiter);
36664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentEquals(mDocumentId, parsedId.mArchiveId,
36764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                "Mismatching document ID. Expected: %s, actual: %s.");
36864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        Preconditions.checkArgumentNotNull(parsedId.mPath, "Not a document within an archive.");
36964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
37079ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        final ZipEntry entry = mEntries.get(parsedId.mPath);
37164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (entry == null) {
37264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            throw new FileNotFoundException();
37364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
37464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
37564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        ParcelFileDescriptor[] pipe;
37664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        InputStream inputStream = null;
37764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        try {
37864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            pipe = ParcelFileDescriptor.createReliablePipe();
37964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            inputStream = mZipFile.getInputStream(entry);
38064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        } catch (IOException e) {
38164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            if (inputStream != null) {
38264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                IoUtils.closeQuietly(inputStream);
38364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            }
38464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            // Ideally we'd simply throw IOException to the caller, but for consistency
38564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            // with DocumentsProvider::openDocument, converting it to IllegalStateException.
38664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            throw new IllegalStateException("Failed to open the document.", e);
38764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
38864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ParcelFileDescriptor outputPipe = pipe[1];
38964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final InputStream finalInputStream = inputStream;
39064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        mExecutor.execute(
39164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                new Runnable() {
39264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                    @Override
39364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                    public void run() {
39464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                        try (final ParcelFileDescriptor.AutoCloseOutputStream outputStream =
39564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                new ParcelFileDescriptor.AutoCloseOutputStream(outputPipe)) {
39664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                            try {
39764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                final byte buffer[] = new byte[32 * 1024];
39864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                int bytes;
39964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                while ((bytes = finalInputStream.read(buffer)) != -1) {
40064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                    if (Thread.interrupted()) {
40164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                        throw new InterruptedException();
40264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                    }
40364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                    if (signal != null) {
40464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                        signal.throwIfCanceled();
40564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                    }
40664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                    outputStream.write(buffer, 0, bytes);
40764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                }
40864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                            } catch (IOException | InterruptedException e) {
40964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                // Catch the exception before the outer try-with-resource closes the
41064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                // pipe with close() instead of closeWithError().
41164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                try {
41264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                    outputPipe.closeWithError(e.getMessage());
41364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                } catch (IOException e2) {
41464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                    Log.e(TAG, "Failed to close the pipe after an error.", e2);
41564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                                }
41664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                            }
41709deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski                        } catch (OperationCanceledException e) {
41809deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski                            // Cancelled gracefully.
41964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                        } catch (IOException e) {
42064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                            Log.e(TAG, "Failed to close the output stream gracefully.", e);
42164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                        } finally {
42264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                            IoUtils.closeQuietly(finalInputStream);
42364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                        }
42464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                    }
42564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                });
42664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
42764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        return pipe[0];
42864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
42964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
43064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    /**
43109deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski     * Opens a thumbnail of a file within an archive.
43209deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski     *
43309deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski     * @see DocumentsProvider.openDocumentThumbnail(String, Point, CancellationSignal))
43409deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski     */
43509deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski    public AssetFileDescriptor openDocumentThumbnail(
43609deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski            String documentId, Point sizeHint, final CancellationSignal signal)
43709deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski            throws FileNotFoundException {
43809deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski        final ParsedDocumentId parsedId = ParsedDocumentId.fromDocumentId(documentId, mIdDelimiter);
43909deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski        Preconditions.checkArgumentEquals(mDocumentId, parsedId.mArchiveId,
44009deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski                "Mismatching document ID. Expected: %s, actual: %s.");
44109deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski        Preconditions.checkArgumentNotNull(parsedId.mPath, "Not a document within an archive.");
44209deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski        Preconditions.checkArgument(getDocumentType(documentId).startsWith("image/"),
44309deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski                "Thumbnails only supported for image/* MIME type.");
44409deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski
44579ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski        final ZipEntry entry = mEntries.get(parsedId.mPath);
44609deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski        if (entry == null) {
44709deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski            throw new FileNotFoundException();
44809deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski        }
44909deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski
450735fc5f89ffb4c2c9e96e370cda7208b222f0773Tomasz Mikolajewski        InputStream inputStream = null;
45174f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski        try {
452735fc5f89ffb4c2c9e96e370cda7208b222f0773Tomasz Mikolajewski            inputStream = mZipFile.getInputStream(entry);
45374f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski            final ExifInterface exif = new ExifInterface(inputStream);
45474f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski            if (exif.hasThumbnail()) {
45574f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski                Bundle extras = null;
45674f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski                switch (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1)) {
45774f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski                    case ExifInterface.ORIENTATION_ROTATE_90:
45874f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski                        extras = new Bundle(1);
45974f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski                        extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 90);
46074f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski                        break;
46174f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski                    case ExifInterface.ORIENTATION_ROTATE_180:
46274f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski                        extras = new Bundle(1);
46374f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski                        extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 180);
46474f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski                        break;
46574f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski                    case ExifInterface.ORIENTATION_ROTATE_270:
46674f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski                        extras = new Bundle(1);
46774f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski                        extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 270);
46874f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski                        break;
46974f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski                }
47074f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski                final long[] range = exif.getThumbnailRange();
47174f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski                return new AssetFileDescriptor(
47274f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski                        openDocument(documentId, "r", signal), range[0], range[1], extras);
47374f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski            }
47474f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski        } catch (IOException e) {
47574f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski            // Ignore the exception, as reading the EXIF may legally fail.
47674f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski            Log.e(TAG, "Failed to obtain thumbnail from EXIF.", e);
477735fc5f89ffb4c2c9e96e370cda7208b222f0773Tomasz Mikolajewski        } finally {
478735fc5f89ffb4c2c9e96e370cda7208b222f0773Tomasz Mikolajewski            IoUtils.closeQuietly(inputStream);
47974f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski        }
48074f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski
48109deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski        return new AssetFileDescriptor(
48209deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski                openDocument(documentId, "r", signal), 0, entry.getSize(), null);
48309deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski    }
48409deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski
48509deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski    /**
48664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * Schedules a gracefully close of the archive after any opened files are closed.
48764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     *
48864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * <p>This method does not block until shutdown. Once called, other methods should not be
48964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     * called.
49064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski     */
49164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    @Override
49264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    public void close() {
49364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        mExecutor.execute(new Runnable() {
49464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            @Override
49564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            public void run() {
49664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                IoUtils.closeQuietly(mZipFile);
49764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            }
49864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        });
49964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        mExecutor.shutdown();
50064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
50164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
50264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private void addCursorRow(MatrixCursor cursor, ZipEntry entry) {
50364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final MatrixCursor.RowBuilder row = cursor.newRow();
50464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final ParsedDocumentId parsedId = new ParsedDocumentId(mDocumentId, entry.getName());
50564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        row.add(Document.COLUMN_DOCUMENT_ID, parsedId.toDocumentId(mIdDelimiter));
50609deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski
50764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final File file = new File(entry.getName());
50864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        row.add(Document.COLUMN_DISPLAY_NAME, file.getName());
50964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        row.add(Document.COLUMN_SIZE, entry.getSize());
51009deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski
51109deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski        final String mimeType = getMimeTypeForEntry(entry);
51209deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski        row.add(Document.COLUMN_MIME_TYPE, mimeType);
51309deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski
51409deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski        final int flags = mimeType.startsWith("image/") ? Document.FLAG_SUPPORTS_THUMBNAIL : 0;
51509deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski        row.add(Document.COLUMN_FLAGS, flags);
51664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
51764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
51864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    private String getMimeTypeForEntry(ZipEntry entry) {
51964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (entry.isDirectory()) {
52064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            return Document.MIME_TYPE_DIR;
52164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
52264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
52364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        final int lastDot = entry.getName().lastIndexOf('.');
52464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        if (lastDot >= 0) {
525aaf68419351d65048712c636cbe107c42cdb6844Tomasz Mikolajewski            final String extension = entry.getName().substring(lastDot + 1).toLowerCase(Locale.US);
52664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            final String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
52764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            if (mimeType != null) {
52864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski                return mimeType;
52964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski            }
53064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        }
53164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski
53264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski        return "application/octet-stream";
53364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski    }
53464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski};
535