| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.documentsui.archives; |
| |
| import android.content.ContentProviderClient; |
| import android.content.res.AssetFileDescriptor; |
| import android.database.Cursor; |
| import android.database.MatrixCursor; |
| import android.database.MatrixCursor.RowBuilder; |
| import android.graphics.Point; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.CancellationSignal; |
| import android.os.FileUtils; |
| import android.os.ParcelFileDescriptor; |
| import android.provider.DocumentsContract; |
| import android.provider.DocumentsContract.Document; |
| import android.provider.DocumentsContract.Root; |
| import android.provider.DocumentsProvider; |
| import android.util.Log; |
| |
| import androidx.annotation.GuardedBy; |
| import androidx.annotation.Nullable; |
| |
| import com.android.documentsui.R; |
| |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Set; |
| |
| /** |
| * Provides basic implementation for creating, extracting and accessing |
| * files within archives exposed by a document provider. |
| * |
| * <p>This class is thread safe. All methods can be called on any thread without |
| * synchronization. |
| */ |
| public class ArchivesProvider extends DocumentsProvider { |
| public static final String AUTHORITY = "com.android.documentsui.archives"; |
| |
| private static final String[] DEFAULT_ROOTS_PROJECTION = new String[]{ |
| Root.COLUMN_ROOT_ID, Root.COLUMN_DOCUMENT_ID, Root.COLUMN_TITLE, Root.COLUMN_FLAGS, |
| Root.COLUMN_ICON}; |
| private static final String TAG = "ArchivesProvider"; |
| private static final String METHOD_ACQUIRE_ARCHIVE = "acquireArchive"; |
| private static final String METHOD_RELEASE_ARCHIVE = "releaseArchive"; |
| private static final Set<String> ZIP_MIME_TYPES = ArchiveRegistry.getSupportList(); |
| |
| @GuardedBy("mArchives") |
| private final Map<Key, Loader> mArchives = new HashMap<>(); |
| |
| @Override |
| public Bundle call(String method, String arg, Bundle extras) { |
| if (METHOD_ACQUIRE_ARCHIVE.equals(method)) { |
| acquireArchive(arg); |
| return null; |
| } |
| |
| if (METHOD_RELEASE_ARCHIVE.equals(method)) { |
| releaseArchive(arg); |
| return null; |
| } |
| |
| return super.call(method, arg, extras); |
| } |
| |
| @Override |
| public boolean onCreate() { |
| return true; |
| } |
| |
| @Override |
| public Cursor queryRoots(String[] projection) { |
| // No roots provided. |
| return new MatrixCursor(projection != null ? projection : DEFAULT_ROOTS_PROJECTION); |
| } |
| |
| @Override |
| public Cursor queryChildDocuments(String documentId, @Nullable String[] projection, |
| @Nullable String sortOrder) |
| throws FileNotFoundException { |
| final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId); |
| final Loader loader = getLoaderOrThrow(documentId); |
| final int status = loader.getStatus(); |
| // If already loaded, then forward the request to the archive. |
| if (status == Loader.STATUS_OPENED) { |
| return loader.get().queryChildDocuments(documentId, projection, sortOrder); |
| } |
| |
| final MatrixCursor cursor = new MatrixCursor( |
| projection != null ? projection : Archive.DEFAULT_PROJECTION); |
| final Bundle bundle = new Bundle(); |
| |
| switch (status) { |
| case Loader.STATUS_OPENING: |
| bundle.putBoolean(DocumentsContract.EXTRA_LOADING, true); |
| break; |
| |
| case Loader.STATUS_FAILED: |
| // Return an empty cursor with EXTRA_LOADING, which shows spinner |
| // in DocumentsUI. Once the archive is loaded, the notification will |
| // be sent, and the directory reloaded. |
| bundle.putString(DocumentsContract.EXTRA_ERROR, |
| getContext().getString(R.string.archive_loading_failed)); |
| break; |
| } |
| |
| cursor.setExtras(bundle); |
| cursor.setNotificationUri(getContext().getContentResolver(), |
| buildUriForArchive(archiveId.mArchiveUri, archiveId.mAccessMode)); |
| return cursor; |
| } |
| |
| /** Overrides a hidden API. */ |
| public Cursor queryChildDocumentsForManage(String parentDocumentId, |
| @Nullable String[] projection, @Nullable String sortOrder) |
| throws FileNotFoundException { |
| // No special handling of Archives in managed mode. |
| return queryChildDocuments(parentDocumentId, projection, sortOrder); |
| } |
| |
| @Override |
| public String getDocumentType(String documentId) throws FileNotFoundException { |
| final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId); |
| if (archiveId.mPath.equals("/")) { |
| return Document.MIME_TYPE_DIR; |
| } |
| |
| final Loader loader = getLoaderOrThrow(documentId); |
| return loader.get().getDocumentType(documentId); |
| } |
| |
| @Override |
| public boolean isChildDocument(String parentDocumentId, String documentId) { |
| final Loader loader = getLoaderOrThrow(documentId); |
| return loader.get().isChildDocument(parentDocumentId, documentId); |
| } |
| |
| @Override |
| public @Nullable Bundle getDocumentMetadata(String documentId) |
| throws FileNotFoundException { |
| |
| final Archive archive = getLoaderOrThrow(documentId).get(); |
| final String mimeType = archive.getDocumentType(documentId); |
| |
| if (!MetadataReader.isSupportedMimeType(mimeType)) { |
| return null; |
| } |
| |
| InputStream stream = null; |
| try { |
| stream = new ParcelFileDescriptor.AutoCloseInputStream( |
| openDocument(documentId, "r", null)); |
| final Bundle metadata = new Bundle(); |
| MetadataReader.getMetadata(metadata, stream, mimeType, null); |
| return metadata; |
| } catch (IOException e) { |
| Log.e(TAG, "An error occurred retrieving the metadata.", e); |
| return null; |
| } finally { |
| FileUtils.closeQuietly(stream); |
| } |
| } |
| |
| @Override |
| public Cursor queryDocument(String documentId, @Nullable String[] projection) |
| throws FileNotFoundException { |
| final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId); |
| if (archiveId.mPath.equals("/")) { |
| try (final Cursor archiveCursor = getContext().getContentResolver().query( |
| archiveId.mArchiveUri, |
| new String[]{Document.COLUMN_DISPLAY_NAME}, |
| null, null, null, null)) { |
| if (archiveCursor == null || !archiveCursor.moveToFirst()) { |
| throw new FileNotFoundException( |
| "Cannot resolve display name of the archive."); |
| } |
| final String displayName = archiveCursor.getString( |
| archiveCursor.getColumnIndex(Document.COLUMN_DISPLAY_NAME)); |
| |
| final MatrixCursor cursor = new MatrixCursor( |
| projection != null ? projection : Archive.DEFAULT_PROJECTION); |
| final RowBuilder row = cursor.newRow(); |
| row.add(Document.COLUMN_DOCUMENT_ID, documentId); |
| row.add(Document.COLUMN_DISPLAY_NAME, displayName); |
| row.add(Document.COLUMN_SIZE, 0); |
| row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); |
| return cursor; |
| } |
| } |
| |
| final Loader loader = getLoaderOrThrow(documentId); |
| return loader.get().queryDocument(documentId, projection); |
| } |
| |
| @Override |
| public String createDocument( |
| String parentDocumentId, String mimeType, String displayName) |
| throws FileNotFoundException { |
| final Loader loader = getLoaderOrThrow(parentDocumentId); |
| return loader.get().createDocument(parentDocumentId, mimeType, displayName); |
| } |
| |
| @Override |
| public ParcelFileDescriptor openDocument( |
| String documentId, String mode, final CancellationSignal signal) |
| throws FileNotFoundException { |
| final Loader loader = getLoaderOrThrow(documentId); |
| return loader.get().openDocument(documentId, mode, signal); |
| } |
| |
| @Override |
| public AssetFileDescriptor openDocumentThumbnail( |
| String documentId, Point sizeHint, final CancellationSignal signal) |
| throws FileNotFoundException { |
| final Loader loader = getLoaderOrThrow(documentId); |
| return loader.get().openDocumentThumbnail(documentId, sizeHint, signal); |
| } |
| |
| /** |
| * Returns true if the passed mime type is supported by the helper. |
| */ |
| public static boolean isSupportedArchiveType(String mimeType) { |
| for (final String zipMimeType : ZIP_MIME_TYPES) { |
| if (zipMimeType.equals(mimeType)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Creates a Uri for accessing an archive with the specified access mode. |
| * |
| * @see ParcelFileDescriptor#MODE_READ |
| * @see ParcelFileDescriptor#MODE_WRITE |
| */ |
| public static Uri buildUriForArchive(Uri externalUri, int accessMode) { |
| return DocumentsContract.buildDocumentUri(AUTHORITY, |
| new ArchiveId(externalUri, accessMode, "/").toDocumentId()); |
| } |
| |
| /** |
| * Acquires an archive. |
| */ |
| public static void acquireArchive(ContentProviderClient client, Uri archiveUri) { |
| Archive.MorePreconditions.checkArgumentEquals(AUTHORITY, archiveUri.getAuthority(), |
| "Mismatching authority. Expected: %s, actual: %s."); |
| final String documentId = DocumentsContract.getDocumentId(archiveUri); |
| |
| try { |
| client.call(METHOD_ACQUIRE_ARCHIVE, documentId, null); |
| } catch (Exception e) { |
| Log.w(TAG, "Failed to acquire archive.", e); |
| } |
| } |
| |
| /** |
| * Releases an archive. |
| */ |
| public static void releaseArchive(ContentProviderClient client, Uri archiveUri) { |
| Archive.MorePreconditions.checkArgumentEquals(AUTHORITY, archiveUri.getAuthority(), |
| "Mismatching authority. Expected: %s, actual: %s."); |
| final String documentId = DocumentsContract.getDocumentId(archiveUri); |
| |
| try { |
| client.call(METHOD_RELEASE_ARCHIVE, documentId, null); |
| } catch (Exception e) { |
| Log.w(TAG, "Failed to release archive.", e); |
| } |
| } |
| |
| /** |
| * The archive won't close until all clients release it. |
| */ |
| private void acquireArchive(String documentId) { |
| final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId); |
| synchronized (mArchives) { |
| final Key key = Key.fromArchiveId(archiveId); |
| Loader loader = mArchives.get(key); |
| if (loader == null) { |
| // TODO: Pass parent Uri so the loader can acquire the parent's notification Uri. |
| loader = new Loader(getContext(), archiveId.mArchiveUri, archiveId.mAccessMode, |
| null); |
| mArchives.put(key, loader); |
| } |
| loader.acquire(); |
| mArchives.put(key, loader); |
| } |
| } |
| |
| /** |
| * If all clients release the archive, then it will be closed. |
| */ |
| private void releaseArchive(String documentId) { |
| final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId); |
| final Key key = Key.fromArchiveId(archiveId); |
| synchronized (mArchives) { |
| final Loader loader = mArchives.get(key); |
| loader.release(); |
| final int status = loader.getStatus(); |
| if (status == Loader.STATUS_CLOSED || status == Loader.STATUS_CLOSING) { |
| mArchives.remove(key); |
| } |
| } |
| } |
| |
| private Loader getLoaderOrThrow(String documentId) { |
| final ArchiveId id = ArchiveId.fromDocumentId(documentId); |
| final Key key = Key.fromArchiveId(id); |
| synchronized (mArchives) { |
| final Loader loader = mArchives.get(key); |
| if (loader == null) { |
| throw new IllegalStateException("Archive not acquired."); |
| } |
| return loader; |
| } |
| } |
| |
| private static class Key { |
| Uri archiveUri; |
| int accessMode; |
| |
| public Key(Uri archiveUri, int accessMode) { |
| this.archiveUri = archiveUri; |
| this.accessMode = accessMode; |
| } |
| |
| public static Key fromArchiveId(ArchiveId id) { |
| return new Key(id.mArchiveUri, id.mAccessMode); |
| } |
| |
| @Override |
| public boolean equals(Object other) { |
| if (other == null) { |
| return false; |
| } |
| if (!(other instanceof Key)) { |
| return false; |
| } |
| return archiveUri.equals(((Key) other).archiveUri) && |
| accessMode == ((Key) other).accessMode; |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(archiveUri, accessMode); |
| } |
| } |
| } |