Android 10(Q)_11(R) 分区存储适配

本文详细介绍了FileAPI和MediaStore在Android中的应用,包括性能分析、新增的MANAGE_EXTERNAL_STORAGE权限、MediaStore的增删改查操作以及通过StorageAccessFramework访问文件集,还涉及了文件集的创建、打开、权限管理和批量操作等内容。
摘要由CSDN通过智能技术生成

File API。
原生库,例如 fopen()。

简单来说就是,可以通过 File() 等API 访问有权限访问的媒体集了。

性能:

通过 File () 等直接通过路径访问的 API 实际上也会映射为MediaStore API 。
按文件路径顺序读取的时候性能相当;随机读取和写入的时候则会更慢,所以还是推荐直接使用 MediaStoreAPI。

3. 新增权限

MANAGE_EXTERNAL_STORAGE : 类似以前的 READ_EXTERNAL_STORAGE + WRITE_EXTERNAL_STORAGE ,除了应用专有目录都可以访问。

应用可通过执行以下操作向用户请求名为所有文件访问权限的特殊应用访问权限:

  1. 在清单中声明 MANAGE_EXTERNAL_STORAGE 权限。
  2. 使用 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 =
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 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) {

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

25585)]

[外链图片转存中…(img-MK1vTxMu-1714927625585)]

[外链图片转存中…(img-x8QOHJeR-1714927625585)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

  • 15
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值