本文最后更新于 93 天前,其中的信息可能已经有所发展或是发生改变。
一、迁移
// 简述步骤
// 对sdcard目录下的文件以及文件夹 批量移动到 当前应用的私有目录下,如:files以及cache目录
二、适配操作
- AndroidQ及以上使用 MediaStore操作媒体等文件,并且应用自己文件归类保存在 沙盒目录内
其中注意 Data字段的变化以及操作修改其他应用的权限申请以及捕获SecurityException异常进行用户授权处理- AndroidQ一下就正常文件操作
val appFilePath = getExternalFilesDir(null)?.path?:""
val appImagePath = getExternalFilesDir(Environment.DIRECTORY_DCIM)?.path?:""
val appCustomPath = getExternalFilesDir("Demo")?.path?:""
val appCachePath = getExternalCacheDir()?.path?:""
// 如果应用卸载后又重新安装,删除卸载之前保存的文件就无法直接删除,或者删除其他应用创建的媒体文件也不能直接删除,
// 此时需要申请READ_EXTERNAL_STORAGE权限,删除需要捕获SecurityException异常
// App卸载后,对应的沙盒目录也会被删除,如果APP想要在卸载时保留沙盒目录下的数据,要在AndroidManifest.xml中声明
// android:hasFragileUserData="true",这样在 APP卸载时就会有弹出框提示用户是否保留应用数据
三、MediaStore操作
- 通过contentResolve等获取uri,在根据uri获取文件描述符,最后使用FileOutputStream(pfd.fileDescriptor)等流将数据流进行存取等操作
- os = resolver.openOutputStream(insertUri)
3.1 ContentValues
/**
* ```
* values.put(MediaStore.Images.Media.IS_PENDING, isPending)
* Android Q , MediaStore中添加 MediaStore.Images.Media.IS_PENDING flag,用来表示文件的 isPending 状态,0是可见,其他不可见
* ```
* @param displayName 文件名
* @param description 描述
* @param mimeType 媒体类型
* @param title 标题
* @param relativePath 相对路径 eg: ${Environment.DIRECTORY_PICTURES}/xxx
* @param isPending 默认0 , 0是可见,其他不可见
*/
// val RELATIVE_PATH = "${Environment.DIRECTORY_PICTURES}${File.separator}img"
/* val values = createContentValues(
"BitmapImage.png",
"This is an image",
"image/png",
"Image.png",
RELATIVE_PATH,
1
)
*/
fun createContentValues(
displayName: String? = null, description: String? = null, mimeType: String? = null, title: String? = null,
relativePath: String? = null, isPending: Int? = 1,
): ContentValues {
return ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, displayName)
put(MediaStore.Images.Media.DESCRIPTION, description)
put(MediaStore.Images.Media.MIME_TYPE, mimeType)
put(MediaStore.Images.Media.TITLE, title)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.Images.Media.RELATIVE_PATH, relativePath)
put(MediaStore.Images.Media.IS_PENDING, isPending)
}
}
}
3.2 访问其他应用创建文件权限检查
when (FileOperator.getContext()
.checkUriPermission(uri, android.os.Process.myPid(), android.os.Process.myUid(), Intent.FLAG_GRANT_READ_URI_PERMISSION)) {
PackageManager.PERMISSION_GRANTED -> {
}
PackageManager.PERMISSION_DENIED -> {
FileOperator.getContext().grantUriPermission(FileOperator.getApplication().packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
}
3.3 完整例子
val imageList = mutableListOf<MediaStoreImage>()
val external = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val cursor: Cursor?
try {
cursor = createMediaCursor(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projectionArgs, sortOrder, querySelectionStatement)
FileLogger.i("Found ${cursor?.count} images")
cursor?.use {
// Cache column indices.
val idColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
val nameColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)
val sizeColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE)
val descColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media.DESCRIPTION)
val titleColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media.TITLE)
val mimeColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media.MIME_TYPE)
val dateModifiedColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED)
while (it.moveToNext()) { //moveToFirst moveToNext
val id = it.getLong(idColumn)
val name = it.getString(nameColumn)
val size = it.getInt(sizeColumn)
val desc = it.getString(descColumn)
val titleRs = it.getString(titleColumn)
val mimeTypeRs = it.getString(mimeColumn)
val dateModified = Date(TimeUnit.SECONDS.toMillis(it.getLong(dateModifiedColumn)))
val contentUri: Uri = ContentUris.withAppendedId(external, id)
imageList += MediaStoreImage(
id, contentUri, name, size.toLong(),
desc, titleRs, mimeTypeRs, dateModified
)
}
if (imageList.isNullOrEmpty()) {
FileLogger.e("查询失败!")
}
imageList.let { l ->
l.forEach { img ->
FileLogger.d("查询成功,Uri路径 ${img.uri}")
}
}
}
return imageList
} catch (e: Exception) {
FileLogger.e("查询失败! ${e.message}")
}
fun createMediaCursor(
uri: Uri, projectionArgs: Array<String>? = arrayOf(MediaStore.Video.Media._ID),
sortOrder: String? = null, querySelectionStatement: FileGlobal.QuerySelectionStatement? = null,
): Cursor? {
// Need the READ_EXTERNAL_STORAGE permission if accessing video files that your app didn't create.
when (FileOperator.getContext()
.checkUriPermission(uri, android.os.Process.myPid(), android.os.Process.myUid(), Intent.FLAG_GRANT_READ_URI_PERMISSION)) {
PackageManager.PERMISSION_GRANTED -> {
}
PackageManager.PERMISSION_DENIED -> {
FileOperator.getContext().grantUriPermission(FileOperator.getApplication().packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
}
return FileOperator.getContext().contentResolver.query(
uri, projectionArgs,
querySelectionStatement?.selection.toString(),
querySelectionStatement?.selectionArgs?.toTypedArray(),
sortOrder
)
}
四、SAF操作
更多查看源码
4.1 选择文件
fun selectFile(activity: Activity, mimeType: String, requestCode: Int) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = mimeType
}
activity.startActivityForResult(intent, requestCode)
}
selectFile(activity, "image/*", requestCode)
五、Uri
5.1 文件描述符
通过文件描述符方位uri
var pfd: ParcelFileDescriptor? = null
try {
pfd = contentResolver.openFileDescriptor(queryUri!!, "r")
if (pfd != null) {
val bitmap = BitmapFactory.decodeFileDescriptor(pfd.fileDescriptor)
imageIv.setImageBitmap(bitmap)
}
} catch (e: IOException) {
e.printStackTrace()
} finally {
pfd?.close()
}
在Android Q以下只使用DATA字段,Android Q及以上不使用DATA字段,改为使用RELATEIVE_PATH字段
5.2 检查uri有效性
fun checkUri(uri: Uri?): Boolean {
if (uri == null) return false
val resolver = FileOperator.getContext().contentResolver
//1. Check Uri
var cursor: Cursor? = null
val isUriExist: Boolean = try {
cursor = resolver.query(uri, null, null, null, null)
//cursor null: content Uri was invalid or some other error occurred
//cursor.moveToFirst() false: Uri was ok but no entry found.
(cursor != null && cursor.moveToFirst())
} catch (t: Throwable) {
FileLogger.e("1.Check Uri Error: ${t.message}")
false
} finally {
try {
cursor?.close()
} catch (t: Throwable) {
}
}
//2. Check File Exist
//如果系统 db 存有 Uri 相关记录, 但是文件失效或者损坏 (If the system db has Uri related records, but the file is invalid or damaged)
var ins: InputStream? = null
val isFileExist: Boolean = try {
ins = resolver.openInputStream(uri)
// file exists
true
} catch (t: Throwable) {
// File was not found eg: open failed: ENOENT (No such file or directory)
FileLogger.e("2. Check File Exist Error: ${t.message}")
false
} finally {
try {
ins?.close()
} catch (t: Throwable) {
}
}
return isUriExist && isFileExist
}