blob: 7c3d773a688ad220cabeed251ef029ea21abc666 [file] [log] [blame]
/*
* Copyright (C) 2013 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.providers.media;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MatrixCursor.RowBuilder;
import android.graphics.BitmapFactory;
import android.graphics.Point;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.provider.BaseColumns;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
import android.provider.DocumentsProvider;
import android.provider.MediaStore;
import android.provider.MediaStore.Audio;
import android.provider.MediaStore.Audio.AlbumColumns;
import android.provider.MediaStore.Audio.Albums;
import android.provider.MediaStore.Audio.ArtistColumns;
import android.provider.MediaStore.Audio.Artists;
import android.provider.MediaStore.Audio.AudioColumns;
import android.provider.MediaStore.Files.FileColumns;
import android.provider.MediaStore.Images;
import android.provider.MediaStore.Images.ImageColumns;
import android.provider.MediaStore.Video;
import android.provider.MediaStore.Video.VideoColumns;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
import libcore.io.IoUtils;
import java.io.File;
import java.io.FileNotFoundException;
/**
* Presents a {@link DocumentsContract} view of {@link MediaProvider} external
* contents.
*/
public class MediaDocumentsProvider extends DocumentsProvider {
private static final String TAG = "MediaDocumentsProvider";
private static final String AUTHORITY = "com.android.providers.media.documents";
private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON,
Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID, Root.COLUMN_MIME_TYPES
};
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
};
private static final String IMAGE_MIME_TYPES = joinNewline("image/*");
private static final String VIDEO_MIME_TYPES = joinNewline("video/*");
private static final String AUDIO_MIME_TYPES = joinNewline(
"audio/*", "application/ogg", "application/x-flac");
private static final String TYPE_IMAGES_ROOT = "images_root";
private static final String TYPE_IMAGES_BUCKET = "images_bucket";
private static final String TYPE_IMAGE = "image";
private static final String TYPE_VIDEOS_ROOT = "videos_root";
private static final String TYPE_VIDEOS_BUCKET = "videos_bucket";
private static final String TYPE_VIDEO = "video";
private static final String TYPE_AUDIO_ROOT = "audio_root";
private static final String TYPE_AUDIO = "audio";
private static final String TYPE_ARTIST = "artist";
private static final String TYPE_ALBUM = "album";
private static boolean sReturnedImagesEmpty = false;
private static boolean sReturnedVideosEmpty = false;
private static boolean sReturnedAudioEmpty = false;
private static String joinNewline(String... args) {
return TextUtils.join("\n", args);
}
private void copyNotificationUri(MatrixCursor result, Cursor cursor) {
result.setNotificationUri(getContext().getContentResolver(), cursor.getNotificationUri());
}
@Override
public boolean onCreate() {
return true;
}
private static void notifyRootsChanged(Context context) {
context.getContentResolver()
.notifyChange(DocumentsContract.buildRootsUri(AUTHORITY), null, false);
}
/**
* When inserting the first item of each type, we need to trigger a roots
* refresh to clear a previously reported {@link Root#FLAG_EMPTY}.
*/
static void onMediaStoreInsert(Context context, String volumeName, int type, long id) {
if (!"external".equals(volumeName)) return;
if (type == FileColumns.MEDIA_TYPE_IMAGE && sReturnedImagesEmpty) {
sReturnedImagesEmpty = false;
notifyRootsChanged(context);
} else if (type == FileColumns.MEDIA_TYPE_VIDEO && sReturnedVideosEmpty) {
sReturnedVideosEmpty = false;
notifyRootsChanged(context);
} else if (type == FileColumns.MEDIA_TYPE_AUDIO && sReturnedAudioEmpty) {
sReturnedAudioEmpty = false;
notifyRootsChanged(context);
}
}
/**
* When deleting an item, we need to revoke any outstanding Uri grants.
*/
static void onMediaStoreDelete(Context context, String volumeName, int type, long id) {
if (!"external".equals(volumeName)) return;
if (type == FileColumns.MEDIA_TYPE_IMAGE) {
final Uri uri = DocumentsContract.buildDocumentUri(
AUTHORITY, getDocIdForIdent(TYPE_IMAGE, id));
context.revokeUriPermission(uri, ~0);
} else if (type == FileColumns.MEDIA_TYPE_VIDEO) {
final Uri uri = DocumentsContract.buildDocumentUri(
AUTHORITY, getDocIdForIdent(TYPE_VIDEO, id));
context.revokeUriPermission(uri, ~0);
} else if (type == FileColumns.MEDIA_TYPE_AUDIO) {
final Uri uri = DocumentsContract.buildDocumentUri(
AUTHORITY, getDocIdForIdent(TYPE_AUDIO, id));
context.revokeUriPermission(uri, ~0);
}
}
private static class Ident {
public String type;
public long id;
}
private static Ident getIdentForDocId(String docId) {
final Ident ident = new Ident();
final int split = docId.indexOf(':');
if (split == -1) {
ident.type = docId;
ident.id = -1;
} else {
ident.type = docId.substring(0, split);
ident.id = Long.parseLong(docId.substring(split + 1));
}
return ident;
}
private static String getDocIdForIdent(String type, long id) {
return type + ":" + id;
}
private static String[] resolveRootProjection(String[] projection) {
return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
}
private static String[] resolveDocumentProjection(String[] projection) {
return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
}
private Uri getUriForDocumentId(String docId) {
final Ident ident = getIdentForDocId(docId);
if (TYPE_IMAGE.equals(ident.type) && ident.id != -1) {
return ContentUris.withAppendedId(
Images.Media.EXTERNAL_CONTENT_URI, ident.id);
} else if (TYPE_VIDEO.equals(ident.type) && ident.id != -1) {
return ContentUris.withAppendedId(
Video.Media.EXTERNAL_CONTENT_URI, ident.id);
} else if (TYPE_AUDIO.equals(ident.type) && ident.id != -1) {
return ContentUris.withAppendedId(
Audio.Media.EXTERNAL_CONTENT_URI, ident.id);
} else {
throw new UnsupportedOperationException("Unsupported document " + docId);
}
}
@Override
public void deleteDocument(String docId) throws FileNotFoundException {
final Uri target = getUriForDocumentId(docId);
// Delegate to real provider
final long token = Binder.clearCallingIdentity();
try {
getContext().getContentResolver().delete(target, null, null);
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
includeImagesRoot(result);
includeVideosRoot(result);
includeAudioRoot(result);
return result;
}
@Override
public Cursor queryDocument(String docId, String[] projection) throws FileNotFoundException {
final ContentResolver resolver = getContext().getContentResolver();
final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
final Ident ident = getIdentForDocId(docId);
final long token = Binder.clearCallingIdentity();
Cursor cursor = null;
try {
if (TYPE_IMAGES_ROOT.equals(ident.type)) {
// single root
includeImagesRootDocument(result);
} else if (TYPE_IMAGES_BUCKET.equals(ident.type)) {
// single bucket
cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI,
ImagesBucketQuery.PROJECTION, ImageColumns.BUCKET_ID + "=" + ident.id,
null, ImagesBucketQuery.SORT_ORDER);
copyNotificationUri(result, cursor);
if (cursor.moveToFirst()) {
includeImagesBucket(result, cursor);
}
} else if (TYPE_IMAGE.equals(ident.type)) {
// single image
cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI,
ImageQuery.PROJECTION, BaseColumns._ID + "=" + ident.id, null,
null);
copyNotificationUri(result, cursor);
if (cursor.moveToFirst()) {
includeImage(result, cursor);
}
} else if (TYPE_VIDEOS_ROOT.equals(ident.type)) {
// single root
includeVideosRootDocument(result);
} else if (TYPE_VIDEOS_BUCKET.equals(ident.type)) {
// single bucket
cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI,
VideosBucketQuery.PROJECTION, VideoColumns.BUCKET_ID + "=" + ident.id,
null, VideosBucketQuery.SORT_ORDER);
copyNotificationUri(result, cursor);
if (cursor.moveToFirst()) {
includeVideosBucket(result, cursor);
}
} else if (TYPE_VIDEO.equals(ident.type)) {
// single video
cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI,
VideoQuery.PROJECTION, BaseColumns._ID + "=" + ident.id, null,
null);
copyNotificationUri(result, cursor);
if (cursor.moveToFirst()) {
includeVideo(result, cursor);
}
} else if (TYPE_AUDIO_ROOT.equals(ident.type)) {
// single root
includeAudioRootDocument(result);
} else if (TYPE_ARTIST.equals(ident.type)) {
// single artist
cursor = resolver.query(Artists.EXTERNAL_CONTENT_URI,
ArtistQuery.PROJECTION, BaseColumns._ID + "=" + ident.id, null,
null);
copyNotificationUri(result, cursor);
if (cursor.moveToFirst()) {
includeArtist(result, cursor);
}
} else if (TYPE_ALBUM.equals(ident.type)) {
// single album
cursor = resolver.query(Albums.EXTERNAL_CONTENT_URI,
AlbumQuery.PROJECTION, BaseColumns._ID + "=" + ident.id, null,
null);
copyNotificationUri(result, cursor);
if (cursor.moveToFirst()) {
includeAlbum(result, cursor);
}
} else if (TYPE_AUDIO.equals(ident.type)) {
// single song
cursor = resolver.query(Audio.Media.EXTERNAL_CONTENT_URI,
SongQuery.PROJECTION, BaseColumns._ID + "=" + ident.id, null,
null);
copyNotificationUri(result, cursor);
if (cursor.moveToFirst()) {
includeAudio(result, cursor);
}
} else {
throw new UnsupportedOperationException("Unsupported document " + docId);
}
} finally {
IoUtils.closeQuietly(cursor);
Binder.restoreCallingIdentity(token);
}
return result;
}
@Override
public Cursor queryChildDocuments(String docId, String[] projection, String sortOrder)
throws FileNotFoundException {
final ContentResolver resolver = getContext().getContentResolver();
final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
final Ident ident = getIdentForDocId(docId);
final long token = Binder.clearCallingIdentity();
Cursor cursor = null;
try {
if (TYPE_IMAGES_ROOT.equals(ident.type)) {
// include all unique buckets
cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI,
ImagesBucketQuery.PROJECTION, null, null, ImagesBucketQuery.SORT_ORDER);
// multiple orders
copyNotificationUri(result, cursor);
long lastId = Long.MIN_VALUE;
while (cursor.moveToNext()) {
final long id = cursor.getLong(ImagesBucketQuery.BUCKET_ID);
if (lastId != id) {
includeImagesBucket(result, cursor);
lastId = id;
}
}
} else if (TYPE_IMAGES_BUCKET.equals(ident.type)) {
// include images under bucket
cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI,
ImageQuery.PROJECTION, ImageColumns.BUCKET_ID + "=" + ident.id,
null, null);
copyNotificationUri(result, cursor);
while (cursor.moveToNext()) {
includeImage(result, cursor);
}
} else if (TYPE_VIDEOS_ROOT.equals(ident.type)) {
// include all unique buckets
cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI,
VideosBucketQuery.PROJECTION, null, null, VideosBucketQuery.SORT_ORDER);
copyNotificationUri(result, cursor);
long lastId = Long.MIN_VALUE;
while (cursor.moveToNext()) {
final long id = cursor.getLong(VideosBucketQuery.BUCKET_ID);
if (lastId != id) {
includeVideosBucket(result, cursor);
lastId = id;
}
}
} else if (TYPE_VIDEOS_BUCKET.equals(ident.type)) {
// include videos under bucket
cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI,
VideoQuery.PROJECTION, VideoColumns.BUCKET_ID + "=" + ident.id,
null, null);
copyNotificationUri(result, cursor);
while (cursor.moveToNext()) {
includeVideo(result, cursor);
}
} else if (TYPE_AUDIO_ROOT.equals(ident.type)) {
// include all artists
cursor = resolver.query(Audio.Artists.EXTERNAL_CONTENT_URI,
ArtistQuery.PROJECTION, null, null, null);
copyNotificationUri(result, cursor);
while (cursor.moveToNext()) {
includeArtist(result, cursor);
}
} else if (TYPE_ARTIST.equals(ident.type)) {
// include all albums under artist
cursor = resolver.query(Artists.Albums.getContentUri("external", ident.id),
AlbumQuery.PROJECTION, null, null, null);
copyNotificationUri(result, cursor);
while (cursor.moveToNext()) {
includeAlbum(result, cursor);
}
} else if (TYPE_ALBUM.equals(ident.type)) {
// include all songs under album
cursor = resolver.query(Audio.Media.EXTERNAL_CONTENT_URI,
SongQuery.PROJECTION, AudioColumns.ALBUM_ID + "=" + ident.id,
null, null);
copyNotificationUri(result, cursor);
while (cursor.moveToNext()) {
includeAudio(result, cursor);
}
} else {
throw new UnsupportedOperationException("Unsupported document " + docId);
}
} finally {
IoUtils.closeQuietly(cursor);
Binder.restoreCallingIdentity(token);
}
return result;
}
@Override
public Cursor queryRecentDocuments(String rootId, String[] projection)
throws FileNotFoundException {
final ContentResolver resolver = getContext().getContentResolver();
final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
final long token = Binder.clearCallingIdentity();
Cursor cursor = null;
try {
if (TYPE_IMAGES_ROOT.equals(rootId)) {
// include all unique buckets
cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI,
ImageQuery.PROJECTION, null, null, ImageColumns.DATE_MODIFIED + " DESC");
copyNotificationUri(result, cursor);
while (cursor.moveToNext() && result.getCount() < 64) {
includeImage(result, cursor);
}
} else if (TYPE_VIDEOS_ROOT.equals(rootId)) {
// include all unique buckets
cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI,
VideoQuery.PROJECTION, null, null, VideoColumns.DATE_MODIFIED + " DESC");
copyNotificationUri(result, cursor);
while (cursor.moveToNext() && result.getCount() < 64) {
includeVideo(result, cursor);
}
} else {
throw new UnsupportedOperationException("Unsupported root " + rootId);
}
} finally {
IoUtils.closeQuietly(cursor);
Binder.restoreCallingIdentity(token);
}
return result;
}
@Override
public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
throws FileNotFoundException {
final Uri target = getUriForDocumentId(docId);
if (!"r".equals(mode)) {
throw new IllegalArgumentException("Media is read-only");
}
// Delegate to real provider
final long token = Binder.clearCallingIdentity();
try {
return getContext().getContentResolver().openFileDescriptor(target, mode);
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override
public AssetFileDescriptor openDocumentThumbnail(
String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
final ContentResolver resolver = getContext().getContentResolver();
final Ident ident = getIdentForDocId(docId);
final long token = Binder.clearCallingIdentity();
try {
if (TYPE_IMAGES_BUCKET.equals(ident.type)) {
final long id = getImageForBucketCleared(ident.id);
return openOrCreateImageThumbnailCleared(id, signal);
} else if (TYPE_IMAGE.equals(ident.type)) {
return openOrCreateImageThumbnailCleared(ident.id, signal);
} else if (TYPE_VIDEOS_BUCKET.equals(ident.type)) {
final long id = getVideoForBucketCleared(ident.id);
return openOrCreateVideoThumbnailCleared(id, signal);
} else if (TYPE_VIDEO.equals(ident.type)) {
return openOrCreateVideoThumbnailCleared(ident.id, signal);
} else {
throw new UnsupportedOperationException("Unsupported document " + docId);
}
} finally {
Binder.restoreCallingIdentity(token);
}
}
private boolean isEmpty(Uri uri) {
final ContentResolver resolver = getContext().getContentResolver();
final long token = Binder.clearCallingIdentity();
Cursor cursor = null;
try {
cursor = resolver.query(uri, new String[] {
BaseColumns._ID }, null, null, null);
return (cursor == null) || (cursor.getCount() == 0);
} finally {
IoUtils.closeQuietly(cursor);
Binder.restoreCallingIdentity(token);
}
}
private void includeImagesRoot(MatrixCursor result) {
int flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_RECENTS;
if (isEmpty(Images.Media.EXTERNAL_CONTENT_URI)) {
flags |= Root.FLAG_EMPTY;
sReturnedImagesEmpty = true;
}
final RowBuilder row = result.newRow();
row.add(Root.COLUMN_ROOT_ID, TYPE_IMAGES_ROOT);
row.add(Root.COLUMN_FLAGS, flags);
row.add(Root.COLUMN_TITLE, getContext().getString(R.string.root_images));
row.add(Root.COLUMN_DOCUMENT_ID, TYPE_IMAGES_ROOT);
row.add(Root.COLUMN_MIME_TYPES, IMAGE_MIME_TYPES);
}
private void includeVideosRoot(MatrixCursor result) {
int flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_RECENTS;
if (isEmpty(Video.Media.EXTERNAL_CONTENT_URI)) {
flags |= Root.FLAG_EMPTY;
sReturnedVideosEmpty = true;
}
final RowBuilder row = result.newRow();
row.add(Root.COLUMN_ROOT_ID, TYPE_VIDEOS_ROOT);
row.add(Root.COLUMN_FLAGS, flags);
row.add(Root.COLUMN_TITLE, getContext().getString(R.string.root_videos));
row.add(Root.COLUMN_DOCUMENT_ID, TYPE_VIDEOS_ROOT);
row.add(Root.COLUMN_MIME_TYPES, VIDEO_MIME_TYPES);
}
private void includeAudioRoot(MatrixCursor result) {
int flags = Root.FLAG_LOCAL_ONLY;
if (isEmpty(Audio.Media.EXTERNAL_CONTENT_URI)) {
flags |= Root.FLAG_EMPTY;
sReturnedAudioEmpty = true;
}
final RowBuilder row = result.newRow();
row.add(Root.COLUMN_ROOT_ID, TYPE_AUDIO_ROOT);
row.add(Root.COLUMN_FLAGS, flags);
row.add(Root.COLUMN_TITLE, getContext().getString(R.string.root_audio));
row.add(Root.COLUMN_DOCUMENT_ID, TYPE_AUDIO_ROOT);
row.add(Root.COLUMN_MIME_TYPES, AUDIO_MIME_TYPES);
}
private void includeImagesRootDocument(MatrixCursor result) {
final RowBuilder row = result.newRow();
row.add(Document.COLUMN_DOCUMENT_ID, TYPE_IMAGES_ROOT);
row.add(Document.COLUMN_DISPLAY_NAME, getContext().getString(R.string.root_images));
row.add(Document.COLUMN_FLAGS,
Document.FLAG_DIR_PREFERS_GRID | Document.FLAG_DIR_PREFERS_LAST_MODIFIED);
row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
}
private void includeVideosRootDocument(MatrixCursor result) {
final RowBuilder row = result.newRow();
row.add(Document.COLUMN_DOCUMENT_ID, TYPE_VIDEOS_ROOT);
row.add(Document.COLUMN_DISPLAY_NAME, getContext().getString(R.string.root_videos));
row.add(Document.COLUMN_FLAGS,
Document.FLAG_DIR_PREFERS_GRID | Document.FLAG_DIR_PREFERS_LAST_MODIFIED);
row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
}
private void includeAudioRootDocument(MatrixCursor result) {
final RowBuilder row = result.newRow();
row.add(Document.COLUMN_DOCUMENT_ID, TYPE_AUDIO_ROOT);
row.add(Document.COLUMN_DISPLAY_NAME, getContext().getString(R.string.root_audio));
row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
}
private interface ImagesBucketQuery {
final String[] PROJECTION = new String[] {
ImageColumns.BUCKET_ID,
ImageColumns.BUCKET_DISPLAY_NAME,
ImageColumns.DATE_MODIFIED };
final String SORT_ORDER = ImageColumns.BUCKET_ID + ", " + ImageColumns.DATE_MODIFIED
+ " DESC";
final int BUCKET_ID = 0;
final int BUCKET_DISPLAY_NAME = 1;
final int DATE_MODIFIED = 2;
}
private void includeImagesBucket(MatrixCursor result, Cursor cursor) {
final long id = cursor.getLong(ImagesBucketQuery.BUCKET_ID);
final String docId = getDocIdForIdent(TYPE_IMAGES_BUCKET, id);
final RowBuilder row = result.newRow();
row.add(Document.COLUMN_DOCUMENT_ID, docId);
row.add(Document.COLUMN_DISPLAY_NAME,
cursor.getString(ImagesBucketQuery.BUCKET_DISPLAY_NAME));
row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
row.add(Document.COLUMN_LAST_MODIFIED,
cursor.getLong(ImagesBucketQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS);
row.add(Document.COLUMN_FLAGS, Document.FLAG_DIR_PREFERS_GRID
| Document.FLAG_SUPPORTS_THUMBNAIL | Document.FLAG_DIR_PREFERS_LAST_MODIFIED);
}
private interface ImageQuery {
final String[] PROJECTION = new String[] {
ImageColumns._ID,
ImageColumns.DISPLAY_NAME,
ImageColumns.MIME_TYPE,
ImageColumns.SIZE,
ImageColumns.DATE_MODIFIED };
final int _ID = 0;
final int DISPLAY_NAME = 1;
final int MIME_TYPE = 2;
final int SIZE = 3;
final int DATE_MODIFIED = 4;
}
private void includeImage(MatrixCursor result, Cursor cursor) {
final long id = cursor.getLong(ImageQuery._ID);
final String docId = getDocIdForIdent(TYPE_IMAGE, id);
final RowBuilder row = result.newRow();
row.add(Document.COLUMN_DOCUMENT_ID, docId);
row.add(Document.COLUMN_DISPLAY_NAME, cursor.getString(ImageQuery.DISPLAY_NAME));
row.add(Document.COLUMN_SIZE, cursor.getLong(ImageQuery.SIZE));
row.add(Document.COLUMN_MIME_TYPE, cursor.getString(ImageQuery.MIME_TYPE));
row.add(Document.COLUMN_LAST_MODIFIED,
cursor.getLong(ImageQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS);
row.add(Document.COLUMN_FLAGS,
Document.FLAG_SUPPORTS_THUMBNAIL | Document.FLAG_SUPPORTS_DELETE);
}
private interface VideosBucketQuery {
final String[] PROJECTION = new String[] {
VideoColumns.BUCKET_ID,
VideoColumns.BUCKET_DISPLAY_NAME,
VideoColumns.DATE_MODIFIED };
final String SORT_ORDER = VideoColumns.BUCKET_ID + ", " + VideoColumns.DATE_MODIFIED
+ " DESC";
final int BUCKET_ID = 0;
final int BUCKET_DISPLAY_NAME = 1;
final int DATE_MODIFIED = 2;
}
private void includeVideosBucket(MatrixCursor result, Cursor cursor) {
final long id = cursor.getLong(VideosBucketQuery.BUCKET_ID);
final String docId = getDocIdForIdent(TYPE_VIDEOS_BUCKET, id);
final RowBuilder row = result.newRow();
row.add(Document.COLUMN_DOCUMENT_ID, docId);
row.add(Document.COLUMN_DISPLAY_NAME,
cursor.getString(VideosBucketQuery.BUCKET_DISPLAY_NAME));
row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
row.add(Document.COLUMN_LAST_MODIFIED,
cursor.getLong(VideosBucketQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS);
row.add(Document.COLUMN_FLAGS, Document.FLAG_DIR_PREFERS_GRID
| Document.FLAG_SUPPORTS_THUMBNAIL | Document.FLAG_DIR_PREFERS_LAST_MODIFIED);
}
private interface VideoQuery {
final String[] PROJECTION = new String[] {
VideoColumns._ID,
VideoColumns.DISPLAY_NAME,
VideoColumns.MIME_TYPE,
VideoColumns.SIZE,
VideoColumns.DATE_MODIFIED };
final int _ID = 0;
final int DISPLAY_NAME = 1;
final int MIME_TYPE = 2;
final int SIZE = 3;
final int DATE_MODIFIED = 4;
}
private void includeVideo(MatrixCursor result, Cursor cursor) {
final long id = cursor.getLong(VideoQuery._ID);
final String docId = getDocIdForIdent(TYPE_VIDEO, id);
final RowBuilder row = result.newRow();
row.add(Document.COLUMN_DOCUMENT_ID, docId);
row.add(Document.COLUMN_DISPLAY_NAME, cursor.getString(VideoQuery.DISPLAY_NAME));
row.add(Document.COLUMN_SIZE, cursor.getLong(VideoQuery.SIZE));
row.add(Document.COLUMN_MIME_TYPE, cursor.getString(VideoQuery.MIME_TYPE));
row.add(Document.COLUMN_LAST_MODIFIED,
cursor.getLong(VideoQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS);
row.add(Document.COLUMN_FLAGS,
Document.FLAG_SUPPORTS_THUMBNAIL | Document.FLAG_SUPPORTS_DELETE);
}
private interface ArtistQuery {
final String[] PROJECTION = new String[] {
BaseColumns._ID,
ArtistColumns.ARTIST };
final int _ID = 0;
final int ARTIST = 1;
}
private void includeArtist(MatrixCursor result, Cursor cursor) {
final long id = cursor.getLong(ArtistQuery._ID);
final String docId = getDocIdForIdent(TYPE_ARTIST, id);
final RowBuilder row = result.newRow();
row.add(Document.COLUMN_DOCUMENT_ID, docId);
row.add(Document.COLUMN_DISPLAY_NAME,
cleanUpMediaDisplayName(cursor.getString(ArtistQuery.ARTIST)));
row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
}
private interface AlbumQuery {
final String[] PROJECTION = new String[] {
BaseColumns._ID,
AlbumColumns.ALBUM };
final int _ID = 0;
final int ALBUM = 1;
}
private void includeAlbum(MatrixCursor result, Cursor cursor) {
final long id = cursor.getLong(AlbumQuery._ID);
final String docId = getDocIdForIdent(TYPE_ALBUM, id);
final RowBuilder row = result.newRow();
row.add(Document.COLUMN_DOCUMENT_ID, docId);
row.add(Document.COLUMN_DISPLAY_NAME,
cleanUpMediaDisplayName(cursor.getString(AlbumQuery.ALBUM)));
row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
}
private interface SongQuery {
final String[] PROJECTION = new String[] {
AudioColumns._ID,
AudioColumns.TITLE,
AudioColumns.MIME_TYPE,
AudioColumns.SIZE,
AudioColumns.DATE_MODIFIED };
final int _ID = 0;
final int TITLE = 1;
final int MIME_TYPE = 2;
final int SIZE = 3;
final int DATE_MODIFIED = 4;
}
private void includeAudio(MatrixCursor result, Cursor cursor) {
final long id = cursor.getLong(SongQuery._ID);
final String docId = getDocIdForIdent(TYPE_AUDIO, id);
final RowBuilder row = result.newRow();
row.add(Document.COLUMN_DOCUMENT_ID, docId);
row.add(Document.COLUMN_DISPLAY_NAME, cursor.getString(SongQuery.TITLE));
row.add(Document.COLUMN_SIZE, cursor.getLong(SongQuery.SIZE));
row.add(Document.COLUMN_MIME_TYPE, cursor.getString(SongQuery.MIME_TYPE));
row.add(Document.COLUMN_LAST_MODIFIED,
cursor.getLong(SongQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS);
row.add(Document.COLUMN_FLAGS, Document.FLAG_SUPPORTS_DELETE);
}
private interface ImagesBucketThumbnailQuery {
final String[] PROJECTION = new String[] {
ImageColumns._ID,
ImageColumns.BUCKET_ID,
ImageColumns.DATE_MODIFIED };
final int _ID = 0;
final int BUCKET_ID = 1;
final int DATE_MODIFIED = 2;
}
private long getImageForBucketCleared(long bucketId) throws FileNotFoundException {
final ContentResolver resolver = getContext().getContentResolver();
Cursor cursor = null;
try {
cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI,
ImagesBucketThumbnailQuery.PROJECTION, ImageColumns.BUCKET_ID + "=" + bucketId,
null, ImageColumns.DATE_MODIFIED + " DESC");
if (cursor.moveToFirst()) {
return cursor.getLong(ImagesBucketThumbnailQuery._ID);
}
} finally {
IoUtils.closeQuietly(cursor);
}
throw new FileNotFoundException("No video found for bucket");
}
private interface ImageThumbnailQuery {
final String[] PROJECTION = new String[] {
Images.Thumbnails.DATA };
final int _DATA = 0;
}
private ParcelFileDescriptor openImageThumbnailCleared(long id, CancellationSignal signal)
throws FileNotFoundException {
final ContentResolver resolver = getContext().getContentResolver();
Cursor cursor = null;
try {
cursor = resolver.query(Images.Thumbnails.EXTERNAL_CONTENT_URI,
ImageThumbnailQuery.PROJECTION, Images.Thumbnails.IMAGE_ID + "=" + id, null,
null, signal);
if (cursor.moveToFirst()) {
final String data = cursor.getString(ImageThumbnailQuery._DATA);
return ParcelFileDescriptor.open(
new File(data), ParcelFileDescriptor.MODE_READ_ONLY);
}
} finally {
IoUtils.closeQuietly(cursor);
}
return null;
}
private AssetFileDescriptor openOrCreateImageThumbnailCleared(
long id, CancellationSignal signal) throws FileNotFoundException {
final ContentResolver resolver = getContext().getContentResolver();
ParcelFileDescriptor pfd = openImageThumbnailCleared(id, signal);
if (pfd == null) {
// No thumbnail yet, so generate. This is messy, since we drop the
// Bitmap on the floor, but its the least-complicated way.
final BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
Images.Thumbnails.getThumbnail(resolver, id, Images.Thumbnails.MINI_KIND, opts);
pfd = openImageThumbnailCleared(id, signal);
}
if (pfd == null) {
// Phoey, fallback to full image
final Uri fullUri = ContentUris.withAppendedId(Images.Media.EXTERNAL_CONTENT_URI, id);
pfd = resolver.openFileDescriptor(fullUri, "r", signal);
}
final int orientation = queryOrientationForImage(id, signal);
final Bundle extras;
if (orientation != 0) {
extras = new Bundle(1);
extras.putInt(DocumentsContract.EXTRA_ORIENTATION, orientation);
} else {
extras = null;
}
return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH, extras);
}
private interface VideosBucketThumbnailQuery {
final String[] PROJECTION = new String[] {
VideoColumns._ID,
VideoColumns.BUCKET_ID,
VideoColumns.DATE_MODIFIED };
final int _ID = 0;
final int BUCKET_ID = 1;
final int DATE_MODIFIED = 2;
}
private long getVideoForBucketCleared(long bucketId)
throws FileNotFoundException {
final ContentResolver resolver = getContext().getContentResolver();
Cursor cursor = null;
try {
cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI,
VideosBucketThumbnailQuery.PROJECTION, VideoColumns.BUCKET_ID + "=" + bucketId,
null, VideoColumns.DATE_MODIFIED + " DESC");
if (cursor.moveToFirst()) {
return cursor.getLong(VideosBucketThumbnailQuery._ID);
}
} finally {
IoUtils.closeQuietly(cursor);
}
throw new FileNotFoundException("No video found for bucket");
}
private interface VideoThumbnailQuery {
final String[] PROJECTION = new String[] {
Video.Thumbnails.DATA };
final int _DATA = 0;
}
private AssetFileDescriptor openVideoThumbnailCleared(long id, CancellationSignal signal)
throws FileNotFoundException {
final ContentResolver resolver = getContext().getContentResolver();
Cursor cursor = null;
try {
cursor = resolver.query(Video.Thumbnails.EXTERNAL_CONTENT_URI,
VideoThumbnailQuery.PROJECTION, Video.Thumbnails.VIDEO_ID + "=" + id, null,
null, signal);
if (cursor.moveToFirst()) {
final String data = cursor.getString(VideoThumbnailQuery._DATA);
return new AssetFileDescriptor(ParcelFileDescriptor.open(
new File(data), ParcelFileDescriptor.MODE_READ_ONLY), 0,
AssetFileDescriptor.UNKNOWN_LENGTH);
}
} finally {
IoUtils.closeQuietly(cursor);
}
return null;
}
private AssetFileDescriptor openOrCreateVideoThumbnailCleared(
long id, CancellationSignal signal) throws FileNotFoundException {
final ContentResolver resolver = getContext().getContentResolver();
AssetFileDescriptor afd = openVideoThumbnailCleared(id, signal);
if (afd == null) {
// No thumbnail yet, so generate. This is messy, since we drop the
// Bitmap on the floor, but its the least-complicated way.
final BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
Video.Thumbnails.getThumbnail(resolver, id, Video.Thumbnails.MINI_KIND, opts);
afd = openVideoThumbnailCleared(id, signal);
}
return afd;
}
private interface ImageOrientationQuery {
final String[] PROJECTION = new String[] {
ImageColumns.ORIENTATION };
final int ORIENTATION = 0;
}
private int queryOrientationForImage(long id, CancellationSignal signal) {
final ContentResolver resolver = getContext().getContentResolver();
Cursor cursor = null;
try {
cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI,
ImageOrientationQuery.PROJECTION, ImageColumns._ID + "=" + id, null, null,
signal);
if (cursor.moveToFirst()) {
return cursor.getInt(ImageOrientationQuery.ORIENTATION);
} else {
Log.w(TAG, "Missing orientation data for " + id);
return 0;
}
} finally {
IoUtils.closeQuietly(cursor);
}
}
private String cleanUpMediaDisplayName(String displayName) {
if (!MediaStore.UNKNOWN_STRING.equals(displayName)) {
return displayName;
}
return getContext().getResources().getString(com.android.internal.R.string.unknownName);
}
}