1 /* 2 * Copyright (C) 2015 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.content.res.AssetFileDescriptor; 21 import android.database.Cursor; 22 import android.database.MatrixCursor; 23 import android.graphics.Point; 24 import android.net.Uri; 25 import android.os.CancellationSignal; 26 import android.os.ParcelFileDescriptor; 27 import android.os.storage.StorageManager; 28 import android.provider.DocumentsContract; 29 import android.provider.DocumentsContract.Document; 30 import android.support.annotation.Nullable; 31 import android.system.ErrnoException; 32 import android.system.Os; 33 import android.system.OsConstants; 34 import android.text.TextUtils; 35 import android.webkit.MimeTypeMap; 36 37 import com.android.internal.annotations.GuardedBy; 38 import com.android.internal.util.Preconditions; 39 40 import java.io.Closeable; 41 import java.io.File; 42 import java.io.FileNotFoundException; 43 import java.util.HashMap; 44 import java.util.List; 45 import java.util.Locale; 46 import java.util.Map; 47 import java.util.concurrent.LinkedBlockingQueue; 48 import java.util.zip.ZipEntry; 49 50 /** 51 * Provides basic implementation for creating, extracting and accessing 52 * files within archives exposed by a document provider. 53 * 54 * <p>This class is thread safe. 55 */ 56 public abstract class Archive implements Closeable { 57 private static final String TAG = "Archive"; 58 59 public static final String[] DEFAULT_PROJECTION = new String[] { 60 Document.COLUMN_DOCUMENT_ID, 61 Document.COLUMN_DISPLAY_NAME, 62 Document.COLUMN_MIME_TYPE, 63 Document.COLUMN_SIZE, 64 Document.COLUMN_FLAGS 65 }; 66 67 final Context mContext; 68 final Uri mArchiveUri; 69 final int mAccessMode; 70 final Uri mNotificationUri; 71 72 // The container as well as values are guarded by mEntries. 73 @GuardedBy("mEntries") 74 final Map<String, ZipEntry> mEntries; 75 76 // The container as well as values and elements of values are guarded by mEntries. 77 @GuardedBy("mEntries") 78 final Map<String, List<ZipEntry>> mTree; 79 Archive( Context context, Uri archiveUri, int accessMode, @Nullable Uri notificationUri)80 Archive( 81 Context context, 82 Uri archiveUri, 83 int accessMode, 84 @Nullable Uri notificationUri) { 85 mContext = context; 86 mArchiveUri = archiveUri; 87 mAccessMode = accessMode; 88 mNotificationUri = notificationUri; 89 90 mTree = new HashMap<>(); 91 mEntries = new HashMap<>(); 92 } 93 94 /** 95 * Returns a valid, normalized path for an entry. 96 */ getEntryPath(ZipEntry entry)97 public static String getEntryPath(ZipEntry entry) { 98 Preconditions.checkArgument(entry.isDirectory() == entry.getName().endsWith("/"), 99 "Ill-formated ZIP-file."); 100 if (entry.getName().startsWith("/")) { 101 return entry.getName(); 102 } else { 103 return "/" + entry.getName(); 104 } 105 } 106 107 /** 108 * Returns true if the file descriptor is seekable. 109 * @param descriptor File descriptor to check. 110 */ canSeek(ParcelFileDescriptor descriptor)111 public static boolean canSeek(ParcelFileDescriptor descriptor) { 112 try { 113 return Os.lseek(descriptor.getFileDescriptor(), 0, 114 OsConstants.SEEK_CUR) == 0; 115 } catch (ErrnoException e) { 116 return false; 117 } 118 } 119 120 /** 121 * Lists child documents of an archive or a directory within an 122 * archive. Must be called only for archives with supported mime type, 123 * or for documents within archives. 124 * 125 * @see DocumentsProvider.queryChildDocuments(String, String[], String) 126 */ queryChildDocuments(String documentId, @Nullable String[] projection, @Nullable String sortOrder)127 public Cursor queryChildDocuments(String documentId, @Nullable String[] projection, 128 @Nullable String sortOrder) throws FileNotFoundException { 129 final ArchiveId parsedParentId = ArchiveId.fromDocumentId(documentId); 130 MorePreconditions.checkArgumentEquals(mArchiveUri, parsedParentId.mArchiveUri, 131 "Mismatching archive Uri. Expected: %s, actual: %s."); 132 133 final MatrixCursor result = new MatrixCursor( 134 projection != null ? projection : DEFAULT_PROJECTION); 135 if (mNotificationUri != null) { 136 result.setNotificationUri(mContext.getContentResolver(), mNotificationUri); 137 } 138 139 synchronized (mEntries) { 140 final List<ZipEntry> parentList = mTree.get(parsedParentId.mPath); 141 if (parentList == null) { 142 throw new FileNotFoundException(); 143 } 144 for (final ZipEntry entry : parentList) { 145 addCursorRow(result, entry); 146 } 147 } 148 return result; 149 } 150 151 /** 152 * Returns a MIME type of a document within an archive. 153 * 154 * @see DocumentsProvider.getDocumentType(String) 155 */ getDocumentType(String documentId)156 public String getDocumentType(String documentId) throws FileNotFoundException { 157 final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId); 158 MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri, 159 "Mismatching archive Uri. Expected: %s, actual: %s."); 160 161 synchronized (mEntries) { 162 final ZipEntry entry = mEntries.get(parsedId.mPath); 163 if (entry == null) { 164 throw new FileNotFoundException(); 165 } 166 return getMimeTypeForEntry(entry); 167 } 168 } 169 170 /** 171 * Returns true if a document within an archive is a child or any descendant of the archive 172 * document or another document within the archive. 173 * 174 * @see DocumentsProvider.isChildDocument(String, String) 175 */ isChildDocument(String parentDocumentId, String documentId)176 public boolean isChildDocument(String parentDocumentId, String documentId) { 177 final ArchiveId parsedParentId = ArchiveId.fromDocumentId(parentDocumentId); 178 final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId); 179 MorePreconditions.checkArgumentEquals(mArchiveUri, parsedParentId.mArchiveUri, 180 "Mismatching archive Uri. Expected: %s, actual: %s."); 181 182 synchronized (mEntries) { 183 final ZipEntry entry = mEntries.get(parsedId.mPath); 184 if (entry == null) { 185 return false; 186 } 187 188 final ZipEntry parentEntry = mEntries.get(parsedParentId.mPath); 189 if (parentEntry == null || !parentEntry.isDirectory()) { 190 return false; 191 } 192 193 // Add a trailing slash even if it's not a directory, so it's easy to check if the 194 // entry is a descendant. 195 String pathWithSlash = entry.isDirectory() ? getEntryPath(entry) 196 : getEntryPath(entry) + "/"; 197 198 return pathWithSlash.startsWith(parsedParentId.mPath) && 199 !parsedParentId.mPath.equals(pathWithSlash); 200 } 201 } 202 203 /** 204 * Returns metadata of a document within an archive. 205 * 206 * @see DocumentsProvider.queryDocument(String, String[]) 207 */ queryDocument(String documentId, @Nullable String[] projection)208 public Cursor queryDocument(String documentId, @Nullable String[] projection) 209 throws FileNotFoundException { 210 final ArchiveId parsedId = ArchiveId.fromDocumentId(documentId); 211 MorePreconditions.checkArgumentEquals(mArchiveUri, parsedId.mArchiveUri, 212 "Mismatching archive Uri. Expected: %s, actual: %s."); 213 214 synchronized (mEntries) { 215 final ZipEntry entry = mEntries.get(parsedId.mPath); 216 if (entry == null) { 217 throw new FileNotFoundException(); 218 } 219 220 final MatrixCursor result = new MatrixCursor( 221 projection != null ? projection : DEFAULT_PROJECTION); 222 if (mNotificationUri != null) { 223 result.setNotificationUri(mContext.getContentResolver(), mNotificationUri); 224 } 225 addCursorRow(result, entry); 226 return result; 227 } 228 } 229 230 /** 231 * Creates a file within an archive. 232 * 233 * @see DocumentsProvider.createDocument(String, String, String)) 234 */ createDocument(String parentDocumentId, String mimeType, String displayName)235 public String createDocument(String parentDocumentId, String mimeType, String displayName) 236 throws FileNotFoundException { 237 throw new UnsupportedOperationException("Creating documents not supported."); 238 } 239 240 /** 241 * Opens a file within an archive. 242 * 243 * @see DocumentsProvider.openDocument(String, String, CancellationSignal)) 244 */ openDocument( String documentId, String mode, @Nullable final CancellationSignal signal)245 public ParcelFileDescriptor openDocument( 246 String documentId, String mode, @Nullable final CancellationSignal signal) 247 throws FileNotFoundException { 248 throw new UnsupportedOperationException("Opening not supported."); 249 } 250 251 /** 252 * Opens a thumbnail of a file within an archive. 253 * 254 * @see DocumentsProvider.openDocumentThumbnail(String, Point, CancellationSignal)) 255 */ openDocumentThumbnail( String documentId, Point sizeHint, final CancellationSignal signal)256 public AssetFileDescriptor openDocumentThumbnail( 257 String documentId, Point sizeHint, final CancellationSignal signal) 258 throws FileNotFoundException { 259 throw new UnsupportedOperationException("Thumbnails not supported."); 260 } 261 262 /** 263 * Creates an archive id for the passed path. 264 */ createArchiveId(String path)265 public ArchiveId createArchiveId(String path) { 266 return new ArchiveId(mArchiveUri, mAccessMode, path); 267 } 268 269 /** 270 * Not thread safe. 271 */ addCursorRow(MatrixCursor cursor, ZipEntry entry)272 void addCursorRow(MatrixCursor cursor, ZipEntry entry) { 273 final MatrixCursor.RowBuilder row = cursor.newRow(); 274 final ArchiveId parsedId = createArchiveId(getEntryPath(entry)); 275 row.add(Document.COLUMN_DOCUMENT_ID, parsedId.toDocumentId()); 276 277 final File file = new File(entry.getName()); 278 row.add(Document.COLUMN_DISPLAY_NAME, file.getName()); 279 row.add(Document.COLUMN_SIZE, entry.getSize()); 280 281 final String mimeType = getMimeTypeForEntry(entry); 282 row.add(Document.COLUMN_MIME_TYPE, mimeType); 283 284 final int flags = mimeType.startsWith("image/") ? Document.FLAG_SUPPORTS_THUMBNAIL : 0; 285 row.add(Document.COLUMN_FLAGS, flags); 286 } 287 getMimeTypeForEntry(ZipEntry entry)288 static String getMimeTypeForEntry(ZipEntry entry) { 289 if (entry.isDirectory()) { 290 return Document.MIME_TYPE_DIR; 291 } 292 293 final int lastDot = entry.getName().lastIndexOf('.'); 294 if (lastDot >= 0) { 295 final String extension = entry.getName().substring(lastDot + 1).toLowerCase(Locale.US); 296 final String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); 297 if (mimeType != null) { 298 return mimeType; 299 } 300 } 301 302 return "application/octet-stream"; 303 } 304 305 // TODO: Upstream to the Preconditions class. 306 // TODO: Move to a separate file. 307 public static class MorePreconditions { checkArgumentEquals(String expected, @Nullable String actual, String message)308 static void checkArgumentEquals(String expected, @Nullable String actual, 309 String message) { 310 if (!TextUtils.equals(expected, actual)) { 311 throw new IllegalArgumentException(String.format(message, 312 String.valueOf(expected), String.valueOf(actual))); 313 } 314 } 315 checkArgumentEquals(Uri expected, @Nullable Uri actual, String message)316 static void checkArgumentEquals(Uri expected, @Nullable Uri actual, 317 String message) { 318 checkArgumentEquals(expected.toString(), actual.toString(), message); 319 } 320 } 321 }; 322