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