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 17package com.android.documentsui.archives; 18 19import android.content.Context; 20import android.net.Uri; 21import android.os.CancellationSignal; 22import android.os.OperationCanceledException; 23import android.os.ParcelFileDescriptor.AutoCloseOutputStream; 24import android.os.ParcelFileDescriptor; 25import android.provider.DocumentsContract.Document; 26import android.support.annotation.Nullable; 27import android.util.Log; 28 29import com.android.internal.annotations.GuardedBy; 30import android.support.annotation.VisibleForTesting; 31 32import libcore.io.IoUtils; 33 34import java.io.FileDescriptor; 35import java.io.FileNotFoundException; 36import java.io.FileOutputStream; 37import java.io.IOException; 38import java.io.InputStream; 39import java.util.ArrayList; 40import java.util.HashSet; 41import java.util.List; 42import java.util.Set; 43import java.util.concurrent.ExecutorService; 44import java.util.concurrent.Executors; 45import java.util.concurrent.RejectedExecutionException; 46import java.util.concurrent.TimeUnit; 47import java.util.zip.ZipEntry; 48import java.util.zip.ZipOutputStream; 49 50/** 51 * Provides basic implementation for creating archives. 52 * 53 * <p>This class is thread safe. 54 */ 55public 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 */ 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 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 */ 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 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 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 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 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