眾所周知,安卓每次出新版本的時候都會收緊權(quán)限,存儲權(quán)限也不例外。雖說官方的意思是為了保護隱私安全,但這些改動著實令開發(fā)者和用戶感到頭疼,尤其是Android/data、Android/obb目錄的訪問。畢竟用戶更難操作,開發(fā)者也要費力適配。那么今天就來探索下怎么適配這些變更點吧。
不同安卓版本存儲權(quán)限差異
1、Android 6.0 之前
應(yīng)用只需要在 AndroidManifest.xml 下聲明以下權(quán)限即可自動獲取存儲權(quán)限:?
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
2、Android 6.0 起
從Android 6.0開始,除了以上操作以外,還需要在代碼中動態(tài)申請權(quán)限。
// 檢查是否有存儲權(quán)限
boolean granted = context.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) ==
? ?PackageManager.PERMISSION_GRANTED;
// 在activity中請求存儲權(quán)限
requestPermissions(new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, 0);
3、Android 10
Android 10 開始引入了沙盒機制,應(yīng)用在 sdcard 中默認只能讀寫私有目錄(即/sdcard/Android/data/[應(yīng)用包名]/),其他目錄即便執(zhí)行前面的操作也無法讀寫。除非在 AndroidManifest.xml 下聲明以下屬性:
<application
? ? ? ?...
? ? ? ?android:requestLegacyExternalStorage="true">
這樣的話就會暫時停用沙盒機制,正常讀寫/sdcard下文件。
4、Android 11
Android 11開始,且應(yīng)用的目標(biāo)版本在30及以上,以上的操作也無法再讀寫sdcard目錄。需要聲明以下權(quán)限:
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
再動態(tài)申請權(quán)限:
// 檢查是否有存儲權(quán)限
boolean granted = Environment.isExternalStorageManager();
// 在activity中請求存儲權(quán)限
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
? .setData(Uri.parse("package:"+getPackageName()));
startActivityForResult(intent, 0);
執(zhí)行以上操作后,sdcard已能夠正常讀寫。
但是,有2個特殊的目錄仍然無法讀寫:
/sdcard/Android/data 和 /sdcard/Android/obb 。
這兩個路徑需要安卓自帶的 DocumentsUI 授權(quán)才能訪問。
首先,最重要的一點,添加 documentfile 依賴(SDK自帶的那個版本有問題):
implementation "androidx.documentfile:documentfile:1.0.1"
Activity請求授權(quán):
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
? ? ? ? ? ? ? ?Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
? ?// 請求Android/data目錄的權(quán)限,Android/obb目錄則把data替換成obb即可。
? ?Uri treeUri = Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3AAndroid%2Fdata");
? ?DocumentFile df = DocumentFile.fromTreeUri(this, treeUri);
? ?if (df != null) {
? ? ? ?intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, df.getUri());
? }
}
startActivityForResult(intent, 1);
還需要在回調(diào)中保存權(quán)限:
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
? ?super.onActivityResult(requestCode, resultCode, data);
? ?Uri uri;
? ?if (data != null && (uri = data.getData()) != null) {
? ? ? ?// 授權(quán)成功
? ? ? ?getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
? } else {
? ? ? ?// 授權(quán)失敗
? }
}
在請求授權(quán)時,會跳轉(zhuǎn)到以下界面。點擊下方按鈕授權(quán)即可。
然后,使用接口獲取文件列表:
// Android 11~12轉(zhuǎn)換路徑。例如:/sdcard/Android/data,轉(zhuǎn)換成:
// Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3AAndroid%2Fdata")
// 路徑 /sdcard/Android/data/com.xxx.yyy,轉(zhuǎn)換成:
// Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3AAndroid%2Fdata%2Fcom.xxx.yyy")
// 以此類推。
Uri pathUri = pathToUri(path);
DocumentFile documentFile = DocumentFile.fromTreeUri(context, pathUri);
if (documentFile != null) {
DocumentFile[] documentFiles = documentFile.listFiles();
for (DocumentFile df : documentFiles) {
// 文件名
String fName = df.getName();
// 路徑
String fPath = path + "/" + fName;
}
}
5、Android 13
Android 13 開始,上面提到的授權(quán) Android/data、Android/obb目錄的方法失效了。請求授權(quán)會出現(xiàn)如下界面:
點擊下方按鈕會提示:
說明安卓13無法再直接授權(quán) Android/data 和 Android/obb 這兩個目錄了。但也并非無計可施。我們可以授權(quán)他們的子目錄。
我們知道,這個目錄下的文件夾名稱一般都是應(yīng)用的包名。我們可以直接用應(yīng)用包名的路徑來請求授權(quán)。
這里要注意,Android 13和Android 11~12的路徑轉(zhuǎn)換規(guī)則不一樣。
// Android 13轉(zhuǎn)換路徑。例如:/sdcard/Android/data/com.xxx.yyy,轉(zhuǎn)換成:
// 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,轉(zhuǎn)換成:
// Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata%2Fcom.xxx.yyy/document/primary%3AAndroid%2Fdata%2Fcom.xxx.yyy%2Ffiles")
// 以此類推。
請求授權(quán)時,出現(xiàn)如下界面。照常授權(quán)即可。
6、Android 14
Android 14對于data、obb目錄的授權(quán)進一步收緊。在Android 14的后期版本和Android 15預(yù)覽版中,以上方法已失效(傳入uri請求授權(quán)只會跳轉(zhuǎn)到sdcard根目錄)。這種情況下,想要訪問data和obb目錄就需要使用Shizuku了。(目前MT、FV就是用這種方法訪問的)
Shizuku的用法可以查閱相關(guān)教程,這里不多贅述。請先將Shizuku啟動,便于后續(xù)使用。
首先,添加Shizuku的依賴:
def shizuku_version = "13.1.5"
implementation "dev.rikka.shizuku:api:$shizuku_version"
// Add this line if you want to support Shizuku
implementation "dev.rikka.shizuku:provider:$shizuku_version"
AndroidManifest.xml添加以下內(nèi)容:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-sdk tools:overrideLibrary="rikka.shizuku.api, rikka.shizuku.provider, rikka.shizuku.shared, rikka.shizuku.aidl" />
<uses-permission android:name="moe.shizuku.manager.permission.API_V23" />
<application>
<provider
android:name="rikka.shizuku.ShizukuProvider"
android:authorities="${applicationId}.shizuku"
android:enabled="true"
android:exported="true"
android:multiprocess="false"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
<meta-data
android:name="moe.shizuku.client.V3_SUPPORT"
android:value="true" />
</application>
</manifest>
判斷Shizuku是否安裝:
private static boolean isShizukuInstalled() {
try {
context.getPackageManager().getPackageInfo("moe.shizuku.privileged.api", 0);
return true;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return false;
}
判斷Shizuku是否可用:
boolean available = Shizuku.pingBinder();
檢查Shizuku是否授權(quán):
boolean granted = Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED;
請求Shizuku權(quán)限:
Shizuku.requestPermission(0);
監(jiān)聽授權(quán)結(jié)果:
Shizuku.addRequestPermissionResultListener(listener);
Shizuku.removeRequestPermissionResultListener(listener);
授權(quán)后,自己定義一個aidl文件:
IFileExplorerService.aidl
package com.magicianguo.fileexplorer.userservice;
import com.magicianguo.fileexplorer.bean.BeanFile;
interface IFileExplorerService {
List<BeanFile> listFiles(String path);
}
BeanFile.java
public class BeanFile implements Parcelable {
public BeanFile(String name, String path, boolean isDir, boolean isGrantedPath, String pathPackageName) {
this.name = name;
this.path = path;
this.isDir = isDir;
this.isGrantedPath = isGrantedPath;
this.pathPackageName = pathPackageName;
}
/**
* 文件名
*/
public String name;
/**
* 文件路徑
*/
public String path;
/**
* 是否文件夾
*/
public boolean isDir;
/**
* 是否被Document授權(quán)的路徑
*/
public boolean isGrantedPath;
/**
* 如果文件夾名稱是應(yīng)用包名,則將包名保存到該字段
*/
public String pathPackageName;
protected BeanFile(Parcel in) {
name = in.readString();
path = in.readString();
isDir = in.readByte() != 0;
isGrantedPath = in.readByte() != 0;
pathPackageName = in.readString();
}
public static final Creator<BeanFile> CREATOR = new Creator<BeanFile>() {
@Override
public BeanFile createFromParcel(Parcel in) {
return new BeanFile(in);
}
@Override
public BeanFile[] newArray(int size) {
return new BeanFile[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeString(name);
dest.writeString(path);
dest.writeByte((byte) (isDir ? 1 : 0));
dest.writeByte((byte) (isGrantedPath ? 1 : 0));
dest.writeString(pathPackageName);
}
}
IFileExplorerService實現(xiàn)類:
public class FileExplorerService extends IFileExplorerService.Stub {
@Override
public List<BeanFile> listFiles(String path) throws RemoteException {
List<BeanFile> list = new ArrayList<>();
File[] files = new File(path).listFiles();
if (files != null) {
for (File f : files) {
list.add(new BeanFile(f.getName(), f.getPath(), f.isDirectory(), false, f.getName()));
}
}
return list;
}
}
然后使用Shizuku綁定UserService:
private static final Shizuku.UserServiceArgs USER_SERVICE_ARGS = new Shizuku.UserServiceArgs(
new ComponentName(packageName, FileExplorerService.class.getName())
).daemon(false).debuggable(BuildConfig.DEBUG).processNameSuffix("file_explorer_service").version(1);
public static IFileExplorerService iFileExplorerService;
private static final ServiceConnection SERVICE_CONNECTION = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d(TAG, "onServiceConnected: ");
iFileExplorerService = IFileExplorerService.Stub.asInterface(service);
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.d(TAG, "onServiceDisconnected: ");
iFileExplorerService = null;
}
};
// 綁定服務(wù)
public static void bindService() {
Shizuku.bindUserService(USER_SERVICE_ARGS, SERVICE_CONNECTION);
}
綁定之后,調(diào)用 aidl 里面的方法來管理文件即可。文章來源:http://www.zghlxwxcb.cn/news/detail-842940.html
源代碼:GitHub - MagicianGuo/Android-FileExplorerDemo: 能夠訪問Android/data(obb)目錄,已適配Android 5.0 ~ 14。安卓高版本可以使用Shizuku授權(quán)。文章來源地址http://www.zghlxwxcb.cn/news/detail-842940.html
到了這里,關(guān)于Android 5.0 ~ 14訪問Android/data(obb)目錄的方法的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!