一、存储空间分类
1、内部存储,无需权限,卸载删除
getCacheDir(): /data/user/0/com.example.storagedemo/cache
getFilesDir(): /data/user/0/com.example.storagedemo/files
2、外部存储,无需权限,卸载删除
getExternalCacheDir(): /storage/emulated/0/Android/data/com.example.storagedemo/cache
getExternalFilesDir(Environment.DIRECTORY_PICTURES): /storage/emulated/0/Android/data/com.example.storagedemo/files/Pictures
3、外部存储,需要权限或通过MediaStore操作,卸载不删除
Environment.getExternalStorageDirectory(): /storage/emulated/0
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES): /storage/emulated/0/Pictures
二、存储空间使用
1、Android 11增加新的权限,允许管理所有文件,可以随意操作存储空间
android.permission.MANAGE_EXTERNAL_STORAGE
// 在AndroidManifest.xml中添加以下权限(Android 11,SDK_INT = 30增加)
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
// 权限检测与申请
class MainActivity : AppCompatActivity(), View.OnClickListener {
companion object {
const val TAG = "StorageDemo"
}
private var mRequestManagePLauncher: ActivityResultLauncher<Intent>? = null
private fun initView() {
mRequestManagePLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (hasManageP()) {
Toast.makeText(this@MainActivity, "申请管理权限成功", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this@MainActivity, "申请管理权限失败,请重试", Toast.LENGTH_SHORT).show()
}
}
}
/**
* 判断管理权限
*/
private fun hasManageP(): Boolean {
return Environment.isExternalStorageManager()
}
/**
* 请求管理权限
*/
private fun requestManagerP() {
mRequestManagePLauncher?.launch(Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION))
.apply {
if (this == null) {
Toast.makeText(this@MainActivity, "跳转异常,请稍后重试", Toast.LENGTH_SHORT).show()
}
}
}
/**
* 如果有管理权限,则可以随意操作存储空间
*/
private fun writeFileWithP() {
if (hasManageP()) {
val writeFile = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
"write.txt"
)
var fOS: FileOutputStream? = null
try {
fOS = FileOutputStream(writeFile)
fOS.write("我是测试内容".toByteArray())
Toast.makeText(
this@MainActivity,
"写入成功:${writeFile.absolutePath}",
Toast.LENGTH_SHORT
).show()
} catch (e: java.lang.Exception) {
Toast.makeText(this@MainActivity, "写入失败:$e", Toast.LENGTH_SHORT).show()
} finally {
try {
fOS?.close()
} catch (e: java.lang.Exception) {
Log.v(TAG, "error:$e")
}
}
} else {
requestManagerP()
}
}
}
2、如果不申请MANAGE_EXTERNAL_STORAGE权限的话,则需要通过MediaStore操作存储空间
通过MediaStore获取到的Uri主要有以下几种:
外部Uri
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
内部Uri
MediaStore.Video.Media.INTERNAL_CONTENT_URI
MediaStore.Audio.Media.INTERNAL_CONTENT_URI
MediaStore.Images.Media.INTERNAL_CONTENT_URI
共享文件,包括媒体文件和非媒体文件
MediaStore.Files.getContentUri("external") // 操作external.db数据库
(1)查询
需要注意:
1、当targetSdk 29时,如果想通过MediaStore获取公共媒体文件,则必须申请READ_EXTERNAL_STORAGE权限;
2、当targetSdk 30时,即使申请了READ_EXTERNAL_STORAGE权限,也可能无法获取到公共媒体文件(自己创建的除外),此时必须申请MANAGE_EXTERNAL_STORAGE才可以继续获取。
class MainActivity : AppCompatActivity(), View.OnClickListener {
companion object {
const val TAG = "StorageDemo"
const val REQUEST_READP_CDOE = 10000
}
private fun queryPic() {
if (hasReadP()) {
val imageExternalUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
// 查询哪几类
val projection = arrayOf(
MediaStore.MediaColumns._ID,
MediaStore.MediaColumns.DATA,
MediaStore.MediaColumns.RELATIVE_PATH,
MediaStore.MediaColumns.DISPLAY_NAME
)
// 查询条件
val selection =
"${MediaStore.MediaColumns.RELATIVE_PATH} = ? "
// 参数
val selectionArgs = arrayOf(
Environment.DIRECTORY_DCIM + "/Camera/"
)
// 排序条件
val order = MediaStore.MediaColumns._ID
val cursor = contentResolver.query(
imageExternalUri,
projection,
selection,
selectionArgs,
order
)
cursor?.apply {
val idIndex = cursor.getColumnIndex(MediaStore.MediaColumns._ID)
val dataIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DATA)
val relativePathIndex = cursor.getColumnIndex(MediaStore.MediaColumns.RELATIVE_PATH)
val displayNameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME)
while (cursor.moveToNext()) {
val log = """
id = ${cursor.getString(idIndex)}
data = ${cursor.getString(dataIndex)}
relativePath = ${cursor.getString(relativePathIndex)}
displayNameIndex = ${cursor.getString(displayNameIndex)}
""".trimIndent()
Log.v(TAG, log)
}
cursor.close()
}
} else {
requestReadP()
}
}
/**
* 判断读取权限
*/
private fun hasReadP(): Boolean {
return ContextCompat.checkSelfPermission(
this,
Manifest.permission.READ_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
}
private fun requestReadP() {
ActivityCompat.requestPermissions(
this,
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE
),
REQUEST_READP_CDOE
)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
if (requestCode == REQUEST_READP_CDOE) {
if (grantResults.isNotEmpty()) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this@MainActivity, "读取权限申请成功", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this@MainActivity, "读取权限申请失败,请稍后重试", Toast.LENGTH_SHORT).show()
}
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}
(2)插入
需要注意:
1、如果插入的文件手动删除时,可能导致MediaStore不更新,再次插入报错:QLiteConstraintException: UNIQUE constraint failed: files._data
2、此时,可以通过改变每次插入的文件名称避免此问题;也可以在插入前先搜索是否有同样信息的文件,执行update或delete,防止出错。
private fun insertPic() {
val imageExternalUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val values = ContentValues()
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM + "/Camera/")
values.put(
MediaStore.MediaColumns.DISPLAY_NAME,
"testPic_${System.currentTimeMillis()}.png"
)
values.put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
val resultUri = contentResolver.insert(imageExternalUri, values)
resultUri?.apply {
val lightOpenBitmap = BitmapFactory.decodeResource(resources, R.drawable.light_open)
val oPS = contentResolver.openOutputStream(resultUri)
try {
lightOpenBitmap.compress(Bitmap.CompressFormat.PNG, 100, oPS)
Toast.makeText(this@MainActivity, "插入成功", Toast.LENGTH_SHORT).show()
} catch (e: java.lang.Exception) {
Toast.makeText(this@MainActivity, "插入失败,请稍后重试", Toast.LENGTH_SHORT).show()
}
oPS?.apply {
try {
oPS.close()
} catch (e: java.lang.Exception) {
Log.v(TAG, "error:$e")
}
}
}
}
(3)更新
需要注意:
1、需要通过查询拿到文件的原始Uri,如果直接通过MediaStore.Images.Media.EXTERNAL_CONTENT_URI进行更新的话,会报错:java.lang.IllegalArgumentException: Movement of content://media/external/images/media which isn't part of well-defined collection not allowed
2、应用只能修改自己插入的文件,其他文件或者应用卸载前插入的文件,应用不再有权限修改,否则会报:android.app.RecoverableSecurityException: com.example.storatedemo1 has no access to content://media/external/images/media/1000014102
3、Android 11手机,申请了MANAGE_EXTERNAL_STORAGE,可随意操作
/**
* 更新
*/
private fun updatePic() {
if (hasReadP()) {
val imageExternalUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val values = ContentValues()
values.put(
MediaStore.MediaColumns.RELATIVE_PATH,
Environment.DIRECTORY_DCIM + "/Camera/"
)
values.put(
MediaStore.MediaColumns.DISPLAY_NAME,
"updatePic_${System.currentTimeMillis()}.png"
)
values.put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
// 查询条件
val selection =
"${MediaStore.MediaColumns.RELATIVE_PATH} = ? AND ${MediaStore.MediaColumns.DISPLAY_NAME} like ?"
// 参数
val selectionArgs = arrayOf(
Environment.DIRECTORY_DCIM + "/Camera/",
"testPic_%.png"
)
val cursor =
contentResolver.query(imageExternalUri, null, selection, selectionArgs, null)
cursor?.apply {
if (cursor.moveToNext()) {
val idIndex = cursor.getColumnIndex(MediaStore.MediaColumns._ID)
val imageUri =
ContentUris.withAppendedId(imageExternalUri, cursor.getLong(idIndex))
val count = contentResolver.update(imageUri, values, null, null)
if (count > 0) {
Toast.makeText(this@MainActivity, "更新成功", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this@MainActivity, "更新失败", Toast.LENGTH_SHORT).show()
}
} else {
Toast.makeText(this@MainActivity, "没有找到要更新的图片", Toast.LENGTH_SHORT).show()
}
cursor.close()
}
} else {
requestReadP()
}
}
(4)删除
需要注意:
1、应用只能删除自己插入的文件,其他文件或者应用卸载前插入的文件,应用不再有权限删除
2、Android 11手机,申请了MANAGE_EXTERNAL_STORAGE,可随意操作
/**
* 删除图片
*/
private fun deletePic() {
if (hasReadP()) {
val imageExternalUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
// 查询条件
val selection =
"${MediaStore.MediaColumns.RELATIVE_PATH} = ? AND ${MediaStore.MediaColumns.DISPLAY_NAME} like ?"
// 参数
val selectionArgs = arrayOf(
Environment.DIRECTORY_DCIM + "/Camera/",
"testPic_%.png"
)
val deleteCount = contentResolver.delete(imageExternalUri, selection, selectionArgs)
Toast.makeText(this@MainActivity, "删除了${deleteCount}行", Toast.LENGTH_SHORT).show()
} else {
requestReadP()
}
}
3、通过SAF操作文件
App可以通过Action来启动系统选择器,让用户做相关的操作:
ACTION_OPEN_DOCUMENT:打开用户选择的文件
ACTION_CREATE_DOCUMENT:在用户选择的位置创建文件
ACTION_OPEN_DOCUMENT_TREE:访问某个目录
class MainActivity : AppCompatActivity() {
private var mSAFLauncher: ActivityResultLauncher<Intent>? = null
private fun openFileSAF() {
// 创建一个intent,并进行跳转
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
}
mSAFLauncher?.launch(intent)
}
private fun initView() {
mSAFLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
// 获取Uri,通过Uri获取文件信息
val uri = it.data?.data
uri?.apply {
val documentFile = DocumentFile.fromSingleUri(this@MainActivity, this)
documentFile?.apply {
val log = """
name = ${documentFile.name}
parentFile = ${documentFile.parentFile}
type = ${documentFile.type}
""".trimIndent()
Log.v(TAG, log)
}
}
}
}
}
}
申请Uri的永久访问权限
val contentResolver = applicationContext.contentResolver
val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
contentResolver.takePersistableUriPermission(uri, takeFlags)
其他信息可查看官方文档
三、附录
1、Media库常用字段表
字段 | 描述 |
---|---|
_id | 主键,自增 |
_data | 绝对路径 |
_size | 大小,单位byte |
_display_name | 文件名,如:aa.jpg |
mime_type | 文件类型,如:image/jpeg |
title | 文件名,无扩展名,如:aa |
bucket_display_name | 直接包含该文件的文件夹名称 |
duration | 时长 |
2、常用mime_type值
名称 | MIME type |
---|---|
png | image/png |
gif | image/gif |
jpeg jpg jpe | image/jpeg |
txt text conf def list log in | text/plain |
mp4 mp4v mpg4 | video/mp4 |
mpga mp2 mp2a mp3 m2a m3 | audio/mpeg |
json | application/json |