1dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski/*
2dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski * Copyright (C) 2017 The Android Open Source Project
3dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski *
4dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski * Licensed under the Apache License, Version 2.0 (the "License");
5dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski * you may not use this file except in compliance with the License.
6dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski * You may obtain a copy of the License at
7dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski *
8dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski *      http://www.apache.org/licenses/LICENSE-2.0
9dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski *
10dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski * Unless required by applicable law or agreed to in writing, software
11dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski * distributed under the License is distributed on an "AS IS" BASIS,
12dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski * See the License for the specific language governing permissions and
14dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski * limitations under the License.
15dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski */
16dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
17dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskipackage com.android.documentsui.archives;
18dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
19dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskiimport android.content.Context;
20dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskiimport android.net.Uri;
21dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskiimport android.os.CancellationSignal;
22dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskiimport android.os.OperationCanceledException;
23a18ea7ecbc82adb7da88e77769895ed60264ea13Tomasz Mikolajewskiimport android.os.ParcelFileDescriptor.AutoCloseOutputStream;
24dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskiimport android.os.ParcelFileDescriptor;
25dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskiimport android.provider.DocumentsContract.Document;
26dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskiimport android.support.annotation.Nullable;
27dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskiimport android.util.Log;
28dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
29dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskiimport com.android.internal.annotations.GuardedBy;
30dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskiimport android.support.annotation.VisibleForTesting;
31dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
32dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskiimport libcore.io.IoUtils;
33dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
34dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskiimport java.io.FileDescriptor;
35dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskiimport java.io.FileNotFoundException;
36dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskiimport java.io.FileOutputStream;
37dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskiimport java.io.IOException;
38dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskiimport java.io.InputStream;
39dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskiimport java.util.ArrayList;
40dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskiimport java.util.HashSet;
41dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskiimport java.util.List;
42dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskiimport java.util.Set;
43dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskiimport java.util.concurrent.ExecutorService;
44dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskiimport java.util.concurrent.Executors;
45dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskiimport java.util.concurrent.RejectedExecutionException;
46dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskiimport java.util.concurrent.TimeUnit;
47dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskiimport java.util.zip.ZipEntry;
48dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskiimport java.util.zip.ZipOutputStream;
49dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
50dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski/**
51dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski * Provides basic implementation for creating archives.
52dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski *
53dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski * <p>This class is thread safe.
54dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski */
55dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewskipublic class WriteableArchive extends Archive {
56dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    private static final String TAG = "WriteableArchive";
57dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
58dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    @GuardedBy("mEntries")
59dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    private final Set<String> mPendingEntries = new HashSet<>();
60dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
61dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    @GuardedBy("mEntries")
62dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    private final ZipOutputStream mZipOutputStream;
63a18ea7ecbc82adb7da88e77769895ed60264ea13Tomasz Mikolajewski    private final AutoCloseOutputStream mOutputStream;
64dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
65a18ea7ecbc82adb7da88e77769895ed60264ea13Tomasz Mikolajewski    /**
66a18ea7ecbc82adb7da88e77769895ed60264ea13Tomasz Mikolajewski     * Takes ownership of the passed file descriptor.
67a18ea7ecbc82adb7da88e77769895ed60264ea13Tomasz Mikolajewski     */
68dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    private WriteableArchive(
69dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            Context context,
70a18ea7ecbc82adb7da88e77769895ed60264ea13Tomasz Mikolajewski            ParcelFileDescriptor fd,
71dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            Uri archiveUri,
72dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            int accessMode,
73dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            @Nullable Uri notificationUri)
74dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            throws IOException {
75dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        super(context, archiveUri, accessMode, notificationUri);
76dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        if (!supportsAccessMode(accessMode)) {
77dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            throw new IllegalStateException("Unsupported access mode.");
78dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        }
79dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
80dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        addEntry(null /* no parent */, new ZipEntry("/"));  // Root entry.
81a18ea7ecbc82adb7da88e77769895ed60264ea13Tomasz Mikolajewski        mOutputStream = new AutoCloseOutputStream(fd);
82a18ea7ecbc82adb7da88e77769895ed60264ea13Tomasz Mikolajewski        mZipOutputStream = new ZipOutputStream(mOutputStream);
83dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    }
84dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
85dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    private void addEntry(@Nullable ZipEntry parentEntry, ZipEntry entry) {
86dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        final String entryPath = getEntryPath(entry);
87dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        synchronized (mEntries) {
88dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            if (entry.isDirectory()) {
89dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                if (!mTree.containsKey(entryPath)) {
90dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                    mTree.put(entryPath, new ArrayList<ZipEntry>());
91dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                }
92dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            }
93dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            mEntries.put(entryPath, entry);
94dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            if (parentEntry != null) {
95dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                mTree.get(getEntryPath(parentEntry)).add(entry);
96dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            }
97dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        }
98dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    }
99dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
100dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    /**
101dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski     * @see ParcelFileDescriptor
102dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski     */
103dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    public static boolean supportsAccessMode(int accessMode) {
104dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        return accessMode == ParcelFileDescriptor.MODE_WRITE_ONLY;
105dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    }
106dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
107dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    /**
108dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski     * Creates a DocumentsArchive instance for writing into an archive file passed
109dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski     * as a file descriptor.
110dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski     *
111dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski     * This method takes ownership for the passed descriptor. The caller must
112dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski     * not use it after passing.
113dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski     *
114dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski     * @param context Context of the provider.
115dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski     * @param descriptor File descriptor for the archive's contents.
116dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski     * @param archiveUri Uri of the archive document.
117dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski     * @param accessMode Access mode for the archive {@see ParcelFileDescriptor}.
118dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski     * @param Uri notificationUri Uri for notifying that the archive file has changed.
119dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski     */
120dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    @VisibleForTesting
121dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    public static WriteableArchive createForParcelFileDescriptor(
122dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            Context context, ParcelFileDescriptor descriptor, Uri archiveUri, int accessMode,
123dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            @Nullable Uri notificationUri)
124dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            throws IOException {
125dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        try {
126a18ea7ecbc82adb7da88e77769895ed60264ea13Tomasz Mikolajewski            return new WriteableArchive(context, descriptor, archiveUri, accessMode,
127a18ea7ecbc82adb7da88e77769895ed60264ea13Tomasz Mikolajewski                    notificationUri);
128dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        } catch (Exception e) {
129dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            // Since the method takes ownership of the passed descriptor, close it
130dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            // on exception.
131dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            IoUtils.closeQuietly(descriptor);
132dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            throw e;
133dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        }
134dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    }
135dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
136dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    @Override
137dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    @VisibleForTesting
138dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    public String createDocument(String parentDocumentId, String mimeType, String displayName)
139dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            throws FileNotFoundException {
140dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        final ArchiveId parsedParentId = ArchiveId.fromDocumentId(parentDocumentId);
141dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        MorePreconditions.checkArgumentEquals(mArchiveUri, parsedParentId.mArchiveUri,
142dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                "Mismatching archive Uri. Expected: %s, actual: %s.");
143dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
144dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        final boolean isDirectory = Document.MIME_TYPE_DIR.equals(mimeType);
145dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        ZipEntry entry;
146dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        String entryPath;
147dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
148dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        synchronized (mEntries) {
149dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            final ZipEntry parentEntry = mEntries.get(parsedParentId.mPath);
150dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
151dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            if (parentEntry == null) {
152dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                throw new FileNotFoundException();
153dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            }
154dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
155dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            if (displayName.indexOf("/") != -1 || ".".equals(displayName) || "..".equals(displayName)) {
156dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                throw new IllegalStateException("Display name contains invalid characters.");
157dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            }
158dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
159dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            if ("".equals(displayName)) {
160dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                throw new IllegalStateException("Display name cannot be empty.");
161dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            }
162dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
163dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
164dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            assert(parentEntry.getName().endsWith("/"));
165dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            final String parentName = "/".equals(parentEntry.getName()) ? "" : parentEntry.getName();
166dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            final String entryName = parentName + displayName + (isDirectory ? "/" : "");
167dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            entry = new ZipEntry(entryName);
168dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            entryPath = getEntryPath(entry);
169dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            entry.setSize(0);
170dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
171dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            if (mEntries.get(entryPath) != null) {
172dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                throw new IllegalStateException("The document already exist: " + entryPath);
173dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            }
174dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            addEntry(parentEntry, entry);
175dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        }
176dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
177dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        if (!isDirectory) {
178dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            // For files, the contents will be written via openDocument. Since the contents
179dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            // must be immediately followed by the contents, defer adding the header until
180dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            // openDocument. All pending entires which haven't been written will be added
181dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            // to the ZIP file in close().
182dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            synchronized (mEntries) {
183dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                mPendingEntries.add(entryPath);
184dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            }
185dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        } else {
186dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            try {
187dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                synchronized (mEntries) {
188dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                    mZipOutputStream.putNextEntry(entry);
189dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                }
190dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            } catch (IOException e) {
191dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                throw new IllegalStateException(
192dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                        "Failed to create a file in the archive: " + entryPath, e);
193dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            }
194dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        }
195dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
196dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        return createArchiveId(entryPath).toDocumentId();
197dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    }
198dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
199dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    @Override
200dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    public ParcelFileDescriptor openDocument(
201dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            String documentId, String mode, @Nullable final CancellationSignal signal)
202dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            throws FileNotFoundException {
203dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        MorePreconditions.checkArgumentEquals("w", mode,
204dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                "Invalid mode. Only writing \"w\" supported, but got: \"%s\".");
205dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId);
206dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri,
207dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                "Mismatching archive Uri. Expected: %s, actual: %s.");
208dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
209dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        final ZipEntry entry;
210dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        synchronized (mEntries) {
211dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            entry = mEntries.get(parsedId.mPath);
212dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            if (entry == null) {
213dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                throw new FileNotFoundException();
214dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            }
215dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
216dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            if (!mPendingEntries.contains(parsedId.mPath)) {
217dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                throw new IllegalStateException("Files can be written only once.");
218dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            }
219dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            mPendingEntries.remove(parsedId.mPath);
220dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        }
221dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
222dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        ParcelFileDescriptor[] pipe;
223dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        try {
224dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            pipe = ParcelFileDescriptor.createReliablePipe();
225dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        } catch (IOException e) {
226dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            // Ideally we'd simply throw IOException to the caller, but for consistency
227dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            // with DocumentsProvider::openDocument, converting it to IllegalStateException.
228dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            throw new IllegalStateException("Failed to open the document.", e);
229dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        }
230dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        final ParcelFileDescriptor inputPipe = pipe[0];
231dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
232dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        try {
233dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            mExecutor.execute(
234dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                    new Runnable() {
235dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                        @Override
236dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                        public void run() {
237dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                            try (final ParcelFileDescriptor.AutoCloseInputStream inputStream =
238dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                    new ParcelFileDescriptor.AutoCloseInputStream(inputPipe)) {
239dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                try {
240dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                    synchronized (mEntries) {
241dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                        mZipOutputStream.putNextEntry(entry);
242dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                        final byte buffer[] = new byte[32 * 1024];
243dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                        int bytes;
244dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                        long size = 0;
245dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                        while ((bytes = inputStream.read(buffer)) != -1) {
246dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                            if (signal != null) {
247dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                                signal.throwIfCanceled();
248dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                            }
249dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                            mZipOutputStream.write(buffer, 0, bytes);
250dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                            size += bytes;
251dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                        }
252dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                        entry.setSize(size);
253dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                        mZipOutputStream.closeEntry();
254dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                    }
255dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                } catch (IOException e) {
256dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                    // Catch the exception before the outer try-with-resource closes
257dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                    // the pipe with close() instead of closeWithError().
258dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                    try {
259dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                        Log.e(TAG, "Failed while writing to a file.", e);
260dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                        inputPipe.closeWithError("Writing failure.");
261dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                    } catch (IOException e2) {
262dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                        Log.e(TAG, "Failed to close the pipe after an error.", e2);
263dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                    }
264dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                }
265dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                            } catch (OperationCanceledException e) {
266dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                // Cancelled gracefully.
267dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                            } catch (IOException e) {
268dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                                // Input stream auto-close error. Close quietly.
269dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                            }
270dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                        }
271dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                    });
272dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        } catch (RejectedExecutionException e) {
273dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            IoUtils.closeQuietly(pipe[0]);
274dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            IoUtils.closeQuietly(pipe[1]);
275dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            throw new IllegalStateException("Failed to initialize pipe.");
276dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        }
277dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
278dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        return pipe[1];
279dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    }
280dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
281dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    /**
282dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski     * Closes the archive. Blocks until all enqueued pipes are completed.
283dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski     */
284dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    @Override
285dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    public void close() {
286dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        // Waits until all enqueued pipe requests are completed.
287dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        mExecutor.shutdown();
288dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        try {
289dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            final boolean result = mExecutor.awaitTermination(
290dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                    Long.MAX_VALUE, TimeUnit.MILLISECONDS);
291dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            assert(result);
292dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        } catch (InterruptedException e) {
293dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            Log.e(TAG, "Opened files failed to be fullly written.", e);
294dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        }
295dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
296dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        // Flush all pending entries. They will all have empty size.
297dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        synchronized (mEntries) {
298dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            for (final String path : mPendingEntries) {
299dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                try {
300dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                    mZipOutputStream.putNextEntry(mEntries.get(path));
301dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                    mZipOutputStream.closeEntry();
302dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                } catch (IOException e) {
303dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                    Log.e(TAG, "Failed to flush empty entries.", e);
304dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                }
305dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            }
306dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski
307dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            try {
308dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                mZipOutputStream.close();
309dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            } catch (IOException e) {
310dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski                Log.e(TAG, "Failed while closing the ZIP file.", e);
311dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski            }
312dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski        }
313a18ea7ecbc82adb7da88e77769895ed60264ea13Tomasz Mikolajewski
314a18ea7ecbc82adb7da88e77769895ed60264ea13Tomasz Mikolajewski        IoUtils.closeQuietly(mOutputStream);
315dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski    }
316dc235d2796593d0275843b7a5897c72c5603f6ccTomasz Mikolajewski};
317