Android11 無(wú)Root 訪問(wèn)data目錄 實(shí)現(xiàn)
- 正文開(kāi)始
- 關(guān)于Android11權(quán)限變化
- 作為普通安卓用戶該如何方便快速地訪問(wèn)Android/data目錄
- 開(kāi)發(fā)者該如何實(shí)現(xiàn)無(wú)ROOT訪問(wèn)Data目錄
- 正式開(kāi)始解決Android/data問(wèn)題
- 獲取某個(gè)文件目錄的權(quán)限
- 回調(diào)并永久保存某個(gè)目錄的權(quán)限
- 通過(guò)DocumentFile Api訪問(wèn)目錄
- 實(shí)現(xiàn)遍歷或管理Android/data文件目錄
- 重要的坑:為什么不直接使用路徑Path來(lái)實(shí)現(xiàn)文件瀏覽呢?
- 解決方案
- SAF方案缺點(diǎn)
- 放大招,ROOT權(quán)限直接解鎖后帶權(quán)訪問(wèn)Data目錄
- 結(jié)語(yǔ)
- 封裝好的工具類
正文開(kāi)始
關(guān)于Android11權(quán)限變化
谷歌在Android11及以上系統(tǒng)中采用了文件沙盒存儲(chǔ)模式,導(dǎo)致第三方應(yīng)用無(wú)法像以前一樣訪問(wèn)Android/data目錄,這是好事。但是我所不能理解的是已經(jīng)獲得"所有文件管理"權(quán)限的APP為何還是限制了,豈不是完全不留給清理、文件管理類軟件后路?實(shí)在不應(yīng)該!
作為普通安卓用戶該如何方便快速地訪問(wèn)Android/data目錄
眾所周知,不能訪問(wèn)Android/data目錄非常不方便,比如要管理QQ、微信接收到的文件、其他App下載的數(shù)據(jù)(如迅雷等等)。
現(xiàn)本人開(kāi)發(fā)的應(yīng)用已實(shí)現(xiàn)無(wú)Root訪問(wèn)Android/data目錄(其中文件瀏覽器功能),并且可以方便地進(jìn)行管理。
軟件下載
歡迎安卓手機(jī)用戶下載使用 和 Android開(kāi)發(fā)者下載預(yù)覽功能的實(shí)現(xiàn)。
App界面預(yù)覽
開(kāi)發(fā)者該如何實(shí)現(xiàn)無(wú)ROOT訪問(wèn)Data目錄
1.首先,可根據(jù)需要獲取所有文件管理權(quán)限:
在清單中聲明:
<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.動(dòng)態(tài)獲取讀寫(xiě)權(quán)限,這個(gè)不用多說(shuō)了吧,如果覺(jué)得麻煩可以使用郭霖大神的permissionX庫(kù)
Github
關(guān)于"管理所有文件"權(quán)限
這個(gè)權(quán)限可以讓你的App跟Android11以前一樣,通過(guò)File API訪問(wèn)所有文件(除Android/data目錄)
如有需要,請(qǐng)?jiān)谇鍐温暶鞑粏⒂蒙澈写鎯?chǔ)
android:preserveLegacyExternalStorage="true"
android:requestLegacyExternalStorage="true"
相關(guān)判斷
//判斷是否需要所有文件權(quán)限
if (!(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager())) {
//表明已經(jīng)有這個(gè)權(quán)限了
}
獲取權(quán)限
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
startActivity(intent);
正式開(kāi)始解決Android/data問(wèn)題
首先,使用的方式是SAF框架(Android Storage Access Framework)
這個(gè)框架在Android4.4就引入了,如果沒(méi)有了解過(guò)的話,可以百度。
獲取某個(gè)文件目錄的權(quán)限
方法很簡(jiǎn)單,使用android.intent.action.OPEN_DOCUMENT_TREE(調(diào)用SAF框架的文件選擇器選擇一個(gè)文件夾)的Intent就可以授權(quán)了
等下會(huì)放出工具類,現(xiàn)在看下例子:
//獲取指定目錄的訪問(wèn)權(quán)限
public static void startFor(String path, Activity context, int REQUEST_CODE_FOR_DIR) {
statusHolder.path = path;//這里主要是我的一個(gè)狀態(tài)保存類,說(shuō)明現(xiàn)在獲取權(quán)限的路徑是他,大家不用管。
String uri = changeToUri(path);//調(diào)用方法,把path轉(zhuǎn)換成可解析的uri文本,這個(gè)方法在下面會(huì)公布
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);//開(kāi)始授權(quán)
}
調(diào)用后的示意圖:
回調(diào)并永久保存某個(gè)目錄的權(quán)限
//返回授權(quán)狀態(tài)
@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));//關(guān)鍵是這里,這個(gè)就是保存這個(gè)目錄的訪問(wèn)權(quán)限
PreferencesUtil.saveString(MainActivity.this, statusHolder.path + "授權(quán)", "true");//我自己處理的邏輯,大家不用管
}
}
權(quán)限授權(quán)并永久保存成功
通過(guò)DocumentFile Api訪問(wèn)目錄
使用起來(lái)非常簡(jiǎn)單
先看看怎么生成DocumentFile對(duì)象
DocumentFile documentFile = DocumentFile.fromTreeUri(context, Uri.parse(fileUriUtils.changeToUri3(path)));
//changeToUri3方法是我封裝好的方法,后面會(huì)用到,這個(gè)是通過(guò)path生成指定可解析URI的方法
真所謂有手就行,調(diào)用DocumentFile.fromTreeUri()方法就可以了,這個(gè)方法說(shuō)的是從一個(gè)文件夾URI生成DocumentFile對(duì)象(treeUri就是文件夾URI)
當(dāng)然還有其他方法:
DocumentFile.fromSingleUri();
DocumentFile.fromFile();
DocumentFile.isDocumentUri();
看名字就明白了,但是我們有的的是一個(gè)文件夾uri,當(dāng)然使用這個(gè)方法來(lái)生成DocumentFile對(duì)象,不同方法生成的DocumentFile對(duì)象有不同效果,如果你用fromTreeUri生成的默認(rèn)是文件夾對(duì)象,有ListFiles() 方法
DocumentFile.ListFiles()也就是列出文件夾里面的全部子文件,類似于File.listFiles()方法
然后就這樣啊,得到了DocumentFile對(duì)象就可以進(jìn)行騷操作了啊,比如列出子文件啊,刪除文件啊,移動(dòng)啊,刪除啊什么的都可以,沒(méi)錯(cuò),Android/data目錄就是這樣進(jìn)行操作和訪問(wèn)的!
實(shí)現(xiàn)遍歷或管理Android/data文件目錄
比較基礎(chǔ),我就不多說(shuō)啦,簡(jiǎn)單講講實(shí)現(xiàn)方案和踩過(guò)的坑。
1.遍歷,跟普通全遍歷沒(méi)啥差別,但是不能通過(guò)直接傳入Path進(jìn)行遍歷
//遍歷示例,不進(jìn)行額外邏輯處理
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);//遞歸調(diào)用
}
}
}
}
2.實(shí)現(xiàn)文件管理器方案(管理Android/data目錄就是這個(gè)方案)
以下僅介紹方法
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{
//加載數(shù)據(jù)
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);
}
}
}
}
}
當(dāng)列表被點(diǎn)擊了,處理方案:
public void onclick(int postion){
file file = arrayList.get(postion);
getFiles(file.getDocumentFile());//獲取該文件夾的document對(duì)象,再把該文件夾遍歷出來(lái)
//然后再次顯示就完事了
}
以上就是模擬實(shí)現(xiàn)文件管理器->文件瀏覽功能,大家應(yīng)該一目了然,只介紹方案。
我實(shí)現(xiàn)的文件管理(Android11上直接免root管理data目錄)
重要的坑:為什么不直接使用路徑Path來(lái)實(shí)現(xiàn)文件瀏覽呢?
對(duì)呀,很明顯使用傳統(tǒng)的通過(guò)文件的path來(lái)實(shí)現(xiàn)文件管理豈不是更加方便?
我也這樣覺(jué)得的,在我當(dāng)時(shí)在對(duì)Android11進(jìn)行適配的時(shí)候?yàn)榱烁膭?dòng)小,肯定是想用這個(gè)方法來(lái)進(jìn)行適配,但是根本行不通!
我們不是獲取了Android/data目錄的權(quán)限了嗎? 明明說(shuō)好的獲取該目錄的權(quán)限后擁有該文件夾及所有子文件的讀寫(xiě)權(quán)限的!
我為什么不能直接通過(guò)調(diào)用changToUri把path轉(zhuǎn)換成uri,再生成DocumentFile對(duì)象呢?
這樣豈不是更加方便嘛? 而且SAF的文件效率比File低多了。
但是試了好幾次后,我確定這樣是不行的!
就算你生成的是Android/data目錄下子文件的正確URI,再生成DocumentFile對(duì)象,還是不行,因?yàn)槟闵傻腄ocumentFile對(duì)象始終指向Android/data(也就是你授權(quán)過(guò)的那個(gè)目錄), 無(wú)解!
剛剛開(kāi)始我還以為是我生成的URI不正確,但是當(dāng)我嘗試再次把我想獲取的子目錄路徑進(jìn)行文件目錄授權(quán)后,再用同一個(gè)URI生成DocumentFile對(duì)象卻能指向正正確目錄了。
看到這里大家應(yīng)該懂了吧,是谷歌對(duì)沒(méi)有授權(quán)的子文件夾目錄進(jìn)行了限制,不讓你直接通過(guò)TreeUri生成正確的Docment對(duì)象,至少在Android/data目錄是這樣的。
現(xiàn)在是不是覺(jué)得谷歌官方解釋: 獲取該目錄的權(quán)限后擁有該文件夾及所有子文件的讀寫(xiě)權(quán)限的!
是放屁?確實(shí)是!
解決方案
既然我們不能直接生成不了已授權(quán)目錄的子目錄DocumentFile對(duì)象,那我能不能試試直接對(duì)應(yīng)子路徑生成DocumentFile對(duì)象(非treeUri),我們?cè)囋囉胒romSingleUri()方法:
//根據(jù)路徑獲得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對(duì)象了,我們又可以用它來(lái)做一些好玩的東西了,比如直接通過(guò)path生成DocumentFile對(duì)象對(duì)某個(gè)文件獲取大小啊、判斷存在狀態(tài)啊,等等。
這個(gè)Android11上Android/data受限后,我覺(jué)得這個(gè)是很好的解決方案了,畢竟可以實(shí)現(xiàn)無(wú)Root訪問(wèn)并實(shí)現(xiàn)管理。
SAF方案缺點(diǎn)
很顯然,通過(guò)SAF文件存儲(chǔ)框架訪問(wèn)文件,速度和效率遠(yuǎn)遠(yuǎn)低于File API,因?yàn)镾AF本來(lái)用途就不是用來(lái)解決Android11/data目錄文件訪問(wèn)的。
但是對(duì)于一些涉及文件管理類的App來(lái)說(shuō)目前這個(gè)算是最全或較優(yōu)的解決方案了。
放大招,ROOT權(quán)限直接解鎖后帶權(quán)訪問(wèn)Data目錄
通過(guò)ROOT權(quán)限執(zhí)行
"chmod -R 777 /storage/emulated/0/Android/data"
命令就可以解鎖Android/data目錄,注意:不可逆。
至于怎么通過(guò)ROOT權(quán)限訪問(wèn)目錄,就需要參考MT文件管理器或張海大神開(kāi)源的文件管理器了
Github
結(jié)語(yǔ)
以上就是我的解決方案了,已經(jīng)完全解決Android11系統(tǒng)訪問(wèn)Android/data的問(wèn)題,有問(wèn)題可以留言哦,我看到會(huì)回復(fù)的,如果您有更好的解決的方案請(qǐng)?jiān)谠u(píng)論區(qū)留言,我會(huì)及時(shí)更新上去。
當(dāng)然,這個(gè)方案肯定會(huì)有些不如意,但是這已經(jīng)是沒(méi)方案中的最好的辦法,畢竟谷歌限制不讓你訪問(wèn)data目錄,我們某些涉及文件管理的應(yīng)用又確實(shí)需要訪問(wèn),方案親測(cè)可用,我已經(jīng)按照以上方案在我的app中進(jìn)行了Android11適配,算是差強(qiáng)人意吧。
我的App:
軟件下載
歡迎各位看官下載體驗(yàn)。
封裝好的工具類
因?yàn)閭€(gè)人項(xiàng)目還在運(yùn)營(yíng)不方便把全部代碼都開(kāi)源至GitHub,所以就放出工具類給大家使用吧。
真的超級(jí)簡(jiǎn)單呀,認(rèn)真看一遍就可以上手了,都是日常操作,對(duì)于各位大佬來(lái)說(shuō)就是有手就行。
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 + "測(cè)試", "document/primary%3A", "測(cè)試").replace("%2F", "/");
}
return path2;
}
//判斷是否已經(jīng)獲取了Data權(quán)限,改改邏輯就能判斷其他目錄,懂得都懂
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;
}
//轉(zhuǎn)換至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;
}
//轉(zhuǎn)換至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));
}
//轉(zhuǎn)換至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();
}
//轉(zhuǎn)換至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);
}
//獲取指定目錄的權(quán)限
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權(quán)限,推薦使用這種方案
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);
}
}