Android 共享存储空间

访问共享存储空间中的媒体文件

1、MediaStore概述

MediaStore是android系统提供的一个多媒体数据库,专门用于存放多媒体信息的,通过ContentResolver即可对数据库进行操作。

MediaStore.Files: 共享的文件,包括多媒体和非多媒体信息;MediaStore.Audio: 存放音频信息;MediaStore.Image: 存放图片信息;MediaStore.Vedio: 存放视频信息;

每个内部类中都又包含了Media,Thumbnails和相应的MediaColumns,分别提供了媒体信息,缩略信息和操作字段。

(1)MediaStore.Images.Media的使用——MediaStore.Images.Media.EXTERNAL_CONTENT_URI

(2)MediaStore.Video.Media的使用——MediaStore.Video.Media.EXTERNAL_CONTENT_URI

(3)MediaStore.Audio.Media的使用——MediaStore.Audio.Media.EXTERNAL_CONTENT_URI

(4)MediaStore.Downloads的使用——MediaStore.Downloads.getContentUri(“external”)

(5)MediaStore.Files的使用——MediaStore.Files.getContentUri(“external”)

2、示例

1、Android保存图片到相册,兼容Android10及以上版本:

// 保存bitmap到相册,并兼容AndroidQ

public static boolean saveBitmap(Context context, Bitmap bitmap) {

if (bitmap == null) {

return false;

}

boolean isSaveSuccess;

if (Build.VERSION.SDK_INT < 29) {

isSaveSuccess = saveImageToGallery(context, bitmap);

} else {

isSaveSuccess = saveImageToGalleryQ(context, bitmap);

}

return isSaveSuccess;

}

/**

* android 10 以下版本

*/

private static boolean saveImageToGallery(Context context, Bitmap image) {

// 首先保存图片

String storePath = AssetPathUtil.INSTANCE.getBitmapFileDir();

File appDir = new File(storePath);

if (!appDir.exists()) {

appDir.mkdir();

}

String fileName = getCharacterAndNumber() + ".png";

File file = new File(appDir, fileName);

try {

FileOutputStream fos = new FileOutputStream(file);

// 通过io流的方式来压缩保存图片

boolean isSuccess = image.compress(Bitmap.CompressFormat.JPEG, 100, fos);

fos.flush();

fos.close();

// 保存图片后发送广播通知更新数据库

Uri uri = Uri.fromFile(file);

context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));

if (isSuccess) {

return true;

} else {

return false;

}

} catch (IOException e) {

e.printStackTrace();

}

return false;

}

/**

* android 10 以上版本

*/

private static boolean saveImageToGalleryQ(Context context, Bitmap image) {

long mImageTime = System.currentTimeMillis();

String mImageFileName = getCharacterAndNumber() + ".png";

final ContentValues values = new ContentValues();

values.put(MediaStore.MediaColumns.RELATIVE_PATH, AssetPathUtil.INSTANCE.getBitmapFileDir());

values.put(MediaStore.MediaColumns.DISPLAY_NAME, mImageFileName);

values.put(MediaStore.MediaColumns.MIME_TYPE, "image/png");

values.put(MediaStore.MediaColumns.DATE_ADDED, mImageTime / 1000);

values.put(MediaStore.MediaColumns.DATE_MODIFIED, mImageTime / 1000);

values.put(MediaStore.MediaColumns.DATE_EXPIRES, (mImageTime + DateUtils.DAY_IN_MILLIS) / 1000);

values.put(MediaStore.MediaColumns.IS_PENDING, 1);

ContentResolver resolver = context.getContentResolver();

final Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);

try {

OutputStream out = resolver.openOutputStream(uri);

if (!image.compress(Bitmap.CompressFormat.PNG, 100, out)) {

return false;

}

values.clear();

values.put(MediaStore.MediaColumns.IS_PENDING, 0);

values.putNull(MediaStore.MediaColumns.DATE_EXPIRES);

resolver.update(uri, values, null, null);

} catch (Exception e) {

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {

resolver.delete(uri, null);

}

return false;

}

return true;

}

2、删除相册资源(Kotlin):

/**

* 保存bitmap到相册,并兼容AndroidQ

*/

fun getBitmapFileDir(): String {

return if (Build.VERSION.SDK_INT < 29) {

// android 10 以下版本

Environment.getExternalStorageDirectory().absolutePath + File.separator + BITMAP_FILE_DIRECTORY

} else {

Environment.DIRECTORY_PICTURES + File.separator + BITMAP_FILE_DIRECTORY

}

}

/**

* 删除保存在【相册指定目录】中的图片

*/

fun deleteBitmapFileDir(context: Context) {

if (Build.VERSION.SDK_INT < 29) {

// android 10 以下版本

val path = getBitmapFileDir()

FileUtils.deleteAllInDir(path)

} else {

deleteImages(context)

}

}

/**

* android 10及以上版本通过ContentResolver删除指定的目录

*/

private fun deleteImages(context: Context) {

var relativePath = getBitmapFileDir()

//判断是否有加斜杠

if (!relativePath.endsWith("/")) {

relativePath += File.separator

}

val projection = arrayOf(MediaStore.Images.Media._ID, MediaStore.Images.Media.RELATIVE_PATH)

val selection = "${MediaStore.Images.Media.RELATIVE_PATH}=?"

val selectionArgs = arrayOf(relativePath)

context.contentResolver.delete(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection, arrayOf(relativePath))

}

3、MediaStore 扩展方法:

/**

* 和媒体相关的工具类

*/

public class MediaUtils {

/**

* 每页获取的媒体数据量(分页加载解决相册卡顿问题)

* */

private static final int PAGE_SIZE = 1000;

/**

* 添加到媒体数据库

* Add to media database

*/

public static Uri saveVideo2Album(String videoPath, int videoWidth, int videoHeight,

int videoTime) {

File file = new File(videoPath);

if (file.exists()) {

Uri uri = null;

long dateTaken = System.currentTimeMillis();

ContentValues values = new ContentValues(11);

// 路径;

values.put(MediaStore.Video.Media.DATA, videoPath);

// 标题;

values.put(MediaStore.Video.Media.TITLE, file.getName());

// 时长

values.put(MediaStore.Video.Media.DURATION, videoTime * 1000);

// 视频宽

values.put(MediaStore.Video.Media.WIDTH, videoWidth);

// 视频高

values.put(MediaStore.Video.Media.HEIGHT, videoHeight);

// 视频大小;

values.put(MediaStore.Video.Media.SIZE, file.length());

// 插入时间;

values.put(MediaStore.Video.Media.DATE_TAKEN, dateTaken);

// 文件名;

values.put(MediaStore.Video.Media.DISPLAY_NAME, file.getName());

// 修改时间;

values.put(MediaStore.Video.Media.DATE_MODIFIED, dateTaken / 1000);

// 添加时间;

values.put(MediaStore.Video.Media.DATE_ADDED, dateTaken / 1000);

values.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");

ContentResolver resolver = Utils.getApp().getContentResolver();

if (resolver != null) {

try {

uri = resolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);

} catch (Exception e) {

e.printStackTrace();

uri = null;

}

}

if (uri == null) {

MediaScannerConnection.scanFile(Utils.getApp(), new String[]{videoPath}, new String[]{"video/*"}, new MediaScannerConnection.OnScanCompletedListener() {

@Override

public void onScanCompleted(String path, Uri uri) {

}

});

}

return uri;

}

return null;

}

/**

* Add video record by android_Q api.

* AndroidQ以上,增加视频文件记录

*

* @param context 上下文,the context

* @param fileName 视频文件名称 the video file name

* @param fileType 视频文件类型 the video file type

* @param relativePath 相对路径 the relative path

* @param duration 文件时长,单位是毫秒The duration of the file, in milliseconds.

* @return String 类型的Uri. The String of Uri

*/

public static String addVideoRecord_Q(Context context, String fileName, String fileType,

String relativePath, long duration) {

if (!AndroidVersionUtils.isAboveAndroid_Q()) {

return null;

}

if (TextUtils.isEmpty(relativePath)) {

return null;

}

relativePath = "Movies/" + relativePath;

ContentResolver resolver = context.getContentResolver();

//设置文件参数到ContentValues中

ContentValues values = new ContentValues();

//设置文件名

values.put(MediaStore.Video.Media.DISPLAY_NAME, fileName);

//设置文件描述,这里以文件名代替

values.put(MediaStore.Video.Media.DESCRIPTION, fileName);

//设置文件类型为image/*

values.put(MediaStore.Video.Media.MIME_TYPE, "video/" + fileType);

values.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis() / 1000);

values.put(MediaStore.Video.Media.DATE_MODIFIED, System.currentTimeMillis() / 1000);

values.put(MediaStore.Video.Media.DURATION, duration);

//注意:MediaStore.Images.Media.RELATIVE_PATH需要targetSdkVersion=29,

//故该方法只可在Android10的手机上执行

values.put(MediaStore.Video.Media.RELATIVE_PATH, relativePath);

//EXTERNAL_CONTENT_URI代表外部存储器

Uri external = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;

//insertUri表示文件保存的uri路径

Uri insertUri = resolver.insert(external, values);

return String.valueOf(insertUri);

}

/**

* Delete video record by android_Q api

* AndroidQ以上删除视频记录

*

* @param context 上下文,the context

* @param uri 文件的uri the uri of file

* @return 删除的数量,如果是0,代表删除失败 The number of deletions. If it is 0, it means the deletion failed

*/

public static int deleteVideoRecord_Q(Context context, Uri uri) {

context = context.getApplicationContext();

ContentResolver contentResolver = context.getContentResolver();

if (contentResolver == null) {

return 0;

}

Cursor cursor = null;

String column = MediaStore.MediaColumns._ID;

int fileID = -1;

try {

cursor = contentResolver.query(uri, new String[]{column}, null, null,

null);

if (cursor != null && cursor.moveToFirst()) {

int column_index = cursor.getColumnIndexOrThrow(column);

fileID = cursor.getInt(column_index);

}

} catch (Exception e) {

LogUtils.e(e);

} finally {

if (cursor != null) {

cursor.close();

}

}

if (fileID >= 0) {

return contentResolver.delete(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, column + "=?", new String[]{String.valueOf(fileID)});

}

return 0;

}

/**

* 获取媒体数据列表

* Gets a list of media data

*

* @param type TYPE_VIDEO = 0; 视频 TYPE_PHOTO = 1;图片;YPE_ALL = 2;图片和视频

*/

public static void getMediaList(final int type, final String[] filter, final MediaCallback callback, int mCurrentPage) {

ThreadUtils.getCachedPool().execute(new Runnable() {

@Override

public void run() {

final List dataList = new ArrayList<>();

if (type == MediaData.TYPE_ALL) {

Cursor cursor = getMediaCursor(MediaData.TYPE_PHOTO, mCurrentPage);

if (cursor != null) {

createMediaData(cursor, filter, dataList, false);

cursor.close();

}

cursor = getMediaCursor(MediaData.TYPE_VIDEO, mCurrentPage);

if (cursor != null) {

createMediaData(cursor, filter, dataList, true);

cursor.close();

}

} else {

Cursor cursor = getMediaCursor(type, mCurrentPage);

if (cursor != null) {

createMediaData(cursor,filter, dataList, type == MediaData.TYPE_VIDEO);

cursor.close();

}

}

if (callback != null) {

ThreadUtils.runOnUiThread(new Runnable() {

@Override

public void run() {

callback.onResult(dataList);

}

});

}

}

});

}

@SuppressLint("InlinedApi")

private static Cursor getMediaCursor(int type, int mCurrentPage) {

String[] projection = null;

Uri uri = null;

String order = null;

if (type == MediaData.TYPE_VIDEO) {

projection = new String[]{MediaStore.Video.Thumbnails._ID

, MediaStore.Video.Media._ID

, MediaStore.Video.Thumbnails.DATA

, MediaStore.Video.Media.DURATION

, MediaStore.Video.Media.SIZE

, MediaStore.Video.Media.DATE_ADDED

, MediaStore.Video.Media.DISPLAY_NAME

, MediaStore.Video.Media.DATE_MODIFIED};

uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;

order = MediaStore.Video.Media.DATE_ADDED;

} else if (type == MediaData.TYPE_PHOTO) {

projection = new String[]{

MediaStore.Images.Media._ID,

MediaStore.Images.Media.DATA,

MediaStore.Images.Media.DATE_ADDED,

MediaStore.Images.Thumbnails.DATA,

MediaStore.MediaColumns.DISPLAY_NAME

};

uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;

order = MediaStore.Images.Media.DATE_ADDED;

}

if (projection == null) {

return null;

}

// 兼容折叠屏,在Android R及以上手机,order中禁止了LIMIT关键字,所以在这里做了适配

if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {

Bundle bundle = new Bundle();

bundle.putInt(ContentResolver.QUERY_ARG_OFFSET, mCurrentPage * PAGE_SIZE);

bundle.putInt(ContentResolver.QUERY_ARG_LIMIT, PAGE_SIZE);

bundle.putStringArray(ContentResolver.QUERY_ARG_SORT_COLUMNS, new String[]{order + " DESC"});

return Utils.getApp().getContentResolver().query(uri, projection, bundle, null);

} else {

String sortOrder = order + " DESC LIMIT " + PAGE_SIZE + " OFFSET " + mCurrentPage * PAGE_SIZE;

return Utils.getApp().getContentResolver().query(uri, projection, null, null, sortOrder);

}

}

/**

* 创建媒体实体类并添加到集合中

* Create a media entity class and add it to the collection

*/

@SuppressLint("InlinedApi")

private static void createMediaData(Cursor cursor, String[] filter, List list, boolean isVideo) {

if (cursor != null) {

String mediaId;

String mediaDate;

String mediaThumbnails;

String mediaDisplayName;

if (isVideo) {

mediaId = MediaStore.Video.Media._ID;

mediaDate = MediaStore.Video.Media.DATE_ADDED;

mediaThumbnails = MediaStore.Video.Thumbnails.DATA;

mediaDisplayName = MediaStore.Video.Media.DISPLAY_NAME;

} else {

mediaId = MediaStore.Images.Media._ID;

mediaDate = MediaStore.Images.Media.DATE_ADDED;

mediaThumbnails = MediaStore.Images.Thumbnails.DATA;

mediaDisplayName = MediaStore.Images.Media.DISPLAY_NAME;

}

while (cursor.moveToNext()) {

int videoId = cursor.getInt(cursor.getColumnIndex(mediaId));

String absolutePath = cursor.getString(cursor.getColumnIndex(mediaThumbnails));

String path = AndroidVersionUtils.isAboveAndroid_Q()? AndroidVersionUtils.getRealPathAndroid_Q(Uri.parse(absolutePath)): absolutePath;

String displayName = cursor.getString(cursor.getColumnIndex(mediaDisplayName));

int timeIndex = cursor.getColumnIndex(mediaDate);

long date = cursor.getLong(timeIndex) * 1000;

if (fileIsValid(path)) {

if (!TextUtils.isEmpty(absolutePath)) {

int lastDot = absolutePath.lastIndexOf(".");

String type = absolutePath.substring(lastDot + 1);

if (!TextUtils.isEmpty(type)) {

type = type.toLowerCase();

if (type.equals("mpg") || type.equals("mkv")) {

continue;

}

//过滤文件类型

boolean isFilter = false;

if (filter != null && filter.length > 0) {

for (int i = 0; i < filter.length; i++) {

String filterItem = filter[i];

if (StringUtils.equals(filterItem, type)) {

isFilter = true;

break;

}

}

}

if (isFilter) {

continue;

}

}

}

MediaData mediaData = new MediaData()

.setId(videoId)

.setPath(path)

.setDate(date)

.setDisplayName(displayName);

if (isVideo) {

mediaData.setType(MediaData.TYPE_VIDEO)

.setDuration(cursor.getLong(cursor.getColumnIndex(MediaStore.Video.Media.DURATION)));

} else {

mediaData.setType(MediaData.TYPE_PHOTO);

}

if (isGif(path)){

mediaData.setIsGif(true);

mediaData.setType(MediaData.TYPE_VIDEO);

}

list.add(mediaData);

}

}

}

}

/**

* 判断是否是gif图片

* Is gif boolean.

*

* @param path the path

* @return the boolean

*/

public static boolean isGif(String path) {

String fileName = FileUtils.getFileSuffix(path);

if (!TextUtils.isEmpty(fileName) && "GIF".equals(fileName.toUpperCase())) {

return true;

}

return false;

}

private static boolean fileIsValid(String filePath) {

if (FileUtils.isAndroidQUriPath(filePath)) {

return true;

}

File file = FileUtils.getFileByPath(filePath);

return file != null && file.exists();

}

public interface MediaCallback {

void onResult(List dataList);

}

}