最近反馈了一个问题,APP存储在相册中的图片,自己上传了华为系统整了一个弹窗,说我们删除了图片,问题就是业务诉求就是同一个文件名的图片认为是同一张图片,然后下载之前先把存在的文件删除了,下载到时候直接整了一个过度文件,下载成功后改名就好,所以我们首先定位的是这个业务诉求里面的删除逻辑。
因为是相册目录,所以解决方式也很简单,将之前成删除代码修改为通过绝对路径在ContentResolver中查询到当前文件的id.然后通过id去查找到文件的uri,最后通过ContentResolver删除查询到的uri即可。通过fileProvider转化出来的Uri和file.delete() 都会被系统检测出来,并且将图片移动到最近删除目录。
我尝试过将图片下载到到download 目录,然后刷新系统图库,系统图库可以识别到这个图片,同时删除的时候也没有被移动到最近删除的目录,通过结果分析过程,应该是系统检测到图片删除,然后备份了一份到最近删除里面,只要最近删除没有,是不是说明就不提示了。那么方案就多种多样了:
- 放到APP自己的目录下系统图库就识别不到了
- 图片放外置卡,不放相册目录和图片目录、视频、音频等目录也行。
- 图片不存为图片,用其他后缀代替,或者无后缀,这个看手机,有的系统即使是这样,放media 等目录下,他也会识别出来是图片,通常可以识别的。
- 图片存到一个.xxx 这种隐藏文件夹下面。
- 过度文件不要放media目录,当下载完成后直接拷贝一次。
- 比如说,现在APP的问题,老板让重命名文件了。直接改业务诉求。
当然,这些结论是如何得出来的呢?当然是Android文件系统的相关知识。因为在这个过程中,逻辑存在卡顿,没有那么当丝滑,所以总结一下。
正文
Android 中的目录
在Android系统中,/system、/storage/emulated/0和/data这三个目录代表了系统目录、外部存储和用户数据目录的不同部分。
/system是Android系统的核心文件目录,其中包含各种系统级别的应用程序和文件。这个目录通常包含系统级别的应用程序、库、字体和其他关键文件。作为系统管理员或高级用户,您可以在这个目录下进行一些系统级的操作,但普通用户通常不需要访问这个目录。
APP私有目录:/data/user/0/包名
/data是Android系统中的用户数据目录,也称为内部存储。这个目录通常包含用户安装的应用程序和它们的数据文件。与外部存储不同,/data目录对于每个用户来说都是私有的,其他用户无法直接访问。这意味着每个用户只能访问自己的应用程序和数据文件。这个目录通常是在用户设备上存储应用程序数据的最主要位置之一。 app私有目录就是在用户数据目录中:/data/user/0/包名。逻辑上,除非你通过contentprovider 提供出去,一般情况下,别的APP是无法访问到你的文件的,同时往这个里面读写文件往往是不需要文件的读写权限的。
外部存储
SD卡或内部存储
/storage/emulated/0是Android系统中的外部存储目录之一。在Android系统中,外部存储通常是指设备上的存储卡或其他可移动存储设备。/storage/emulated/0目录表示外部存储设备的根目录,这个目录下通常包含设备的公共目录(如图片、音乐、下载等)和其他应用程序的数据文件。对于应用程序来说,这个目录通常是它们共享数据的默认位置之一。
在Android 10(API级别29)之前,使用MediaStore.Files.getContentUri("external")
可以访问外部存储设备上的文件。但是,从Android 10开始,由于隐私和安全性的考虑,此API已被弃用,并且无法再访问外部存储设备上的文件。所以用下面的这个:
MediaStore.Files.getContentUri("external_primary")
对于公有目录的读写,则必须使用MediaStore
提供的API或是SAF
(存储访问框架),意味着我们不能再使用File那一套来随意操作公有目录了。
在Android 11中,没有了之前的兼容模式,不能用File的那一套,意味着以前访问 和读取App外置卡的目录失效了。使用分区存储的应用对自己创建的文件始终拥有读/写权限,无论文件是否位于应用的私有目录内。因此,如果您的应用仅保存和访问自己创建的文件,则无需请求获得
READ_EXTERNAL_STORAGE
或WRITE_EXTERNAL_STORAGE
权限。若要访问其他应用创建的文件,则需要READ_EXTERNAL_STORAGE
权限。并且仍然只能使用MediaStore
提供的API或是SAF
(存储访问框架)访问。
需要注意的是,MediaStore
提供的API只能访问:图片、视频、音频,如果需要访问其它任意格式的文件,需要使用SAF
(存储访问框架),它会调用系统内置的文件浏览器供用户自主选择文件。
外部SD卡和USB驱动
这是是系统支持的。
MediaStore.Files.getContentUri("external_secondary")
总结
总结起来,这三个目录的区别在于:
- /system是Android系统的核心文件目录,包含各种系统级的应用程序和文件。
- /storage/emulated/0是外部存储的根目录,包含公共文件和其他应用程序的数据文件。
- /data是用户数据的目录,也称为内部存储,只供当前用户访问和使用。
文件
在Android中,对于一些文件的读写,随着系统的限制,就出现了3种类型的定位表达方式:
- /external/images/media/201 像这种URI 类型的。
- /storage/emulated/0/DCIM/Homate/iButler-26862-1694506242593.jpg 这种绝对路径类型的。
- content:// 这种通过fileProvider 提供出去的。
URI 类型
这个对象可以用于网络,但是这里主要用于描述本地文件,随着系统管控,我们处理相册、视频、音频等等media文件的时候,包括增删改查等操作,都需要使用这个对象。
uri 转绝对路径或转file
public File getFileFromUri(Context context, Uri uri) {
String filePath = null;
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(uri, new String[] { MediaStore.Images.Media.DATA }, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
filePath = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
}
} finally {
if (cursor != null) {
cursor.close();
}
}
return new File(filePath);
}
uri 转bitmap
public Bitmap getBitmapFromUri(Context context, Uri uri) throws IOException {
ParcelFileDescriptor parcelFileDescriptor = context.getContentResolver().openFileDescriptor(uri, "r");
FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
parcelFileDescriptor.close();
return bitmap;
}
Uri 输入流
val contentResolver = applicationContext.contentResolver
@Throws(IOException::class)
private fun readTextFromUri(uri: Uri): String {
val stringBuilder = StringBuilder()
contentResolver.openInputStream(uri)?.use { inputStream ->
BufferedReader(InputStreamReader(inputStream)).use { reader ->
var line: String? = reader.readLine()
while (line != null) {
stringBuilder.append(line)
line = reader.readLine()
}
}
}
return stringBuilder.toString()
}
uri 输出流
val contentResolver = applicationContext.contentResolver
private fun alterDocument(uri: Uri) {
try {
contentResolver.openFileDescriptor(uri, "w")?.use {
FileOutputStream(it.fileDescriptor).use {
it.write(
("Overwritten at ${System.currentTimeMillis()}\n")
.toByteArray()
)
}
}
} catch (e: FileNotFoundException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
}
uri 转 content://
- 从Uri对象中获取原始文件路径:
String filePath = uri.getPath();
- 如果原始文件路径以"/“开头,则将其替换为"content://”。否则,将原始文件路径与"content://"连接起来:
if (filePath.startsWith("/")) {
filePath = "content://" + filePath;
} else {
filePath = "content://" + filePath;
}
请注意,以"content://"开头的路径是表示通过Content Provider访问文件的标准方式。通过这种方式,您的应用程序可以安全地访问和操作文件,而不必关心文件的实际存储位置和访问权限。
绝对路径转URI
在 Android 中,可以使用 FileProvider
将文件的绝对路径转换为 Uri
。以下是一个示例:
- 首先,在你的 AndroidManifest.xml 文件中添加
FileProvider
的声明:
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
- 在 res/xml/ 下创建一个名为 file_paths.xml 的文件,并添加你的外部存储路径:
<paths>
<external-path name="external_files" path="." />
</paths>
- 然后,你可以使用以下代码将文件的绝对路径转换为
Uri
:
public Uri getUriFromFilePath(Context context, String filePath) {
File file = new File(filePath);
return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file);
}
注意:在 Android 10 (API级别 29) 及更高版本中,由于 Scoped Storage 的限制,你无法直接访问设备的外部存储。所以,你可能需要请求用户授予你访问外部存储的权限,或者将文件保存在应用的内部存储中。 这种方式获取到的uri,可以删除文件,但是会被华为系统提醒,然后把图片整到一个最近删除里面。
ContentResolver来查找图片的URI
在Android中,您可以使用ContentResolver来查找图片的URI。给定图片的路径,您可以使用以下代码来查找其对应的URI:
public Uri getImageUriFromPath(Context context, String imagePath) {
Cursor cursor = context.getContentResolver().query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[] { MediaStore.Images.Media._ID },
MediaStore.Images.Media.DATA + "=? ",
new String[] { imagePath },
null);
if (cursor == null) {
return null;
}
try {
if (cursor.moveToFirst()) {
int id = cursor.getInt(cursor.getColumnIndex(MediaStore.Images.Media._ID));
return MediaStore.Images.Media.EXTERNAL_CONTENT_URI.buildUpon()
.appendPath(String.valueOf(id))
.build();
}
} finally {
cursor.close();
}
return null;
}
而且这种找到的uri 删除的时候,不会被华为系统提醒。
content://
在Android中,文件路径以"content://"开头是因为它使用了Content Provider。Content Provider是Android中的一种机制,用于在应用程序之间共享数据。它提供了一种标准接口,允许不同的应用程序访问和操作同一份数据。
使用Content Provider可以确保数据的安全性和一致性,因为它允许对数据进行访问控制、事务处理和冲突解决。它还提供了一种统一的方式来访问不同类型的数据,包括文件、数据库、网络资源等。
当您在Android中使用文件路径时,使用"content://"前缀表示您正在通过Content Provider访问文件。这样可以确保您的应用程序能够正确地获取和操作文件,而不必关心文件的实际存储位置和访问权限。
转URI
在Android中,文件路径以"content://"开头,表示通过Content Provider访问文件。要将"content://"开头的路径转换为Uri对象,可以使用Uri.parse()
方法。例如:
String filePath = "content://com.example.app.provider/files/example.txt";
Uri uri = Uri.parse(filePath);
转绝对路径
要将Uri对象转换为绝对路径,可以使用Uri.getPath()
方法。例如:
String absolutePath = uri.getPath();
如果你看到了这里,觉得文章写得不错就给个赞呗?
更多Android进阶指南 可以扫码 解锁更多Android进阶资料
敲代码不易,关注一下吧。ღ( ´・ᴗ・` )