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