/* * 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 android.support.provider; import android.content.Context; import android.content.res.AssetFileDescriptor; import android.database.Cursor; import android.database.MatrixCursor; import android.graphics.Point; import android.net.Uri; import android.os.CancellationSignal; import android.os.OperationCanceledException; import android.os.ParcelFileDescriptor; import android.provider.DocumentsContract.Document; import android.provider.DocumentsProvider; import android.support.annotation.Nullable; import android.util.Log; import android.webkit.MimeTypeMap; import java.io.Closeable; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.lang.IllegalArgumentException; import java.lang.IllegalStateException; import java.lang.UnsupportedOperationException; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; /** * Provides basic implementation for creating, extracting and accessing * files within archives exposed by a document provider. The id delimiter * must be a character which is not used in document ids generated by the * document provider. * *
This class is thread safe.
*
* @hide
*/
public class DocumentArchive implements Closeable {
private static final String TAG = "DocumentArchive";
private static final String[] DEFAULT_PROJECTION = new String[] {
Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_MIME_TYPE,
Document.COLUMN_SIZE,
Document.COLUMN_FLAGS
};
private final Context mContext;
private final String mDocumentId;
private final char mIdDelimiter;
private final Uri mNotificationUri;
private final ZipFile mZipFile;
private final ExecutorService mExecutor;
private final Map Note, that this method should be used only if the document does not exist
* on the local storage. A snapshot file will be created, which may be slower
* and consume significant resources, in contrast to using
* {@see createForLocalFile(Context, File, String, char, Uri}.
*
* @param context Context of the provider.
* @param descriptor File descriptor for the archive's contents.
* @param documentId ID of the archive document.
* @param idDelimiter Delimiter for constructing IDs of documents within the archive.
* The delimiter must never be used for IDs of other documents.
* @param Uri notificationUri Uri for notifying that the archive file has changed.
* @see createForLocalFile(Context, File, String, char, Uri)
*/
public static DocumentArchive createForParcelFileDescriptor(
Context context, ParcelFileDescriptor descriptor, String documentId,
char idDelimiter, @Nullable Uri notificationUri)
throws IOException {
File snapshotFile = null;
try {
// Create a copy of the archive, as ZipFile doesn't operate on streams.
// Moreover, ZipInputStream would be inefficient for large files on
// pipes.
snapshotFile = File.createTempFile("android.support.provider.snapshot{",
"}.zip", context.getCacheDir());
try (
final FileOutputStream outputStream =
new ParcelFileDescriptor.AutoCloseOutputStream(
ParcelFileDescriptor.open(
snapshotFile, ParcelFileDescriptor.MODE_WRITE_ONLY));
final ParcelFileDescriptor.AutoCloseInputStream inputStream =
new ParcelFileDescriptor.AutoCloseInputStream(descriptor);
) {
final byte[] buffer = new byte[32 * 1024];
int bytes;
while ((bytes = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytes);
}
outputStream.flush();
return new DocumentArchive(context, snapshotFile, documentId, idDelimiter,
notificationUri);
}
} finally {
// On UNIX the file will be still available for processes which opened it, even
// after deleting it. Remove it ASAP, as it won't be used by anyone else.
if (snapshotFile != null) {
snapshotFile.delete();
}
}
}
/**
* Lists child documents of an archive or a directory within an
* archive. Must be called only for archives with supported mime type,
* or for documents within archives.
*
* @see DocumentsProvider.queryChildDocuments(String, String[], String)
*/
public Cursor queryChildDocuments(String documentId, @Nullable String[] projection,
@Nullable String sortOrder) throws FileNotFoundException {
final ParsedDocumentId parsedParentId = ParsedDocumentId.fromDocumentId(
documentId, mIdDelimiter);
Preconditions.checkArgumentEquals(mDocumentId, parsedParentId.mArchiveId,
"Mismatching document ID. Expected: %s, actual: %s.");
final String parentPath = parsedParentId.mPath != null ? parsedParentId.mPath : "/";
final MatrixCursor result = new MatrixCursor(
projection != null ? projection : DEFAULT_PROJECTION);
if (mNotificationUri != null) {
result.setNotificationUri(mContext.getContentResolver(), mNotificationUri);
}
final List This method does not block until shutdown. Once called, other methods should not be
* called.
*/
@Override
public void close() {
mExecutor.execute(new Runnable() {
@Override
public void run() {
IoUtils.closeQuietly(mZipFile);
}
});
mExecutor.shutdown();
}
private void addCursorRow(MatrixCursor cursor, ZipEntry entry) {
final MatrixCursor.RowBuilder row = cursor.newRow();
final ParsedDocumentId parsedId = new ParsedDocumentId(mDocumentId, entry.getName());
row.add(Document.COLUMN_DOCUMENT_ID, parsedId.toDocumentId(mIdDelimiter));
final File file = new File(entry.getName());
row.add(Document.COLUMN_DISPLAY_NAME, file.getName());
row.add(Document.COLUMN_SIZE, entry.getSize());
final String mimeType = getMimeTypeForEntry(entry);
row.add(Document.COLUMN_MIME_TYPE, mimeType);
final int flags = mimeType.startsWith("image/") ? Document.FLAG_SUPPORTS_THUMBNAIL : 0;
row.add(Document.COLUMN_FLAGS, flags);
}
private String getMimeTypeForEntry(ZipEntry entry) {
if (entry.isDirectory()) {
return Document.MIME_TYPE_DIR;
}
final int lastDot = entry.getName().lastIndexOf('.');
if (lastDot >= 0) {
final String extension = entry.getName().substring(lastDot + 1).toLowerCase();
final String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mimeType != null) {
return mimeType;
}
}
return "application/octet-stream";
}
};