blob: fb96337e8a4c7fe2fd323c5cfc3365f7279c6202 [file] [log] [blame]
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +09001/*
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
17package com.android.documentsui.archives;
18
19import android.content.Context;
20import android.content.res.AssetFileDescriptor;
21import android.content.res.Configuration;
22import android.database.ContentObserver;
23import android.database.Cursor;
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090024import android.database.MatrixCursor.RowBuilder;
Tomasz Mikolajewski5a9d1002016-10-19 15:00:40 +090025import android.database.MatrixCursor;
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090026import android.graphics.Point;
27import android.net.Uri;
Tomasz Mikolajewski5a9d1002016-10-19 15:00:40 +090028import android.os.Bundle;
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090029import android.os.CancellationSignal;
30import android.os.ParcelFileDescriptor;
31import android.provider.DocumentsContract.Document;
32import android.provider.DocumentsContract;
33import android.provider.DocumentsProvider;
34import android.support.annotation.Nullable;
35import android.util.Log;
36import android.util.LruCache;
37
38import com.android.internal.annotations.GuardedBy;
39import com.android.internal.util.Preconditions;
40
41import java.io.Closeable;
42import java.io.File;
43import java.io.FileNotFoundException;
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090044import java.util.HashMap;
45import java.util.Map;
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090046import java.util.concurrent.locks.Lock;
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090047
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 */
55public 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 Mikolajewski5a9d1002016-10-19 15:00:40 +090095 final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId);
Tomasz Mikolajewskidbd6b8b2016-09-29 15:27:37 +090096 Loader loader = null;
97 try {
98 loader = obtainInstance(documentId);
Tomasz Mikolajewski5a9d1002016-10-19 15:00:40 +090099 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 Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900112 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 Mikolajewski1794d442016-10-26 15:18:59 +0900152 try (final Cursor archiveCursor = getContext().getContentResolver().query(
153 archiveId.mArchiveUri,
154 new String[] { Document.COLUMN_DISPLAY_NAME },
155 null, null, null, null)) {
Tomasz Mikolajewskia3527112016-10-27 11:28:14 +0900156 if (archiveCursor == null || !archiveCursor.moveToFirst()) {
Tomasz Mikolajewski1794d442016-10-26 15:18:59 +0900157 throw new FileNotFoundException(
158 "Cannot resolve display name of the archive.");
159 }
Tomasz Mikolajewski1794d442016-10-26 15:18:59 +0900160 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 Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900172 }
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 Mikolajewskidbd6b8b2016-09-29 15:27:37 +0900290}