• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.support.provider;
18 
19 import android.content.res.AssetFileDescriptor;
20 import android.content.res.Configuration;
21 import android.database.ContentObserver;
22 import android.database.Cursor;
23 import android.database.MatrixCursor;
24 import android.graphics.Point;
25 import android.net.Uri;
26 import android.os.CancellationSignal;
27 import android.os.ParcelFileDescriptor;
28 import android.provider.DocumentsContract.Document;
29 import android.provider.DocumentsProvider;
30 import android.support.annotation.Nullable;
31 import android.util.Log;
32 import android.util.LruCache;
33 
34 import java.io.Closeable;
35 import java.io.File;
36 import java.io.FileNotFoundException;
37 import java.io.IOException;
38 import java.util.HashMap;
39 import java.util.Map;
40 import java.util.concurrent.Callable;
41 import java.util.concurrent.locks.Lock;
42 import java.util.concurrent.locks.ReadWriteLock;
43 import java.util.concurrent.locks.ReentrantReadWriteLock;
44 
45 /**
46  * Provides basic implementation for creating, extracting and accessing
47  * files within archives exposed by a document provider.
48  *
49  * <p>This class is thread safe. All methods can be called on any thread without
50  * synchronization.
51  *
52  * TODO: Update the documentation. b/26047732
53  * @hide
54  */
55 public class DocumentArchiveHelper implements Closeable {
56     /**
57      * Cursor column to be used for passing the local file path for documents.
58      * If it's not specified, then a snapshot will be created, which is slower
59      * and consumes more resources.
60      *
61      * <p>Type: STRING
62      */
63     public static final String COLUMN_LOCAL_FILE_PATH = "local_file_path";
64 
65     private static final String TAG = "DocumentArchiveHelper";
66     private static final int OPENED_ARCHIVES_CACHE_SIZE = 4;
67     private static final String[] ZIP_MIME_TYPES = {
68             "application/zip", "application/x-zip", "application/x-zip-compressed"
69     };
70 
71     private final DocumentsProvider mProvider;
72     private final char mIdDelimiter;
73 
74     // @GuardedBy("mArchives")
75     private final LruCache<String, Loader> mArchives =
76             new LruCache<String, Loader>(OPENED_ARCHIVES_CACHE_SIZE) {
77                 @Override
78                 public void entryRemoved(boolean evicted, String key,
79                         Loader oldValue, Loader newValue) {
80                     oldValue.getWriteLock().lock();
81                     try {
82                         oldValue.get().close();
83                     } catch (FileNotFoundException e) {
84                         Log.e(TAG, "Failed to close an archive as it no longer exists.");
85                     } finally {
86                         oldValue.getWriteLock().unlock();
87                     }
88                 }
89             };
90 
91     /**
92      * Creates a helper for handling archived documents.
93      *
94      * @param provider Instance of a documents provider which provides archived documents.
95      * @param idDelimiter A character used to create document IDs within archives. Can be any
96      *            character which is not used in any other document ID. If your provider uses
97      *            numbers as document IDs, the delimiter can be eg. a colon. However if your
98      *            provider uses paths, then a delimiter can be any character not allowed in the
99      *            path, which is often \0.
100      */
DocumentArchiveHelper(DocumentsProvider provider, char idDelimiter)101     public DocumentArchiveHelper(DocumentsProvider provider, char idDelimiter) {
102         mProvider = provider;
103         mIdDelimiter = idDelimiter;
104     }
105 
106     /**
107      * Lists child documents of an archive or a directory within an
108      * archive. Must be called only for archives with supported mime type,
109      * or for documents within archives.
110      *
111      * @see DocumentsProvider.queryChildDocuments(String, String[], String)
112      */
queryChildDocuments(String documentId, @Nullable String[] projection, @Nullable String sortOrder)113     public Cursor queryChildDocuments(String documentId, @Nullable String[] projection,
114             @Nullable String sortOrder)
115             throws FileNotFoundException {
116         Loader loader = null;
117         try {
118             loader = obtainInstance(documentId);
119             return loader.get().queryChildDocuments(documentId, projection, sortOrder);
120         } finally {
121             releaseInstance(loader);
122         }
123     }
124 
125     /**
126      * Returns a MIME type of a document within an archive.
127      *
128      * @see DocumentsProvider.getDocumentType(String)
129      */
getDocumentType(String documentId)130     public String getDocumentType(String documentId) throws FileNotFoundException {
131         Loader loader = null;
132         try {
133             loader = obtainInstance(documentId);
134             return loader.get().getDocumentType(documentId);
135         } finally {
136             releaseInstance(loader);
137         }
138     }
139 
140     /**
141      * Returns true if a document within an archive is a child or any descendant of the archive
142      * document or another document within the archive.
143      *
144      * @see DocumentsProvider.isChildDocument(String, String)
145      */
isChildDocument(String parentDocumentId, String documentId)146     public boolean isChildDocument(String parentDocumentId, String documentId) {
147         Loader loader = null;
148         try {
149             loader = obtainInstance(documentId);
150             return loader.get().isChildDocument(parentDocumentId, documentId);
151         } catch (FileNotFoundException e) {
152             throw new IllegalStateException(e);
153         } finally {
154             releaseInstance(loader);
155         }
156     }
157 
158     /**
159      * Returns metadata of a document within an archive.
160      *
161      * @see DocumentsProvider.queryDocument(String, String[])
162      */
queryDocument(String documentId, @Nullable String[] projection)163     public Cursor queryDocument(String documentId, @Nullable String[] projection)
164             throws FileNotFoundException {
165         Loader loader = null;
166         try {
167             loader = obtainInstance(documentId);
168             return loader.get().queryDocument(documentId, projection);
169         } finally {
170             releaseInstance(loader);
171         }
172     }
173 
174     /**
175      * Opens a file within an archive.
176      *
177      * @see DocumentsProvider.openDocument(String, String, CancellationSignal))
178      */
openDocument( String documentId, String mode, final CancellationSignal signal)179     public ParcelFileDescriptor openDocument(
180             String documentId, String mode, final CancellationSignal signal)
181             throws FileNotFoundException {
182         Loader loader = null;
183         try {
184             loader = obtainInstance(documentId);
185             return loader.get().openDocument(documentId, mode, signal);
186         } finally {
187             releaseInstance(loader);
188         }
189     }
190 
191     /**
192      * Opens a thumbnail of a file within an archive.
193      *
194      * @see DocumentsProvider.openDocumentThumbnail(String, Point, CancellationSignal))
195      */
openDocumentThumbnail( String documentId, Point sizeHint, final CancellationSignal signal)196     public AssetFileDescriptor openDocumentThumbnail(
197             String documentId, Point sizeHint, final CancellationSignal signal)
198             throws FileNotFoundException {
199         Loader loader = null;
200         try {
201             loader = obtainInstance(documentId);
202             return loader.get().openDocumentThumbnail(documentId, sizeHint, signal);
203         } finally {
204             releaseInstance(loader);
205         }
206     }
207 
208     /**
209      * Returns true if the passed document ID is for a document within an archive.
210      */
isArchivedDocument(String documentId)211     public boolean isArchivedDocument(String documentId) {
212         return ParsedDocumentId.hasPath(documentId, mIdDelimiter);
213     }
214 
215     /**
216      * Returns true if the passed mime type is supported by the helper.
217      */
isSupportedArchiveType(String mimeType)218     public boolean isSupportedArchiveType(String mimeType) {
219         for (final String zipMimeType : ZIP_MIME_TYPES) {
220             if (zipMimeType.equals(mimeType)) {
221                 return true;
222             }
223         }
224         return false;
225     }
226 
227     /**
228      * Closes the helper and disposes all existing archives. It will block until all ongoing
229      * operations on each opened archive are finished.
230      */
231     @Override
close()232     public void close() {
233         synchronized (mArchives) {
234             mArchives.evictAll();
235         }
236     }
237 
238     /**
239      * Releases resources for an archive with the specified document ID. It will block until all
240      * operations on the archive are finished. If not opened, the method does nothing.
241      *
242      * <p>Calling this method is optional. The helper automatically closes the least recently used
243      * archives if too many archives are opened.
244      *
245      * @param archiveDocumentId ID of the archive file.
246      */
closeArchive(String documentId)247     public void closeArchive(String documentId) {
248         synchronized (mArchives) {
249             mArchives.remove(documentId);
250         }
251     }
252 
obtainInstance(String documentId)253     private Loader obtainInstance(String documentId) throws FileNotFoundException {
254         Loader loader;
255         synchronized (mArchives) {
256             loader = getInstanceUncheckedLocked(documentId);
257             loader.getReadLock().lock();
258         }
259         return loader;
260     }
261 
releaseInstance(@ullable Loader loader)262     private void releaseInstance(@Nullable Loader loader) {
263         if (loader != null) {
264             loader.getReadLock().unlock();
265         }
266     }
267 
getInstanceUncheckedLocked(String documentId)268     private Loader getInstanceUncheckedLocked(String documentId)
269             throws FileNotFoundException {
270         try {
271             final ParsedDocumentId id = ParsedDocumentId.fromDocumentId(documentId, mIdDelimiter);
272             if (mArchives.get(id.mArchiveId) != null) {
273                 return mArchives.get(id.mArchiveId);
274             }
275 
276             final Cursor cursor = mProvider.queryDocument(id.mArchiveId, new String[]
277                     { Document.COLUMN_MIME_TYPE, COLUMN_LOCAL_FILE_PATH });
278             cursor.moveToFirst();
279             final String mimeType = cursor.getString(cursor.getColumnIndex(
280                     Document.COLUMN_MIME_TYPE));
281             Preconditions.checkArgument(isSupportedArchiveType(mimeType),
282                     "Unsupported archive type.");
283             final int columnIndex = cursor.getColumnIndex(COLUMN_LOCAL_FILE_PATH);
284             final String localFilePath = columnIndex != -1 ? cursor.getString(columnIndex) : null;
285             final File localFile = localFilePath != null ? new File(localFilePath) : null;
286             final Uri notificationUri = cursor.getNotificationUri();
287             final Loader loader = new Loader(mProvider, localFile, id, mIdDelimiter,
288                     notificationUri);
289 
290             // Remove the instance from mArchives collection once the archive file changes.
291             if (notificationUri != null) {
292                 final LruCache<String, Loader> finalArchives = mArchives;
293                 mProvider.getContext().getContentResolver().registerContentObserver(notificationUri,
294                         false,
295                         new ContentObserver(null) {
296                             @Override
297                             public void onChange(boolean selfChange, Uri uri) {
298                                 synchronized (mArchives) {
299                                     final Loader currentLoader = mArchives.get(id.mArchiveId);
300                                     if (currentLoader == loader) {
301                                         mArchives.remove(id.mArchiveId);
302                                     }
303                                 }
304                             }
305                         });
306             }
307 
308             mArchives.put(id.mArchiveId, loader);
309             return loader;
310         } catch (IOException e) {
311             // DocumentsProvider doesn't use IOException. For consistency convert it to
312             // IllegalStateException.
313             throw new IllegalStateException(e);
314         }
315     }
316 
317     /**
318      * Loads an instance of DocumentArchive lazily.
319      */
320     private static final class Loader {
321         private final DocumentsProvider mProvider;
322         private final File mLocalFile;
323         private final ParsedDocumentId mId;
324         private final char mIdDelimiter;
325         private final Uri mNotificationUri;
326         private final ReentrantReadWriteLock mLock = new ReentrantReadWriteLock();
327         private DocumentArchive mArchive = null;
328 
Loader(DocumentsProvider provider, @Nullable File localFile, ParsedDocumentId id, char idDelimiter, Uri notificationUri)329         Loader(DocumentsProvider provider, @Nullable File localFile, ParsedDocumentId id,
330                 char idDelimiter, Uri notificationUri) {
331             this.mProvider = provider;
332             this.mLocalFile = localFile;
333             this.mId = id;
334             this.mIdDelimiter = idDelimiter;
335             this.mNotificationUri = notificationUri;
336         }
337 
get()338         synchronized DocumentArchive get() throws FileNotFoundException {
339             if (mArchive != null) {
340                 return mArchive;
341             }
342 
343             try {
344                 if (mLocalFile != null) {
345                     mArchive = DocumentArchive.createForLocalFile(
346                             mProvider.getContext(), mLocalFile, mId.mArchiveId, mIdDelimiter,
347                             mNotificationUri);
348                 } else {
349                     mArchive = DocumentArchive.createForParcelFileDescriptor(
350                             mProvider.getContext(),
351                             mProvider.openDocument(mId.mArchiveId, "r", null /* signal */),
352                             mId.mArchiveId, mIdDelimiter, mNotificationUri);
353                 }
354             } catch (IOException e) {
355                 throw new IllegalStateException(e);
356             }
357 
358             return mArchive;
359         }
360 
getReadLock()361         Lock getReadLock() {
362             return mLock.readLock();
363         }
364 
getWriteLock()365         Lock getWriteLock() {
366             return mLock.writeLock();
367         }
368     }
369 }
370