Android 5.0 ~ 14访问Android/data(obb)目录的方法

立山  金牌会员 | 2024-6-21 00:27:29 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 697|帖子 697|积分 2091

众所周知,安卓每次出新版本的时候都会收紧权限,存储权限也不例外。虽说官方的意思是为了保护隐私安全,但这些改动着实令开发者和用户感到头疼,尤其是Android/data、Android/obb目录的访问。究竟用户更难操纵,开发者也要费力适配。那么今天就来探索下怎么适配这些变动点吧。
差别安卓版本存储权限差别

1、Android 6.0 之前

应用只需要在 AndroidManifest.xml 下声明以下权限即可主动获取存储权限: 
  1. <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
  2. <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
复制代码
2、Android 6.0 起

从Android 6.0开始,除了以上操纵以外,还需要在代码中动态申请权限。
  1. // 检查是否有存储权限
  2. boolean granted = context.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) ==
  3.    PackageManager.PERMISSION_GRANTED;
  4. // 在activity中请求存储权限
  5. requestPermissions(new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, 0);
复制代码
3、Android 10

Android 10 开始引入了沙盒机制,应用在 sdcard 中默认只能读写私有目录(即/sdcard/Android/data/[应用包名]/),其他目录即便执行前面的操纵也无法读写。除非在 AndroidManifest.xml 下声明以下属性:
  1. <application
  2.        ...
  3.        android:requestLegacyExternalStorage="true">
复制代码
这样的话就会临时停用沙盒机制,正常读写/sdcard下文件。
4、Android 11

Android 11开始,且应用的目标版本在30及以上,以上的操纵也无法再读写sdcard目录。需要声明以下权限:
  1. <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
复制代码
再动态申请权限:
  1. // 检查是否有存储权限
  2. boolean granted = Environment.isExternalStorageManager();
复制代码
  1. // 在activity中请求存储权限
  2. Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
  3.    .setData(Uri.parse("package:"+getPackageName()));
  4. startActivityForResult(intent, 0);
复制代码
执行以上操纵后,sdcard已可以或许正常读写。
但是,有2个特别的目录仍然无法读写:
/sdcard/Android/data/sdcard/Android/obb
这两个路径需要安卓自带的 DocumentsUI 授权才能访问。
首先,最重要的一点,添加 documentfile 依靠(SDK自带的谁人版本有问题):
  1. implementation "androidx.documentfile:documentfile:1.0.1"
复制代码
Activity哀求授权:
  1. Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
  2. intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
  3.                Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
  4. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  5.    // 请求Android/data目录的权限,Android/obb目录则把data替换成obb即可。
  6.    Uri treeUri = Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3AAndroid%2Fdata");
  7.    DocumentFile df = DocumentFile.fromTreeUri(this, treeUri);
  8.    if (df != null) {
  9.        intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, df.getUri());
  10.    }
  11. }
  12. startActivityForResult(intent, 1);
复制代码
还需要在回调中生存权限:
  1. @Override
  2. protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
  3.    super.onActivityResult(requestCode, resultCode, data);
  4.    Uri uri;
  5.    if (data != null && (uri = data.getData()) != null) {
  6.        // 授权成功
  7.        getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
  8.    } else {
  9.        // 授权失败
  10.    }
  11. }
复制代码
在哀求授权时,会跳转到以下界面。点击下方按钮授权即可。


然后,使用接口获取文件列表:
  1. // Android 11~12转换路径。例如:/sdcard/Android/data,转换成:
  2. // Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3AAndroid%2Fdata")
  3. // 路径 /sdcard/Android/data/com.xxx.yyy,转换成:
  4. // Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3AAndroid%2Fdata%2Fcom.xxx.yyy")
  5. // 以此类推。
  6. Uri pathUri = pathToUri(path);
  7. DocumentFile documentFile = DocumentFile.fromTreeUri(context, pathUri);
  8. if (documentFile != null) {
  9.     DocumentFile[] documentFiles = documentFile.listFiles();
  10.     for (DocumentFile df : documentFiles) {
  11.         // 文件名
  12.         String fName = df.getName();
  13.         // 路径
  14.         String fPath = path + "/" + fName;
  15.     }
  16. }
复制代码
5、Android 13

Android 13 开始,上面提到的授权 Android/data、Android/obb目录的方法失效了。哀求授权会出现如下界面:

点击下方按钮会提示:

说明安卓13无法再直接授权 Android/data 和 Android/obb 这两个目录了。但也并非无计可施。我们可以授权他们的子目录。
我们知道,这个目录下的文件夹名称一样平常都是应用的包名。我们可以直接用应用包名的路径来哀求授权。
这里要留意,Android 13和Android 11~12的路径转换规则不一样。
// Android 13转换路径。比方:/sdcard/Android/data/com.xxx.yyy,转换成:
// Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata%2Fcom.xxx.yyy/document/primary%3AAndroid%2Fdata%2Fcom.xxx.yyy")
// 路径 /sdcard/Android/data/com.xxx.yyy/files,转换成:
// Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata%2Fcom.xxx.yyy/document/primary%3AAndroid%2Fdata%2Fcom.xxx.yyy%2Ffiles")
// 以此类推。

哀求授权时,出现如下界面。照常授权即可。

6、Android 14

Android 14对于data、obb目录的授权进一步收紧。在Android 14的后期版本和Android 15预览版中,以上方法已失效(传入uri哀求授权只会跳转到sdcard根目录)。这种情况下,想要访问data和obb目录就需要使用Shizuku了。(现在MT、FV就是用这种方法访问的)
Shizuku的用法可以查阅干系教程,这里不多赘述。请先将Shizuku启动,便于后续使用。
首先,添加Shizuku的依靠:
  1. def shizuku_version = "13.1.5"
  2. implementation "dev.rikka.shizuku:api:$shizuku_version"
  3. // Add this line if you want to support Shizuku
  4. implementation "dev.rikka.shizuku:provider:$shizuku_version"
复制代码
AndroidManifest.xml添加以下内容:
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <manifest xmlns:android="http://schemas.android.com/apk/res/android"
  3.     xmlns:tools="http://schemas.android.com/tools">
  4.     <uses-sdk tools:overrideLibrary="rikka.shizuku.api, rikka.shizuku.provider, rikka.shizuku.shared, rikka.shizuku.aidl" />
  5.     <uses-permission android:name="moe.shizuku.manager.permission.API_V23" />
  6.     <application>
  7.         <provider
  8.             android:name="rikka.shizuku.ShizukuProvider"
  9.             android:authorities="${applicationId}.shizuku"
  10.             android:enabled="true"
  11.             android:exported="true"
  12.             android:multiprocess="false"
  13.             android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
  14.         <meta-data
  15.             android:name="moe.shizuku.client.V3_SUPPORT"
  16.             android:value="true" />
  17.     </application>
  18. </manifest>
复制代码
判断Shizuku是否安装:
  1. private static boolean isShizukuInstalled() {
  2.     try {
  3.         context.getPackageManager().getPackageInfo("moe.shizuku.privileged.api", 0);
  4.         return true;
  5.     } catch (PackageManager.NameNotFoundException e) {
  6.         e.printStackTrace();
  7.     }
  8.     return false;
  9. }
复制代码
判断Shizuku是否可用:
  1. boolean available = Shizuku.pingBinder();
复制代码
检查Shizuku是否授权:
  1. boolean granted = Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED;
复制代码
哀求Shizuku权限:
  1. Shizuku.requestPermission(0);
复制代码
监听授权结果:
  1. Shizuku.addRequestPermissionResultListener(listener);
  2. Shizuku.removeRequestPermissionResultListener(listener);
复制代码
授权后,自己定义一个aidl文件:
IFileExplorerService.aidl
  1. package com.magicianguo.fileexplorer.userservice;
  2. import com.magicianguo.fileexplorer.bean.BeanFile;
  3. interface IFileExplorerService {
  4.     List<BeanFile> listFiles(String path);
  5. }
复制代码
BeanFile.java
  1. public class BeanFile implements Parcelable {
  2.     public BeanFile(String name, String path, boolean isDir, boolean isGrantedPath, String pathPackageName) {
  3.         this.name = name;
  4.         this.path = path;
  5.         this.isDir = isDir;
  6.         this.isGrantedPath = isGrantedPath;
  7.         this.pathPackageName = pathPackageName;
  8.     }
  9.     /**
  10.      * 文件名
  11.      */
  12.     public String name;
  13.     /**
  14.      * 文件路径
  15.      */
  16.     public String path;
  17.     /**
  18.      * 是否文件夹
  19.      */
  20.     public boolean isDir;
  21.     /**
  22.      * 是否被Document授权的路径
  23.      */
  24.     public boolean isGrantedPath;
  25.     /**
  26.      * 如果文件夹名称是应用包名,则将包名保存到该字段
  27.      */
  28.     public String pathPackageName;
  29.     protected BeanFile(Parcel in) {
  30.         name = in.readString();
  31.         path = in.readString();
  32.         isDir = in.readByte() != 0;
  33.         isGrantedPath = in.readByte() != 0;
  34.         pathPackageName = in.readString();
  35.     }
  36.     public static final Creator<BeanFile> CREATOR = new Creator<BeanFile>() {
  37.         @Override
  38.         public BeanFile createFromParcel(Parcel in) {
  39.             return new BeanFile(in);
  40.         }
  41.         @Override
  42.         public BeanFile[] newArray(int size) {
  43.             return new BeanFile[size];
  44.         }
  45.     };
  46.     @Override
  47.     public int describeContents() {
  48.         return 0;
  49.     }
  50.     @Override
  51.     public void writeToParcel(@NonNull Parcel dest, int flags) {
  52.         dest.writeString(name);
  53.         dest.writeString(path);
  54.         dest.writeByte((byte) (isDir ? 1 : 0));
  55.         dest.writeByte((byte) (isGrantedPath ? 1 : 0));
  56.         dest.writeString(pathPackageName);
  57.     }
  58. }
复制代码
IFileExplorerService实现类:
  1. public class FileExplorerService extends IFileExplorerService.Stub {
  2.     @Override
  3.     public List<BeanFile> listFiles(String path) throws RemoteException {
  4.         List<BeanFile> list = new ArrayList<>();
  5.         File[] files = new File(path).listFiles();
  6.         if (files != null) {
  7.             for (File f : files) {
  8.                 list.add(new BeanFile(f.getName(), f.getPath(), f.isDirectory(), false, f.getName()));
  9.             }
  10.         }
  11.         return list;
  12.     }
  13. }
复制代码
然后使用Shizuku绑定UserService:
  1. private static final Shizuku.UserServiceArgs USER_SERVICE_ARGS = new Shizuku.UserServiceArgs(
  2.     new ComponentName(packageName, FileExplorerService.class.getName())
  3. ).daemon(false).debuggable(BuildConfig.DEBUG).processNameSuffix("file_explorer_service").version(1);
  4. public static IFileExplorerService iFileExplorerService;
  5. private static final ServiceConnection SERVICE_CONNECTION = new ServiceConnection() {
  6.     @Override
  7.     public void onServiceConnected(ComponentName name, IBinder service) {
  8.         Log.d(TAG, "onServiceConnected: ");
  9.         iFileExplorerService = IFileExplorerService.Stub.asInterface(service);
  10.     }
  11.     @Override
  12.     public void onServiceDisconnected(ComponentName name) {
  13.         Log.d(TAG, "onServiceDisconnected: ");
  14.         iFileExplorerService = null;
  15.     }
  16. };
  17. // 绑定服务
  18. public static void bindService() {
  19.     Shizuku.bindUserService(USER_SERVICE_ARGS, SERVICE_CONNECTION);
  20. }
复制代码
绑定之后,调用 aidl 内里的方法来管理文件即可。

2024/05/18 更新
现在发现了新的Document授权方式,可以或许在Android 13、14上直接授权Android/data(obb)目录。
例1:Android/data目录哀求授权,可使用如下Uri(“\u200B”是零宽空格):
  1.     Uri.Builder uriBuilder = new Uri.Builder()
  2.         .scheme("content")
  3.         .authority("com.android.externalstorage.documents")
  4.         .appendPath("tree")
  5.         .appendPath("primary:A\u200Bndroid/data")
  6.         .appendPath("document")
  7.         .appendPath("primary:A\u200Bndroid/data");
  8. // 相当于 content://com.android.externalstorage.documents/tree/primary%3AA%E2%80%8Bndroid%2Fdata/document/primary%3AA%E2%80%8Bndroid%2Fdata
复制代码
例2:Android/data/com.xxx.yyy目录哀求授权,可使用如下Uri:
  1.     Uri.Builder uriBuilder = new Uri.Builder()
  2.         .scheme("content")
  3.         .authority("com.android.externalstorage.documents")
  4.         .appendPath("tree")
  5.         .appendPath("primary:A\u200Bndroid/data")
  6.         .appendPath("document")
  7.         .appendPath("primary:A\u200Bndroid/data/com.xxx.yyy");
  8. // 相当于 content://com.android.externalstorage.documents/tree/primary%3AA%E2%80%8Bndroid%2Fdata/document/primary%3AA%E2%80%8Bndroid%2Fdata%2Fcom.xxx.yyy
复制代码
也就是说,和之前安卓11、12的唯一区别就是把“Android”改成“A\u200Bndroid”。

源代码:GitHub - MagicianGuo/Android-FileExplorerDemo: 可以或许访问Android/data(obb)目录,已适配Android 5.0 ~ 14。安卓高版本可以使用Shizuku授权。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

立山

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表