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到相册,并兼容AndroidQpublic 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() {@Overridepublic 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() {@Overridepublic void run() {final List<MediaData> 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() {@Overridepublic 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<MediaData> 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<MediaData> dataList);}
}