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