/*
* 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.
*
*
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 ZIP_MIME_TYPES = ArchiveRegistry.getSupportList();
@GuardedBy("mArchives")
private final Map 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);
}
}
}