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