Tomasz Mikolajewski | dbd6b8b | 2016-09-29 15:27:37 +0900 | [diff] [blame] | 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.content.res.Configuration; |
| 22 | import android.database.ContentObserver; |
| 23 | import android.database.Cursor; |
Tomasz Mikolajewski | dbd6b8b | 2016-09-29 15:27:37 +0900 | [diff] [blame] | 24 | import android.database.MatrixCursor.RowBuilder; |
Tomasz Mikolajewski | 5a9d100 | 2016-10-19 15:00:40 +0900 | [diff] [blame] | 25 | import android.database.MatrixCursor; |
Tomasz Mikolajewski | dbd6b8b | 2016-09-29 15:27:37 +0900 | [diff] [blame] | 26 | import android.graphics.Point; |
| 27 | import android.net.Uri; |
Tomasz Mikolajewski | 5a9d100 | 2016-10-19 15:00:40 +0900 | [diff] [blame] | 28 | import android.os.Bundle; |
Tomasz Mikolajewski | dbd6b8b | 2016-09-29 15:27:37 +0900 | [diff] [blame] | 29 | import android.os.CancellationSignal; |
| 30 | import android.os.ParcelFileDescriptor; |
| 31 | import android.provider.DocumentsContract.Document; |
| 32 | import android.provider.DocumentsContract; |
| 33 | import android.provider.DocumentsProvider; |
| 34 | import android.support.annotation.Nullable; |
| 35 | import android.util.Log; |
| 36 | import android.util.LruCache; |
| 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; |
Tomasz Mikolajewski | dbd6b8b | 2016-09-29 15:27:37 +0900 | [diff] [blame] | 44 | import java.util.HashMap; |
| 45 | import java.util.Map; |
Tomasz Mikolajewski | dbd6b8b | 2016-09-29 15:27:37 +0900 | [diff] [blame] | 46 | import java.util.concurrent.locks.Lock; |
Tomasz Mikolajewski | dbd6b8b | 2016-09-29 15:27:37 +0900 | [diff] [blame] | 47 | |
| 48 | /** |
| 49 | * Provides basic implementation for creating, extracting and accessing |
| 50 | * files within archives exposed by a document provider. |
| 51 | * |
| 52 | * <p>This class is thread safe. All methods can be called on any thread without |
| 53 | * synchronization. |
| 54 | */ |
| 55 | public class ArchivesProvider extends DocumentsProvider implements Closeable { |
| 56 | public static final String AUTHORITY = "com.android.documentsui.archives"; |
| 57 | |
| 58 | private static final String TAG = "ArchivesProvider"; |
| 59 | private static final int OPENED_ARCHIVES_CACHE_SIZE = 4; |
| 60 | private static final String[] ZIP_MIME_TYPES = { |
| 61 | "application/zip", "application/x-zip", "application/x-zip-compressed" |
| 62 | }; |
| 63 | |
| 64 | @GuardedBy("mArchives") |
| 65 | private final LruCache<Uri, Loader> mArchives = |
| 66 | new LruCache<Uri, Loader>(OPENED_ARCHIVES_CACHE_SIZE) { |
| 67 | @Override |
| 68 | public void entryRemoved(boolean evicted, Uri key, |
| 69 | Loader oldValue, Loader newValue) { |
| 70 | oldValue.getWriteLock().lock(); |
| 71 | try { |
| 72 | oldValue.get().close(); |
| 73 | } catch (FileNotFoundException e) { |
| 74 | Log.e(TAG, "Failed to close an archive as it no longer exists."); |
| 75 | } finally { |
| 76 | oldValue.getWriteLock().unlock(); |
| 77 | } |
| 78 | } |
| 79 | }; |
| 80 | |
| 81 | @Override |
| 82 | public boolean onCreate() { |
| 83 | return true; |
| 84 | } |
| 85 | |
| 86 | @Override |
| 87 | public Cursor queryRoots(String[] projection) { |
| 88 | throw new UnsupportedOperationException(); |
| 89 | } |
| 90 | |
| 91 | @Override |
| 92 | public Cursor queryChildDocuments(String documentId, @Nullable String[] projection, |
| 93 | @Nullable String sortOrder) |
| 94 | throws FileNotFoundException { |
Tomasz Mikolajewski | 5a9d100 | 2016-10-19 15:00:40 +0900 | [diff] [blame] | 95 | final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId); |
Tomasz Mikolajewski | dbd6b8b | 2016-09-29 15:27:37 +0900 | [diff] [blame] | 96 | Loader loader = null; |
| 97 | try { |
| 98 | loader = obtainInstance(documentId); |
Tomasz Mikolajewski | 5a9d100 | 2016-10-19 15:00:40 +0900 | [diff] [blame] | 99 | if (loader.mArchive == null) { |
| 100 | final MatrixCursor cursor = new MatrixCursor( |
| 101 | projection != null ? projection : Archive.DEFAULT_PROJECTION); |
| 102 | // Return an empty cursor with EXTRA_LOADING, which shows spinner |
| 103 | // in DocumentsUI. Once the archive is loaded, the notification will |
| 104 | // be sent, and the directory reloaded. |
| 105 | final Bundle bundle = new Bundle(); |
| 106 | bundle.putBoolean(DocumentsContract.EXTRA_LOADING, true); |
| 107 | cursor.setExtras(bundle); |
| 108 | cursor.setNotificationUri(getContext().getContentResolver(), |
| 109 | buildUriForArchive(archiveId.mArchiveUri)); |
| 110 | return cursor; |
| 111 | } |
Tomasz Mikolajewski | dbd6b8b | 2016-09-29 15:27:37 +0900 | [diff] [blame] | 112 | return loader.get().queryChildDocuments(documentId, projection, sortOrder); |
| 113 | } finally { |
| 114 | releaseInstance(loader); |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | @Override |
| 119 | public String getDocumentType(String documentId) throws FileNotFoundException { |
| 120 | final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId); |
| 121 | if (archiveId.mPath.equals("/")) { |
| 122 | return Document.MIME_TYPE_DIR; |
| 123 | } |
| 124 | |
| 125 | Loader loader = null; |
| 126 | try { |
| 127 | loader = obtainInstance(documentId); |
| 128 | return loader.get().getDocumentType(documentId); |
| 129 | } finally { |
| 130 | releaseInstance(loader); |
| 131 | } |
| 132 | } |
| 133 | |
| 134 | @Override |
| 135 | public boolean isChildDocument(String parentDocumentId, String documentId) { |
| 136 | Loader loader = null; |
| 137 | try { |
| 138 | loader = obtainInstance(documentId); |
| 139 | return loader.get().isChildDocument(parentDocumentId, documentId); |
| 140 | } catch (FileNotFoundException e) { |
| 141 | throw new IllegalStateException(e); |
| 142 | } finally { |
| 143 | releaseInstance(loader); |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | @Override |
| 148 | public Cursor queryDocument(String documentId, @Nullable String[] projection) |
| 149 | throws FileNotFoundException { |
| 150 | final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId); |
| 151 | if (archiveId.mPath.equals("/")) { |
Tomasz Mikolajewski | 1794d44 | 2016-10-26 15:18:59 +0900 | [diff] [blame] | 152 | try (final Cursor archiveCursor = getContext().getContentResolver().query( |
| 153 | archiveId.mArchiveUri, |
| 154 | new String[] { Document.COLUMN_DISPLAY_NAME }, |
| 155 | null, null, null, null)) { |
Tomasz Mikolajewski | a352711 | 2016-10-27 11:28:14 +0900 | [diff] [blame^] | 156 | if (archiveCursor == null || !archiveCursor.moveToFirst()) { |
Tomasz Mikolajewski | 1794d44 | 2016-10-26 15:18:59 +0900 | [diff] [blame] | 157 | throw new FileNotFoundException( |
| 158 | "Cannot resolve display name of the archive."); |
| 159 | } |
Tomasz Mikolajewski | 1794d44 | 2016-10-26 15:18:59 +0900 | [diff] [blame] | 160 | final String displayName = archiveCursor.getString( |
| 161 | archiveCursor.getColumnIndex(Document.COLUMN_DISPLAY_NAME)); |
| 162 | |
| 163 | final MatrixCursor cursor = new MatrixCursor( |
| 164 | projection != null ? projection : Archive.DEFAULT_PROJECTION); |
| 165 | final RowBuilder row = cursor.newRow(); |
| 166 | row.add(Document.COLUMN_DOCUMENT_ID, documentId); |
| 167 | row.add(Document.COLUMN_DISPLAY_NAME, displayName); |
| 168 | row.add(Document.COLUMN_SIZE, 0); |
| 169 | row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); |
| 170 | return cursor; |
| 171 | } |
Tomasz Mikolajewski | dbd6b8b | 2016-09-29 15:27:37 +0900 | [diff] [blame] | 172 | } |
| 173 | |
| 174 | Loader loader = null; |
| 175 | try { |
| 176 | loader = obtainInstance(documentId); |
| 177 | return loader.get().queryDocument(documentId, projection); |
| 178 | } finally { |
| 179 | releaseInstance(loader); |
| 180 | } |
| 181 | } |
| 182 | |
| 183 | @Override |
| 184 | public ParcelFileDescriptor openDocument( |
| 185 | String documentId, String mode, final CancellationSignal signal) |
| 186 | throws FileNotFoundException { |
| 187 | Loader loader = null; |
| 188 | try { |
| 189 | loader = obtainInstance(documentId); |
| 190 | return loader.get().openDocument(documentId, mode, signal); |
| 191 | } finally { |
| 192 | releaseInstance(loader); |
| 193 | } |
| 194 | } |
| 195 | |
| 196 | @Override |
| 197 | public AssetFileDescriptor openDocumentThumbnail( |
| 198 | String documentId, Point sizeHint, final CancellationSignal signal) |
| 199 | throws FileNotFoundException { |
| 200 | Loader loader = null; |
| 201 | try { |
| 202 | loader = obtainInstance(documentId); |
| 203 | return loader.get().openDocumentThumbnail(documentId, sizeHint, signal); |
| 204 | } finally { |
| 205 | releaseInstance(loader); |
| 206 | } |
| 207 | } |
| 208 | |
| 209 | /** |
| 210 | * Returns true if the passed mime type is supported by the helper. |
| 211 | */ |
| 212 | public static boolean isSupportedArchiveType(String mimeType) { |
| 213 | for (final String zipMimeType : ZIP_MIME_TYPES) { |
| 214 | if (zipMimeType.equals(mimeType)) { |
| 215 | return true; |
| 216 | } |
| 217 | } |
| 218 | return false; |
| 219 | } |
| 220 | |
| 221 | public static Uri buildUriForArchive(Uri archiveUri) { |
| 222 | return DocumentsContract.buildDocumentUri( |
| 223 | AUTHORITY, new ArchiveId(archiveUri, "/").toDocumentId()); |
| 224 | } |
| 225 | |
| 226 | /** |
| 227 | * Closes the helper and disposes all existing archives. It will block until all ongoing |
| 228 | * operations on each opened archive are finished. |
| 229 | */ |
| 230 | @Override |
| 231 | // TODO: Wire close() to call(). |
| 232 | public void close() { |
| 233 | synchronized (mArchives) { |
| 234 | mArchives.evictAll(); |
| 235 | } |
| 236 | } |
| 237 | |
| 238 | private Loader obtainInstance(String documentId) throws FileNotFoundException { |
| 239 | Loader loader; |
| 240 | synchronized (mArchives) { |
| 241 | loader = getInstanceUncheckedLocked(documentId); |
| 242 | loader.getReadLock().lock(); |
| 243 | } |
| 244 | return loader; |
| 245 | } |
| 246 | |
| 247 | private void releaseInstance(@Nullable Loader loader) { |
| 248 | if (loader != null) { |
| 249 | loader.getReadLock().unlock(); |
| 250 | } |
| 251 | } |
| 252 | |
| 253 | private Loader getInstanceUncheckedLocked(String documentId) |
| 254 | throws FileNotFoundException { |
| 255 | final ArchiveId id = ArchiveId.fromDocumentId(documentId); |
| 256 | if (mArchives.get(id.mArchiveUri) != null) { |
| 257 | return mArchives.get(id.mArchiveUri); |
| 258 | } |
| 259 | |
| 260 | final Cursor cursor = getContext().getContentResolver().query( |
| 261 | id.mArchiveUri, new String[] { Document.COLUMN_MIME_TYPE }, null, null, null); |
| 262 | cursor.moveToFirst(); |
| 263 | final String mimeType = cursor.getString(cursor.getColumnIndex( |
| 264 | Document.COLUMN_MIME_TYPE)); |
| 265 | Preconditions.checkArgument(isSupportedArchiveType(mimeType)); |
| 266 | final Uri notificationUri = cursor.getNotificationUri(); |
| 267 | final Loader loader = new Loader(getContext(), id.mArchiveUri, notificationUri); |
| 268 | |
| 269 | // Remove the instance from mArchives collection once the archive file changes. |
| 270 | if (notificationUri != null) { |
| 271 | final LruCache<Uri, Loader> finalArchives = mArchives; |
| 272 | getContext().getContentResolver().registerContentObserver(notificationUri, |
| 273 | false, |
| 274 | new ContentObserver(null) { |
| 275 | @Override |
| 276 | public void onChange(boolean selfChange, Uri uri) { |
| 277 | synchronized (mArchives) { |
| 278 | final Loader currentLoader = mArchives.get(id.mArchiveUri); |
| 279 | if (currentLoader == loader) { |
| 280 | mArchives.remove(id.mArchiveUri); |
| 281 | } |
| 282 | } |
| 283 | } |
| 284 | }); |
| 285 | } |
| 286 | |
| 287 | mArchives.put(id.mArchiveUri, loader); |
| 288 | return loader; |
| 289 | } |
Tomasz Mikolajewski | dbd6b8b | 2016-09-29 15:27:37 +0900 | [diff] [blame] | 290 | } |