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.ContentProviderClient; 20 import android.content.ContentResolver; 21 import android.content.Context; 22 import android.content.res.AssetFileDescriptor; 23 import android.content.res.Configuration; 24 import android.database.ContentObserver; 25 import android.database.Cursor; 26 import android.database.MatrixCursor.RowBuilder; 27 import android.database.MatrixCursor; 28 import android.graphics.Point; 29 import android.net.Uri; 30 import android.os.Bundle; 31 import android.os.CancellationSignal; 32 import android.os.ParcelFileDescriptor; 33 import android.provider.DocumentsContract.Document; 34 import android.provider.DocumentsContract.Root; 35 import android.provider.DocumentsContract; 36 import android.provider.DocumentsProvider; 37 import android.support.annotation.Nullable; 38 import android.util.Log; 39 40 import com.android.documentsui.R; 41 import com.android.internal.annotations.GuardedBy; 42 import com.android.internal.util.Preconditions; 43 44 import java.io.Closeable; 45 import java.io.File; 46 import java.io.FileNotFoundException; 47 import java.io.IOException; 48 import java.util.HashMap; 49 import java.util.Map; 50 import java.util.Objects; 51 import java.util.concurrent.locks.Lock; 52 53 /** 54 * Provides basic implementation for creating, extracting and accessing 55 * files within archives exposed by a document provider. 56 * 57 * <p>This class is thread safe. All methods can be called on any thread without 58 * synchronization. 59 */ 60 public class ArchivesProvider extends DocumentsProvider { 61 public static final String AUTHORITY = "com.android.documentsui.archives"; 62 63 private static final String[] DEFAULT_ROOTS_PROJECTION = new String[] { 64 Root.COLUMN_ROOT_ID, Root.COLUMN_DOCUMENT_ID, Root.COLUMN_TITLE, Root.COLUMN_FLAGS, 65 Root.COLUMN_ICON }; 66 private static final String TAG = "ArchivesProvider"; 67 private static final String METHOD_ACQUIRE_ARCHIVE = "acquireArchive"; 68 private static final String METHOD_RELEASE_ARCHIVE = "releaseArchive"; 69 private static final String[] ZIP_MIME_TYPES = { 70 "application/zip", "application/x-zip", "application/x-zip-compressed" 71 }; 72 73 @GuardedBy("mArchives") 74 private final Map<Key, Loader> mArchives = new HashMap<Key, Loader>(); 75 76 @Override call(String method, String arg, Bundle extras)77 public Bundle call(String method, String arg, Bundle extras) { 78 if (METHOD_ACQUIRE_ARCHIVE.equals(method)) { 79 acquireArchive(arg); 80 return null; 81 } 82 83 if (METHOD_RELEASE_ARCHIVE.equals(method)) { 84 releaseArchive(arg); 85 return null; 86 } 87 88 return super.call(method, arg, extras); 89 } 90 91 @Override onCreate()92 public boolean onCreate() { 93 return true; 94 } 95 96 @Override queryRoots(String[] projection)97 public Cursor queryRoots(String[] projection) { 98 // No roots provided. 99 return new MatrixCursor(projection != null ? projection : DEFAULT_ROOTS_PROJECTION); 100 } 101 102 @Override queryChildDocuments(String documentId, @Nullable String[] projection, @Nullable String sortOrder)103 public Cursor queryChildDocuments(String documentId, @Nullable String[] projection, 104 @Nullable String sortOrder) 105 throws FileNotFoundException { 106 final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId); 107 final Loader loader = getLoaderOrThrow(documentId); 108 final int status = loader.getStatus(); 109 // If already loaded, then forward the request to the archive. 110 if (status == Loader.STATUS_OPENED) { 111 return loader.get().queryChildDocuments(documentId, projection, sortOrder); 112 } 113 114 final MatrixCursor cursor = new MatrixCursor( 115 projection != null ? projection : Archive.DEFAULT_PROJECTION); 116 final Bundle bundle = new Bundle(); 117 118 switch (status) { 119 case Loader.STATUS_OPENING: 120 bundle.putBoolean(DocumentsContract.EXTRA_LOADING, true); 121 break; 122 123 case Loader.STATUS_FAILED: 124 // Return an empty cursor with EXTRA_LOADING, which shows spinner 125 // in DocumentsUI. Once the archive is loaded, the notification will 126 // be sent, and the directory reloaded. 127 bundle.putString(DocumentsContract.EXTRA_ERROR, 128 getContext().getString(R.string.archive_loading_failed)); 129 break; 130 } 131 132 cursor.setExtras(bundle); 133 cursor.setNotificationUri(getContext().getContentResolver(), 134 buildUriForArchive(archiveId.mArchiveUri, archiveId.mAccessMode)); 135 return cursor; 136 } 137 138 @Override getDocumentType(String documentId)139 public String getDocumentType(String documentId) throws FileNotFoundException { 140 final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId); 141 if (archiveId.mPath.equals("/")) { 142 return Document.MIME_TYPE_DIR; 143 } 144 145 final Loader loader = getLoaderOrThrow(documentId); 146 return loader.get().getDocumentType(documentId); 147 } 148 149 @Override isChildDocument(String parentDocumentId, String documentId)150 public boolean isChildDocument(String parentDocumentId, String documentId) { 151 final Loader loader = getLoaderOrThrow(documentId); 152 return loader.get().isChildDocument(parentDocumentId, documentId); 153 } 154 155 @Override queryDocument(String documentId, @Nullable String[] projection)156 public Cursor queryDocument(String documentId, @Nullable String[] projection) 157 throws FileNotFoundException { 158 final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId); 159 if (archiveId.mPath.equals("/")) { 160 try (final Cursor archiveCursor = getContext().getContentResolver().query( 161 archiveId.mArchiveUri, 162 new String[] { Document.COLUMN_DISPLAY_NAME }, 163 null, null, null, null)) { 164 if (archiveCursor == null || !archiveCursor.moveToFirst()) { 165 throw new FileNotFoundException( 166 "Cannot resolve display name of the archive."); 167 } 168 final String displayName = archiveCursor.getString( 169 archiveCursor.getColumnIndex(Document.COLUMN_DISPLAY_NAME)); 170 171 final MatrixCursor cursor = new MatrixCursor( 172 projection != null ? projection : Archive.DEFAULT_PROJECTION); 173 final RowBuilder row = cursor.newRow(); 174 row.add(Document.COLUMN_DOCUMENT_ID, documentId); 175 row.add(Document.COLUMN_DISPLAY_NAME, displayName); 176 row.add(Document.COLUMN_SIZE, 0); 177 row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); 178 return cursor; 179 } 180 } 181 182 final Loader loader = getLoaderOrThrow(documentId); 183 return loader.get().queryDocument(documentId, projection); 184 } 185 186 @Override createDocument( String parentDocumentId, String mimeType, String displayName)187 public String createDocument( 188 String parentDocumentId, String mimeType, String displayName) 189 throws FileNotFoundException { 190 final Loader loader = getLoaderOrThrow(parentDocumentId); 191 return loader.get().createDocument(parentDocumentId, mimeType, displayName); 192 } 193 194 @Override openDocument( String documentId, String mode, final CancellationSignal signal)195 public ParcelFileDescriptor openDocument( 196 String documentId, String mode, final CancellationSignal signal) 197 throws FileNotFoundException { 198 final Loader loader = getLoaderOrThrow(documentId); 199 return loader.get().openDocument(documentId, mode, signal); 200 } 201 202 @Override openDocumentThumbnail( String documentId, Point sizeHint, final CancellationSignal signal)203 public AssetFileDescriptor openDocumentThumbnail( 204 String documentId, Point sizeHint, final CancellationSignal signal) 205 throws FileNotFoundException { 206 final Loader loader = getLoaderOrThrow(documentId); 207 return loader.get().openDocumentThumbnail(documentId, sizeHint, signal); 208 } 209 210 /** 211 * Returns true if the passed mime type is supported by the helper. 212 */ isSupportedArchiveType(String mimeType)213 public static boolean isSupportedArchiveType(String mimeType) { 214 for (final String zipMimeType : ZIP_MIME_TYPES) { 215 if (zipMimeType.equals(mimeType)) { 216 return true; 217 } 218 } 219 return false; 220 } 221 222 /** 223 * Creates a Uri for accessing an archive with the specified access mode. 224 * 225 * @see ParcelFileDescriptor#MODE_READ 226 * @see ParcelFileDescriptor#MODE_WRITE 227 */ buildUriForArchive(Uri externalUri, int accessMode)228 public static Uri buildUriForArchive(Uri externalUri, int accessMode) { 229 return DocumentsContract.buildDocumentUri(AUTHORITY, 230 new ArchiveId(externalUri, accessMode, "/").toDocumentId()); 231 } 232 233 /** 234 * Acquires an archive. 235 */ acquireArchive(ContentProviderClient client, Uri archiveUri)236 public static void acquireArchive(ContentProviderClient client, Uri archiveUri) { 237 Archive.MorePreconditions.checkArgumentEquals(AUTHORITY, archiveUri.getAuthority(), 238 "Mismatching authority. Expected: %s, actual: %s."); 239 final String documentId = DocumentsContract.getDocumentId(archiveUri); 240 241 try { 242 client.call(METHOD_ACQUIRE_ARCHIVE, documentId, null); 243 } catch (Exception e) { 244 Log.w(TAG, "Failed to acquire archive.", e); 245 } 246 } 247 248 /** 249 * Releases an archive. 250 */ releaseArchive(ContentProviderClient client, Uri archiveUri)251 public static void releaseArchive(ContentProviderClient client, Uri archiveUri) { 252 Archive.MorePreconditions.checkArgumentEquals(AUTHORITY, archiveUri.getAuthority(), 253 "Mismatching authority. Expected: %s, actual: %s."); 254 final String documentId = DocumentsContract.getDocumentId(archiveUri); 255 256 try { 257 client.call(METHOD_RELEASE_ARCHIVE, documentId, null); 258 } catch (Exception e) { 259 Log.w(TAG, "Failed to release archive.", e); 260 } 261 } 262 263 /** 264 * The archive won't close until all clients release it. 265 */ acquireArchive(String documentId)266 private void acquireArchive(String documentId) { 267 final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId); 268 synchronized (mArchives) { 269 final Key key = Key.fromArchiveId(archiveId); 270 Loader loader = mArchives.get(key); 271 if (loader == null) { 272 // TODO: Pass parent Uri so the loader can acquire the parent's notification Uri. 273 loader = new Loader(getContext(), archiveId.mArchiveUri, archiveId.mAccessMode, 274 null); 275 mArchives.put(key, loader); 276 } 277 loader.acquire(); 278 mArchives.put(key, loader); 279 } 280 } 281 282 /** 283 * If all clients release the archive, then it will be closed. 284 */ releaseArchive(String documentId)285 private void releaseArchive(String documentId) { 286 final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId); 287 final Key key = Key.fromArchiveId(archiveId); 288 synchronized (mArchives) { 289 final Loader loader = mArchives.get(key); 290 loader.release(); 291 final int status = loader.getStatus(); 292 if (status == Loader.STATUS_CLOSED || status == Loader.STATUS_CLOSING) { 293 mArchives.remove(key); 294 } 295 } 296 } 297 getLoaderOrThrow(String documentId)298 private Loader getLoaderOrThrow(String documentId) { 299 final ArchiveId id = ArchiveId.fromDocumentId(documentId); 300 final Key key = Key.fromArchiveId(id); 301 synchronized (mArchives) { 302 final Loader loader = mArchives.get(key); 303 if (loader == null) { 304 throw new IllegalStateException("Archive not acquired."); 305 } 306 return loader; 307 } 308 } 309 310 private static class Key { 311 Uri archiveUri; 312 int accessMode; 313 Key(Uri archiveUri, int accessMode)314 public Key(Uri archiveUri, int accessMode) { 315 this.archiveUri = archiveUri; 316 this.accessMode = accessMode; 317 } 318 fromArchiveId(ArchiveId id)319 public static Key fromArchiveId(ArchiveId id) { 320 return new Key(id.mArchiveUri, id.mAccessMode); 321 } 322 323 @Override equals(Object other)324 public boolean equals(Object other) { 325 if (other == null) { 326 return false; 327 } 328 if (!(other instanceof Key)) { 329 return false; 330 } 331 return archiveUri.equals(((Key) other).archiveUri) && 332 accessMode == ((Key) other).accessMode; 333 } 334 335 @Override hashCode()336 public int hashCode() { 337 return Objects.hash(archiveUri, accessMode); 338 } 339 } 340 } 341