Android11 無Root 訪問data目錄 實現
- 正文開始
- 關于Android11權限變化
- 作為普通安卓用戶該如何方便快速地訪問Android/data目錄
- 開發者該如何實現無ROOT訪問Data目錄
- 正式開始解決Android/data問題
- 獲取某個文件目錄的權限
- 回調并永久保存某個目錄的權限
- 通過DocumentFile Api訪問目錄
- 實現遍歷或管理Android/data文件目錄
- 重要的坑:為什么不直接使用路徑Path來實現文件瀏覽呢?
- 解決方案
- SAF方案缺點
- 放大招,ROOT權限直接解鎖后帶權訪問Data目錄
- 結語
- 封裝好的工具類
正文開始
關于Android11權限變化
谷歌在Android11及以上系統中采用了文件沙盒存儲模式,導致第三方應用無法像以前一樣訪問Android/data目錄,這是好事。但是我所不能理解的是已經獲得"所有文件管理"權限的APP為何還是限制了,豈不是完全不留給清理、文件管理類軟件后路?實在不應該!
作為普通安卓用戶該如何方便快速地訪問Android/data目錄
眾所周知,不能訪問Android/data目錄非常不方便,比如要管理QQ、微信接收到的文件、其他App下載的數據(如迅雷等等)。
現本人開發的應用已實現無Root訪問Android/data目錄(其中文件瀏覽器功能),并且可以方便地進行管理。
軟件下載
歡迎安卓手機用戶下載使用 和 Android開發者下載預覽功能的實現。
App界面預覽
開發者該如何實現無ROOT訪問Data目錄
1.首先,可根據需要獲取所有文件管理權限:
在清單中聲明:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
2.動態獲取讀寫權限,這個不用多說了吧,如果覺得麻煩可以使用郭霖大神的permissionX庫
Github
關于"管理所有文件"權限
這個權限可以讓你的App跟Android11以前一樣,通過File API訪問所有文件(除Android/data目錄)
如有需要,請在清單聲明不啟用沙盒存儲
android:preserveLegacyExternalStorage="true"
android:requestLegacyExternalStorage="true"
相關判斷
//判斷是否需要所有文件權限
if (!(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager())) {
//表明已經有這個權限了
}
獲取權限
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
startActivity(intent);
正式開始解決Android/data問題
首先,使用的方式是SAF框架(Android Storage Access Framework)
這個框架在Android4.4就引入了,如果沒有了解過的話,可以百度。
獲取某個文件目錄的權限
方法很簡單,使用android.intent.action.OPEN_DOCUMENT_TREE(調用SAF框架的文件選擇器選擇一個文件夾)的Intent就可以授權了
等下會放出工具類,現在看下例子:
//獲取指定目錄的訪問權限
public static void startFor(String path, Activity context, int REQUEST_CODE_FOR_DIR) {
statusHolder.path = path;//這里主要是我的一個狀態保存類,說明現在獲取權限的路徑是他,大家不用管。
String uri = changeToUri(path);//調用方法,把path轉換成可解析的uri文本,這個方法在下面會公布
Uri parse = Uri.parse(uri);
Intent intent = new Intent("android.intent.action.OPEN_DOCUMENT_TREE");
intent.addFlags(
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) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, parse);
}
context.startActivityForResult(intent, REQUEST_CODE_FOR_DIR);//開始授權
}
調用后的示意圖:
回調并永久保存某個目錄的權限
//返回授權狀態
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
Uri uri;
if (data == null) {
return;
}
if (requestCode == REQUEST_CODE_FOR_DIR && (uri = data.getData()) != null) {
getContentResolver().takePersistableUriPermission(uri, data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION));//關鍵是這里,這個就是保存這個目錄的訪問權限
PreferencesUtil.saveString(MainActivity.this, statusHolder.path + "授權", "true");//我自己處理的邏輯,大家不用管
}
}
權限授權并永久保存成功
通過DocumentFile Api訪問目錄
使用起來非常簡單
先看看怎么生成DocumentFile對象
DocumentFile documentFile = DocumentFile.fromTreeUri(context, Uri.parse(fileUriUtils.changeToUri3(path)));
//changeToUri3方法是我封裝好的方法,后面會用到,這個是通過path生成指定可解析URI的方法
真所謂有手就行,調用DocumentFile.fromTreeUri()方法就可以了,這個方法說的是從一個文件夾URI生成DocumentFile對象(treeUri就是文件夾URI)
當然還有其他方法:
DocumentFile.fromSingleUri();
DocumentFile.fromFile();
DocumentFile.isDocumentUri();
看名字就明白了,但是我們有的的是一個文件夾uri,當然使用這個方法來生成DocumentFile對象,不同方法生成的DocumentFile對象有不同效果,如果你用fromTreeUri生成的默認是文件夾對象,有ListFiles() 方法
DocumentFile.ListFiles()也就是列出文件夾里面的全部子文件,類似于File.listFiles()方法
然后就這樣啊,得到了DocumentFile對象就可以進行騷操作了啊,比如列出子文件啊,刪除文件啊,移動啊,刪除啊什么的都可以,沒錯,Android/data目錄就是這樣進行操作和訪問的!
實現遍歷或管理Android/data文件目錄
比較基礎,我就不多說啦,簡單講講實現方案和踩過的坑。
1.遍歷,跟普通全遍歷沒啥差別,但是不能通過直接傳入Path進行遍歷
//遍歷示例,不進行額外邏輯處理
void getFiles(DocumentFile documentFile) {
Log.d("文件:", documentFile.getName());
if (documentFile.isDirectory()) {
for (DocumentFile file : documentFile.listFiles()) {
Log.d("子文件", file.getName());
if (file.isDirectory()) {
getFiles(file);//遞歸調用
}
}
}
}
2.實現文件管理器方案(管理Android/data目錄就是這個方案)
以下僅介紹方法
class file{
String title;
DocumentFile documentFile;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public DocumentFile getDocumentFile() {
return documentFile;
}
public void setDocumentFile(DocumentFile documentFile) {
this.documentFile = documentFile;
}
}
MainActivity{
//加載數據
void getFiles(DocumentFile documentFile) {
ArrayList<file> arrayList = new ArrayList<>();
if (documentFile.isDirectory()) {
for (DocumentFile documentFile_inner : documentFile.listFiles()) {
file file = new file();
file.setTitle(documentFile_inner.getName());
file.setDocumentFile(documentFile_inner);
}
}
}
}
}
當列表被點擊了,處理方案:
public void onclick(int postion){
file file = arrayList.get(postion);
getFiles(file.getDocumentFile());//獲取該文件夾的document對象,再把該文件夾遍歷出來
//然后再次顯示就完事了
}
以上就是模擬實現文件管理器->文件瀏覽功能,大家應該一目了然,只介紹方案。
我實現的文件管理(Android11上直接免root管理data目錄)
重要的坑:為什么不直接使用路徑Path來實現文件瀏覽呢?
對呀,很明顯使用傳統的通過文件的path來實現文件管理豈不是更加方便?
我也這樣覺得的,在我當時在對Android11進行適配的時候為了改動小,肯定是想用這個方法來進行適配,但是根本行不通!
我們不是獲取了Android/data目錄的權限了嗎? 明明說好的獲取該目錄的權限后擁有該文件夾及所有子文件的讀寫權限的!
我為什么不能直接通過調用changToUri把path轉換成uri,再生成DocumentFile對象呢?
這樣豈不是更加方便嘛? 而且SAF的文件效率比File低多了。
但是試了好幾次后,我確定這樣是不行的!
就算你生成的是Android/data目錄下子文件的正確URI,再生成DocumentFile對象,還是不行,因為你生成的DocumentFile對象始終指向Android/data(也就是你授權過的那個目錄), 無解!
剛剛開始我還以為是我生成的URI不正確,但是當我嘗試再次把我想獲取的子目錄路徑進行文件目錄授權后,再用同一個URI生成DocumentFile對象卻能指向正正確目錄了。
看到這里大家應該懂了吧,是谷歌對沒有授權的子文件夾目錄進行了限制,不讓你直接通過TreeUri生成正確的Docment對象,至少在Android/data目錄是這樣的。
現在是不是覺得谷歌官方解釋: 獲取該目錄的權限后擁有該文件夾及所有子文件的讀寫權限的!
是放屁?確實是!
解決方案
既然我們不能直接生成不了已授權目錄的子目錄DocumentFile對象,那我能不能試試直接對應子路徑生成DocumentFile對象(非treeUri),我們試試用fromSingleUri()方法:
//根據路徑獲得document文件
public static DocumentFile getDoucmentFile(Context context, String path) {
if (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
String path2 = path.replace("/storage/emulated/0/", "").replace("/", "%2F");
return DocumentFile.fromSingleUri(context, Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3A" + path2));
}
很顯然,可以了!可以生成正確的DocumentFile對象了,我們又可以用它來做一些好玩的東西了,比如直接通過path生成DocumentFile對象對某個文件獲取大小啊、判斷存在狀態啊,等等。
這個Android11上Android/data受限后,我覺得這個是很好的解決方案了,畢竟可以實現無Root訪問并實現管理。
SAF方案缺點
很顯然,通過SAF文件存儲框架訪問文件,速度和效率遠遠低于File API,因為SAF本來用途就不是用來解決Android11/data目錄文件訪問的。
但是對于一些涉及文件管理類的App來說目前這個算是最全或較優的解決方案了。
放大招,ROOT權限直接解鎖后帶權訪問Data目錄
通過ROOT權限執行
"chmod -R 777 /storage/emulated/0/Android/data"
命令就可以解鎖Android/data目錄,注意:不可逆。
至于怎么通過ROOT權限訪問目錄,就需要參考MT文件管理器或張海大神開源的文件管理器了
Github
結語
以上就是我的解決方案了,已經完全解決Android11系統訪問Android/data的問題,有問題可以留言哦,我看到會回復的,如果您有更好的解決的方案請在評論區留言,我會及時更新上去。
當然,這個方案肯定會有些不如意,但是這已經是沒方案中的最好的辦法,畢竟谷歌限制不讓你訪問data目錄,我們某些涉及文件管理的應用又確實需要訪問,方案親測可用,我已經按照以上方案在我的app中進行了Android11適配,算是差強人意吧。
我的App:
軟件下載
歡迎各位看官下載體驗。
封裝好的工具類
因為個人項目還在運營不方便把全部代碼都開源至GitHub,所以就放出工具類給大家使用吧。
真的超級簡單呀,認真看一遍就可以上手了,都是日常操作,對于各位大佬來說就是有手就行。
public class fileUriUtils {
public static String root = Environment.getExternalStorageDirectory().getPath() + "/";
public static String treeToPath(String path) {
String path2;
if (path.contains("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary")) {
path2 = path.replace("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3A", root);
path2 = path2.replace("%2F", "/");
} else {
path2 = root + textUtils.getSubString(path + "測試", "document/primary%3A", "測試").replace("%2F", "/");
}
return path2;
}
//判斷是否已經獲取了Data權限,改改邏輯就能判斷其他目錄,懂得都懂
public static boolean isGrant(Context context) {
for (UriPermission persistedUriPermission : context.getContentResolver().getPersistedUriPermissions()) {
if (persistedUriPermission.isReadPermission() && persistedUriPermission.getUri().toString().equals("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata")) {
return true;
}
}
return false;
}
//直接返回DocumentFile
public static DocumentFile getDocumentFilePath(Context context, String path, String sdCardUri) {
DocumentFile document = DocumentFile.fromTreeUri(context, Uri.parse(sdCardUri));
String[] parts = path.split("/");
for (int i = 3; i < parts.length; i++) {
document = document.findFile(parts[i]);
}
return document;
}
//轉換至uriTree的路徑
public static String changeToUri(String path) {
if (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
String path2 = path.replace("/storage/emulated/0/", "").replace("/", "%2F");
return "content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3A" + path2;
}
//轉換至uriTree的路徑
public static DocumentFile getDoucmentFile(Context context, String path) {
if (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
String path2 = path.replace("/storage/emulated/0/", "").replace("/", "%2F");
return DocumentFile.fromSingleUri(context, Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3A" + path2));
}
//轉換至uriTree的路徑
public static String changeToUri2(String path) {
String[] paths = path.replaceAll("/storage/emulated/0/Android/data", "").split("/");
StringBuilder stringBuilder = new StringBuilder("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3AAndroid%2Fdata");
for (String p : paths) {
if (p.length() == 0) continue;
stringBuilder.append("%2F").append(p);
}
return stringBuilder.toString();
}
//轉換至uriTree的路徑
public static String changeToUri3(String path) {
path = path.replace("/storage/emulated/0/", "").replace("/", "%2F");
return ("content://com.android.externalstorage.documents/tree/primary%3A" + path);
}
//獲取指定目錄的權限
public static void startFor(String path, Activity context, int REQUEST_CODE_FOR_DIR) {
statusHolder.path = path;
String uri = changeToUri(path);
Uri parse = Uri.parse(uri);
Intent intent = new Intent("android.intent.action.OPEN_DOCUMENT_TREE");
intent.addFlags(
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) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, parse);
}
context.startActivityForResult(intent, REQUEST_CODE_FOR_DIR);
}
//直接獲取data權限,推薦使用這種方案
public static void startForRoot(Activity context, int REQUEST_CODE_FOR_DIR) {
Uri uri1 = Uri.parse("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata");
// DocumentFile documentFile = DocumentFile.fromTreeUri(context, uri1);
String uri = changeToUri(Environment.getExternalStorageDirectory().getPath());
uri = uri + "/document/primary%3A" + Environment.getExternalStorageDirectory().getPath().replace("/storage/emulated/0/", "").replace("/", "%2F");
Uri parse = Uri.parse(uri);
DocumentFile documentFile = DocumentFile.fromTreeUri(context, uri1);
Intent intent1 = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent1.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);
intent1.putExtra(DocumentsContract.EXTRA_INITIAL_URI, documentFile.getUri());
context.startActivityForResult(intent1, REQUEST_CODE_FOR_DIR);
}
}