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