| /* |
| * 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 static android.content.ContentResolver.EXTRA_SIZE; |
| import static android.content.ContentResolver.QUERY_ARG_SQL_LIMIT; |
| import static android.provider.DocumentsContract.QUERY_ARG_DISPLAY_NAME; |
| import static android.provider.DocumentsContract.QUERY_ARG_EXCLUDE_MEDIA; |
| import static android.provider.DocumentsContract.QUERY_ARG_FILE_SIZE_OVER; |
| import static android.provider.DocumentsContract.QUERY_ARG_LAST_MODIFIED_AFTER; |
| import static android.provider.DocumentsContract.QUERY_ARG_MIME_TYPES; |
| import static android.provider.MediaStore.GET_MEDIA_URI_CALL; |
| |
| 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.Point; |
| import android.media.ExifInterface; |
| import android.media.MediaMetadata; |
| import android.net.Uri; |
| import android.os.Binder; |
| import android.os.Bundle; |
| import android.os.CancellationSignal; |
| import android.os.ParcelFileDescriptor; |
| import android.os.UserHandle; |
| import android.os.UserManager; |
| 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; |
| 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.DateFormat; |
| import android.text.format.DateUtils; |
| import android.util.Log; |
| import android.util.Pair; |
| |
| import androidx.annotation.Nullable; |
| import androidx.core.content.MimeTypeFilter; |
| |
| import com.android.providers.media.util.FileUtils; |
| |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Objects; |
| |
| /** |
| * Presents a {@link DocumentsContract} view of {@link MediaProvider} external |
| * contents. |
| */ |
| public class MediaDocumentsProvider extends DocumentsProvider { |
| private static final String TAG = "MediaDocumentsProvider"; |
| |
| public static final String AUTHORITY = "com.android.providers.media.documents"; |
| |
| private static final String SUPPORTED_QUERY_ARGS = joinNewline( |
| DocumentsContract.QUERY_ARG_DISPLAY_NAME, |
| DocumentsContract.QUERY_ARG_FILE_SIZE_OVER, |
| DocumentsContract.QUERY_ARG_LAST_MODIFIED_AFTER, |
| DocumentsContract.QUERY_ARG_MIME_TYPES); |
| |
| 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, |
| Root.COLUMN_QUERY_ARGS |
| }; |
| |
| 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 DOCUMENT_MIME_TYPES = joinNewline("*/*"); |
| |
| static final String TYPE_IMAGES_ROOT = "images_root"; |
| static final String TYPE_IMAGES_BUCKET = "images_bucket"; |
| static final String TYPE_IMAGE = "image"; |
| |
| static final String TYPE_VIDEOS_ROOT = "videos_root"; |
| static final String TYPE_VIDEOS_BUCKET = "videos_bucket"; |
| static final String TYPE_VIDEO = "video"; |
| |
| static final String TYPE_AUDIO_ROOT = "audio_root"; |
| static final String TYPE_AUDIO = "audio"; |
| static final String TYPE_ARTIST = "artist"; |
| static final String TYPE_ALBUM = "album"; |
| |
| static final String TYPE_DOCUMENTS_ROOT = "documents_root"; |
| static final String TYPE_DOCUMENTS_BUCKET = "documents_bucket"; |
| static final String TYPE_DOCUMENT = "document"; |
| |
| private static volatile boolean sMediaStoreReady = false; |
| |
| private static volatile boolean sReturnedImagesEmpty = false; |
| private static volatile boolean sReturnedVideosEmpty = false; |
| private static volatile boolean sReturnedAudioEmpty = false; |
| private static volatile boolean sReturnedDocumentsEmpty = false; |
| |
| private static String joinNewline(String... args) { |
| return TextUtils.join("\n", args); |
| } |
| |
| public static final String METADATA_KEY_AUDIO = "android.media.metadata.audio"; |
| public static final String METADATA_KEY_VIDEO = "android.media.metadata.video"; |
| |
| /* |
| * A mapping between media columns and metadata tag names. These keys of the |
| * map form the projection for queries against the media store database. |
| */ |
| private static final Map<String, String> IMAGE_COLUMN_MAP = new HashMap<>(); |
| private static final Map<String, String> VIDEO_COLUMN_MAP = new HashMap<>(); |
| private static final Map<String, String> AUDIO_COLUMN_MAP = new HashMap<>(); |
| |
| static { |
| /** |
| * Note that for images (jpegs at least) we'll first try an alternate |
| * means of extracting metadata, one that provides more data. But if |
| * that fails, or if the image type is not JPEG, we fall back to these columns. |
| */ |
| IMAGE_COLUMN_MAP.put(ImageColumns.WIDTH, ExifInterface.TAG_IMAGE_WIDTH); |
| IMAGE_COLUMN_MAP.put(ImageColumns.HEIGHT, ExifInterface.TAG_IMAGE_LENGTH); |
| IMAGE_COLUMN_MAP.put(ImageColumns.DATE_TAKEN, ExifInterface.TAG_DATETIME); |
| |
| VIDEO_COLUMN_MAP.put(VideoColumns.DURATION, MediaMetadata.METADATA_KEY_DURATION); |
| VIDEO_COLUMN_MAP.put(VideoColumns.HEIGHT, ExifInterface.TAG_IMAGE_LENGTH); |
| VIDEO_COLUMN_MAP.put(VideoColumns.WIDTH, ExifInterface.TAG_IMAGE_WIDTH); |
| VIDEO_COLUMN_MAP.put(VideoColumns.DATE_TAKEN, MediaMetadata.METADATA_KEY_DATE); |
| |
| AUDIO_COLUMN_MAP.put(AudioColumns.ARTIST, MediaMetadata.METADATA_KEY_ARTIST); |
| AUDIO_COLUMN_MAP.put(AudioColumns.COMPOSER, MediaMetadata.METADATA_KEY_COMPOSER); |
| AUDIO_COLUMN_MAP.put(AudioColumns.ALBUM, MediaMetadata.METADATA_KEY_ALBUM); |
| AUDIO_COLUMN_MAP.put(AudioColumns.YEAR, MediaMetadata.METADATA_KEY_YEAR); |
| AUDIO_COLUMN_MAP.put(AudioColumns.DURATION, MediaMetadata.METADATA_KEY_DURATION); |
| } |
| |
| @Override |
| public boolean onCreate() { |
| notifyRootsChanged(getContext()); |
| return true; |
| } |
| |
| private void enforceShellRestrictions() { |
| final int callingAppId = UserHandle.getAppId(Binder.getCallingUid()); |
| if (callingAppId == android.os.Process.SHELL_UID |
| && getContext().getSystemService(UserManager.class) |
| .hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER)) { |
| throw new SecurityException( |
| "Shell user cannot access files for user " + UserHandle.myUserId()); |
| } |
| } |
| |
| private static void notifyRootsChanged(Context context) { |
| context.getContentResolver() |
| .notifyChange(DocumentsContract.buildRootsUri(AUTHORITY), null, false); |
| } |
| |
| /** |
| * When underlying provider is ready, we kick off a notification of roots |
| * changed so they can be refreshed. |
| */ |
| static void onMediaStoreReady(Context context) { |
| sMediaStoreReady = true; |
| notifyRootsChanged(context); |
| } |
| |
| /** |
| * 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 (MediaStore.VOLUME_INTERNAL.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); |
| } else if (type == FileColumns.MEDIA_TYPE_DOCUMENT && sReturnedDocumentsEmpty) { |
| sReturnedDocumentsEmpty = 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 (MediaStore.VOLUME_INTERNAL.equals(volumeName)) return; |
| |
| if (type == FileColumns.MEDIA_TYPE_IMAGE) { |
| final Uri uri = DocumentsContract.buildDocumentUri( |
| AUTHORITY, getDocIdForIdent(TYPE_IMAGE, id)); |
| context.revokeUriPermission(uri, ~0); |
| notifyRootsChanged(context); |
| } else if (type == FileColumns.MEDIA_TYPE_VIDEO) { |
| final Uri uri = DocumentsContract.buildDocumentUri( |
| AUTHORITY, getDocIdForIdent(TYPE_VIDEO, id)); |
| context.revokeUriPermission(uri, ~0); |
| notifyRootsChanged(context); |
| } else if (type == FileColumns.MEDIA_TYPE_AUDIO) { |
| final Uri uri = DocumentsContract.buildDocumentUri( |
| AUTHORITY, getDocIdForIdent(TYPE_AUDIO, id)); |
| context.revokeUriPermission(uri, ~0); |
| notifyRootsChanged(context); |
| } else if (type == FileColumns.MEDIA_TYPE_DOCUMENT) { |
| final Uri uri = DocumentsContract.buildDocumentUri( |
| AUTHORITY, getDocIdForIdent(TYPE_DOCUMENT, id)); |
| context.revokeUriPermission(uri, ~0); |
| notifyRootsChanged(context); |
| } |
| } |
| |
| 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; |
| } |
| |
| static Pair<String, String[]> buildSearchSelection(String displayName, |
| String[] mimeTypes, long lastModifiedAfter, long fileSizeOver, String columnDisplayName, |
| String columnMimeType, String columnLastModified, String columnFileSize) { |
| StringBuilder selection = new StringBuilder(); |
| final List<String> selectionArgs = new ArrayList<>(); |
| |
| if (!displayName.isEmpty()) { |
| selection.append(columnDisplayName + " LIKE ?"); |
| selectionArgs.add("%" + displayName + "%"); |
| } |
| |
| if (lastModifiedAfter != -1) { |
| if (selection.length() > 0) { |
| selection.append(" AND "); |
| } |
| |
| // The units of DATE_MODIFIED are seconds since 1970. |
| // The units of lastModified are milliseconds since 1970. |
| selection.append(columnLastModified + " > " + lastModifiedAfter / 1000); |
| } |
| |
| if (fileSizeOver != -1) { |
| if (selection.length() > 0) { |
| selection.append(" AND "); |
| } |
| |
| selection.append(columnFileSize + " > " + fileSizeOver); |
| } |
| |
| if (mimeTypes != null && mimeTypes.length > 0) { |
| if (selection.length() > 0) { |
| selection.append(" AND "); |
| } |
| |
| selection.append("("); |
| final List<String> tempSelectionArgs = new ArrayList<>(); |
| final StringBuilder tempSelection = new StringBuilder(); |
| List<String> wildcardMimeTypeList = new ArrayList<>(); |
| for (int i = 0; i < mimeTypes.length; ++i) { |
| final String mimeType = mimeTypes[i]; |
| if (!TextUtils.isEmpty(mimeType) && mimeType.endsWith("/*")) { |
| wildcardMimeTypeList.add(mimeType); |
| continue; |
| } |
| |
| if (tempSelectionArgs.size() > 0) { |
| tempSelection.append(","); |
| } |
| tempSelection.append("?"); |
| tempSelectionArgs.add(mimeType); |
| } |
| |
| for (int i = 0; i < wildcardMimeTypeList.size(); i++) { |
| selection.append(columnMimeType + " LIKE ?") |
| .append((i != wildcardMimeTypeList.size() - 1) ? " OR " : ""); |
| final String mimeType = wildcardMimeTypeList.get(i); |
| selectionArgs.add(mimeType.substring(0, mimeType.length() - 1) + "%"); |
| } |
| |
| if (tempSelectionArgs.size() > 0) { |
| if (wildcardMimeTypeList.size() > 0) { |
| selection.append(" OR "); |
| } |
| selection.append(columnMimeType + " IN (") |
| .append(tempSelection.toString()) |
| .append(")"); |
| selectionArgs.addAll(tempSelectionArgs); |
| } |
| |
| selection.append(")"); |
| } |
| |
| return new Pair<>(selection.toString(), selectionArgs.toArray(new String[0])); |
| } |
| |
| static Pair<String, String[]> addDocumentSelection(String selection, |
| String[] selectionArgs) { |
| String retSelection = ""; |
| final List<String> retSelectionArgs = new ArrayList<>(); |
| if (!TextUtils.isEmpty(selection) && selectionArgs != null) { |
| retSelection = selection + " AND "; |
| for (int i = 0; i < selectionArgs.length; i++) { |
| retSelectionArgs.add(selectionArgs[i]); |
| } |
| } |
| retSelection += FileColumns.MEDIA_TYPE + "=?"; |
| retSelectionArgs.add("" + FileColumns.MEDIA_TYPE_DOCUMENT); |
| return new Pair<>(retSelection, retSelectionArgs.toArray(new String[0])); |
| } |
| |
| /** |
| * Check whether filter mime type and get the matched mime types. |
| * If we don't need to filter mime type, the matchedMimeTypes will be empty. |
| * |
| * @param mimeTypes the mime types to test |
| * @param filter the filter. It is "image/*" or "video/*" or "audio/*". |
| * @param matchedMimeTypes the matched mime types will add into this. |
| * @return true, should do mime type filter. false, no need. |
| */ |
| private static boolean shouldFilterMimeType(String[] mimeTypes, String filter, |
| List<String> matchedMimeTypes) { |
| matchedMimeTypes.clear(); |
| boolean shouldQueryMimeType = true; |
| if (mimeTypes != null) { |
| for (int i = 0; i < mimeTypes.length; i++) { |
| // If the mime type is "*/*" or "image/*" or "video/*" or "audio/*", |
| // we don't need to filter mime type. |
| if (TextUtils.equals(mimeTypes[i], "*/*") || |
| TextUtils.equals(mimeTypes[i], filter)) { |
| matchedMimeTypes.clear(); |
| shouldQueryMimeType = false; |
| break; |
| } |
| if (MimeTypeFilter.matches(mimeTypes[i], filter)) { |
| matchedMimeTypes.add(mimeTypes[i]); |
| } |
| } |
| } else { |
| shouldQueryMimeType = false; |
| } |
| |
| return shouldQueryMimeType; |
| } |
| |
| 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 if (TYPE_DOCUMENT.equals(ident.type) && ident.id != -1) { |
| return ContentUris.withAppendedId( |
| Files.EXTERNAL_CONTENT_URI, ident.id); |
| } else { |
| throw new UnsupportedOperationException("Unsupported document " + docId); |
| } |
| } |
| |
| @Override |
| public Bundle call(String method, String arg, Bundle extras) { |
| Bundle bundle = super.call(method, arg, extras); |
| if (bundle == null && !TextUtils.isEmpty(method)) { |
| switch (method) { |
| case GET_MEDIA_URI_CALL: { |
| getContext().enforceCallingOrSelfPermission( |
| android.Manifest.permission.WRITE_MEDIA_STORAGE, TAG); |
| final Uri documentUri = extras.getParcelable(MediaStore.EXTRA_URI); |
| final String docId = DocumentsContract.getDocumentId(documentUri); |
| final Bundle out = new Bundle(); |
| final Uri uri = getUriForDocumentId(docId); |
| out.putParcelable(MediaStore.EXTRA_URI, uri); |
| return out; |
| } |
| default: |
| Log.w(TAG, "unknown method passed to call(): " + method); |
| } |
| } |
| return bundle; |
| } |
| |
| @Override |
| public void deleteDocument(String docId) throws FileNotFoundException { |
| enforceShellRestrictions(); |
| 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 @Nullable Bundle getDocumentMetadata(String docId) throws FileNotFoundException { |
| enforceShellRestrictions(); |
| return getDocumentMetadataFromIndex(docId); |
| } |
| |
| public @Nullable Bundle getDocumentMetadataFromIndex(String docId) |
| throws FileNotFoundException { |
| |
| final Ident ident = getIdentForDocId(docId); |
| |
| Map<String, String> columnMap = null; |
| String tagType; |
| Uri query; |
| |
| switch (ident.type) { |
| case TYPE_IMAGE: |
| columnMap = IMAGE_COLUMN_MAP; |
| tagType = DocumentsContract.METADATA_EXIF; |
| query = Images.Media.EXTERNAL_CONTENT_URI; |
| break; |
| case TYPE_VIDEO: |
| columnMap = VIDEO_COLUMN_MAP; |
| tagType = METADATA_KEY_VIDEO; |
| query = Video.Media.EXTERNAL_CONTENT_URI; |
| break; |
| case TYPE_AUDIO: |
| columnMap = AUDIO_COLUMN_MAP; |
| tagType = METADATA_KEY_AUDIO; |
| query = Audio.Media.EXTERNAL_CONTENT_URI; |
| break; |
| default: |
| // Unsupported file type. |
| throw new FileNotFoundException( |
| "Metadata request for unsupported file type: " + ident.type); |
| } |
| |
| final long token = Binder.clearCallingIdentity(); |
| Cursor cursor = null; |
| Bundle result = null; |
| |
| final ContentResolver resolver = getContext().getContentResolver(); |
| Collection<String> columns = columnMap.keySet(); |
| String[] projection = columns.toArray(new String[columns.size()]); |
| try { |
| cursor = resolver.query( |
| query, |
| projection, |
| BaseColumns._ID + "=?", |
| new String[]{Long.toString(ident.id)}, |
| null); |
| |
| if (!cursor.moveToFirst()) { |
| throw new FileNotFoundException("Can't find document id: " + docId); |
| } |
| |
| final Bundle metadata = extractMetadataFromCursor(cursor, columnMap); |
| result = new Bundle(); |
| result.putBundle(tagType, metadata); |
| result.putStringArray( |
| DocumentsContract.METADATA_TYPES, |
| new String[]{tagType}); |
| } finally { |
| FileUtils.closeQuietly(cursor); |
| Binder.restoreCallingIdentity(token); |
| } |
| return result; |
| } |
| |
| private static Bundle extractMetadataFromCursor(Cursor cursor, Map<String, String> columns) { |
| |
| assert (cursor.getCount() == 1); |
| |
| final Bundle metadata = new Bundle(); |
| for (String col : columns.keySet()) { |
| |
| int index = cursor.getColumnIndex(col); |
| String bundleTag = columns.get(col); |
| |
| // Special case to be able to pull longs out of a cursor, as long is not a supported |
| // field of getType. |
| if (ExifInterface.TAG_DATETIME.equals(bundleTag)) { |
| if (!cursor.isNull(index)) { |
| // format string to be consistent with how EXIF interface formats the date. |
| long date = cursor.getLong(index); |
| String format = DateFormat.getBestDateTimePattern(Locale.getDefault(), |
| "MMM dd, yyyy, hh:mm"); |
| metadata.putString(bundleTag, DateFormat.format(format, date).toString()); |
| } |
| continue; |
| } |
| |
| switch (cursor.getType(index)) { |
| case Cursor.FIELD_TYPE_INTEGER: |
| metadata.putInt(bundleTag, cursor.getInt(index)); |
| break; |
| case Cursor.FIELD_TYPE_FLOAT: |
| //Errors on the side of greater precision since interface doesnt support doubles |
| metadata.putFloat(bundleTag, cursor.getFloat(index)); |
| break; |
| case Cursor.FIELD_TYPE_STRING: |
| metadata.putString(bundleTag, cursor.getString(index)); |
| break; |
| case Cursor.FIELD_TYPE_BLOB: |
| Log.d(TAG, "Unsupported type, blob, for col: " + bundleTag); |
| break; |
| case Cursor.FIELD_TYPE_NULL: |
| Log.d(TAG, "Unsupported type, null, for col: " + bundleTag); |
| break; |
| default: |
| throw new RuntimeException("Data type not supported"); |
| } |
| } |
| |
| return metadata; |
| } |
| |
| @Override |
| public Cursor queryRoots(String[] projection) throws FileNotFoundException { |
| final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection)); |
| // Skip all roots when the underlying provider isn't ready yet so that |
| // we avoid triggering an ANR; we'll circle back to notify and refresh |
| // once it's ready |
| if (sMediaStoreReady) { |
| includeImagesRoot(result); |
| includeVideosRoot(result); |
| includeAudioRoot(result); |
| includeDocumentsRoot(result); |
| } |
| return result; |
| } |
| |
| @Override |
| public Cursor queryDocument(String docId, String[] projection) throws FileNotFoundException { |
| enforceShellRestrictions(); |
| final ContentResolver resolver = getContext().getContentResolver(); |
| final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); |
| final Ident ident = getIdentForDocId(docId); |
| final String[] queryArgs = new String[] { Long.toString(ident.id) } ; |
| |
| 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 + "=?", |
| queryArgs, ImagesBucketQuery.SORT_ORDER); |
| result.setNotificationUri(resolver, Images.Media.EXTERNAL_CONTENT_URI); |
| 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 + "=?", queryArgs, |
| null); |
| result.setNotificationUri(resolver, Images.Media.EXTERNAL_CONTENT_URI); |
| 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 + "=?", |
| queryArgs, VideosBucketQuery.SORT_ORDER); |
| result.setNotificationUri(resolver, Video.Media.EXTERNAL_CONTENT_URI); |
| 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 + "=?", queryArgs, |
| null); |
| result.setNotificationUri(resolver, Video.Media.EXTERNAL_CONTENT_URI); |
| 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 + "=?", queryArgs, |
| null); |
| result.setNotificationUri(resolver, Audio.Media.EXTERNAL_CONTENT_URI); |
| 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 + "=?", queryArgs, |
| null); |
| result.setNotificationUri(resolver, Audio.Media.EXTERNAL_CONTENT_URI); |
| 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 + "=?", queryArgs, |
| null); |
| result.setNotificationUri(resolver, Audio.Media.EXTERNAL_CONTENT_URI); |
| if (cursor.moveToFirst()) { |
| includeAudio(result, cursor); |
| } |
| } else if (TYPE_DOCUMENTS_ROOT.equals(ident.type)) { |
| // single root |
| includeDocumentsRootDocument(result); |
| } else if (TYPE_DOCUMENTS_BUCKET.equals(ident.type)) { |
| // single bucket |
| final Pair<String, String[]> selectionPair = addDocumentSelection( |
| FileColumns.BUCKET_ID + "=?", queryArgs); |
| cursor = resolver.query(Files.EXTERNAL_CONTENT_URI, DocumentsBucketQuery.PROJECTION, |
| selectionPair.first, selectionPair.second, DocumentsBucketQuery.SORT_ORDER); |
| result.setNotificationUri(resolver, Files.EXTERNAL_CONTENT_URI); |
| if (cursor.moveToFirst()) { |
| includeDocumentsBucket(result, cursor); |
| } |
| } else if (TYPE_DOCUMENT.equals(ident.type)) { |
| // single document |
| cursor = resolver.query(Files.EXTERNAL_CONTENT_URI, DocumentQuery.PROJECTION, |
| FileColumns._ID + "=?", queryArgs, null); |
| result.setNotificationUri(resolver, Files.EXTERNAL_CONTENT_URI); |
| if (cursor.moveToFirst()) { |
| includeDocument(result, cursor); |
| } |
| } else { |
| throw new UnsupportedOperationException("Unsupported document " + docId); |
| } |
| } finally { |
| FileUtils.closeQuietly(cursor); |
| Binder.restoreCallingIdentity(token); |
| } |
| return result; |
| } |
| |
| @Override |
| public Cursor queryChildDocuments(String docId, String[] projection, String sortOrder) |
| throws FileNotFoundException { |
| enforceShellRestrictions(); |
| final ContentResolver resolver = getContext().getContentResolver(); |
| final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); |
| final Ident ident = getIdentForDocId(docId); |
| final String[] queryArgs = new String[] { Long.toString(ident.id) } ; |
| |
| 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 |
| result.setNotificationUri(resolver, Images.Media.EXTERNAL_CONTENT_URI); |
| 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 + "=?", |
| queryArgs, null); |
| result.setNotificationUri(resolver, Images.Media.EXTERNAL_CONTENT_URI); |
| 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); |
| result.setNotificationUri(resolver, Video.Media.EXTERNAL_CONTENT_URI); |
| 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 + "=?", |
| queryArgs, null); |
| result.setNotificationUri(resolver, Video.Media.EXTERNAL_CONTENT_URI); |
| 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); |
| result.setNotificationUri(resolver, Audio.Media.EXTERNAL_CONTENT_URI); |
| 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); |
| result.setNotificationUri(resolver, Audio.Media.EXTERNAL_CONTENT_URI); |
| 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 + "=?", |
| queryArgs, null); |
| result.setNotificationUri(resolver, Audio.Media.EXTERNAL_CONTENT_URI); |
| while (cursor.moveToNext()) { |
| includeAudio(result, cursor); |
| } |
| } else if (TYPE_DOCUMENTS_ROOT.equals(ident.type)) { |
| // include all unique buckets |
| final Pair<String, String[]> selectionPair = addDocumentSelection(null, null); |
| cursor = resolver.query(Files.EXTERNAL_CONTENT_URI, DocumentsBucketQuery.PROJECTION, |
| selectionPair.first, selectionPair.second, DocumentsBucketQuery.SORT_ORDER); |
| result.setNotificationUri(resolver, Files.EXTERNAL_CONTENT_URI); |
| long lastId = Long.MIN_VALUE; |
| while (cursor.moveToNext()) { |
| final long id = cursor.getLong(DocumentsBucketQuery.BUCKET_ID); |
| if (lastId != id) { |
| includeDocumentsBucket(result, cursor); |
| lastId = id; |
| } |
| } |
| } else if (TYPE_DOCUMENTS_BUCKET.equals(ident.type)) { |
| // include documents under bucket |
| final Pair<String, String[]> selectionPair = addDocumentSelection( |
| FileColumns.BUCKET_ID + "=?", queryArgs); |
| cursor = resolver.query(Files.EXTERNAL_CONTENT_URI, DocumentQuery.PROJECTION, |
| selectionPair.first, selectionPair.second, null); |
| result.setNotificationUri(resolver, Files.EXTERNAL_CONTENT_URI); |
| while (cursor.moveToNext()) { |
| includeDocument(result, cursor); |
| } |
| } else { |
| throw new UnsupportedOperationException("Unsupported document " + docId); |
| } |
| } finally { |
| FileUtils.closeQuietly(cursor); |
| Binder.restoreCallingIdentity(token); |
| } |
| return result; |
| } |
| |
| @Override |
| public Cursor queryRecentDocuments(String rootId, String[] projection, |
| @Nullable Bundle queryArgs, @Nullable CancellationSignal signal) |
| throws FileNotFoundException { |
| enforceShellRestrictions(); |
| final ContentResolver resolver = getContext().getContentResolver(); |
| final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); |
| |
| final long token = Binder.clearCallingIdentity(); |
| |
| int limit = -1; |
| if (queryArgs != null) { |
| limit = queryArgs.getInt(ContentResolver.QUERY_ARG_LIMIT, -1); |
| } |
| if (limit < 0) { |
| // Use default value, and no QUERY_ARG* is honored. |
| limit = 64; |
| } else { |
| // We are honoring the QUERY_ARG_LIMIT. |
| Bundle extras = new Bundle(); |
| result.setExtras(extras); |
| extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS, new String[]{ |
| ContentResolver.QUERY_ARG_LIMIT |
| }); |
| } |
| |
| 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"); |
| result.setNotificationUri(resolver, Images.Media.EXTERNAL_CONTENT_URI); |
| while (cursor.moveToNext() && result.getCount() < limit) { |
| 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"); |
| result.setNotificationUri(resolver, Video.Media.EXTERNAL_CONTENT_URI); |
| while (cursor.moveToNext() && result.getCount() < limit) { |
| includeVideo(result, cursor); |
| } |
| } else if (TYPE_DOCUMENTS_ROOT.equals(rootId)) { |
| // include all unique buckets |
| final Pair<String, String[]> selectionPair = addDocumentSelection(null, null); |
| cursor = resolver.query(Files.EXTERNAL_CONTENT_URI, DocumentQuery.PROJECTION, |
| selectionPair.first, selectionPair.second, |
| FileColumns.DATE_MODIFIED + " DESC"); |
| result.setNotificationUri(resolver, Files.EXTERNAL_CONTENT_URI); |
| while (cursor.moveToNext() && result.getCount() < limit) { |
| includeDocument(result, cursor); |
| } |
| } else { |
| throw new UnsupportedOperationException("Unsupported root " + rootId); |
| } |
| } finally { |
| FileUtils.closeQuietly(cursor); |
| Binder.restoreCallingIdentity(token); |
| } |
| return result; |
| } |
| |
| @Override |
| public Cursor querySearchDocuments(String rootId, String[] projection, Bundle queryArgs) |
| throws FileNotFoundException { |
| enforceShellRestrictions(); |
| final ContentResolver resolver = getContext().getContentResolver(); |
| final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); |
| |
| final long token = Binder.clearCallingIdentity(); |
| |
| final String displayName = queryArgs.getString(DocumentsContract.QUERY_ARG_DISPLAY_NAME, |
| "" /* defaultValue */); |
| final long lastModifiedAfter = queryArgs.getLong( |
| DocumentsContract.QUERY_ARG_LAST_MODIFIED_AFTER, -1 /* defaultValue */); |
| final long fileSizeOver = queryArgs.getLong(DocumentsContract.QUERY_ARG_FILE_SIZE_OVER, |
| -1 /* defaultValue */); |
| final String[] mimeTypes = queryArgs.getStringArray(DocumentsContract.QUERY_ARG_MIME_TYPES); |
| final ArrayList<String> matchedMimeTypes = new ArrayList<>(); |
| |
| Cursor cursor = null; |
| try { |
| if (TYPE_IMAGES_ROOT.equals(rootId)) { |
| final boolean shouldFilterMimeType = shouldFilterMimeType(mimeTypes, "image/*", |
| matchedMimeTypes); |
| |
| // If the queried mime types didn't match the root, we don't need to |
| // query the provider. Ex: the queried mime type is "video/*", but the root |
| // is images root. |
| if (mimeTypes == null || !shouldFilterMimeType || matchedMimeTypes.size() > 0) { |
| final Pair<String, String[]> selectionPair = buildSearchSelection(displayName, |
| matchedMimeTypes.toArray(new String[0]), lastModifiedAfter, |
| fileSizeOver, ImageColumns.DISPLAY_NAME, ImageColumns.MIME_TYPE, |
| ImageColumns.DATE_MODIFIED, ImageColumns.SIZE); |
| |
| cursor = resolver.query(Images.Media.EXTERNAL_CONTENT_URI, |
| ImageQuery.PROJECTION, |
| selectionPair.first, selectionPair.second, |
| ImageColumns.DATE_MODIFIED + " DESC"); |
| |
| result.setNotificationUri(resolver, Images.Media.EXTERNAL_CONTENT_URI); |
| while (cursor.moveToNext()) { |
| includeImage(result, cursor); |
| } |
| } |
| } else if (TYPE_VIDEOS_ROOT.equals(rootId)) { |
| final boolean shouldFilterMimeType = shouldFilterMimeType(mimeTypes, "video/*", |
| matchedMimeTypes); |
| |
| // If the queried mime types didn't match the root, we don't need to |
| // query the provider. |
| if (mimeTypes == null || !shouldFilterMimeType || matchedMimeTypes.size() > 0) { |
| final Pair<String, String[]> selectionPair = buildSearchSelection(displayName, |
| matchedMimeTypes.toArray(new String[0]), lastModifiedAfter, |
| fileSizeOver, VideoColumns.DISPLAY_NAME, VideoColumns.MIME_TYPE, |
| VideoColumns.DATE_MODIFIED, VideoColumns.SIZE); |
| cursor = resolver.query(Video.Media.EXTERNAL_CONTENT_URI, VideoQuery.PROJECTION, |
| selectionPair.first, selectionPair.second, |
| VideoColumns.DATE_MODIFIED + " DESC"); |
| result.setNotificationUri(resolver, Video.Media.EXTERNAL_CONTENT_URI); |
| while (cursor.moveToNext()) { |
| includeVideo(result, cursor); |
| } |
| } |
| } else if (TYPE_AUDIO_ROOT.equals(rootId)) { |
| final boolean shouldFilterMimeType = shouldFilterMimeType(mimeTypes, "audio/*", |
| matchedMimeTypes); |
| |
| // If the queried mime types didn't match the root, we don't need to |
| // query the provider. |
| if (mimeTypes == null || !shouldFilterMimeType || matchedMimeTypes.size() > 0) { |
| final Pair<String, String[]> selectionPair = buildSearchSelection(displayName, |
| matchedMimeTypes.toArray(new String[0]), lastModifiedAfter, |
| fileSizeOver, AudioColumns.DISPLAY_NAME, AudioColumns.MIME_TYPE, |
| AudioColumns.DATE_MODIFIED, AudioColumns.SIZE); |
| |
| cursor = resolver.query(Audio.Media.EXTERNAL_CONTENT_URI, SongQuery.PROJECTION, |
| selectionPair.first, selectionPair.second, |
| AudioColumns.DATE_MODIFIED + " DESC"); |
| result.setNotificationUri(resolver, Audio.Media.EXTERNAL_CONTENT_URI); |
| while (cursor.moveToNext()) { |
| includeAudio(result, cursor); |
| } |
| } |
| } else if (TYPE_DOCUMENTS_ROOT.equals(rootId)) { |
| final Pair<String, String[]> initialSelectionPair = buildSearchSelection( |
| displayName, mimeTypes, lastModifiedAfter, fileSizeOver, |
| FileColumns.DISPLAY_NAME, FileColumns.MIME_TYPE, FileColumns.DATE_MODIFIED, |
| FileColumns.SIZE); |
| final Pair<String, String[]> selectionPair = addDocumentSelection( |
| initialSelectionPair.first, initialSelectionPair.second); |
| |
| cursor = resolver.query(Files.EXTERNAL_CONTENT_URI, DocumentQuery.PROJECTION, |
| selectionPair.first, selectionPair.second, |
| FileColumns.DATE_MODIFIED + " DESC"); |
| result.setNotificationUri(resolver, Files.EXTERNAL_CONTENT_URI); |
| while (cursor.moveToNext()) { |
| includeDocument(result, cursor); |
| } |
| } else { |
| throw new UnsupportedOperationException("Unsupported root " + rootId); |
| } |
| } finally { |
| FileUtils.closeQuietly(cursor); |
| Binder.restoreCallingIdentity(token); |
| } |
| |
| final String[] handledQueryArgs = getHandledQueryArguments(queryArgs); |
| if (handledQueryArgs.length > 0) { |
| final Bundle extras = new Bundle(); |
| extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS, handledQueryArgs); |
| result.setExtras(extras); |
| } |
| |
| return result; |
| } |
| |
| public static String[] getHandledQueryArguments(Bundle queryArgs) { |
| if (queryArgs == null) { |
| return new String[0]; |
| } |
| |
| final ArrayList<String> args = new ArrayList<>(); |
| |
| if (queryArgs.keySet().contains(QUERY_ARG_EXCLUDE_MEDIA)) { |
| args.add(QUERY_ARG_EXCLUDE_MEDIA); |
| } |
| |
| if (queryArgs.keySet().contains(QUERY_ARG_DISPLAY_NAME)) { |
| args.add(QUERY_ARG_DISPLAY_NAME); |
| } |
| |
| if (queryArgs.keySet().contains(QUERY_ARG_FILE_SIZE_OVER)) { |
| args.add(QUERY_ARG_FILE_SIZE_OVER); |
| } |
| |
| if (queryArgs.keySet().contains(QUERY_ARG_LAST_MODIFIED_AFTER)) { |
| args.add(QUERY_ARG_LAST_MODIFIED_AFTER); |
| } |
| |
| if (queryArgs.keySet().contains(QUERY_ARG_MIME_TYPES)) { |
| args.add(QUERY_ARG_MIME_TYPES); |
| } |
| return args.toArray(new String[0]); |
| } |
| |
| @Override |
| public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal) |
| throws FileNotFoundException { |
| enforceShellRestrictions(); |
| final Uri target = getUriForDocumentId(docId); |
| final int callingUid = Binder.getCallingUid(); |
| |
| if (!"r".equals(mode)) { |
| throw new IllegalArgumentException("Media is read-only"); |
| } |
| |
| // Delegate to real provider |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| return openFileForRead(target, callingUid); |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| public ParcelFileDescriptor openFileForRead(final Uri target, final int callingUid) |
| throws FileNotFoundException { |
| final Bundle opts = new Bundle(); |
| opts.putInt(MediaStore.EXTRA_MEDIA_CAPABILITIES_UID, callingUid); |
| |
| AssetFileDescriptor afd = |
| getContext().getContentResolver().openTypedAssetFileDescriptor(target, "*/*", |
| opts); |
| if (afd == null) { |
| return null; |
| } |
| |
| return afd.getParcelFileDescriptor(); |
| } |
| |
| @Override |
| public AssetFileDescriptor openDocumentThumbnail( |
| String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException { |
| enforceShellRestrictions(); |
| 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, sizeHint, signal); |
| } else if (TYPE_IMAGE.equals(ident.type)) { |
| return openOrCreateImageThumbnailCleared(ident.id, sizeHint, signal); |
| } else if (TYPE_VIDEOS_BUCKET.equals(ident.type)) { |
| final long id = getVideoForBucketCleared(ident.id); |
| return openOrCreateVideoThumbnailCleared(id, sizeHint, signal); |
| } else if (TYPE_VIDEO.equals(ident.type)) { |
| return openOrCreateVideoThumbnailCleared(ident.id, sizeHint, 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(); |
| Bundle extras = new Bundle(); |
| extras.putString(QUERY_ARG_SQL_LIMIT, "1"); |
| try (Cursor cursor = resolver.query(uri, new String[]{FileColumns._ID}, extras, null)) { |
| if (cursor.moveToFirst()) { |
| return cursor.getInt(0) == 0; |
| } else { |
| // No count information means we need to assume empty |
| return true; |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| private void includeImagesRoot(MatrixCursor result) { |
| int flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_RECENTS | Root.FLAG_SUPPORTS_SEARCH; |
| 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); |
| row.add(Root.COLUMN_QUERY_ARGS, SUPPORTED_QUERY_ARGS); |
| } |
| |
| private void includeVideosRoot(MatrixCursor result) { |
| int flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_RECENTS | Root.FLAG_SUPPORTS_SEARCH; |
| 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); |
| row.add(Root.COLUMN_QUERY_ARGS, SUPPORTED_QUERY_ARGS); |
| } |
| |
| private void includeAudioRoot(MatrixCursor result) { |
| int flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_SEARCH; |
| 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); |
| row.add(Root.COLUMN_QUERY_ARGS, SUPPORTED_QUERY_ARGS); |
| } |
| |
| private void includeDocumentsRoot(MatrixCursor result) { |
| int flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_RECENTS | Root.FLAG_SUPPORTS_SEARCH; |
| if (isEmpty(Files.EXTERNAL_CONTENT_URI)) { |
| flags |= Root.FLAG_EMPTY; |
| sReturnedDocumentsEmpty = true; |
| } |
| |
| final RowBuilder row = result.newRow(); |
| row.add(Root.COLUMN_ROOT_ID, TYPE_DOCUMENTS_ROOT); |
| row.add(Root.COLUMN_FLAGS, flags); |
| row.add(Root.COLUMN_TITLE, getContext().getString(R.string.root_documents)); |
| row.add(Root.COLUMN_DOCUMENT_ID, TYPE_DOCUMENTS_ROOT); |
| row.add(Root.COLUMN_MIME_TYPES, DOCUMENT_MIME_TYPES); |
| row.add(Root.COLUMN_QUERY_ARGS, SUPPORTED_QUERY_ARGS); |
| } |
| |
| 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 void includeDocumentsRootDocument(MatrixCursor result) { |
| final RowBuilder row = result.newRow(); |
| row.add(Document.COLUMN_DOCUMENT_ID, TYPE_DOCUMENTS_ROOT); |
| row.add(Document.COLUMN_DISPLAY_NAME, getContext().getString(R.string.root_documents)); |
| 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 interface ImagesBucketQuery { |
| final String[] PROJECTION = new String[] { |
| ImageColumns.BUCKET_ID, |
| ImageColumns.BUCKET_DISPLAY_NAME, |
| ImageColumns.DATE_MODIFIED, |
| ImageColumns.VOLUME_NAME }; |
| 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; |
| final int VOLUME_NAME = 3; |
| } |
| |
| 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, cleanUpMediaBucketName( |
| cursor.getString(ImagesBucketQuery.BUCKET_DISPLAY_NAME), |
| cursor.getString(ImagesBucketQuery.VOLUME_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 |
| | Document.FLAG_SUPPORTS_METADATA); |
| } |
| |
| private interface VideosBucketQuery { |
| final String[] PROJECTION = new String[] { |
| VideoColumns.BUCKET_ID, |
| VideoColumns.BUCKET_DISPLAY_NAME, |
| VideoColumns.DATE_MODIFIED, |
| VideoColumns.VOLUME_NAME }; |
| 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; |
| final int VOLUME_NAME = 3; |
| } |
| |
| 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, cleanUpMediaBucketName( |
| cursor.getString(VideosBucketQuery.BUCKET_DISPLAY_NAME), |
| cursor.getString(VideosBucketQuery.VOLUME_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 |
| | Document.FLAG_SUPPORTS_METADATA); |
| } |
| |
| private interface DocumentsBucketQuery { |
| final String[] PROJECTION = new String[] { |
| FileColumns.BUCKET_ID, |
| FileColumns.BUCKET_DISPLAY_NAME, |
| FileColumns.DATE_MODIFIED, |
| FileColumns.VOLUME_NAME }; |
| final String SORT_ORDER = FileColumns.BUCKET_ID + ", " + FileColumns.DATE_MODIFIED |
| + " DESC"; |
| |
| final int BUCKET_ID = 0; |
| final int BUCKET_DISPLAY_NAME = 1; |
| final int DATE_MODIFIED = 2; |
| final int VOLUME_NAME = 3; |
| } |
| |
| private void includeDocumentsBucket(MatrixCursor result, Cursor cursor) { |
| final long id = cursor.getLong(DocumentsBucketQuery.BUCKET_ID); |
| final String docId = getDocIdForIdent(TYPE_DOCUMENTS_BUCKET, id); |
| |
| final RowBuilder row = result.newRow(); |
| row.add(Document.COLUMN_DOCUMENT_ID, docId); |
| row.add(Document.COLUMN_DISPLAY_NAME, cleanUpMediaBucketName( |
| cursor.getString(DocumentsBucketQuery.BUCKET_DISPLAY_NAME), |
| cursor.getString(DocumentsBucketQuery.VOLUME_NAME))); |
| row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); |
| row.add(Document.COLUMN_LAST_MODIFIED, |
| cursor.getLong(DocumentsBucketQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS); |
| row.add(Document.COLUMN_FLAGS, |
| Document.FLAG_DIR_PREFERS_GRID | Document.FLAG_DIR_PREFERS_LAST_MODIFIED); |
| } |
| |
| private interface DocumentQuery { |
| final String[] PROJECTION = new String[] { |
| FileColumns._ID, |
| FileColumns.DISPLAY_NAME, |
| FileColumns.MIME_TYPE, |
| FileColumns.SIZE, |
| FileColumns.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 includeDocument(MatrixCursor result, Cursor cursor) { |
| final long id = cursor.getLong(DocumentQuery._ID); |
| final String docId = getDocIdForIdent(TYPE_DOCUMENT, id); |
| |
| final RowBuilder row = result.newRow(); |
| row.add(Document.COLUMN_DOCUMENT_ID, docId); |
| row.add(Document.COLUMN_DISPLAY_NAME, cursor.getString(DocumentQuery.DISPLAY_NAME)); |
| row.add(Document.COLUMN_SIZE, cursor.getLong(DocumentQuery.SIZE)); |
| row.add(Document.COLUMN_MIME_TYPE, cursor.getString(DocumentQuery.MIME_TYPE)); |
| row.add(Document.COLUMN_LAST_MODIFIED, |
| cursor.getLong(DocumentQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS); |
| row.add(Document.COLUMN_FLAGS, 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[] { |
| AlbumColumns.ALBUM_ID, |
| AlbumColumns.ALBUM }; |
| |
| final int ALBUM_ID = 0; |
| final int ALBUM = 1; |
| } |
| |
| private void includeAlbum(MatrixCursor result, Cursor cursor) { |
| final long id = cursor.getLong(AlbumQuery.ALBUM_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.DISPLAY_NAME, |
| AudioColumns.MIME_TYPE, |
| AudioColumns.SIZE, |
| AudioColumns.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 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.DISPLAY_NAME)); |
| 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 |
| | Document.FLAG_SUPPORTS_METADATA); |
| } |
| |
| 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 { |
| FileUtils.closeQuietly(cursor); |
| } |
| throw new FileNotFoundException("No video found for bucket"); |
| } |
| |
| private AssetFileDescriptor openOrCreateImageThumbnailCleared(long id, Point size, |
| CancellationSignal signal) throws FileNotFoundException { |
| final Bundle opts = new Bundle(); |
| opts.putParcelable(EXTRA_SIZE, size); |
| |
| final Uri uri = ContentUris.withAppendedId(Images.Media.EXTERNAL_CONTENT_URI, id); |
| return getContext().getContentResolver().openTypedAssetFile(uri, "image/*", opts, signal); |
| } |
| |
| 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 { |
| FileUtils.closeQuietly(cursor); |
| } |
| throw new FileNotFoundException("No video found for bucket"); |
| } |
| |
| private AssetFileDescriptor openOrCreateVideoThumbnailCleared(long id, Point size, |
| CancellationSignal signal) throws FileNotFoundException { |
| final Bundle opts = new Bundle(); |
| opts.putParcelable(EXTRA_SIZE, size); |
| |
| final Uri uri = ContentUris.withAppendedId(Video.Media.EXTERNAL_CONTENT_URI, id); |
| return getContext().getContentResolver().openTypedAssetFile(uri, "image/*", opts, signal); |
| } |
| |
| private String cleanUpMediaDisplayName(String displayName) { |
| if (!MediaStore.UNKNOWN_STRING.equals(displayName)) { |
| return displayName; |
| } |
| return getContext().getResources().getString(R.string.unknown); |
| } |
| |
| private String cleanUpMediaBucketName(String bucketDisplayName, String volumeName) { |
| if (!TextUtils.isEmpty(bucketDisplayName)) { |
| return bucketDisplayName; |
| } else if (!Objects.equals(volumeName, MediaStore.VOLUME_EXTERNAL_PRIMARY)) { |
| return volumeName; |
| } else { |
| return getContext().getResources().getString(R.string.unknown); |
| } |
| } |
| } |