Posted in

【Android】就想下载个文件到 SD 卡,怎就这么难?快把代码拿走吧_AI阅读总结 — 包阅AI

包阅导读总结

1. 关键词:Android、SD 卡、下载文件、权限管理、存储访问

2. 总结:

本文主要探讨了 Android 下载文件到 SD 卡的实现方式,介绍了权限管理的变化节点,包括不同 Android 版本的存储访问框架和权限模型的改动。重点阐述了三种下载文件到 SD 卡的方案,分别是直接申请 SD 卡权限、使用 DocumentFile Api 方案和使用 SAF 让用户选择文件保存路径方案,并对各方案的适用场景进行了说明。

3. 主要内容:

– Android 下载文件到 SD 卡权限管理的变化

– 介绍了从 Android 5.0 到 14 版本中与存储访问和权限相关的重大节点变化

– 下载文件到 SD 卡的方案

– 直接申请 SD 卡权限

– 申请写入文件权限,兼容不同机型

– 使用 DocumentFile Api 的方案

– 手动获取路径,创建文件和获取 Uri 进行写入

– 使用 SAF 的选择

– 让用户选择文件保存路径,获取 Uri 后进行文件存储

– 总结

– 强调不同方案的适用场景,不推荐使用危险权限方案

思维导图:

文章地址:https://mp.weixin.qq.com/s/mNwRQ9OQqY8Nb7a7fxfuwg

文章来源:mp.weixin.qq.com

作者:Newki

发布时间:2024/8/4 6:29

语言:中文

总字数:3217字

预计阅读时间:13分钟

评分:80分

标签:Android开发,权限管理,文件下载,SD卡操作,存储访问框架(SAF)


以下为原文内容

本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com

点击公众关注号,“技术干货”及时达!

Android下载文件到SD卡的几种方式

前言

Android的版本更新算是跟权限管理犟上了,每次版本更新都是权限管理的改动,导致我现在就想简简单单的实现一个下载文件到 SD 卡怎么就一路坎坷呢。

本来之前我们的应用下载文件到沙盒很容易得,也不需要处理权限,但是沙盒中的文件用户看不到无法操作,所以我们需要改动为下载到外置 SD 卡让用户在设备自带的文件管理器中可以查看与操作。

不就改动一个路径的事情吗?又要处理权限组,然后申请权限,target33 的还不让申请内存卡权限,需要用多媒体权限替代…烦不甚烦。

那么我们应该如何实现一个简单的下载文件到 SD 卡呢?

一、方案1.权限的变化与实现

大家可能或多或少的都知道Android的SD卡权限收紧,几个重大的节点:

Android 5.0 存储访问框架(SAF):引入了存储访问框架,用于提供一种更加安全和细粒度的文件访问方式。通过 SAF,用户可以选择授予应用访问特定文件或目录的权限。

Android 6.0 运行时权限:引入了运行时权限模型,应用需要在运行时动态请求存储权限,而不是在安装时获得。这进一步提高了用户的控制权。

Android 7.0 File URI 限制:文件URI被限制,应用不能直接通过file://访问其他应用的文件,必须使用ContentProvider来共享文件。

Android 10

分区存储(Scoped Storage):引入了分区存储,限制了应用对外部存储的访问。应用只能访问自己的应用专属目录和一些特定的公共目录(如Download、Pictures等)。

媒体文件访问权限:应用可以通过特定的媒体存储API访问和操作媒体文件(如图片、音频、视频),而不需要全局存储权限。

Android 11

进一步收紧分区存储:分区存储变得更加严格,应用对外部存储的访问进一步受限。

MANAGE_EXTERNAL_STORAGE 权限:引入了新的权限,允许某些应用访问所有的外部存储文件,但这个权限的使用受到严格限制,需要通过Google Play审核。

媒体存储访问权限:应用可以请求访问特定类型的媒体文件,更加细粒度的访问控制。

Android 12

特定作用域的媒体访问:为不同类型的媒体文件提供更细致的访问控制,实现了更细粒度的权限管理。

Android 13 与 Android 14 倒是没有对权限进行大的改动,目前已经趋于稳定。

顺便说一嘴 android:requestLegacyExternalStorage=”true” 是为 Android 10 提供的一个临时适配方案,帮助应用在过渡到分区存储时继续使用传统的存储访问模式。开发者应当尽快适配分区存储,以确保应用在未来的Android版本上能够正常运行。在 Android 14 的年代了已经不需要用这种方案,请丢到历史的垃圾堆,老老实实的按谷歌标准来适配。

讲了这么多跟我下载一个文件到SD卡有毛关系,我就问我就要直接申请SD卡权限,就要下载到SD卡行不行,啰里八嗦的。

嗯,行也不行,看机器的系统版本。比如:

    File downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
String downloadPath = downloadDir.getAbsolutePath();

MyOkhttpUtil.okHttpDownloadFile("http://www.baidu.com/income-eligibility-criteria.pdf",
new CallBackUtil.CallBackFile(downloadPath, fileName) {
@Override
public void onFailure(Call call, Exception e) {
LoadingDialogManager.get().dismissLoading();
YYLogUtils.e("downLoadMessageFile--onFailure");
ToastUtils.get().showFailText(CommUtils.getContext(), "File download failed");
}

@Override
public void onProgress(float progress, long total) {
super.onProgress(progress, total);
}

@Override
public void onResponse(Call call, File response) {
LoadingDialogManager.get().dismissLoading();
YYLogUtils.w("downLoadMessageFile--Success--path=" + response.getAbsolutePath());
ToastUtils.get().showSuccessText(CommUtils.getContext(),
"File download successful, save path: " + response.getAbsolutePath());
}
});

我自己封装一个 OkHttp 的下载方法,直接下载到 download 文件夹。

大家觉得能下载成功吗?行还是不行?给大家10秒钟考虑。

下面给出答案,安卓7的机器不行,安卓13的机器行。(Targert 33)

image.png

那我给他们加上权限申请呢

  PermissionEngine.get().requestPermission(activity, new PermissionEngine.OnSuccessCallback() {
@Override
public void onSuccess() {
}
}, Manifest.permission_group.STORAGE);

咦?不能这么用了?那我单独申请!

  PermissionEngine.get().requestPermission(activity, new PermissionEngine.OnSuccessCallback() {
@Override
public void onSuccess() {
}
},Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE);


我尼玛…我写个文件跟多媒体权限有半毛钱关系?

其实真没关系,「其实我们读取文件和写入文件是不同的操作逻辑,并且多媒体文件与普通文件也是不同的操作逻辑。」

其他的方式我们不提,因为今天的主题只是下载写入文件而已,跟多媒体的读取权限没关系,我们只需要申请写入文件的权限就可以兼容安卓10以下的版本。

二、直接申请SD卡权限

最简单的方案,我们直接申请写入文件就可以兼容安卓10以前和以后的机型。

 PermissionEngine.get().requestPermission(activity, new PermissionEngine.OnSuccessCallback() {
@Override
public void onSuccess() {

//SD卡权限申请成功之后再次尝试
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
// 获取SD卡中Download目录的路径
File downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
String downloadPath = downloadDir.getAbsolutePath();

YYLogUtils.w("准备下的路径为:" + downloadPath + fileName);

LoadingDialogManager.get().showLoading(activity);

MyOkhttpUtil.okHttpDownloadFile(downloadUrl, new CallBackUtil.CallBackFile(downloadPath, fileName) {
@Override
public void onFailure(Call call, Exception e) {
LoadingDialogManager.get().dismissLoading();
YYLogUtils.e("downLoadMessageFile--onFailure");
ToastUtils.get().showFailText(CommUtils.getContext(), "File download failed");
}

@Override
public void onProgress(float progress, long total) {
super.onProgress(progress, total);
}

@Override
public void onResponse(Call call, File response) {
LoadingDialogManager.get().dismissLoading();
YYLogUtils.w("downLoadMessageFile--Success--path=" + response.getAbsolutePath());
ToastUtils.get().showSuccessText(CommUtils.getContext(),
"File download successful, save path: " + response.getAbsolutePath());
}
});

} else {
YYLogUtils.e("SD card not available or not writable.");
}


}
}, Manifest.permission.WRITE_EXTERNAL_STORAGE);

Android 13:

Android 7:

还初步测试了 Android 11,12,14等机型都没问题。

三、方案2.使用DocumentFile Api的方案

除了这个方法我们也能直接用 DocumentFile 的方案来实现,由于我们知道要下载到哪一个文件夹,我们直接手动的获取到路径,然后包装为 DocumentFile ,再通过 DocumentFile 的方式创建对应的子文件,并获取到 Uri,再通过 Uri 获取到 outputstream ,有了这个流不管是copy本地文件还是获取网络文件就能正常的写入了。

大致代码如下:

private void downloadFile(DocumentFile selectedDir, String fileName) {
String url = "http://example.com/file.pdf";
File parent = new File(selectedDir.getPath());
DocumentFile documentFile = DocumentFile.fromFile(parent);
DocumentFile subDocumentFile = documentFile.createFile("application/pdf", fileName);
Uri uri = subDocumentFile.getUri();

OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(url)
.build();

client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
// 处理下载失败的情况
}

@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
InputStream inputStream = response.body().byteStream();
OutputStream outputStream = null;
try {
outputStream = getContentResolver().openOutputStream(uri);
if (outputStream != null) {
copyInputStreamToOutputStream(inputStream, outputStream);
} else {
// 处理输出流为空的情况
}
} finally {
if (inputStream != null) {
inputStream.close();
}
if (outputStream != null) {
outputStream.close();
}
}
}
}
});
}

private void copyInputStreamToOutputStream(InputStream inputStream, OutputStream outputStream) throws IOException {
byte[] buffer = new byte[4096];
int len;
while ((len = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
}
}


咦?为什么要用 DocumentFile ?直接用 File 不行吗 ?还真不行有兼容性问题,具体可以看看我早期的文章【Android操作文件也太难了趴,File vs DocumentFile 的异同】。这是我刚接触博客写的文章,现在看来很粗糙了,大家见谅。

总结来说就是 Android10 以上的系统是无法使用 File 来写入的,除了一些特殊文件夹才能写入。Android10 以上的设备想写入自定义文件夹中的文件,还是推荐使用 DocumentFile 的方案。

四、方案3.使用SAF的选择

你这都是指定了下载文件的路径,如果我想让用户自己选择使用哪一个文件夹呢?这就引出了第三种方案,SAF让用户自己选择文件保存路径。

我们先用 DOCUMENT_TREE 的方式打开 SAF:

    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
activity.startActivityForResult(intent, 1027);

此时我们会进入文件选择的系统页面,每一个机型和系统样式会有不同,不过操作都是大同小异。

image.png
image.png

接下来等用户选择/创建文件夹之后,我们就能获取到这个 Uri 了:

  @Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 1027 && resultCode == Activity.RESULT_OK) {
if (data != null) {
Uri treeUri = data.getData();
DocumentFile pickedDir = DocumentFile.fromTreeUri(this, treeUri);
if (pickedDir != null) {
// 使用pickedDir来下载文件
Uri uri = pickedDir.getUri();
String name = pickedDir.getName();
boolean canRead = pickedDir.canRead();
boolean canWrite = pickedDir.canWrite();
YYLogUtils.w("uri:" + uri + " name:" + name + " canRead:" + canRead + " canWrite:" + canWrite);
}
}
}
}

有了 Uri 了还需要我再说了吗,把上面的代码套过来,直接输入流和输出流对接就保存文件了。

总结

本文简单的介绍了 Android 系统权限收紧的介绍,并且对于如何「写入文件」方面做了几种适配案的探讨。

再次强调,本文只针对文件的写入,对于文件的读取是不适用的,那是另外的事情,对于多媒体图片视频等文件的写入和读取又是另另外的事了,万不可混为一谈。

对于文件的写入其实除了以上的方案还可以用 MANAGE_DOCUMENTS 的权限,但是但是个人强烈不推荐使用 android.permission.MANAGE_DOCUMENTS 这样的危险权限方案。上架 Google Play 会受限需要声明安全不说,而且随着越来越收紧的权限,后面难免也要再次改。并且危险权限是会回收的,过一段时间需要用户再次去确定,用户的使用感受上来说也并不好。系统对于这些危险权限给出很多提示,搞得用户也害怕,所以如非必要不要用这个权限。

那么再次总结一下,下载到指定目录可以考虑直接动态申请写入SD卡权限是最简单的,其次我们可以使用 DocumentFile 的 Api 去创建指定的文件,可以通过流的方式存储文件,再此基础上如果想让用户自己选择存放的目录则需要 ACTION_OPEN_DOCUMENT_TREE 的方式打开 SAF 的目录选择,并存储文件。

目前这三种方案都是可以适配到Android的各版本并且有各自的使用场景,如果你只想下载一个文件到目录直接用方案1,如果你想下载到更深的目录,那么需要你自己创建文件夹和文件可以考虑用方案2,如果你想让用户自己选择文件夹去存储那么久使用方案3。

对于相关的 DocumentFile 文件详细操作可以参考我早前的文章,链接在文章内容中,而本文只给出了简单的 Demo,相信大家看完之后都会如何处理下载文件到外置SD卡目录了。

那么文章到处就告一段落了,当然如果你有其他的更多的更好的实现方式,也希望大家能评论区交流一起学习进步。如果我的文章有错别字,不通顺的,或者代码、注释、有错漏的地方,同学们都可以指出修正。

如果感觉本文对你有一点的启发和帮助,还望你能点赞支持一下,你的支持对我真的很重要。

Ok,这一期就此完结。