/*
* Copyright (C) 2007 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.provider;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.UriPermission;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteException;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.media.MiniThumbFile;
import android.media.ThumbnailUtils;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.service.media.CameraPrewarmService;
import android.util.Log;
import libcore.io.IoUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.List;
/**
* The Media provider contains meta data for all available media on both internal
* and external storage devices.
*/
public final class MediaStore {
private final static String TAG = "MediaStore";
public static final String AUTHORITY = "media";
private static final String CONTENT_AUTHORITY_SLASH = "content://" + AUTHORITY + "/";
/**
* Broadcast Action: A broadcast to indicate the end of an MTP session with the host.
* This broadcast is only sent if MTP activity has modified the media database during the
* most recent MTP session.
*
* @hide
*/
public static final String ACTION_MTP_SESSION_END = "android.provider.action.MTP_SESSION_END";
/**
* The method name used by the media scanner and mtp to tell the media provider to
* rescan and reclassify that have become unhidden because of renaming folders or
* removing nomedia files
* @hide
*/
public static final String UNHIDE_CALL = "unhide";
/**
* This is for internal use by the media scanner only.
* Name of the (optional) Uri parameter that determines whether to skip deleting
* the file pointed to by the _data column, when deleting the database entry.
* The only appropriate value for this parameter is "false", in which case the
* delete will be skipped. Note especially that setting this to true, or omitting
* the parameter altogether, will perform the default action, which is different
* for different types of media.
* @hide
*/
public static final String PARAM_DELETE_DATA = "deletedata";
/**
* Activity Action: Launch a music player.
* The activity should be able to play, browse, or manipulate music files stored on the device.
*
* @deprecated Use {@link android.content.Intent#CATEGORY_APP_MUSIC} instead.
*/
@Deprecated
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String INTENT_ACTION_MUSIC_PLAYER = "android.intent.action.MUSIC_PLAYER";
/**
* Activity Action: Perform a search for media.
* Contains at least the {@link android.app.SearchManager#QUERY} extra.
* May also contain any combination of the following extras:
* EXTRA_MEDIA_ARTIST, EXTRA_MEDIA_ALBUM, EXTRA_MEDIA_TITLE, EXTRA_MEDIA_FOCUS
*
* @see android.provider.MediaStore#EXTRA_MEDIA_ARTIST
* @see android.provider.MediaStore#EXTRA_MEDIA_ALBUM
* @see android.provider.MediaStore#EXTRA_MEDIA_TITLE
* @see android.provider.MediaStore#EXTRA_MEDIA_FOCUS
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String INTENT_ACTION_MEDIA_SEARCH = "android.intent.action.MEDIA_SEARCH";
/**
* An intent to perform a search for music media and automatically play content from the
* result when possible. This can be fired, for example, by the result of a voice recognition
* command to listen to music.
*
This intent always includes the {@link android.provider.MediaStore#EXTRA_MEDIA_FOCUS}
* and {@link android.app.SearchManager#QUERY} extras. The
* {@link android.provider.MediaStore#EXTRA_MEDIA_FOCUS} extra determines the search mode, and
* the value of the {@link android.app.SearchManager#QUERY} extra depends on the search mode.
* For more information about the search modes for this intent, see
* Play music based
* on a search query in Common
* Intents.
*
* This intent makes the most sense for apps that can support large-scale search of music,
* such as services connected to an online database of music which can be streamed and played
* on the device.
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH =
"android.media.action.MEDIA_PLAY_FROM_SEARCH";
/**
* An intent to perform a search for readable media and automatically play content from the
* result when possible. This can be fired, for example, by the result of a voice recognition
* command to read a book or magazine.
*
* Contains the {@link android.app.SearchManager#QUERY} extra, which is a string that can
* contain any type of unstructured text search, like the name of a book or magazine, an author
* a genre, a publisher, or any combination of these.
*
* Because this intent includes an open-ended unstructured search string, it makes the most
* sense for apps that can support large-scale search of text media, such as services connected
* to an online database of books and/or magazines which can be read on the device.
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String INTENT_ACTION_TEXT_OPEN_FROM_SEARCH =
"android.media.action.TEXT_OPEN_FROM_SEARCH";
/**
* An intent to perform a search for video media and automatically play content from the
* result when possible. This can be fired, for example, by the result of a voice recognition
* command to play movies.
*
* Contains the {@link android.app.SearchManager#QUERY} extra, which is a string that can
* contain any type of unstructured video search, like the name of a movie, one or more actors,
* a genre, or any combination of these.
*
* Because this intent includes an open-ended unstructured search string, it makes the most
* sense for apps that can support large-scale search of video, such as services connected to an
* online database of videos which can be streamed and played on the device.
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String INTENT_ACTION_VIDEO_PLAY_FROM_SEARCH =
"android.media.action.VIDEO_PLAY_FROM_SEARCH";
/**
* The name of the Intent-extra used to define the artist
*/
public static final String EXTRA_MEDIA_ARTIST = "android.intent.extra.artist";
/**
* The name of the Intent-extra used to define the album
*/
public static final String EXTRA_MEDIA_ALBUM = "android.intent.extra.album";
/**
* The name of the Intent-extra used to define the song title
*/
public static final String EXTRA_MEDIA_TITLE = "android.intent.extra.title";
/**
* The name of the Intent-extra used to define the genre.
*/
public static final String EXTRA_MEDIA_GENRE = "android.intent.extra.genre";
/**
* The name of the Intent-extra used to define the playlist.
*/
public static final String EXTRA_MEDIA_PLAYLIST = "android.intent.extra.playlist";
/**
* The name of the Intent-extra used to define the radio channel.
*/
public static final String EXTRA_MEDIA_RADIO_CHANNEL = "android.intent.extra.radio_channel";
/**
* The name of the Intent-extra used to define the search focus. The search focus
* indicates whether the search should be for things related to the artist, album
* or song that is identified by the other extras.
*/
public static final String EXTRA_MEDIA_FOCUS = "android.intent.extra.focus";
/**
* The name of the Intent-extra used to control the orientation of a ViewImage or a MovieView.
* This is an int property that overrides the activity's requestedOrientation.
* @see android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
*/
public static final String EXTRA_SCREEN_ORIENTATION = "android.intent.extra.screenOrientation";
/**
* The name of an Intent-extra used to control the UI of a ViewImage.
* This is a boolean property that overrides the activity's default fullscreen state.
*/
public static final String EXTRA_FULL_SCREEN = "android.intent.extra.fullScreen";
/**
* The name of an Intent-extra used to control the UI of a ViewImage.
* This is a boolean property that specifies whether or not to show action icons.
*/
public static final String EXTRA_SHOW_ACTION_ICONS = "android.intent.extra.showActionIcons";
/**
* The name of the Intent-extra used to control the onCompletion behavior of a MovieView.
* This is a boolean property that specifies whether or not to finish the MovieView activity
* when the movie completes playing. The default value is true, which means to automatically
* exit the movie player activity when the movie completes playing.
*/
public static final String EXTRA_FINISH_ON_COMPLETION = "android.intent.extra.finishOnCompletion";
/**
* The name of the Intent action used to launch a camera in still image mode.
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String INTENT_ACTION_STILL_IMAGE_CAMERA = "android.media.action.STILL_IMAGE_CAMERA";
/**
* Name under which an activity handling {@link #INTENT_ACTION_STILL_IMAGE_CAMERA} or
* {@link #INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE} publishes the service name for its prewarm
* service.
*
* This meta-data should reference the fully qualified class name of the prewarm service
* extending {@link CameraPrewarmService}.
*
* The prewarm service will get bound and receive a prewarm signal
* {@link CameraPrewarmService#onPrewarm()} when a camera launch intent fire might be imminent.
* An application implementing a prewarm service should do the absolute minimum amount of work
* to initialize the camera in order to reduce startup time in likely case that shortly after a
* camera launch intent would be sent.
*/
public static final String META_DATA_STILL_IMAGE_CAMERA_PREWARM_SERVICE =
"android.media.still_image_camera_preview_service";
/**
* The name of the Intent action used to launch a camera in still image mode
* for use when the device is secured (e.g. with a pin, password, pattern,
* or face unlock). Applications responding to this intent must not expose
* any personal content like existing photos or videos on the device. The
* applications should be careful not to share any photo or video with other
* applications or internet. The activity should use {@link
* android.view.WindowManager.LayoutParams#FLAG_SHOW_WHEN_LOCKED} to display
* on top of the lock screen while secured. There is no activity stack when
* this flag is used, so launching more than one activity is strongly
* discouraged.
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE =
"android.media.action.STILL_IMAGE_CAMERA_SECURE";
/**
* The name of the Intent action used to launch a camera in video mode.
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String INTENT_ACTION_VIDEO_CAMERA = "android.media.action.VIDEO_CAMERA";
/**
* Standard Intent action that can be sent to have the camera application
* capture an image and return it.
*
* The caller may pass an extra EXTRA_OUTPUT to control where this image will be written.
* If the EXTRA_OUTPUT is not present, then a small sized image is returned as a Bitmap
* object in the extra field. This is useful for applications that only need a small image.
* If the EXTRA_OUTPUT is present, then the full-sized image will be written to the Uri
* value of EXTRA_OUTPUT.
* As of {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this uri can also be supplied through
* {@link android.content.Intent#setClipData(ClipData)}. If using this approach, you still must
* supply the uri through the EXTRA_OUTPUT field for compatibility with old applications.
* If you don't set a ClipData, it will be copied there for you when calling
* {@link Context#startActivity(Intent)}.
*
*
Note: if you app targets {@link android.os.Build.VERSION_CODES#M M} and above
* and declares as using the {@link android.Manifest.permission#CAMERA} permission which
* is not granted, then attempting to use this action will result in a {@link
* java.lang.SecurityException}.
*
* @see #EXTRA_OUTPUT
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public final static String ACTION_IMAGE_CAPTURE = "android.media.action.IMAGE_CAPTURE";
/**
* Intent action that can be sent to have the camera application capture an image and return
* it when the device is secured (e.g. with a pin, password, pattern, or face unlock).
* Applications responding to this intent must not expose any personal content like existing
* photos or videos on the device. The applications should be careful not to share any photo
* or video with other applications or internet. The activity should use {@link
* android.view.WindowManager.LayoutParams#FLAG_SHOW_WHEN_LOCKED} to display on top of the
* lock screen while secured. There is no activity stack when this flag is used, so
* launching more than one activity is strongly discouraged.
*
* The caller may pass an extra EXTRA_OUTPUT to control where this image will be written.
* If the EXTRA_OUTPUT is not present, then a small sized image is returned as a Bitmap
* object in the extra field. This is useful for applications that only need a small image.
* If the EXTRA_OUTPUT is present, then the full-sized image will be written to the Uri
* value of EXTRA_OUTPUT.
* As of {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this uri can also be supplied through
* {@link android.content.Intent#setClipData(ClipData)}. If using this approach, you still must
* supply the uri through the EXTRA_OUTPUT field for compatibility with old applications.
* If you don't set a ClipData, it will be copied there for you when calling
* {@link Context#startActivity(Intent)}.
*
* @see #ACTION_IMAGE_CAPTURE
* @see #EXTRA_OUTPUT
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_IMAGE_CAPTURE_SECURE =
"android.media.action.IMAGE_CAPTURE_SECURE";
/**
* Standard Intent action that can be sent to have the camera application
* capture a video and return it.
*
* The caller may pass in an extra EXTRA_VIDEO_QUALITY to control the video quality.
*
* The caller may pass in an extra EXTRA_OUTPUT to control
* where the video is written. If EXTRA_OUTPUT is not present the video will be
* written to the standard location for videos, and the Uri of that location will be
* returned in the data field of the Uri.
* As of {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this uri can also be supplied through
* {@link android.content.Intent#setClipData(ClipData)}. If using this approach, you still must
* supply the uri through the EXTRA_OUTPUT field for compatibility with old applications.
* If you don't set a ClipData, it will be copied there for you when calling
* {@link Context#startActivity(Intent)}.
*
*
Note: if you app targets {@link android.os.Build.VERSION_CODES#M M} and above
* and declares as using the {@link android.Manifest.permission#CAMERA} permission which
* is not granted, then atempting to use this action will result in a {@link
* java.lang.SecurityException}.
*
* @see #EXTRA_OUTPUT
* @see #EXTRA_VIDEO_QUALITY
* @see #EXTRA_SIZE_LIMIT
* @see #EXTRA_DURATION_LIMIT
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public final static String ACTION_VIDEO_CAPTURE = "android.media.action.VIDEO_CAPTURE";
/**
* The name of the Intent-extra used to control the quality of a recorded video. This is an
* integer property. Currently value 0 means low quality, suitable for MMS messages, and
* value 1 means high quality. In the future other quality levels may be added.
*/
public final static String EXTRA_VIDEO_QUALITY = "android.intent.extra.videoQuality";
/**
* Specify the maximum allowed size.
*/
public final static String EXTRA_SIZE_LIMIT = "android.intent.extra.sizeLimit";
/**
* Specify the maximum allowed recording duration in seconds.
*/
public final static String EXTRA_DURATION_LIMIT = "android.intent.extra.durationLimit";
/**
* The name of the Intent-extra used to indicate a content resolver Uri to be used to
* store the requested image or video.
*/
public final static String EXTRA_OUTPUT = "output";
/**
* The string that is used when a media attribute is not known. For example,
* if an audio file does not have any meta data, the artist and album columns
* will be set to this value.
*/
public static final String UNKNOWN_STRING = "";
/**
* Common fields for most MediaProvider tables
*/
public interface MediaColumns extends BaseColumns {
/**
* Path to the file on disk.
*
* Note that apps may not have filesystem permissions to directly access
* this path. Instead of trying to open this path directly, apps should
* use {@link ContentResolver#openFileDescriptor(Uri, String)} to gain
* access.
*
* Type: TEXT
*/
public static final String DATA = "_data";
/**
* The size of the file in bytes
*
Type: INTEGER (long)
*/
public static final String SIZE = "_size";
/**
* The display name of the file
* Type: TEXT
*/
public static final String DISPLAY_NAME = "_display_name";
/**
* The title of the content
* Type: TEXT
*/
public static final String TITLE = "title";
/**
* The time the file was added to the media provider
* Units are seconds since 1970.
* Type: INTEGER (long)
*/
public static final String DATE_ADDED = "date_added";
/**
* The time the file was last modified
* Units are seconds since 1970.
* NOTE: This is for internal use by the media scanner. Do not modify this field.
* Type: INTEGER (long)
*/
public static final String DATE_MODIFIED = "date_modified";
/**
* The MIME type of the file
* Type: TEXT
*/
public static final String MIME_TYPE = "mime_type";
/**
* The MTP object handle of a newly transfered file.
* Used to pass the new file's object handle through the media scanner
* from MTP to the media provider
* For internal use only by MTP, media scanner and media provider.
* Type: INTEGER
* @hide
*/
public static final String MEDIA_SCANNER_NEW_OBJECT_ID = "media_scanner_new_object_id";
/**
* Non-zero if the media file is drm-protected
* Type: INTEGER (boolean)
* @hide
*/
public static final String IS_DRM = "is_drm";
/**
* The width of the image/video in pixels.
*/
public static final String WIDTH = "width";
/**
* The height of the image/video in pixels.
*/
public static final String HEIGHT = "height";
}
/**
* Media provider table containing an index of all files in the media storage,
* including non-media files. This should be used by applications that work with
* non-media file types (text, HTML, PDF, etc) as well as applications that need to
* work with multiple media file types in a single query.
*/
public static final class Files {
/**
* Get the content:// style URI for the files table on the
* given volume.
*
* @param volumeName the name of the volume to get the URI for
* @return the URI to the files table on the given volume
*/
public static Uri getContentUri(String volumeName) {
return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName +
"/file");
}
/**
* Get the content:// style URI for a single row in the files table on the
* given volume.
*
* @param volumeName the name of the volume to get the URI for
* @param rowId the file to get the URI for
* @return the URI to the files table on the given volume
*/
public static final Uri getContentUri(String volumeName,
long rowId) {
return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName
+ "/file/" + rowId);
}
/**
* For use only by the MTP implementation.
* @hide
*/
public static Uri getMtpObjectsUri(String volumeName) {
return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName +
"/object");
}
/**
* For use only by the MTP implementation.
* @hide
*/
public static final Uri getMtpObjectsUri(String volumeName,
long fileId) {
return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName
+ "/object/" + fileId);
}
/**
* Used to implement the MTP GetObjectReferences and SetObjectReferences commands.
* @hide
*/
public static final Uri getMtpReferencesUri(String volumeName,
long fileId) {
return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName
+ "/object/" + fileId + "/references");
}
/**
* Used to trigger special logic for directories.
* @hide
*/
public static final Uri getDirectoryUri(String volumeName) {
return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName + "/dir");
}
/**
* Fields for master table for all media files.
* Table also contains MediaColumns._ID, DATA, SIZE and DATE_MODIFIED.
*/
public interface FileColumns extends MediaColumns {
/**
* The MTP storage ID of the file
* Type: INTEGER
* @hide
*/
public static final String STORAGE_ID = "storage_id";
/**
* The MTP format code of the file
* Type: INTEGER
* @hide
*/
public static final String FORMAT = "format";
/**
* The index of the parent directory of the file
* Type: INTEGER
*/
public static final String PARENT = "parent";
/**
* The MIME type of the file
* Type: TEXT
*/
public static final String MIME_TYPE = "mime_type";
/**
* The title of the content
* Type: TEXT
*/
public static final String TITLE = "title";
/**
* The media type (audio, video, image or playlist)
* of the file, or 0 for not a media file
* Type: TEXT
*/
public static final String MEDIA_TYPE = "media_type";
/**
* Constant for the {@link #MEDIA_TYPE} column indicating that file
* is not an audio, image, video or playlist file.
*/
public static final int MEDIA_TYPE_NONE = 0;
/**
* Constant for the {@link #MEDIA_TYPE} column indicating that file is an image file.
*/
public static final int MEDIA_TYPE_IMAGE = 1;
/**
* Constant for the {@link #MEDIA_TYPE} column indicating that file is an audio file.
*/
public static final int MEDIA_TYPE_AUDIO = 2;
/**
* Constant for the {@link #MEDIA_TYPE} column indicating that file is a video file.
*/
public static final int MEDIA_TYPE_VIDEO = 3;
/**
* Constant for the {@link #MEDIA_TYPE} column indicating that file is a playlist file.
*/
public static final int MEDIA_TYPE_PLAYLIST = 4;
}
}
/**
* This class is used internally by Images.Thumbnails and Video.Thumbnails, it's not intended
* to be accessed elsewhere.
*/
private static class InternalThumbnails implements BaseColumns {
private static final int MINI_KIND = 1;
private static final int FULL_SCREEN_KIND = 2;
private static final int MICRO_KIND = 3;
private static final String[] PROJECTION = new String[] {_ID, MediaColumns.DATA};
static final int DEFAULT_GROUP_ID = 0;
private static final Object sThumbBufLock = new Object();
private static byte[] sThumbBuf;
private static Bitmap getMiniThumbFromFile(
Cursor c, Uri baseUri, ContentResolver cr, BitmapFactory.Options options) {
Bitmap bitmap = null;
Uri thumbUri = null;
try {
long thumbId = c.getLong(0);
String filePath = c.getString(1);
thumbUri = ContentUris.withAppendedId(baseUri, thumbId);
ParcelFileDescriptor pfdInput = cr.openFileDescriptor(thumbUri, "r");
bitmap = BitmapFactory.decodeFileDescriptor(
pfdInput.getFileDescriptor(), null, options);
pfdInput.close();
} catch (FileNotFoundException ex) {
Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex);
} catch (IOException ex) {
Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex);
} catch (OutOfMemoryError ex) {
Log.e(TAG, "failed to allocate memory for thumbnail "
+ thumbUri + "; " + ex);
}
return bitmap;
}
/**
* This method cancels the thumbnail request so clients waiting for getThumbnail will be
* interrupted and return immediately. Only the original process which made the getThumbnail
* requests can cancel their own requests.
*
* @param cr ContentResolver
* @param origId original image or video id. use -1 to cancel all requests.
* @param groupId the same groupId used in getThumbnail
* @param baseUri the base URI of requested thumbnails
*/
static void cancelThumbnailRequest(ContentResolver cr, long origId, Uri baseUri,
long groupId) {
Uri cancelUri = baseUri.buildUpon().appendQueryParameter("cancel", "1")
.appendQueryParameter("orig_id", String.valueOf(origId))
.appendQueryParameter("group_id", String.valueOf(groupId)).build();
Cursor c = null;
try {
c = cr.query(cancelUri, PROJECTION, null, null, null);
}
finally {
if (c != null) c.close();
}
}
/**
* This method ensure thumbnails associated with origId are generated and decode the byte
* stream from database (MICRO_KIND) or file (MINI_KIND).
*
* Special optimization has been done to avoid further IPC communication for MICRO_KIND
* thumbnails.
*
* @param cr ContentResolver
* @param origId original image or video id
* @param kind could be MINI_KIND or MICRO_KIND
* @param options this is only used for MINI_KIND when decoding the Bitmap
* @param baseUri the base URI of requested thumbnails
* @param groupId the id of group to which this request belongs
* @return Bitmap bitmap of specified thumbnail kind
*/
static Bitmap getThumbnail(ContentResolver cr, long origId, long groupId, int kind,
BitmapFactory.Options options, Uri baseUri, boolean isVideo) {
Bitmap bitmap = null;
// Log.v(TAG, "getThumbnail: origId="+origId+", kind="+kind+", isVideo="+isVideo);
// If the magic is non-zero, we simply return thumbnail if it does exist.
// querying MediaProvider and simply return thumbnail.
MiniThumbFile thumbFile = new MiniThumbFile(isVideo ? Video.Media.EXTERNAL_CONTENT_URI
: Images.Media.EXTERNAL_CONTENT_URI);
Cursor c = null;
try {
long magic = thumbFile.getMagic(origId);
if (magic != 0) {
if (kind == MICRO_KIND) {
synchronized (sThumbBufLock) {
if (sThumbBuf == null) {
sThumbBuf = new byte[MiniThumbFile.BYTES_PER_MINTHUMB];
}
if (thumbFile.getMiniThumbFromFile(origId, sThumbBuf) != null) {
bitmap = BitmapFactory.decodeByteArray(sThumbBuf, 0, sThumbBuf.length);
if (bitmap == null) {
Log.w(TAG, "couldn't decode byte array.");
}
}
}
return bitmap;
} else if (kind == MINI_KIND) {
String column = isVideo ? "video_id=" : "image_id=";
c = cr.query(baseUri, PROJECTION, column + origId, null, null);
if (c != null && c.moveToFirst()) {
bitmap = getMiniThumbFromFile(c, baseUri, cr, options);
if (bitmap != null) {
return bitmap;
}
}
}
}
Uri blockingUri = baseUri.buildUpon().appendQueryParameter("blocking", "1")
.appendQueryParameter("orig_id", String.valueOf(origId))
.appendQueryParameter("group_id", String.valueOf(groupId)).build();
if (c != null) c.close();
c = cr.query(blockingUri, PROJECTION, null, null, null);
// This happens when original image/video doesn't exist.
if (c == null) return null;
// Assuming thumbnail has been generated, at least original image exists.
if (kind == MICRO_KIND) {
synchronized (sThumbBufLock) {
if (sThumbBuf == null) {
sThumbBuf = new byte[MiniThumbFile.BYTES_PER_MINTHUMB];
}
Arrays.fill(sThumbBuf, (byte)0);
if (thumbFile.getMiniThumbFromFile(origId, sThumbBuf) != null) {
bitmap = BitmapFactory.decodeByteArray(sThumbBuf, 0, sThumbBuf.length);
if (bitmap == null) {
Log.w(TAG, "couldn't decode byte array.");
}
}
}
} else if (kind == MINI_KIND) {
if (c.moveToFirst()) {
bitmap = getMiniThumbFromFile(c, baseUri, cr, options);
}
} else {
throw new IllegalArgumentException("Unsupported kind: " + kind);
}
// We probably run out of space, so create the thumbnail in memory.
if (bitmap == null) {
Log.v(TAG, "Create the thumbnail in memory: origId=" + origId
+ ", kind=" + kind + ", isVideo="+isVideo);
Uri uri = Uri.parse(
baseUri.buildUpon().appendPath(String.valueOf(origId))
.toString().replaceFirst("thumbnails", "media"));
if (c != null) c.close();
c = cr.query(uri, PROJECTION, null, null, null);
if (c == null || !c.moveToFirst()) {
return null;
}
String filePath = c.getString(1);
if (filePath != null) {
if (isVideo) {
bitmap = ThumbnailUtils.createVideoThumbnail(filePath, kind);
} else {
bitmap = ThumbnailUtils.createImageThumbnail(filePath, kind);
}
}
}
} catch (SQLiteException ex) {
Log.w(TAG, ex);
} finally {
if (c != null) c.close();
// To avoid file descriptor leak in application process.
thumbFile.deactivate();
thumbFile = null;
}
return bitmap;
}
}
/**
* Contains meta data for all available images.
*/
public static final class Images {
public interface ImageColumns extends MediaColumns {
/**
* The description of the image
* Type: TEXT
*/
public static final String DESCRIPTION = "description";
/**
* The picasa id of the image
* Type: TEXT
*/
public static final String PICASA_ID = "picasa_id";
/**
* Whether the video should be published as public or private
* Type: INTEGER
*/
public static final String IS_PRIVATE = "isprivate";
/**
* The latitude where the image was captured.
* Type: DOUBLE
*/
public static final String LATITUDE = "latitude";
/**
* The longitude where the image was captured.
* Type: DOUBLE
*/
public static final String LONGITUDE = "longitude";
/**
* The date & time that the image was taken in units
* of milliseconds since jan 1, 1970.
* Type: INTEGER
*/
public static final String DATE_TAKEN = "datetaken";
/**
* The orientation for the image expressed as degrees.
* Only degrees 0, 90, 180, 270 will work.
* Type: INTEGER
*/
public static final String ORIENTATION = "orientation";
/**
* The mini thumb id.
* Type: INTEGER
*/
public static final String MINI_THUMB_MAGIC = "mini_thumb_magic";
/**
* The bucket id of the image. This is a read-only property that
* is automatically computed from the DATA column.
* Type: TEXT
*/
public static final String BUCKET_ID = "bucket_id";
/**
* The bucket display name of the image. This is a read-only property that
* is automatically computed from the DATA column.
* Type: TEXT
*/
public static final String BUCKET_DISPLAY_NAME = "bucket_display_name";
}
public static final class Media implements ImageColumns {
public static final Cursor query(ContentResolver cr, Uri uri, String[] projection) {
return cr.query(uri, projection, null, null, DEFAULT_SORT_ORDER);
}
public static final Cursor query(ContentResolver cr, Uri uri, String[] projection,
String where, String orderBy) {
return cr.query(uri, projection, where,
null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy);
}
public static final Cursor query(ContentResolver cr, Uri uri, String[] projection,
String selection, String [] selectionArgs, String orderBy) {
return cr.query(uri, projection, selection,
selectionArgs, orderBy == null ? DEFAULT_SORT_ORDER : orderBy);
}
/**
* Retrieves an image for the given url as a {@link Bitmap}.
*
* @param cr The content resolver to use
* @param url The url of the image
* @throws FileNotFoundException
* @throws IOException
*/
public static final Bitmap getBitmap(ContentResolver cr, Uri url)
throws FileNotFoundException, IOException {
InputStream input = cr.openInputStream(url);
Bitmap bitmap = BitmapFactory.decodeStream(input);
input.close();
return bitmap;
}
/**
* Insert an image and create a thumbnail for it.
*
* @param cr The content resolver to use
* @param imagePath The path to the image to insert
* @param name The name of the image
* @param description The description of the image
* @return The URL to the newly created image
* @throws FileNotFoundException
*/
public static final String insertImage(ContentResolver cr, String imagePath,
String name, String description) throws FileNotFoundException {
// Check if file exists with a FileInputStream
FileInputStream stream = new FileInputStream(imagePath);
try {
Bitmap bm = BitmapFactory.decodeFile(imagePath);
String ret = insertImage(cr, bm, name, description);
bm.recycle();
return ret;
} finally {
try {
stream.close();
} catch (IOException e) {
}
}
}
private static final Bitmap StoreThumbnail(
ContentResolver cr,
Bitmap source,
long id,
float width, float height,
int kind) {
// create the matrix to scale it
Matrix matrix = new Matrix();
float scaleX = width / source.getWidth();
float scaleY = height / source.getHeight();
matrix.setScale(scaleX, scaleY);
Bitmap thumb = Bitmap.createBitmap(source, 0, 0,
source.getWidth(),
source.getHeight(), matrix,
true);
ContentValues values = new ContentValues(4);
values.put(Images.Thumbnails.KIND, kind);
values.put(Images.Thumbnails.IMAGE_ID, (int)id);
values.put(Images.Thumbnails.HEIGHT, thumb.getHeight());
values.put(Images.Thumbnails.WIDTH, thumb.getWidth());
Uri url = cr.insert(Images.Thumbnails.EXTERNAL_CONTENT_URI, values);
try {
OutputStream thumbOut = cr.openOutputStream(url);
thumb.compress(Bitmap.CompressFormat.JPEG, 100, thumbOut);
thumbOut.close();
return thumb;
}
catch (FileNotFoundException ex) {
return null;
}
catch (IOException ex) {
return null;
}
}
/**
* Insert an image and create a thumbnail for it.
*
* @param cr The content resolver to use
* @param source The stream to use for the image
* @param title The name of the image
* @param description The description of the image
* @return The URL to the newly created image, or null
if the image failed to be stored
* for any reason.
*/
public static final String insertImage(ContentResolver cr, Bitmap source,
String title, String description) {
ContentValues values = new ContentValues();
values.put(Images.Media.TITLE, title);
values.put(Images.Media.DESCRIPTION, description);
values.put(Images.Media.MIME_TYPE, "image/jpeg");
Uri url = null;
String stringUrl = null; /* value to be returned */
try {
url = cr.insert(EXTERNAL_CONTENT_URI, values);
if (source != null) {
OutputStream imageOut = cr.openOutputStream(url);
try {
source.compress(Bitmap.CompressFormat.JPEG, 50, imageOut);
} finally {
imageOut.close();
}
long id = ContentUris.parseId(url);
// Wait until MINI_KIND thumbnail is generated.
Bitmap miniThumb = Images.Thumbnails.getThumbnail(cr, id,
Images.Thumbnails.MINI_KIND, null);
// This is for backward compatibility.
Bitmap microThumb = StoreThumbnail(cr, miniThumb, id, 50F, 50F,
Images.Thumbnails.MICRO_KIND);
} else {
Log.e(TAG, "Failed to create thumbnail, removing original");
cr.delete(url, null, null);
url = null;
}
} catch (Exception e) {
Log.e(TAG, "Failed to insert image", e);
if (url != null) {
cr.delete(url, null, null);
url = null;
}
}
if (url != null) {
stringUrl = url.toString();
}
return stringUrl;
}
/**
* Get the content:// style URI for the image media table on the
* given volume.
*
* @param volumeName the name of the volume to get the URI for
* @return the URI to the image media table on the given volume
*/
public static Uri getContentUri(String volumeName) {
return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName +
"/images/media");
}
/**
* The content:// style URI for the internal storage.
*/
public static final Uri INTERNAL_CONTENT_URI =
getContentUri("internal");
/**
* The content:// style URI for the "primary" external storage
* volume.
*/
public static final Uri EXTERNAL_CONTENT_URI =
getContentUri("external");
/**
* The MIME type of of this directory of
* images. Note that each entry in this directory will have a standard
* image MIME type as appropriate -- for example, image/jpeg.
*/
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/image";
/**
* The default sort order for this table
*/
public static final String DEFAULT_SORT_ORDER = ImageColumns.BUCKET_DISPLAY_NAME;
}
/**
* This class allows developers to query and get two kinds of thumbnails:
* MINI_KIND: 512 x 384 thumbnail
* MICRO_KIND: 96 x 96 thumbnail
*/
public static class Thumbnails implements BaseColumns {
public static final Cursor query(ContentResolver cr, Uri uri, String[] projection) {
return cr.query(uri, projection, null, null, DEFAULT_SORT_ORDER);
}
public static final Cursor queryMiniThumbnails(ContentResolver cr, Uri uri, int kind,
String[] projection) {
return cr.query(uri, projection, "kind = " + kind, null, DEFAULT_SORT_ORDER);
}
public static final Cursor queryMiniThumbnail(ContentResolver cr, long origId, int kind,
String[] projection) {
return cr.query(EXTERNAL_CONTENT_URI, projection,
IMAGE_ID + " = " + origId + " AND " + KIND + " = " +
kind, null, null);
}
/**
* This method cancels the thumbnail request so clients waiting for getThumbnail will be
* interrupted and return immediately. Only the original process which made the getThumbnail
* requests can cancel their own requests.
*
* @param cr ContentResolver
* @param origId original image id
*/
public static void cancelThumbnailRequest(ContentResolver cr, long origId) {
InternalThumbnails.cancelThumbnailRequest(cr, origId, EXTERNAL_CONTENT_URI,
InternalThumbnails.DEFAULT_GROUP_ID);
}
/**
* This method checks if the thumbnails of the specified image (origId) has been created.
* It will be blocked until the thumbnails are generated.
*
* @param cr ContentResolver used to dispatch queries to MediaProvider.
* @param origId Original image id associated with thumbnail of interest.
* @param kind The type of thumbnail to fetch. Should be either MINI_KIND or MICRO_KIND.
* @param options this is only used for MINI_KIND when decoding the Bitmap
* @return A Bitmap instance. It could be null if the original image
* associated with origId doesn't exist or memory is not enough.
*/
public static Bitmap getThumbnail(ContentResolver cr, long origId, int kind,
BitmapFactory.Options options) {
return InternalThumbnails.getThumbnail(cr, origId,
InternalThumbnails.DEFAULT_GROUP_ID, kind, options,
EXTERNAL_CONTENT_URI, false);
}
/**
* This method cancels the thumbnail request so clients waiting for getThumbnail will be
* interrupted and return immediately. Only the original process which made the getThumbnail
* requests can cancel their own requests.
*
* @param cr ContentResolver
* @param origId original image id
* @param groupId the same groupId used in getThumbnail.
*/
public static void cancelThumbnailRequest(ContentResolver cr, long origId, long groupId) {
InternalThumbnails.cancelThumbnailRequest(cr, origId, EXTERNAL_CONTENT_URI, groupId);
}
/**
* This method checks if the thumbnails of the specified image (origId) has been created.
* It will be blocked until the thumbnails are generated.
*
* @param cr ContentResolver used to dispatch queries to MediaProvider.
* @param origId Original image id associated with thumbnail of interest.
* @param groupId the id of group to which this request belongs
* @param kind The type of thumbnail to fetch. Should be either MINI_KIND or MICRO_KIND.
* @param options this is only used for MINI_KIND when decoding the Bitmap
* @return A Bitmap instance. It could be null if the original image
* associated with origId doesn't exist or memory is not enough.
*/
public static Bitmap getThumbnail(ContentResolver cr, long origId, long groupId,
int kind, BitmapFactory.Options options) {
return InternalThumbnails.getThumbnail(cr, origId, groupId, kind, options,
EXTERNAL_CONTENT_URI, false);
}
/**
* Get the content:// style URI for the image media table on the
* given volume.
*
* @param volumeName the name of the volume to get the URI for
* @return the URI to the image media table on the given volume
*/
public static Uri getContentUri(String volumeName) {
return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName +
"/images/thumbnails");
}
/**
* The content:// style URI for the internal storage.
*/
public static final Uri INTERNAL_CONTENT_URI =
getContentUri("internal");
/**
* The content:// style URI for the "primary" external storage
* volume.
*/
public static final Uri EXTERNAL_CONTENT_URI =
getContentUri("external");
/**
* The default sort order for this table
*/
public static final String DEFAULT_SORT_ORDER = "image_id ASC";
/**
* Path to the thumbnail file on disk.
*
* Note that apps may not have filesystem permissions to directly
* access this path. Instead of trying to open this path directly,
* apps should use
* {@link ContentResolver#openFileDescriptor(Uri, String)} to gain
* access.
*
* Type: TEXT
*/
public static final String DATA = "_data";
/**
* The original image for the thumbnal
*
Type: INTEGER (ID from Images table)
*/
public static final String IMAGE_ID = "image_id";
/**
* The kind of the thumbnail
* Type: INTEGER (One of the values below)
*/
public static final String KIND = "kind";
public static final int MINI_KIND = 1;
public static final int FULL_SCREEN_KIND = 2;
public static final int MICRO_KIND = 3;
/**
* The blob raw data of thumbnail
* Type: DATA STREAM
*/
public static final String THUMB_DATA = "thumb_data";
/**
* The width of the thumbnal
* Type: INTEGER (long)
*/
public static final String WIDTH = "width";
/**
* The height of the thumbnail
* Type: INTEGER (long)
*/
public static final String HEIGHT = "height";
}
}
/**
* Container for all audio content.
*/
public static final class Audio {
/**
* Columns for audio file that show up in multiple tables.
*/
public interface AudioColumns extends MediaColumns {
/**
* A non human readable key calculated from the TITLE, used for
* searching, sorting and grouping
* Type: TEXT
*/
public static final String TITLE_KEY = "title_key";
/**
* The duration of the audio file, in ms
* Type: INTEGER (long)
*/
public static final String DURATION = "duration";
/**
* The position, in ms, playback was at when playback for this file
* was last stopped.
* Type: INTEGER (long)
*/
public static final String BOOKMARK = "bookmark";
/**
* The id of the artist who created the audio file, if any
* Type: INTEGER (long)
*/
public static final String ARTIST_ID = "artist_id";
/**
* The artist who created the audio file, if any
* Type: TEXT
*/
public static final String ARTIST = "artist";
/**
* The artist credited for the album that contains the audio file
* Type: TEXT
* @hide
*/
public static final String ALBUM_ARTIST = "album_artist";
/**
* Whether the song is part of a compilation
* Type: TEXT
* @hide
*/
public static final String COMPILATION = "compilation";
/**
* A non human readable key calculated from the ARTIST, used for
* searching, sorting and grouping
* Type: TEXT
*/
public static final String ARTIST_KEY = "artist_key";
/**
* The composer of the audio file, if any
* Type: TEXT
*/
public static final String COMPOSER = "composer";
/**
* The id of the album the audio file is from, if any
* Type: INTEGER (long)
*/
public static final String ALBUM_ID = "album_id";
/**
* The album the audio file is from, if any
* Type: TEXT
*/
public static final String ALBUM = "album";
/**
* A non human readable key calculated from the ALBUM, used for
* searching, sorting and grouping
* Type: TEXT
*/
public static final String ALBUM_KEY = "album_key";
/**
* The track number of this song on the album, if any.
* This number encodes both the track number and the
* disc number. For multi-disc sets, this number will
* be 1xxx for tracks on the first disc, 2xxx for tracks
* on the second disc, etc.
* Type: INTEGER
*/
public static final String TRACK = "track";
/**
* The year the audio file was recorded, if any
* Type: INTEGER
*/
public static final String YEAR = "year";
/**
* Non-zero if the audio file is music
* Type: INTEGER (boolean)
*/
public static final String IS_MUSIC = "is_music";
/**
* Non-zero if the audio file is a podcast
* Type: INTEGER (boolean)
*/
public static final String IS_PODCAST = "is_podcast";
/**
* Non-zero if the audio file may be a ringtone
* Type: INTEGER (boolean)
*/
public static final String IS_RINGTONE = "is_ringtone";
/**
* Non-zero if the audio file may be an alarm
* Type: INTEGER (boolean)
*/
public static final String IS_ALARM = "is_alarm";
/**
* Non-zero if the audio file may be a notification sound
* Type: INTEGER (boolean)
*/
public static final String IS_NOTIFICATION = "is_notification";
/**
* The genre of the audio file, if any
* Type: TEXT
* Does not exist in the database - only used by the media scanner for inserts.
* @hide
*/
public static final String GENRE = "genre";
}
/**
* Converts a name to a "key" that can be used for grouping, sorting
* and searching.
* The rules that govern this conversion are:
* - remove 'special' characters like ()[]'!?.,
* - remove leading/trailing spaces
* - convert everything to lowercase
* - remove leading "the ", "an " and "a "
* - remove trailing ", the|an|a"
* - remove accents. This step leaves us with CollationKey data,
* which is not human readable
*
* @param name The artist or album name to convert
* @return The "key" for the given name.
*/
public static String keyFor(String name) {
if (name != null) {
boolean sortfirst = false;
if (name.equals(UNKNOWN_STRING)) {
return "\001";
}
// Check if the first character is \001. We use this to
// force sorting of certain special files, like the silent ringtone.
if (name.startsWith("\001")) {
sortfirst = true;
}
name = name.trim().toLowerCase();
if (name.startsWith("the ")) {
name = name.substring(4);
}
if (name.startsWith("an ")) {
name = name.substring(3);
}
if (name.startsWith("a ")) {
name = name.substring(2);
}
if (name.endsWith(", the") || name.endsWith(",the") ||
name.endsWith(", an") || name.endsWith(",an") ||
name.endsWith(", a") || name.endsWith(",a")) {
name = name.substring(0, name.lastIndexOf(','));
}
name = name.replaceAll("[\\[\\]\\(\\)\"'.,?!]", "").trim();
if (name.length() > 0) {
// Insert a separator between the characters to avoid
// matches on a partial character. If we ever change
// to start-of-word-only matches, this can be removed.
StringBuilder b = new StringBuilder();
b.append('.');
int nl = name.length();
for (int i = 0; i < nl; i++) {
b.append(name.charAt(i));
b.append('.');
}
name = b.toString();
String key = DatabaseUtils.getCollationKey(name);
if (sortfirst) {
key = "\001" + key;
}
return key;
} else {
return "";
}
}
return null;
}
public static final class Media implements AudioColumns {
private static final String[] EXTERNAL_PATHS;
static {
String secondary_storage = System.getenv("SECONDARY_STORAGE");
if (secondary_storage != null) {
EXTERNAL_PATHS = secondary_storage.split(":");
} else {
EXTERNAL_PATHS = new String[0];
}
}
/**
* Get the content:// style URI for the audio media table on the
* given volume.
*
* @param volumeName the name of the volume to get the URI for
* @return the URI to the audio media table on the given volume
*/
public static Uri getContentUri(String volumeName) {
return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName +
"/audio/media");
}
public static Uri getContentUriForPath(String path) {
for (String ep : EXTERNAL_PATHS) {
if (path.startsWith(ep)) {
return EXTERNAL_CONTENT_URI;
}
}
return (path.startsWith(Environment.getExternalStorageDirectory().getPath()) ?
EXTERNAL_CONTENT_URI : INTERNAL_CONTENT_URI);
}
/**
* The content:// style URI for the internal storage.
*/
public static final Uri INTERNAL_CONTENT_URI =
getContentUri("internal");
/**
* The content:// style URI for the "primary" external storage
* volume.
*/
public static final Uri EXTERNAL_CONTENT_URI =
getContentUri("external");
/**
* The MIME type for this table.
*/
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/audio";
/**
* The MIME type for an audio track.
*/
public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/audio";
/**
* The default sort order for this table
*/
public static final String DEFAULT_SORT_ORDER = TITLE_KEY;
/**
* Activity Action: Start SoundRecorder application.
* Input: nothing.
*
Output: An uri to the recorded sound stored in the Media Library
* if the recording was successful.
* May also contain the extra EXTRA_MAX_BYTES.
* @see #EXTRA_MAX_BYTES
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String RECORD_SOUND_ACTION =
"android.provider.MediaStore.RECORD_SOUND";
/**
* The name of the Intent-extra used to define a maximum file size for
* a recording made by the SoundRecorder application.
*
* @see #RECORD_SOUND_ACTION
*/
public static final String EXTRA_MAX_BYTES =
"android.provider.MediaStore.extra.MAX_BYTES";
}
/**
* Columns representing an audio genre
*/
public interface GenresColumns {
/**
* The name of the genre
*
Type: TEXT
*/
public static final String NAME = "name";
}
/**
* Contains all genres for audio files
*/
public static final class Genres implements BaseColumns, GenresColumns {
/**
* Get the content:// style URI for the audio genres table on the
* given volume.
*
* @param volumeName the name of the volume to get the URI for
* @return the URI to the audio genres table on the given volume
*/
public static Uri getContentUri(String volumeName) {
return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName +
"/audio/genres");
}
/**
* Get the content:// style URI for querying the genres of an audio file.
*
* @param volumeName the name of the volume to get the URI for
* @param audioId the ID of the audio file for which to retrieve the genres
* @return the URI to for querying the genres for the audio file
* with the given the volume and audioID
*/
public static Uri getContentUriForAudioId(String volumeName, int audioId) {
return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName +
"/audio/media/" + audioId + "/genres");
}
/**
* The content:// style URI for the internal storage.
*/
public static final Uri INTERNAL_CONTENT_URI =
getContentUri("internal");
/**
* The content:// style URI for the "primary" external storage
* volume.
*/
public static final Uri EXTERNAL_CONTENT_URI =
getContentUri("external");
/**
* The MIME type for this table.
*/
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/genre";
/**
* The MIME type for entries in this table.
*/
public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/genre";
/**
* The default sort order for this table
*/
public static final String DEFAULT_SORT_ORDER = NAME;
/**
* Sub-directory of each genre containing all members.
*/
public static final class Members implements AudioColumns {
public static final Uri getContentUri(String volumeName,
long genreId) {
return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName
+ "/audio/genres/" + genreId + "/members");
}
/**
* A subdirectory of each genre containing all member audio files.
*/
public static final String CONTENT_DIRECTORY = "members";
/**
* The default sort order for this table
*/
public static final String DEFAULT_SORT_ORDER = TITLE_KEY;
/**
* The ID of the audio file
* Type: INTEGER (long)
*/
public static final String AUDIO_ID = "audio_id";
/**
* The ID of the genre
* Type: INTEGER (long)
*/
public static final String GENRE_ID = "genre_id";
}
}
/**
* Columns representing a playlist
*/
public interface PlaylistsColumns {
/**
* The name of the playlist
* Type: TEXT
*/
public static final String NAME = "name";
/**
* Path to the playlist file on disk.
*
* Note that apps may not have filesystem permissions to directly
* access this path. Instead of trying to open this path directly,
* apps should use
* {@link ContentResolver#openFileDescriptor(Uri, String)} to gain
* access.
*
* Type: TEXT
*/
public static final String DATA = "_data";
/**
* The time the file was added to the media provider
* Units are seconds since 1970.
*
Type: INTEGER (long)
*/
public static final String DATE_ADDED = "date_added";
/**
* The time the file was last modified
* Units are seconds since 1970.
* NOTE: This is for internal use by the media scanner. Do not modify this field.
* Type: INTEGER (long)
*/
public static final String DATE_MODIFIED = "date_modified";
}
/**
* Contains playlists for audio files
*/
public static final class Playlists implements BaseColumns,
PlaylistsColumns {
/**
* Get the content:// style URI for the audio playlists table on the
* given volume.
*
* @param volumeName the name of the volume to get the URI for
* @return the URI to the audio playlists table on the given volume
*/
public static Uri getContentUri(String volumeName) {
return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName +
"/audio/playlists");
}
/**
* The content:// style URI for the internal storage.
*/
public static final Uri INTERNAL_CONTENT_URI =
getContentUri("internal");
/**
* The content:// style URI for the "primary" external storage
* volume.
*/
public static final Uri EXTERNAL_CONTENT_URI =
getContentUri("external");
/**
* The MIME type for this table.
*/
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/playlist";
/**
* The MIME type for entries in this table.
*/
public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/playlist";
/**
* The default sort order for this table
*/
public static final String DEFAULT_SORT_ORDER = NAME;
/**
* Sub-directory of each playlist containing all members.
*/
public static final class Members implements AudioColumns {
public static final Uri getContentUri(String volumeName,
long playlistId) {
return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName
+ "/audio/playlists/" + playlistId + "/members");
}
/**
* Convenience method to move a playlist item to a new location
* @param res The content resolver to use
* @param playlistId The numeric id of the playlist
* @param from The position of the item to move
* @param to The position to move the item to
* @return true on success
*/
public static final boolean moveItem(ContentResolver res,
long playlistId, int from, int to) {
Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external",
playlistId)
.buildUpon()
.appendEncodedPath(String.valueOf(from))
.appendQueryParameter("move", "true")
.build();
ContentValues values = new ContentValues();
values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, to);
return res.update(uri, values, null, null) != 0;
}
/**
* The ID within the playlist.
*/
public static final String _ID = "_id";
/**
* A subdirectory of each playlist containing all member audio
* files.
*/
public static final String CONTENT_DIRECTORY = "members";
/**
* The ID of the audio file
* Type: INTEGER (long)
*/
public static final String AUDIO_ID = "audio_id";
/**
* The ID of the playlist
* Type: INTEGER (long)
*/
public static final String PLAYLIST_ID = "playlist_id";
/**
* The order of the songs in the playlist
* Type: INTEGER (long)>
*/
public static final String PLAY_ORDER = "play_order";
/**
* The default sort order for this table
*/
public static final String DEFAULT_SORT_ORDER = PLAY_ORDER;
}
}
/**
* Columns representing an artist
*/
public interface ArtistColumns {
/**
* The artist who created the audio file, if any
* Type: TEXT
*/
public static final String ARTIST = "artist";
/**
* A non human readable key calculated from the ARTIST, used for
* searching, sorting and grouping
* Type: TEXT
*/
public static final String ARTIST_KEY = "artist_key";
/**
* The number of albums in the database for this artist
*/
public static final String NUMBER_OF_ALBUMS = "number_of_albums";
/**
* The number of albums in the database for this artist
*/
public static final String NUMBER_OF_TRACKS = "number_of_tracks";
}
/**
* Contains artists for audio files
*/
public static final class Artists implements BaseColumns, ArtistColumns {
/**
* Get the content:// style URI for the artists table on the
* given volume.
*
* @param volumeName the name of the volume to get the URI for
* @return the URI to the audio artists table on the given volume
*/
public static Uri getContentUri(String volumeName) {
return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName +
"/audio/artists");
}
/**
* The content:// style URI for the internal storage.
*/
public static final Uri INTERNAL_CONTENT_URI =
getContentUri("internal");
/**
* The content:// style URI for the "primary" external storage
* volume.
*/
public static final Uri EXTERNAL_CONTENT_URI =
getContentUri("external");
/**
* The MIME type for this table.
*/
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/artists";
/**
* The MIME type for entries in this table.
*/
public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/artist";
/**
* The default sort order for this table
*/
public static final String DEFAULT_SORT_ORDER = ARTIST_KEY;
/**
* Sub-directory of each artist containing all albums on which
* a song by the artist appears.
*/
public static final class Albums implements AlbumColumns {
public static final Uri getContentUri(String volumeName,
long artistId) {
return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName
+ "/audio/artists/" + artistId + "/albums");
}
}
}
/**
* Columns representing an album
*/
public interface AlbumColumns {
/**
* The id for the album
* Type: INTEGER
*/
public static final String ALBUM_ID = "album_id";
/**
* The album on which the audio file appears, if any
* Type: TEXT
*/
public static final String ALBUM = "album";
/**
* The artist whose songs appear on this album
* Type: TEXT
*/
public static final String ARTIST = "artist";
/**
* The number of songs on this album
* Type: INTEGER
*/
public static final String NUMBER_OF_SONGS = "numsongs";
/**
* This column is available when getting album info via artist,
* and indicates the number of songs on the album by the given
* artist.
* Type: INTEGER
*/
public static final String NUMBER_OF_SONGS_FOR_ARTIST = "numsongs_by_artist";
/**
* The year in which the earliest songs
* on this album were released. This will often
* be the same as {@link #LAST_YEAR}, but for compilation albums
* they might differ.
* Type: INTEGER
*/
public static final String FIRST_YEAR = "minyear";
/**
* The year in which the latest songs
* on this album were released. This will often
* be the same as {@link #FIRST_YEAR}, but for compilation albums
* they might differ.
* Type: INTEGER
*/
public static final String LAST_YEAR = "maxyear";
/**
* A non human readable key calculated from the ALBUM, used for
* searching, sorting and grouping
* Type: TEXT
*/
public static final String ALBUM_KEY = "album_key";
/**
* Cached album art.
* Type: TEXT
*/
public static final String ALBUM_ART = "album_art";
}
/**
* Contains artists for audio files
*/
public static final class Albums implements BaseColumns, AlbumColumns {
/**
* Get the content:// style URI for the albums table on the
* given volume.
*
* @param volumeName the name of the volume to get the URI for
* @return the URI to the audio albums table on the given volume
*/
public static Uri getContentUri(String volumeName) {
return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName +
"/audio/albums");
}
/**
* The content:// style URI for the internal storage.
*/
public static final Uri INTERNAL_CONTENT_URI =
getContentUri("internal");
/**
* The content:// style URI for the "primary" external storage
* volume.
*/
public static final Uri EXTERNAL_CONTENT_URI =
getContentUri("external");
/**
* The MIME type for this table.
*/
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/albums";
/**
* The MIME type for entries in this table.
*/
public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/album";
/**
* The default sort order for this table
*/
public static final String DEFAULT_SORT_ORDER = ALBUM_KEY;
}
public static final class Radio {
/**
* The MIME type for entries in this table.
*/
public static final String ENTRY_CONTENT_TYPE = "vnd.android.cursor.item/radio";
// Not instantiable.
private Radio() { }
}
}
public static final class Video {
/**
* The default sort order for this table.
*/
public static final String DEFAULT_SORT_ORDER = MediaColumns.DISPLAY_NAME;
public static final Cursor query(ContentResolver cr, Uri uri, String[] projection) {
return cr.query(uri, projection, null, null, DEFAULT_SORT_ORDER);
}
public interface VideoColumns extends MediaColumns {
/**
* The duration of the video file, in ms
* Type: INTEGER (long)
*/
public static final String DURATION = "duration";
/**
* The artist who created the video file, if any
* Type: TEXT
*/
public static final String ARTIST = "artist";
/**
* The album the video file is from, if any
* Type: TEXT
*/
public static final String ALBUM = "album";
/**
* The resolution of the video file, formatted as "XxY"
* Type: TEXT
*/
public static final String RESOLUTION = "resolution";
/**
* The description of the video recording
* Type: TEXT
*/
public static final String DESCRIPTION = "description";
/**
* Whether the video should be published as public or private
* Type: INTEGER
*/
public static final String IS_PRIVATE = "isprivate";
/**
* The user-added tags associated with a video
* Type: TEXT
*/
public static final String TAGS = "tags";
/**
* The YouTube category of the video
* Type: TEXT
*/
public static final String CATEGORY = "category";
/**
* The language of the video
* Type: TEXT
*/
public static final String LANGUAGE = "language";
/**
* The latitude where the video was captured.
* Type: DOUBLE
*/
public static final String LATITUDE = "latitude";
/**
* The longitude where the video was captured.
* Type: DOUBLE
*/
public static final String LONGITUDE = "longitude";
/**
* The date & time that the video was taken in units
* of milliseconds since jan 1, 1970.
* Type: INTEGER
*/
public static final String DATE_TAKEN = "datetaken";
/**
* The mini thumb id.
* Type: INTEGER
*/
public static final String MINI_THUMB_MAGIC = "mini_thumb_magic";
/**
* The bucket id of the video. This is a read-only property that
* is automatically computed from the DATA column.
* Type: TEXT
*/
public static final String BUCKET_ID = "bucket_id";
/**
* The bucket display name of the video. This is a read-only property that
* is automatically computed from the DATA column.
* Type: TEXT
*/
public static final String BUCKET_DISPLAY_NAME = "bucket_display_name";
/**
* The bookmark for the video. Time in ms. Represents the location in the video that the
* video should start playing at the next time it is opened. If the value is null or
* out of the range 0..DURATION-1 then the video should start playing from the
* beginning.
* Type: INTEGER
*/
public static final String BOOKMARK = "bookmark";
}
public static final class Media implements VideoColumns {
/**
* Get the content:// style URI for the video media table on the
* given volume.
*
* @param volumeName the name of the volume to get the URI for
* @return the URI to the video media table on the given volume
*/
public static Uri getContentUri(String volumeName) {
return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName +
"/video/media");
}
/**
* The content:// style URI for the internal storage.
*/
public static final Uri INTERNAL_CONTENT_URI =
getContentUri("internal");
/**
* The content:// style URI for the "primary" external storage
* volume.
*/
public static final Uri EXTERNAL_CONTENT_URI =
getContentUri("external");
/**
* The MIME type for this table.
*/
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/video";
/**
* The default sort order for this table
*/
public static final String DEFAULT_SORT_ORDER = TITLE;
}
/**
* This class allows developers to query and get two kinds of thumbnails:
* MINI_KIND: 512 x 384 thumbnail
* MICRO_KIND: 96 x 96 thumbnail
*
*/
public static class Thumbnails implements BaseColumns {
/**
* This method cancels the thumbnail request so clients waiting for getThumbnail will be
* interrupted and return immediately. Only the original process which made the getThumbnail
* requests can cancel their own requests.
*
* @param cr ContentResolver
* @param origId original video id
*/
public static void cancelThumbnailRequest(ContentResolver cr, long origId) {
InternalThumbnails.cancelThumbnailRequest(cr, origId, EXTERNAL_CONTENT_URI,
InternalThumbnails.DEFAULT_GROUP_ID);
}
/**
* This method checks if the thumbnails of the specified image (origId) has been created.
* It will be blocked until the thumbnails are generated.
*
* @param cr ContentResolver used to dispatch queries to MediaProvider.
* @param origId Original image id associated with thumbnail of interest.
* @param kind The type of thumbnail to fetch. Should be either MINI_KIND or MICRO_KIND.
* @param options this is only used for MINI_KIND when decoding the Bitmap
* @return A Bitmap instance. It could be null if the original image
* associated with origId doesn't exist or memory is not enough.
*/
public static Bitmap getThumbnail(ContentResolver cr, long origId, int kind,
BitmapFactory.Options options) {
return InternalThumbnails.getThumbnail(cr, origId,
InternalThumbnails.DEFAULT_GROUP_ID, kind, options,
EXTERNAL_CONTENT_URI, true);
}
/**
* This method checks if the thumbnails of the specified image (origId) has been created.
* It will be blocked until the thumbnails are generated.
*
* @param cr ContentResolver used to dispatch queries to MediaProvider.
* @param origId Original image id associated with thumbnail of interest.
* @param groupId the id of group to which this request belongs
* @param kind The type of thumbnail to fetch. Should be either MINI_KIND or MICRO_KIND
* @param options this is only used for MINI_KIND when decoding the Bitmap
* @return A Bitmap instance. It could be null if the original image associated with
* origId doesn't exist or memory is not enough.
*/
public static Bitmap getThumbnail(ContentResolver cr, long origId, long groupId,
int kind, BitmapFactory.Options options) {
return InternalThumbnails.getThumbnail(cr, origId, groupId, kind, options,
EXTERNAL_CONTENT_URI, true);
}
/**
* This method cancels the thumbnail request so clients waiting for getThumbnail will be
* interrupted and return immediately. Only the original process which made the getThumbnail
* requests can cancel their own requests.
*
* @param cr ContentResolver
* @param origId original video id
* @param groupId the same groupId used in getThumbnail.
*/
public static void cancelThumbnailRequest(ContentResolver cr, long origId, long groupId) {
InternalThumbnails.cancelThumbnailRequest(cr, origId, EXTERNAL_CONTENT_URI, groupId);
}
/**
* Get the content:// style URI for the image media table on the
* given volume.
*
* @param volumeName the name of the volume to get the URI for
* @return the URI to the image media table on the given volume
*/
public static Uri getContentUri(String volumeName) {
return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName +
"/video/thumbnails");
}
/**
* The content:// style URI for the internal storage.
*/
public static final Uri INTERNAL_CONTENT_URI =
getContentUri("internal");
/**
* The content:// style URI for the "primary" external storage
* volume.
*/
public static final Uri EXTERNAL_CONTENT_URI =
getContentUri("external");
/**
* The default sort order for this table
*/
public static final String DEFAULT_SORT_ORDER = "video_id ASC";
/**
* Path to the thumbnail file on disk.
*
* Note that apps may not have filesystem permissions to directly
* access this path. Instead of trying to open this path directly,
* apps should use
* {@link ContentResolver#openFileDescriptor(Uri, String)} to gain
* access.
*
* Type: TEXT
*/
public static final String DATA = "_data";
/**
* The original image for the thumbnal
*
Type: INTEGER (ID from Video table)
*/
public static final String VIDEO_ID = "video_id";
/**
* The kind of the thumbnail
* Type: INTEGER (One of the values below)
*/
public static final String KIND = "kind";
public static final int MINI_KIND = 1;
public static final int FULL_SCREEN_KIND = 2;
public static final int MICRO_KIND = 3;
/**
* The width of the thumbnal
* Type: INTEGER (long)
*/
public static final String WIDTH = "width";
/**
* The height of the thumbnail
* Type: INTEGER (long)
*/
public static final String HEIGHT = "height";
}
}
/**
* Uri for querying the state of the media scanner.
*/
public static Uri getMediaScannerUri() {
return Uri.parse(CONTENT_AUTHORITY_SLASH + "none/media_scanner");
}
/**
* Name of current volume being scanned by the media scanner.
*/
public static final String MEDIA_SCANNER_VOLUME = "volume";
/**
* Name of the file signaling the media scanner to ignore media in the containing directory
* and its subdirectories. Developers should use this to avoid application graphics showing
* up in the Gallery and likewise prevent application sounds and music from showing up in
* the Music app.
*/
public static final String MEDIA_IGNORE_FILENAME = ".nomedia";
/**
* Get the media provider's version.
* Applications that import data from the media provider into their own caches
* can use this to detect that the media provider changed, and reimport data
* as needed. No other assumptions should be made about the meaning of the version.
* @param context Context to use for performing the query.
* @return A version string, or null if the version could not be determined.
*/
public static String getVersion(Context context) {
Cursor c = context.getContentResolver().query(
Uri.parse(CONTENT_AUTHORITY_SLASH + "none/version"),
null, null, null, null);
if (c != null) {
try {
if (c.moveToFirst()) {
return c.getString(0);
}
} finally {
c.close();
}
}
return null;
}
/**
* Gets a URI backed by a {@link DocumentsProvider} that points to the same media
* file as the specified mediaUri. This allows apps who have permissions to access
* media files in Storage Access Framework to perform file operations through that
* on media files.
*
* Note: this method doesn't grant any URI permission. Callers need to obtain
* permission before calling this method. One way to obtain permission is through
* a 3-step process:
*
* - Call {@link android.os.storage.StorageManager#getStorageVolume(File)} to
* obtain the {@link android.os.storage.StorageVolume} of a media file;
*
* - Invoke the intent returned by
* {@link android.os.storage.StorageVolume#createAccessIntent(String)} to
* obtain the access of the volume or one of its specific subdirectories;
*
* - Check whether permission is granted and take persistent permission.
*
* @param mediaUri the media URI which document URI is requested
* @return the document URI
*/
public static Uri getDocumentUri(Context context, Uri mediaUri) {
try {
final ContentResolver resolver = context.getContentResolver();
final String path = getFilePath(resolver, mediaUri);
final List uriPermissions = resolver.getPersistedUriPermissions();
return getDocumentUri(resolver, path, uriPermissions);
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
}
private static String getFilePath(ContentResolver resolver, Uri mediaUri)
throws RemoteException {
try (ContentProviderClient client =
resolver.acquireUnstableContentProviderClient(AUTHORITY)) {
final Cursor c = client.query(
mediaUri,
new String[]{ MediaColumns.DATA },
null, /* selection */
null, /* selectionArg */
null /* sortOrder */);
final String path;
try {
if (c.getCount() == 0) {
throw new IllegalStateException("Not found media file under URI: " + mediaUri);
}
if (!c.moveToFirst()) {
throw new IllegalStateException("Failed to move cursor to the first item.");
}
path = c.getString(0);
} finally {
IoUtils.closeQuietly(c);
}
return path;
}
}
private static Uri getDocumentUri(
ContentResolver resolver, String path, List uriPermissions)
throws RemoteException {
try (ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) {
final Bundle in = new Bundle();
in.putParcelableList(
DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY + ".extra.uriPermissions",
uriPermissions);
final Bundle out = client.call("getDocumentId", path, in);
return out.getParcelable(DocumentsContract.EXTRA_URI);
}
}
}