ReadableArchive.java revision a903c2cdad65d0fe163a4152faa6bd05e2888b22
1d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski/*
2d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski * Copyright (C) 2017 The Android Open Source Project
3d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski *
4d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski * Licensed under the Apache License, Version 2.0 (the "License");
5d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski * you may not use this file except in compliance with the License.
6d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski * You may obtain a copy of the License at
7d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski *
8d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski *      http://www.apache.org/licenses/LICENSE-2.0
9d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski *
10d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski * Unless required by applicable law or agreed to in writing, software
11d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski * distributed under the License is distributed on an "AS IS" BASIS,
12d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski * See the License for the specific language governing permissions and
14d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski * limitations under the License.
15d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski */
16d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
17d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskipackage com.android.documentsui.archives;
18d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
19d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskiimport android.content.Context;
20d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskiimport android.content.res.AssetFileDescriptor;
21d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskiimport android.graphics.Point;
22d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskiimport android.media.ExifInterface;
23d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskiimport android.net.Uri;
24d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskiimport android.os.Bundle;
25d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskiimport android.os.CancellationSignal;
26d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskiimport android.os.OperationCanceledException;
27d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskiimport android.os.ParcelFileDescriptor;
28d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskiimport android.provider.DocumentsContract;
29d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskiimport android.support.annotation.Nullable;
30d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskiimport android.util.Log;
31d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskiimport android.util.jar.StrictJarFile;
32d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
3379a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewskiimport com.android.internal.annotations.GuardedBy;
34d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskiimport com.android.internal.util.Preconditions;
35d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
36d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskiimport libcore.io.IoUtils;
37d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
38d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskiimport java.io.File;
39d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskiimport java.io.FileDescriptor;
40d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskiimport java.io.FileNotFoundException;
41d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskiimport java.io.FileOutputStream;
42d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskiimport java.io.IOException;
43d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskiimport java.io.InputStream;
44d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskiimport java.util.ArrayList;
4579a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewskiimport java.util.HashSet;
46d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskiimport java.util.Iterator;
47d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskiimport java.util.List;
4879a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewskiimport java.util.Set;
49d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskiimport java.util.Stack;
503b135ef27a918143489cae0d99770890f3bbb3b6Tomasz Mikolajewskiimport java.util.concurrent.LinkedBlockingQueue;
5179a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewskiimport java.util.concurrent.RejectedExecutionException;
523b135ef27a918143489cae0d99770890f3bbb3b6Tomasz Mikolajewskiimport java.util.concurrent.ThreadPoolExecutor;
533b135ef27a918143489cae0d99770890f3bbb3b6Tomasz Mikolajewskiimport java.util.concurrent.TimeUnit;
54d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskiimport java.util.zip.ZipEntry;
55d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
56d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski/**
57d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski * Provides basic implementation for extracting and accessing
58d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski * files within archives exposed by a document provider.
59d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski *
60d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski * <p>This class is thread safe.
61d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski */
62d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewskipublic class ReadableArchive extends Archive {
637bb3bdce2d2eb8e279073420151e48a4a0fb60c7Tomasz Mikolajewski    private static final String TAG = "ReadableArchive";
64d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
6579a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski    @GuardedBy("mEnqueuedOutputPipes")
667bb3bdce2d2eb8e279073420151e48a4a0fb60c7Tomasz Mikolajewski    private final Set<ParcelFileDescriptor> mEnqueuedOutputPipes = new HashSet<>();
677bb3bdce2d2eb8e279073420151e48a4a0fb60c7Tomasz Mikolajewski    private final ThreadPoolExecutor mExecutor;
68d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski    private final StrictJarFile mZipFile;
69d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
70d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski    private ReadableArchive(
71d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            Context context,
72d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            @Nullable File file,
73d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            @Nullable FileDescriptor fd,
74d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            Uri archiveUri,
75d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            int accessMode,
76d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            @Nullable Uri notificationUri)
77d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            throws IOException {
78d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        super(context, archiveUri, accessMode, notificationUri);
79d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        if (!supportsAccessMode(accessMode)) {
80d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            throw new IllegalStateException("Unsupported access mode.");
81d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        }
82d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
833b135ef27a918143489cae0d99770890f3bbb3b6Tomasz Mikolajewski        // At most 8 active threads. All threads idling for more than a minute will
843b135ef27a918143489cae0d99770890f3bbb3b6Tomasz Mikolajewski        // be closed.
853b135ef27a918143489cae0d99770890f3bbb3b6Tomasz Mikolajewski        mExecutor = new ThreadPoolExecutor(8, 8, 60, TimeUnit.SECONDS,
863b135ef27a918143489cae0d99770890f3bbb3b6Tomasz Mikolajewski                new LinkedBlockingQueue<Runnable>());
873b135ef27a918143489cae0d99770890f3bbb3b6Tomasz Mikolajewski        mExecutor.allowCoreThreadTimeOut(true);
883b135ef27a918143489cae0d99770890f3bbb3b6Tomasz Mikolajewski
89d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        mZipFile = file != null ?
90d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                new StrictJarFile(file.getPath(), false /* verify */,
91d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                        false /* signatures */) :
92d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                new StrictJarFile(fd, false /* verify */, false /* signatures */);
93d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
94d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        ZipEntry entry;
95d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        String entryPath;
96d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        final Iterator<ZipEntry> it = mZipFile.iterator();
97d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        final Stack<ZipEntry> stack = new Stack<>();
98d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        while (it.hasNext()) {
99d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            entry = it.next();
100d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            if (entry.isDirectory() != entry.getName().endsWith("/")) {
101d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                throw new IOException(
102d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                        "Directories must have a trailing slash, and files must not.");
103d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            }
104d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            entryPath = getEntryPath(entry);
105d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            if (mEntries.containsKey(entryPath)) {
106d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                throw new IOException("Multiple entries with the same name are not supported.");
107d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            }
108d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            mEntries.put(entryPath, entry);
109d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            if (entry.isDirectory()) {
110d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                mTree.put(entryPath, new ArrayList<ZipEntry>());
111d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            }
112d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            if (!"/".equals(entryPath)) { // Skip root, as it doesn't have a parent.
113d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                stack.push(entry);
114d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            }
115d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        }
116d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
117d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        int delimiterIndex;
118d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        String parentPath;
119d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        ZipEntry parentEntry;
120d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        List<ZipEntry> parentList;
121d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
122d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        // Go through all directories recursively and build a tree structure.
123d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        while (stack.size() > 0) {
124d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            entry = stack.pop();
125d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
126d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            entryPath = getEntryPath(entry);
127d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            delimiterIndex = entryPath.lastIndexOf('/', entry.isDirectory()
128d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                    ? entryPath.length() - 2 : entryPath.length() - 1);
129d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            parentPath = entryPath.substring(0, delimiterIndex) + "/";
130d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
131d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            parentList = mTree.get(parentPath);
132d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
133d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            if (parentList == null) {
134d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                // The ZIP file doesn't contain all directories leading to the entry.
135d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                // It's rare, but can happen in a valid ZIP archive. In such case create a
136d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                // fake ZipEntry and add it on top of the stack to process it next.
137d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                parentEntry = new ZipEntry(parentPath);
138d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                parentEntry.setSize(0);
139d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                parentEntry.setTime(entry.getTime());
140d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                mEntries.put(parentPath, parentEntry);
141d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
142d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                if (!"/".equals(parentPath)) {
143d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                    stack.push(parentEntry);
144d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                }
145d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
146d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                parentList = new ArrayList<>();
147d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                mTree.put(parentPath, parentList);
148d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            }
149d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
150d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            parentList.add(entry);
151d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        }
152d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski    }
153d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
154d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski    /**
155d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski     * @see ParcelFileDescriptor
156d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski     */
157d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski    public static boolean supportsAccessMode(int accessMode) {
158d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        return accessMode == ParcelFileDescriptor.MODE_READ_ONLY;
159d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski    }
160d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
161d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski    /**
162d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski     * Creates a DocumentsArchive instance for opening, browsing and accessing
163d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski     * documents within the archive passed as a file descriptor.
164d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski     *
165d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski     * If the file descriptor is not seekable, then a snapshot will be created.
166d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski     *
167d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski     * This method takes ownership for the passed descriptor. The caller must
168a903c2cdad65d0fe163a4152faa6bd05e2888b22Tomasz Mikolajewski     * not use it after passing.
169d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski     *
170d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski     * @param context Context of the provider.
171d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski     * @param descriptor File descriptor for the archive's contents.
172d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski     * @param archiveUri Uri of the archive document.
173d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski     * @param accessMode Access mode for the archive {@see ParcelFileDescriptor}.
174d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski     * @param Uri notificationUri Uri for notifying that the archive file has changed.
175d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski     */
176d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski    public static ReadableArchive createForParcelFileDescriptor(
177d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            Context context, ParcelFileDescriptor descriptor, Uri archiveUri, int accessMode,
178d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            @Nullable Uri notificationUri)
179d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            throws IOException {
180d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        FileDescriptor fd = null;
181d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        try {
182d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            if (canSeek(descriptor)) {
183d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                fd = new FileDescriptor();
184d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                fd.setInt$(descriptor.detachFd());
185d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                return new ReadableArchive(context, null, fd, archiveUri, accessMode,
186d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                        notificationUri);
187d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            }
188d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
189d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            // Fallback for non-seekable file descriptors.
190d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            File snapshotFile = null;
191d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            try {
192d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                // Create a copy of the archive, as ZipFile doesn't operate on streams.
193d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                // Moreover, ZipInputStream would be inefficient for large files on
194d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                // pipes.
195d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                snapshotFile = File.createTempFile("com.android.documentsui.snapshot{",
196d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                        "}.zip", context.getCacheDir());
197d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
198d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                try (
199d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                    final FileOutputStream outputStream =
200d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                            new ParcelFileDescriptor.AutoCloseOutputStream(
201d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                                    ParcelFileDescriptor.open(
202d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                                            snapshotFile, ParcelFileDescriptor.MODE_WRITE_ONLY));
203d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                    final ParcelFileDescriptor.AutoCloseInputStream inputStream =
204d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                            new ParcelFileDescriptor.AutoCloseInputStream(descriptor);
205d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                ) {
206d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                    final byte[] buffer = new byte[32 * 1024];
207d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                    int bytes;
208d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                    while ((bytes = inputStream.read(buffer)) != -1) {
209d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                        outputStream.write(buffer, 0, bytes);
210d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                    }
211d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                    outputStream.flush();
212d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                }
213d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                return new ReadableArchive(context, snapshotFile, null, archiveUri, accessMode,
214d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                        notificationUri);
215d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            } finally {
216d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                // On UNIX the file will be still available for processes which opened it, even
217d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                // after deleting it. Remove it ASAP, as it won't be used by anyone else.
218d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                if (snapshotFile != null) {
219d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                    snapshotFile.delete();
220d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                }
221d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            }
222d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        } catch (Exception e) {
223d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            // Since the method takes ownership of the passed descriptor, close it
224d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            // on exception.
225d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            IoUtils.closeQuietly(descriptor);
226d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            IoUtils.closeQuietly(fd);
227d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            throw e;
228d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        }
229d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski    }
230d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
2317bb3bdce2d2eb8e279073420151e48a4a0fb60c7Tomasz Mikolajewski    @Override
232d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski    public ParcelFileDescriptor openDocument(
233d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            String documentId, String mode, @Nullable final CancellationSignal signal)
234d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            throws FileNotFoundException {
235d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        MorePreconditions.checkArgumentEquals("r", mode,
236d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                "Invalid mode. Only reading \"r\" supported, but got: \"%s\".");
237d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId);
238d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri,
239d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                "Mismatching archive Uri. Expected: %s, actual: %s.");
240d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
241d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        final ZipEntry entry = mEntries.get(parsedId.mPath);
242d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        if (entry == null) {
243d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            throw new FileNotFoundException();
244d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        }
245d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
246d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        ParcelFileDescriptor[] pipe;
247d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        try {
248d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            pipe = ParcelFileDescriptor.createReliablePipe();
249d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        } catch (IOException e) {
250d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            // Ideally we'd simply throw IOException to the caller, but for consistency
251d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            // with DocumentsProvider::openDocument, converting it to IllegalStateException.
252d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            throw new IllegalStateException("Failed to open the document.", e);
253d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        }
254d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        final InputStream inputStream = mZipFile.getInputStream(entry);
255d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        final ParcelFileDescriptor outputPipe = pipe[1];
25679a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski
25779a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski        synchronized (mEnqueuedOutputPipes) {
25879a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski            mEnqueuedOutputPipes.add(outputPipe);
25979a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski        }
26079a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski
26179a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski        try {
26279a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski            mExecutor.execute(
26379a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                    new Runnable() {
26479a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                        @Override
26579a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                        public void run() {
26679a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                            synchronized (mEnqueuedOutputPipes) {
26779a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                                mEnqueuedOutputPipes.remove(outputPipe);
26879a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                            }
26979a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                            try (final ParcelFileDescriptor.AutoCloseOutputStream outputStream =
27079a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                                    new ParcelFileDescriptor.AutoCloseOutputStream(outputPipe)) {
27179a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                                try {
27279a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                                    final byte buffer[] = new byte[32 * 1024];
27379a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                                    int bytes;
27479a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                                    while ((bytes = inputStream.read(buffer)) != -1) {
27579a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                                        if (Thread.interrupted()) {
27679a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                                            throw new InterruptedException();
27779a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                                        }
27879a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                                        if (signal != null) {
27979a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                                            signal.throwIfCanceled();
28079a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                                        }
28179a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                                        outputStream.write(buffer, 0, bytes);
282d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                                    }
28379a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                                } catch (IOException | InterruptedException e) {
28479a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                                    // Catch the exception before the outer try-with-resource closes
28579a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                                    // the pipe with close() instead of closeWithError().
28679a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                                    try {
287a903c2cdad65d0fe163a4152faa6bd05e2888b22Tomasz Mikolajewski                                        Log.e(TAG, "Failed while reading a file.", e);
288a903c2cdad65d0fe163a4152faa6bd05e2888b22Tomasz Mikolajewski                                        outputPipe.closeWithError("Reading failure.");
28979a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                                    } catch (IOException e2) {
29079a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                                        Log.e(TAG, "Failed to close the pipe after an error.", e2);
291d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                                    }
292d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                                }
29379a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                            } catch (OperationCanceledException e) {
29479a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                                // Cancelled gracefully.
29579a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                            } catch (IOException e) {
29679a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                                Log.e(TAG, "Failed to close the output stream gracefully.", e);
29779a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                            } finally {
29879a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                                IoUtils.closeQuietly(inputStream);
299d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                            }
300d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                        }
30179a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                    });
30279a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski        } catch (RejectedExecutionException e) {
30379a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski            IoUtils.closeQuietly(pipe[0]);
30479a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski            IoUtils.closeQuietly(pipe[1]);
30579a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski            synchronized (mEnqueuedOutputPipes) {
30679a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                mEnqueuedOutputPipes.remove(outputPipe);
30779a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski            }
30879a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski            throw new IllegalStateException("Failed to initialize pipe.");
30979a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski        }
310d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
311d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        return pipe[0];
312d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski    }
313d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
3147bb3bdce2d2eb8e279073420151e48a4a0fb60c7Tomasz Mikolajewski    @Override
315d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski    public AssetFileDescriptor openDocumentThumbnail(
316d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            String documentId, Point sizeHint, final CancellationSignal signal)
317d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            throws FileNotFoundException {
318d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId);
319d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri,
320d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                "Mismatching archive Uri. Expected: %s, actual: %s.");
321d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        Preconditions.checkArgument(getDocumentType(documentId).startsWith("image/"),
322d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                "Thumbnails only supported for image/* MIME type.");
323d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
324d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        final ZipEntry entry = mEntries.get(parsedId.mPath);
325d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        if (entry == null) {
326d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            throw new FileNotFoundException();
327d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        }
328d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
329d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        InputStream inputStream = null;
330d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        try {
331d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            inputStream = mZipFile.getInputStream(entry);
332d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            final ExifInterface exif = new ExifInterface(inputStream);
333d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            if (exif.hasThumbnail()) {
334d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                Bundle extras = null;
335d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                switch (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1)) {
336d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                    case ExifInterface.ORIENTATION_ROTATE_90:
337d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                        extras = new Bundle(1);
338d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                        extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 90);
339d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                        break;
340d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                    case ExifInterface.ORIENTATION_ROTATE_180:
341d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                        extras = new Bundle(1);
342d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                        extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 180);
343d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                        break;
344d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                    case ExifInterface.ORIENTATION_ROTATE_270:
345d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                        extras = new Bundle(1);
346d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                        extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 270);
347d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                        break;
348d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                }
349d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                final long[] range = exif.getThumbnailRange();
350d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                return new AssetFileDescriptor(
351d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                        openDocument(documentId, "r", signal), range[0], range[1], extras);
352d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            }
353d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        } catch (IOException e) {
354d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            // Ignore the exception, as reading the EXIF may legally fail.
355d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            Log.e(TAG, "Failed to obtain thumbnail from EXIF.", e);
356d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        } finally {
357d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            IoUtils.closeQuietly(inputStream);
358d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        }
359d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
360d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        return new AssetFileDescriptor(
361d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski                openDocument(documentId, "r", signal), 0, entry.getSize(), null);
362d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski    }
363d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski
3643b135ef27a918143489cae0d99770890f3bbb3b6Tomasz Mikolajewski    /**
3653b135ef27a918143489cae0d99770890f3bbb3b6Tomasz Mikolajewski     * Closes an archive.
3663b135ef27a918143489cae0d99770890f3bbb3b6Tomasz Mikolajewski     *
3673b135ef27a918143489cae0d99770890f3bbb3b6Tomasz Mikolajewski     * <p>This method does not block until shutdown. Once called, other methods should not be
3683b135ef27a918143489cae0d99770890f3bbb3b6Tomasz Mikolajewski     * called. Any active pipes will be terminated.
3693b135ef27a918143489cae0d99770890f3bbb3b6Tomasz Mikolajewski     */
370d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski    @Override
371d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski    public void close() {
3723b135ef27a918143489cae0d99770890f3bbb3b6Tomasz Mikolajewski        mExecutor.shutdownNow();
37379a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski        synchronized (mEnqueuedOutputPipes) {
37479a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski            for (ParcelFileDescriptor outputPipe : mEnqueuedOutputPipes) {
37579a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                try {
37679a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                    outputPipe.closeWithError("Archive closed.");
37779a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                } catch (IOException e2) {
37879a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                    // Silent close.
37979a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski                }
38079a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski            }
38179a4e10f62794bbb0ce676df7cc36c8edf5c510fTomasz Mikolajewski        }
382d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        try {
383d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            mZipFile.close();
384d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        } catch (IOException e) {
385d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski            // Silent close.
386d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski        }
387d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski    }
388d683f9756b0c56d28693c4d0269217e8fcebf76aTomasz Mikolajewski};
389