事出起因
我经常用到文件管理器,需要导入导出文件,每次面对外部储存中杂乱的文件列表,作为一个处女座强迫症患者,非常不爽~
为什么呢
众所周知,Android储存权限一直被大家饱受诟病,开发者拿到读写权限之后可以肆意妄为,可是有些开发者太傻X了,在用户存储空间中一通乱搞!
我们随便看一下某些app在外部储存创建的目录:
第一类傻X:
直接创建文件夹用来保存图片视频的(你放到Pictures、Download目录下不行吗)
第二类傻X:
直接将缓存文件放在外面的,用户根本不知道你这是谁建的也不知道干啥的
终极傻X:
直接创建包名文件夹,你谁啊,企鹅了不起啊
有些人可能觉得没什么,反正有多少用户天天看文件管理器啊,但是遇到强迫症患者我必须好好纠正这些坏毛病!
说一说读写权限
读写权限是获取手机外部存储权限,也就是:外部储存空间整个目录下你可以随便读写。除了保存图片视频和一些较大文件的时候,大部分文件都可以放在App私有目录下,而App私有目录读写是不需要权限的,我们的大部分临时文件、缓存、kv、数据库都是放在这里。
所以我们考虑一下:
在不读写大文件或者和其他app数据的时候,我们真的需要读写权限吗?
Android文件系统
Android文件权限一直在变化,但是依旧没什么卵用,该怎么样还是怎么样,我之前在聊一聊Android存储行为的变化 - 掘金 (juejin.cn)中说了分区存储和权限变化,建议大家可以看下。
由于早期SdCard存在,Android一直沿用了这个传统,虽然如今大部分手机没有SdCard了,在手机管理器中依然可见,其实这是系统映射的虚拟地址,我们一般称为外部存储空间。
APP私有空间
内有储存分为内部私有和外部私有,就是这部分空间是app私有的,理论上其他app无法访问。在手机内部储存空间和外部储存空间上都为你的app安排了这样一个空间,我们一般用来缓存文件、存储敏感文件。
内存空间私有目录:
一般以包名为名称,在哪里呢?看图::
通过系统api访问到的路径:/data/user/0/app_packageName/...
对应的真是目录:/data/date/app_packageName/...
外部储存空间下的app私有目录:
在Android10之前此目录依旧是不安全的,其他app获取外部读写权限之后依旧可以读写。在Android11上彻底引入分区储存才算是安全了,属于真正的app私有目录了。
怎么解决呢
这个要看开发者的自觉程度了,有些项目老,很多人不愿意为此付出修改的代价。
说一下我的做法:
- 图片、视频、文档等资源放入系统DCIM、Pictures、Video、Download、Document对应目录下,可以使用
MediaStore和FileProvider插入。 - 内存私有空间存储敏感文件
- 外部私有空间存储临时缓存
工具类
在此分享我常用的文件管理工具类,祝你文件不再混乱
FilePath:文件路径管理工具类
object FilePath {
/*----------------------外部:分区存储目录------------------------*/
/**
* 分区存储-Cache目录
*/
fun getAppExternalCachePath(subDir: String?=null):String{
val path = StringBuilder(getContext().externalCacheDir?.absolutePath)
subDir?.let {
path.append(File.separator).append(it).append(File.separator)
}
val dir = File(path.toString())
if (!dir.exists()) dir.mkdir()
return path.toString()
}
/**
* 分区存储-File目录
*/
fun getAppExternalFilePath(subDir: String?=null):String{
val path = getContext().getExternalFilesDir(subDir)?.absolutePath
val dir = File(path.toString())
if (!dir.exists()) dir.mkdir()
return path.toString()
}
/*--------------------------------------------------*/
/*-----------------------内部:私有目录--------------------------*/
/**
* 私有目录-files
*/
fun getAppFilePath(subDir:String?=null): String {
val path = StringBuilder(getContext().filesDir.absolutePath)
subDir?.let {
path.append(File.separator).append(it).append(File.separator)
}
val dir = File(path.toString())
if (!dir.exists()) dir.mkdir()
return path.toString()
}
/**
* 私有目录-cache
*/
fun getAppCachePath(subDir:String?=null):String{
val path = StringBuilder(getContext().cacheDir.absolutePath)
subDir?.let {
path.append(File.separator).append(it).append(File.separator)
}
val dir = File(path.toString())
if (!dir.exists()) dir.mkdir()
return path.toString()
}
/*------------------------cache子目录------------------------*/
fun getAudioPathEndWithSeparator(): String {
return getAppCachePath("audio")
}
fun getTxtPathEndWithSeparator(): String {
return getAppCachePath("txt")
}
fun getMp3PathEndWithSeparator(): String {
return getAppCachePath("mp3")
}
fun getTempPathEndWithSeparator(): String {
return getAppCachePath("temp")
}
/*--------------------------------------------------*/
/*-----------------外部:公共目录(需要权限)----------------*/
/**
* Pictures
*/
fun getExternalPicturesPath(subDir:String?=null): String{
val path = StringBuilder(Environment.getExternalStorageDirectory().absolutePath)
.append(File.separator)
.append(Environment.DIRECTORY_PICTURES)
subDir?.let {
path.append(File.separator).append(it).append(File.separator)
}
val dir = File(path.toString())
if (!dir.exists()) dir.mkdir()
return path.toString()
}
/**
* Download
*/
fun getExternalDownloadPath(subDir:String?=null): String{
val path = StringBuilder(Environment.getExternalStorageDirectory().absolutePath)
.append(File.separator)
.append(Environment.DIRECTORY_DOWNLOADS)
subDir?.let {
path.append(File.separator).append(it).append(File.separator)
}
val dir = File(path.toString())
if (!dir.exists()) dir.mkdir()
return path.toString()
}
/**
* DCIM
*/
fun getExternalCameraPath( subDir:String?=null): String{
val path = StringBuilder(Environment.getExternalStorageDirectory().absolutePath)
.append(File.separator)
.append(Environment.DIRECTORY_DCIM)
subDir?.let {
path.append(File.separator).append(it).append(File.separator)
}
val dir = File(path.toString())
if (!dir.exists()) dir.mkdir()
return path.toString()
}
/**
* Music
*/
fun getExternalMusicPath(subDir:String?=null): String{
val path = StringBuilder(Environment.getExternalStorageDirectory().absolutePath)
.append(File.separator)
.append(Environment.DIRECTORY_MUSIC)
subDir?.let {
path.append(File.separator).append(it).append(File.separator)
}
val dir = File(path.toString())
if (!dir.exists()) dir.mkdir()
return path.toString()
}
/*---------------------------------------------------------*/
}
FileUtil文件操作工具类:
object FileUtil{
/**
* File转Uri
*/
fun file2Uri( file: File?): Uri?{
if (file==null) return null
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//适配Android 7.0文件权限,通过FileProvider创建一个content类型的Uri
FileProvider.getUriForFile(getContext(), "${getContext().packageName}.fileProvider", file)
} else {
Uri.fromFile(file)
}
}
/**
* 将文件转换成byte数组
*/
fun file2Byte(file: File?): ByteArray? {
if (file==null) return null
var buffer: ByteArray? = null
try {
val fis = FileInputStream(file)
val bos = ByteArrayOutputStream()
val b = ByteArray(1024)
var n: Int
while (fis.read(b).also { n = it } != -1) {
bos.write(b, 0, n)
}
fis.close()
bos.close()
buffer = bos.toByteArray()
} catch (e: FileNotFoundException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
return buffer
}
/**
* Uri转File
*/
fun uri2File(uri: Uri?): File? {
if (uri==null) return null
var file:File ?= File(uri.toString())
if (file!=null && file.exists()) return file
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
when (uri.scheme) {
ContentResolver.SCHEME_FILE -> {
file = File(requireNotNull(uri.path))
}
ContentResolver.SCHEME_CONTENT -> {
//把文件保存到沙盒
val contentResolver = getContext().contentResolver
val displayName = "${TimeUtil.getCurrentTime()}.${
MimeTypeMap.getSingleton().getExtensionFromMimeType(
contentResolver.getType(uri)
)
}".replace(".bin","")
val ios = contentResolver.openInputStream(uri)
if (ios != null) {
file = File(FilePath.getTxtPathEndWithSeparator(), displayName).apply {
val fos = FileOutputStream(this)
FileUtils.copy(ios, fos)
fos.close()
ios.close()
}
}
}
else -> {
}
}
return file
}else{
var path: String? = null
when(uri.scheme){
"file" -> {
path = uri.encodedPath
if (path != null) {
path = Uri.decode(path)
val cr = getContext().contentResolver
val buff = StringBuffer()
buff.append("(").append(MediaStore.Images.ImageColumns.DATA).append("=")
.append("'$path'").append(")")
val cur: Cursor? = cr.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
arrayOf(
MediaStore.Images.ImageColumns._ID,
MediaStore.Images.ImageColumns.DATA
),
buff.toString(),
null,
null
)
var index = 0
var dataIdx = 0
cur?.let {
cur.moveToFirst()
while (!cur.isAfterLast()) {
index = cur.getColumnIndex(MediaStore.Images.ImageColumns._ID)
index = cur.getInt(index)
dataIdx = cur.getColumnIndex(MediaStore.Images.ImageColumns.DATA)
path = cur.getString(dataIdx)
cur.moveToNext()
}
cur.close()
}
if (index == 0) {
} else {
val u = Uri.parse("content://media/external/images/media/$index")
println("temp uri is :$u")
}
}
}
"content" -> {
// 4.2.2以后
val proj = arrayOf(MediaStore.Images.Media.DATA)
val cursor: Cursor? = getContext().contentResolver.query(uri, proj, null, null, null)
cursor?.let {
if (cursor.moveToFirst()) {
val columnIndex: Int = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
path = cursor.getString(columnIndex)
}
cursor.close()
}
}
else -> {
//Log.i(TAG, "Uri Scheme:" + uri.getScheme());
}
}
return File(path)
}
}
/**
* 删除文件夹
*/
fun deleteRecursive(fileOrDirectory: File) {
if (fileOrDirectory.isDirectory) for (child in fileOrDirectory.listFiles()) deleteRecursive(
child
)
fileOrDirectory.delete()
}
fun deleteCacheDir() = thread{
File(FilePath.getTempPathEndWithSeparator()).deleteRecursively()
File(FilePath.getMp3PathEndWithSeparator()).deleteRecursively()
File(FilePath.getTxtPathEndWithSeparator()).deleteRecursively()
File(FilePath.getAudioPathEndWithSeparator()).deleteRecursively()
}
}
还有一些媒体操作工具类,比如保存图片、视频,创建PDF文件,txt文件。这些都放在GitHub上啦,有兴趣的可以去看下,参考: