通过 MediaStore API
对媒体集、文件集进行媒体/文件的添加、对 自身APP 创建的 媒体/文件 进行查询、修改、删除的操作。
-
需要申请
READ_EXTERNAL_STORAGE
权限:
通过MediaStore API
对所有的媒体集进行查询、修改、删除的操作。 -
调用
Storage Access Framework API
:
会启动系统的文件选择器向用户申请操作指定的文件
新的访问方式:
Android 11 ®:
Android 11 ® 在 Android 10 (Q) 中分区存储的基础上进行了调整
1. 新增执行批量操作
为实现各种设备之间的一致性并增加用户便利性,Android 11 向 MediaStore API 中添加了多种方法。对于希望简化特定媒体文件更改流程(例如在原位置编辑照片)的应用而言,这些方法尤为有用。
MediaStore API 新增的方法
方法 | 说明 |
---|---|
createWriteRequest (ContentResolver, Collection) | 用户向应用授予对指定媒体文件组的写入访问权限的请求。 |
createFavoriteRequest (ContentResolver, Collection, boolean) | 用户将设备上指定的媒体文件标记为 “收藏” 的请求。对该文件具有读取访问权限的任何应用都可以看到用户已将该文件标记为 “收藏”。 |
createTrashRequest (ContentResolver, Collection, boolean) | 用户将指定的媒体文件放入设备垃圾箱的请求。垃圾箱中的内容在特定时间段(默认为 7 天)后会永久删除。 |
createDeleteRequest (ContentResolver, Collection) | 用户立即永久删除指定的媒体文件(而不是先将其放入垃圾箱)的请求。 |
系统在调用以上任何一个方法后,会构建一个 PendingIntent 对象。应用调用此 intent 后,用户会看到一个对话框,请求用户同意应用更新或删除指定的媒体文件。
2. 使用直接文件路径和原生库访问文件
为了帮助您的应用更顺畅地使用第三方媒体库,Android 11 允许您使用除 MediaStore API 之外的 API 访问共享存储空间中的媒体文件。不过,您也可以转而选择使用以下任一 API 直接访问媒体文件:
File API。
原生库,例如 fopen()。
简单来说就是,可以通过 File()
等API 访问有权限访问的媒体集了。
性能:
通过 File ()
等直接通过路径访问的 API 实际上也会映射为MediaStore
API 。
按文件路径顺序读取的时候性能相当;随机读取和写入的时候则会更慢,所以还是推荐直接使用 MediaStore
API。
3. 新增权限
MANAGE_EXTERNAL_STORAGE
: 类似以前的 READ_EXTERNAL_STORAGE
+ WRITE_EXTERNAL_STORAGE
,除了应用专有目录都可以访问。
应用可通过执行以下操作向用户请求名为所有文件访问权限的特殊应用访问权限:
- 在清单中声明
MANAGE_EXTERNAL_STORAGE
权限。 - 使用
ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION
intent 操作将用户引导至一个系统设置页面,在该页面上,用户可以为您的应用启用以下选项:授予所有文件的管理权限。
- 在 Google Play 上架的话,需要提交使用此权限的说明,只有指定的几种类型的 APP 才能使用。
Sample
-
使用
MediaStore
增删改查媒体集 -
使用
Storage Access Framework
访问文件集
1. 媒体集
1) 查询媒体集(需要 READ_EXTERNAL_STORAGE 权限)
实际上 MediaStore
是以前就有的 API ,不同的是过去主要通过 MediaStore.Video.Media._DATA
这个 colum 请求原始数据,可以得到绝对Uri
,现在需要请求MediaStore.Video.Media._ID
来得到相对Uri
再进行处理。
// Need the READ_EXTERNAL_STORAGE permission if accessing video files that your
// app didn’t create.
// Container for information about each video.
data class Video(
val uri: Uri,
val name: String,
val duration: Int,
val size: Int
)
val videoList = mutableListOf()
val projection = arrayOf(
MediaStore.Video.Media._ID,
MediaStore.Video.Media.DISPLAY_NAME,
MediaStore.Video.Media.DURATION,
MediaStore.Video.Media.SIZE
)
// Show only videos that are at least 5 minutes in duration.
val selection = “${MediaStore.Video.Media.DURATION} >= ?”
val selectionArgs = arrayOf(
TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES).toString()
)
// Display videos in alphabetical order based on their display name.
val sortOrder = “${MediaStore.Video.Media.DISPLAY_NAME} ASC”
val query = ContentResolver.query(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
projection,
selection,
selectionArgs,
sortOrder
)
query?.use { cursor ->
// Cache column indices.
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)
val nameColumn =
cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME)
val durationColumn =
cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION)
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE)
while (cursor.moveToNext()) {
// Get values of columns for a given video.
val id = cursor.getLong(idColumn)
val name = cursor.getString(nameColumn)
val duration = cursor.getInt(durationColumn)
val size = cursor.getInt(sizeColumn)
val contentUri: Uri = ContentUris.withAppendedId(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
id
)
// Stores column values and the contentUri in a local object
// that represents the media file.
videoList += Video(contentUri, name, duration, size)
}
}
2)插入媒体集(无需权限)
// Add a media item that other apps shouldn’t see until the item is
// fully written to the media store.
val resolver = applicationContext.contentResolver
// Find all audio files on the primary external storage device.
// On API <= 28, use VOLUME_EXTERNAL instead.
val audioCollection = MediaStore.Audio.Media
.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val songDetails = ContentValues().apply {
put(MediaStore.Audio.Media.DISPLAY_NAME, “My Workout Playlist.mp3”)
put(MediaStore.Audio.Media.IS_PENDING, 1)
}
val songContentUri = resolver.insert(audioCollection, songDetails)
resolver.openFileDescriptor(songContentUri, “w”, null).use { pfd ->
// Write data into the pending audio file.
}
// Now that we’re finished, release the “pending” status, and allow other apps
// to play the audio track.
songDetails.clear()
songDetails.put(MediaStore.Audio.Media.IS_PENDING, 0)
resolver.update(songContentUri, songDetails, null, null)
3)更新自己创建的媒体集(无需权限)
删除类似
// Updates an existing media item.
val mediaId = // MediaStore.Audio.Media._ID of item to update.
val resolver = applicationContext.contentResolver
// When performing a single item update, prefer using the ID
val selection = “${MediaStore.Audio.Media._ID} = ?”
// By using selection + args we protect against improper escaping of // values.
val selectionArgs = arrayOf(mediaId.toString())
// Update an existing song.
val updatedSongDetails = ContentValues().apply {
put(MediaStore.Audio.Media.DISPLAY_NAME, “My Favorite Song.mp3”)
}
// Use the individual song’s URI to represent the collection that’s
// updated.
val numSongsUpdated = resolver.update(
myFavoriteSongUri,
updatedSongDetails,
selection,
selectionArgs)
4)更新/删除其它媒体创建的媒体集
若已经开启分区存储则会抛出 RecoverableSecurityException
,捕获并通过SAF
请求权限
// Apply a grayscale filter to the image at the given content URI.
try {
contentResolver.openFileDescriptor(image-content-uri, “w”)?.use {
setGrayscaleFilter(it)
}
} catch (securityException: SecurityException) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val recoverableSecurityException = securityException as?
RecoverableSecurityException ?:
throw RuntimeException(securityException.message, securityException)
val intentSender =
recoverableSecu
《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享
rityException.userAction.actionIntent.intentSender
intentSender?.let {
startIntentSenderForResult(intentSender, image-request-code,
null, 0, 0, 0, null)
}
} else {
throw RuntimeException(securityException.message, securityException)
}
}
2. 文件集 (通过 SAF)
1)创建文档
注:创建操作若重名的话不会覆盖原文档,会添加 (1) 最为后缀,如 document.pdf -> document(1).pdf
// Request code for creating a PDF document.
const val CREATE_FILE = 1
private fun createFile(pickerInitialUri: Uri) {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = “application/pdf”
putExtra(Intent.EXTRA_TITLE, “invoice.pdf”)
// Optionally, specify a URI for the directory that should be opened in
// the system file picker before your app creates the document.
putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
}
startActivityForResult(intent, CREATE_FILE)
}
2)打开文档
建议使用 type 设置 MIME 类型
// Request code for selecting a PDF document.
const val PICK_PDF_FILE = 2
fun openFile(pickerInitialUri: uri) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = “application/pdf”
// Optionally, specify a URI for the file that should appear in the
// system file picker when it loads.
putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
}
startActivityForResult(intent, PICK_PDF_FILE)
}
3)授予对目录内容的访问权限
用户选择目录后,可访问该目录下的所有内容
Android 11 中无法访问 Downloads
fun openDirectory(pickerInitialUri: Uri) {
// Choose a directory using the system’s file picker.
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
// Provide read access to files and sub-directories in the user-selected
// directory.
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
// Optionally, specify a URI for the directory that should be opened in
// the system file picker when it loads.
putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
}
startActivityForResult(intent, your-request-code)
}
4)永久获取目录访问权限
上面提到的授权是临时性的,重启后则会失效。可以通过下面的方法获取相应目录永久性的权限
val contentResolver = applicationContext.contentResolver
val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
// Check for the freshest data.
contentResolver.takePersistableUriPermission(uri, takeFlags)
5)SAF API 响应
SAF API
调用后都是通过 onActivityResult
来相应动作
override fun onActivityResult(
requestCode: Int, resultCode: Int, resultData: Intent?) {
if (requestCode == your-request-code
&& resultCode == Activity.RESULT_OK) {
// The result data contains a URI for the document or directory that
keFlags)
5)SAF API 响应
SAF API
调用后都是通过 onActivityResult
来相应动作
override fun onActivityResult(
requestCode: Int, resultCode: Int, resultData: Intent?) {
if (requestCode == your-request-code
&& resultCode == Activity.RESULT_OK) {
// The result data contains a URI for the document or directory that