前言
在学习《第一行代码》播放SD卡根目录中的音乐时,碰到了无法读音乐文件的问题android.system.ErrnoException: open failed: EACCES (Permission denied)
经过查阅相关资料,现把个人的解决方法记录下来
Android 6.0(不含)以下
在6.0之前的版本,申请sd卡读写权限不是一件难事,只需要在AndroidManifest.xml中加入下列代码,权限会在app安装时申请,READ可以不添加。因为在8.0以前,申请WRITE会自动获取 一组 相关的权限,其中就包括READ权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
//READ可以不添加。因为在8.0以前,申请WRITE会自动获取 一组 相关的权限,其中就包括READ权限,因此仅申请WRITE权限也能用
Android 6.0(含)以上
Android6.0之后,Google为Android加入了运行时申请权限的机制,因此除了在AndroidManifest.xml中加入权限申请代码之外,在java代码中还要申请相关权限。Android6.0以后都要有动态申请权限
判断是否授权READ和WRITE
int permission_write=ContextCompat.checkSelfPermission(MainActivity.this,
Manifest.permission.WRITE_EXTERNAL_STORAGE);
int permission_read=ContextCompat.checkSelfPermission(MainActivity.this,
Manifest.permission.READ_EXTERNAL_STORAGE);
if(permission_write!= PackageManager.PERMISSION_GRANTED
|| permission_read!=PackageManager.PERMISSION_GRANTED){
Toast.makeText(this, "正在请求权限", Toast.LENGTH_SHORT).show();
//申请权限,特征码自定义为1,可在回调时进行相关判断
ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE,Manifest.permission.READ_EXTERNAL_STORAGE},1);
}
在MainActivity.java中重写方法OnRequestPermissionResult,其中常量1是申请权限时填入的特征码
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode){
case 1:
if(grantResults.length>0 && grantResults[0]==PackageManager.PERMISSION_GRANTED){
//权限已成功申请
}else{
//用户拒绝授权
Toast.makeText(this, "无法获取SD卡读写权限", Toast.LENGTH_SHORT).show();
finish();
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
Android 8.0申请的注意事项
在Android 8.0中,Google完善了相关机制,“所申即所得”,你申请了WRITE权限 不会同时给予READ权限,因此要对外部存储进行读写,需要申请WRITE和READ两个权限,按上边代码申请就可以了。
开发者文档:
在 Android 8.0 之前,如果应用在运行时请求权限并且被授予该权限,系统会错误地将属于同一权限组并且在清单中注册的其他权限也一起授予应用。
对于针对 Android 8.0 的应用,此行为已被纠正。系统只会授予应用明确请求的权限。然而,一旦用户为应用授予某个权限,则所有后续对该权限组中权限的请求都将被自动批准。
例如,假设某个应用在其清单中列出 READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE。应用请求 READ_EXTERNAL_STORAGE,并且用户授予了该权限。如果该应用针对的是 API 级别 24 或更低级别,系统还会同时授予 WRITE_EXTERNAL_STORAGE,因为该权限也属于同一 STORAGE 权限组并且也在清单中注册过。如果该应用针对的是 Android 8.0,则系统此时仅会授予 READ_EXTERNAL_STORAGE;不过,如果该应用后来又请求 WRITE_EXTERNAL_STORAGE,则系统会立即授予该权限,而不会提示用户。
Android 9.0申请的注意事项
在Android 9.0上即使正确使用了上边的代码,也会提示出现无权限的问题 Permission denied
从网上各处找到的资料显示:Google修改移除了 WRITE_MEDIA_STORAGE 权限相关权限,导致了外部 SD 卡存储不可写的问题
Android 9.0中sdcard 的权限和挂载问题:解决办法:
①适配DocumentFile
由于之前应用使用了 java.io.File 接口操作外置 SD 卡文件,期望对代码的修改量最小,则最好的方式是对已有的 File 操作再做一次封装。
由于 Android 9.0 之前系统应用默认是可以通过 java.io.File 接口写入外置 SD卡 的,而如果作为公开市场第三方应用却在 4.4 之后就不可写,而且有的厂商定制版本 Android 9.0 外置 SD 卡也是可以直接写入而不需要 DocumentFile 接口,DocumentFile 接口也没有 java.io.File 效率高。
所以最好的办法是先检查是否有文件写入权限,如果有写入权限,则直接使用 File 接口操作,如果没有权限再检查文件是否在外置 SD 卡,如果文件在 SD 卡则使用 DocumentFile 接口操作
参考自:
②修改cpp文件,增加-w权限
alps_p0_mp2\update\alps\system\vold\model\PublicVolume.cpp
if (!(mFusePid = fork())) {
if (getMountFlags() & MountFlags::kPrimary) {
if (execl(kFusePath, kFusePath,
"-u", "1023", // AID_MEDIA_RW
"-g", "1023", // AID_MEDIA_RW
"-U", std::to_string(getMountUserId()).c_str(),
"-w",
mRawPath.c_str(),
stableName.c_str(),
NULL)) {
PLOG(ERROR) << "Failed to exec";
}
} else {
if (execl(kFusePath, kFusePath,
"-u", "1023", // AID_MEDIA_RW
"-g", "1023", // AID_MEDIA_RW
"-U", std::to_string(getMountUserId()).c_str(),
"-w",//add by for add sdcard permission 就是这样
mRawPath.c_str(),
stableName.c_str(),
NULL)) {
PLOG(ERROR) << "Failed to exec";
}
}
③修改targetSdkVersion为27(不推荐,无法在应用商店上架)
这方法看别人试过,我自己没试过。已经被这个版本的权限问题弄吐了
Android Q(10.0)申请的注意事项
注意:
早期测试版本中引入的专门用于操作媒体文件的权限
READ_MEDIA_IMAGES,
READ_MEDIA_AUDIO,
READ_MEDIA_VIDEO – 现已过时!
为了能给用户提供对文件的更多控制并限制文件混乱,Android Q改变了应用程序访问设备外部存储上文件的方式,例如存储在路径/ sdcard中的文件。Android Q继续使用READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限,这些权限对应于存储面向用户的运行时权限。但是,默认情况下targetSdkVersion设置为Android Q的应用(以及manifest清单开启属性来启动这个变更的应用)会获得一个沙盒视图到外部存储。此类应用程序只能看到其特定于应用程序的目录和特定媒体类型,因此应用程序不需要请求任何其他用户权限。
也就是说从Android Q(10.0) sdk>=29 开始,系统为每个app提供了一个隔离环境(分区存储),参考下列资料,有十分详细的说明,这里给出解决办法
参考资料:
开发者文档https://developer.android.google.cn/training/data-storage/files/external-scoped?hl=zh_cn
解决办法
①在 AndroidManifest.xml 中加入 android:requestLegacyExternalStorage=“true”
加入这句代码之后会启动兼容模式:停用分区存储,但是要留意的是
警告:2020年,主要平台版本将要求所有应用都使用分区存储,无论应用的目标 SDK 级别是多少。因此,您应该提前确保您的应用能够使用分区存储。为此,请确保针对搭载 Android 10(API 级别 29)及更高版本的设备启用了该行为。
在您的应用完全兼容分区存储之前,您可以根据应用的目标 SDK 级别或 requestLegacyExternalStorage 清单属性,暂时选择停用分区存储:
以 Android 9(API 级别 28)或更低版本为目标平台。
如果以 Android 10 或更高版本为目标平台,请在应用的清单文件中将 requestLegacyExternalStorage 的值设为 true:
<manifest ... >
<!-- This attribute is "false" by default on apps targeting
Android 10 or higher. -->
<application android:requestLegacyExternalStorage="true" ... >
...
</application>
</manifest>
注意:如果某个应用在安装时启用了传统外部存储,则该应用会保持此模式,直到卸载为止。无论设备后续是否升级为搭载 Android 10 或更高版本,或者应用后续是否更新为以 Android 10 或更高版本为目标平台,此兼容性行为均适用。
要测试以 Android 9 或更低版本为目标平台的应用在使用分区存储时的行为,您可以通过将 requestLegacyExternalStorage 的值设为 false 来选择启用该行为。
②访问媒体文件,使用MediaStore。
③访问其他应用创建的任何其他文件,使用系统提供的存储访问框架
原谅本人水平有限,尚未能贴出相关代码。
但各位可以在开发者文档中找到相关的解决方法
开发者帮助文档:有关AndroidQ的适配资料
华为开发者帮助文档:https://developer.huawei.com/consumer/cn/doc/50127
请注意,华为开发者帮助文档里边有些方法已经过时,测试版时使用的方法在正式版中可能无法使用,如
早期测试版本中引入的专门用于操作媒体文件的权限
READ_MEDIA_IMAGES,
READ_MEDIA_AUDIO,
READ_MEDIA_VIDEO
现已过时!