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 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     /** Overrides a hidden API. */
queryChildDocumentsForManage(String parentDocumentId, @Nullable String[] projection, @Nullable String sortOrder)133     public Cursor queryChildDocumentsForManage(String parentDocumentId,
134             @Nullable String[] projection, @Nullable String sortOrder)
135             throws FileNotFoundException {
136         // No special handling of Archives in managed mode.
137         return queryChildDocuments(parentDocumentId, projection, sortOrder);
138     }
139 
140     @Override
getDocumentType(String documentId)141     public String getDocumentType(String documentId) throws FileNotFoundException {
142         final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId);
143         if (archiveId.mPath.equals("/")) {
144             return Document.MIME_TYPE_DIR;
145         }
146 
147         final Loader loader = getLoaderOrThrow(documentId);
148         return loader.get().getDocumentType(documentId);
149     }
150 
151     @Override
isChildDocument(String parentDocumentId, String documentId)152     public boolean isChildDocument(String parentDocumentId, String documentId) {
153         final Loader loader = getLoaderOrThrow(documentId);
154         return loader.get().isChildDocument(parentDocumentId, documentId);
155     }
156 
157     @Override
getDocumentMetadata(String documentId)158     public @Nullable Bundle getDocumentMetadata(String documentId)
159             throws FileNotFoundException {
160 
161         final Archive archive = getLoaderOrThrow(documentId).get();
162         final String mimeType = archive.getDocumentType(documentId);
163 
164         if (!MetadataReader.isSupportedMimeType(mimeType)) {
165             return null;
166         }
167 
168         InputStream stream = null;
169         try {
170             stream = new ParcelFileDescriptor.AutoCloseInputStream(
171                     openDocument(documentId, "r", null));
172             final Bundle metadata = new Bundle();
173             MetadataReader.getMetadata(metadata, stream, mimeType, null);
174             return metadata;
175         } catch (IOException e) {
176             Log.e(TAG, "An error occurred retrieving the metadata.", e);
177             return null;
178         } finally {
179             FileUtils.closeQuietly(stream);
180         }
181     }
182 
183     @Override
queryDocument(String documentId, @Nullable String[] projection)184     public Cursor queryDocument(String documentId, @Nullable String[] projection)
185             throws FileNotFoundException {
186         final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId);
187         if (archiveId.mPath.equals("/")) {
188             try (final Cursor archiveCursor = getContext().getContentResolver().query(
189                     archiveId.mArchiveUri,
190                     new String[]{Document.COLUMN_DISPLAY_NAME},
191                     null, null, null, null)) {
192                 if (archiveCursor == null || !archiveCursor.moveToFirst()) {
193                     throw new FileNotFoundException(
194                             "Cannot resolve display name of the archive.");
195                 }
196                 final String displayName = archiveCursor.getString(
197                         archiveCursor.getColumnIndex(Document.COLUMN_DISPLAY_NAME));
198 
199                 final MatrixCursor cursor = new MatrixCursor(
200                         projection != null ? projection : Archive.DEFAULT_PROJECTION);
201                 final RowBuilder row = cursor.newRow();
202                 row.add(Document.COLUMN_DOCUMENT_ID, documentId);
203                 row.add(Document.COLUMN_DISPLAY_NAME, displayName);
204                 row.add(Document.COLUMN_SIZE, 0);
205                 row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
206                 return cursor;
207             }
208         }
209 
210         final Loader loader = getLoaderOrThrow(documentId);
211         return loader.get().queryDocument(documentId, projection);
212     }
213 
214     @Override
createDocument( String parentDocumentId, String mimeType, String displayName)215     public String createDocument(
216             String parentDocumentId, String mimeType, String displayName)
217             throws FileNotFoundException {
218         final Loader loader = getLoaderOrThrow(parentDocumentId);
219         return loader.get().createDocument(parentDocumentId, mimeType, displayName);
220     }
221 
222     @Override
openDocument( String documentId, String mode, final CancellationSignal signal)223     public ParcelFileDescriptor openDocument(
224             String documentId, String mode, final CancellationSignal signal)
225             throws FileNotFoundException {
226         final Loader loader = getLoaderOrThrow(documentId);
227         return loader.get().openDocument(documentId, mode, signal);
228     }
229 
230     @Override
openDocumentThumbnail( String documentId, Point sizeHint, final CancellationSignal signal)231     public AssetFileDescriptor openDocumentThumbnail(
232             String documentId, Point sizeHint, final CancellationSignal signal)
233             throws FileNotFoundException {
234         final Loader loader = getLoaderOrThrow(documentId);
235         return loader.get().openDocumentThumbnail(documentId, sizeHint, signal);
236     }
237 
238     /**
239      * Returns true if the passed mime type is supported by the helper.
240      */
isSupportedArchiveType(String mimeType)241     public static boolean isSupportedArchiveType(String mimeType) {
242         for (final String zipMimeType : ZIP_MIME_TYPES) {
243             if (zipMimeType.equals(mimeType)) {
244                 return true;
245             }
246         }
247         return false;
248     }
249 
250     /**
251      * Creates a Uri for accessing an archive with the specified access mode.
252      *
253      * @see ParcelFileDescriptor#MODE_READ
254      * @see ParcelFileDescriptor#MODE_WRITE
255      */
buildUriForArchive(Uri externalUri, int accessMode)256     public static Uri buildUriForArchive(Uri externalUri, int accessMode) {
257         return DocumentsContract.buildDocumentUri(AUTHORITY,
258                 new ArchiveId(externalUri, accessMode, "/").toDocumentId());
259     }
260 
261     /**
262      * Acquires an archive.
263      */
acquireArchive(ContentProviderClient client, Uri archiveUri)264     public static void acquireArchive(ContentProviderClient client, Uri archiveUri) {
265         Archive.MorePreconditions.checkArgumentEquals(AUTHORITY, archiveUri.getAuthority(),
266                 "Mismatching authority. Expected: %s, actual: %s.");
267         final String documentId = DocumentsContract.getDocumentId(archiveUri);
268 
269         try {
270             client.call(METHOD_ACQUIRE_ARCHIVE, documentId, null);
271         } catch (Exception e) {
272             Log.w(TAG, "Failed to acquire archive.", e);
273         }
274     }
275 
276     /**
277      * Releases an archive.
278      */
releaseArchive(ContentProviderClient client, Uri archiveUri)279     public static void releaseArchive(ContentProviderClient client, Uri archiveUri) {
280         Archive.MorePreconditions.checkArgumentEquals(AUTHORITY, archiveUri.getAuthority(),
281                 "Mismatching authority. Expected: %s, actual: %s.");
282         final String documentId = DocumentsContract.getDocumentId(archiveUri);
283 
284         try {
285             client.call(METHOD_RELEASE_ARCHIVE, documentId, null);
286         } catch (Exception e) {
287             Log.w(TAG, "Failed to release archive.", e);
288         }
289     }
290 
291     /**
292      * The archive won't close until all clients release it.
293      */
acquireArchive(String documentId)294     private void acquireArchive(String documentId) {
295         final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId);
296         synchronized (mArchives) {
297             final Key key = Key.fromArchiveId(archiveId);
298             Loader loader = mArchives.get(key);
299             if (loader == null) {
300                 // TODO: Pass parent Uri so the loader can acquire the parent's notification Uri.
301                 loader = new Loader(getContext(), archiveId.mArchiveUri, archiveId.mAccessMode,
302                         null);
303                 mArchives.put(key, loader);
304             }
305             loader.acquire();
306             mArchives.put(key, loader);
307         }
308     }
309 
310     /**
311      * If all clients release the archive, then it will be closed.
312      */
releaseArchive(String documentId)313     private void releaseArchive(String documentId) {
314         final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId);
315         final Key key = Key.fromArchiveId(archiveId);
316         synchronized (mArchives) {
317             final Loader loader = mArchives.get(key);
318             loader.release();
319             final int status = loader.getStatus();
320             if (status == Loader.STATUS_CLOSED || status == Loader.STATUS_CLOSING) {
321                 mArchives.remove(key);
322             }
323         }
324     }
325 
getLoaderOrThrow(String documentId)326     private Loader getLoaderOrThrow(String documentId) {
327         final ArchiveId id = ArchiveId.fromDocumentId(documentId);
328         final Key key = Key.fromArchiveId(id);
329         synchronized (mArchives) {
330             final Loader loader = mArchives.get(key);
331             if (loader == null) {
332                 throw new IllegalStateException("Archive not acquired.");
333             }
334             return loader;
335         }
336     }
337 
338     private static class Key {
339         Uri archiveUri;
340         int accessMode;
341 
Key(Uri archiveUri, int accessMode)342         public Key(Uri archiveUri, int accessMode) {
343             this.archiveUri = archiveUri;
344             this.accessMode = accessMode;
345         }
346 
fromArchiveId(ArchiveId id)347         public static Key fromArchiveId(ArchiveId id) {
348             return new Key(id.mArchiveUri, id.mAccessMode);
349         }
350 
351         @Override
equals(Object other)352         public boolean equals(Object other) {
353             if (other == null) {
354                 return false;
355             }
356             if (!(other instanceof Key)) {
357                 return false;
358             }
359             return archiveUri.equals(((Key) other).archiveUri) &&
360                     accessMode == ((Key) other).accessMode;
361         }
362 
363         @Override
hashCode()364         public int hashCode() {
365             return Objects.hash(archiveUri, accessMode);
366         }
367     }
368 }
369