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
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 = recoverableSecurityException.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 // the user selected. resultData?.data?.also { uri -> // Perform operations on the document using its
《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享
URI. } } }
6) 其它操作
除了上面的操作之外,对文档其它的复制、移动等操作都是通过设置不同的 FLAG 来实现,见 Document.COLUMN_FLAGS
3. 批量操作媒体集
构建一个媒体集的写入操作 createWriteRequest()
val urisToModify = /* A collection of content URIs to modify. */ val editPendingIntent = MediaStore.createWriteRequest(contentResolver, urisToModify)
// Launch a system prompt requesting user permission for the operation. startIntentSenderForResult(editPendingIntent.intentSender, EDIT_REQUEST_CODE, null, 0, 0, 0)
//相应 override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { … when (requestCode) { EDIT_REQUEST_CODE -> if (resultCode == Activity.RESULT_OK) { /* Edit request granted; proceed. */
|