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