前言
Android R上分區(qū)存儲的限制得到進一步加強,無論APP的targetsdkversion是多少,都將無法訪問Android/data和Android/obb這二個應(yīng)用私有目錄渊季。這無疑對會部分APP的業(yè)務(wù)場景及用戶體驗造成沖擊,典型的如下
- 文件管理類軟件:微信罚渐、QQ傳輸?shù)奈募o法展示給用戶以便捷使用
- 垃圾清理類軟件:清理緩存功能受阻
“你有你的張良計却汉,我有我的過墻梯”,現(xiàn)市面上文件管理類軟件(如MT管理器)已解決上述系統(tǒng)限制荷并,本文將淺析其實現(xiàn)方案合砂,并主要分析以下2個問題:
- SAF是通過何種方式訪問文件系統(tǒng)的,MediaStore API ? File API ? Native Code 源织?
- SAF為何能訪問Android/data目錄
實現(xiàn)方案
其實現(xiàn)方案很簡單翩伪,就是通過Intent ACTION_OPEN_DOCUMENT_TREE,啟動SAF讓用戶授權(quán)訪問Android/data目錄谈息,屬于官方公開的方法缘屹。
前提是APP的targetsdkversion要小于30。
文檔鏈接:
文檔訪問限制
授予對目錄內(nèi)容的訪問權(quán)限
基本使用
- 通過Intent啟動SAF授權(quán)界面侠仇,注意URI的百分號編解碼(%3A和%2F)轻姿,別隨意替換,否則SAF無法導(dǎo)航到Android/data目錄
@TargetApi(26)
private void requestAccessAndroidData(Activity activity){
try {
Uri uri = Uri.parse("content://com.android.externalstorage.documents/document/primary%3AAndroid%2Fdata");
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, uri);
//flag看實際業(yè)務(wù)需要可再補充
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
activity.startActivityForResult(intent, 6666);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 值必須為document uri 或者是帶document id的document tree uri
* eg.
* document uri:
* "content://com.android.externalstorage.documents/document/primary%3AAndroid%2Fdata"
*
* document tree uri with document id:
* content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3AAndroid%2Fdata%2Ffoo
*/
public static final String EXTRA_INITIAL_URI = "android.provider.extra.INITIAL_URI";
- 在用戶同意授權(quán)后逻炊,持久化uri權(quán)限(否則關(guān)機重啟或授權(quán)界面finish后互亮,APP就無權(quán)限訪問了),并只能通過DocumentFile進行業(yè)務(wù)操作嗅骄,F(xiàn)ile API操作是無效的胳挎,此授權(quán)只是授權(quán)uri操作,并未授權(quán)文件系統(tǒng)溺森,后續(xù)章節(jié)有說明慕爬。
implementation "androidx.documentfile:documentfile:1.0.1"
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case 6666:
if (resultCode == Activity.RESULT_OK) {
//persist uri
getContentResolver().takePersistableUriPermission(data.getData(),
Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
//now use DocumentFile to do some file op
DocumentFile documentFile = DocumentFile
.fromTreeUri(this, data.getData());
DocumentFile[] files = documentFile.listFiles();
//補充說明下授權(quán)文件夾后窑眯,文件夾中的子文件的uri格式如下,可自行按格式拼接直接訪問子文件:
//content://com.android.externalstorage.documents/tree/primary%3ATest%2Ftest/document/primary%3ATest%2Ftest%2F666.mp3
......
}
break;
default:
break;
}
}
- 注意這個授權(quán)用戶是可以撤回的医窿,通過點擊應(yīng)用信息界面的存儲磅甩,就會看到撤回界面,所以業(yè)務(wù)需要去動態(tài)判斷
public boolean isGrantAndroidData(Context context) {
for (UriPermission persistedUriPermission : context.getContentResolver().getPersistedUriPermissions()) {
if (persistedUriPermission.getUri().toString().
equals("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata")) {
return true;
}
}
return false;
}
拓展
通過前面二個章節(jié)姥卢,已經(jīng)介紹了實現(xiàn)方案的基本使用卷要,下面就該分析本文的亮點內(nèi)容了
- SAF是通過何種方式訪問文件系統(tǒng)的,MediaStore API ? File API ? Native Code 独榴?
- SAF為何能訪問Android/data目錄
存儲訪問框架(SAF)簡介
為方便后續(xù)講解僧叉,先簡單回顧下SAF
APP:
com.example.photos就是我們自己的APP
System UI:
com.google.android.documentsui,一般稱作DoucmentUI棺榔,就是上文中啟動的授權(quán)界面APP瓶堕,它只是個UI殼子
DocumentProvider:
DocumentUI中數(shù)據(jù)的提供者,這個Provider可以有很多
com.android.externalstorage症歇,是本地文件系統(tǒng)的Provider
關(guān)于SAF更詳細介紹郎笆,請參考官方存儲訪問框架
經(jīng)過SAF的簡單介紹,分析目標(biāo)很明確忘晤,那就是com.android.externalstorage
SAF是通過何種方式訪問文件系統(tǒng)的
先安利幾個AOSP源碼查看網(wǎng)址:
官方的Android Code Search
國內(nèi)的AOSP XREF
PS:后文源碼鏈接都用的是XREF宛蚓,方便國內(nèi)查看
從DocumentFile#listFile入手,經(jīng)過源碼跟蹤會發(fā)現(xiàn)最終會調(diào)用 DocumentsProvider#queryChildDocuments方法
public abstract class DocumentsProvider extends ContentProvider {
.......
@Override
public final Cursor query(
Uri uri, String[] projection, Bundle queryArgs, CancellationSignal cancellationSignal) {
switch (mMatcher.match(uri)) {
......
case MATCH_CHILDREN:
case MATCH_CHILDREN_TREE:
.......
return queryChildDocuments(getDocumentId(uri), projection, queryArgs);
......
default:
throw new UnsupportedOperationException("Unsupported Uri " + uri);
}
} catch (FileNotFoundException e) {
Log.w(TAG, "Failed during query", e);
return null;
}
}
......
}
接下來看看com.android.externalstorage中DocumentProvider的實現(xiàn)類
ExternalStorageProvider:
frameworks/base/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
import com.android.internal.content.FileSystemProvider;
public class ExternalStorageProvider extends FileSystemProvider
queryChildDocuments的實現(xiàn)位于其父類 FileSystemProvider
public abstract class FileSystemProvider extends DocumentsProvider {
......
private Cursor queryChildDocuments(
String parentDocumentId, String[] projection, String sortOrder,
@NonNull Predicate<File> filter) throws FileNotFoundException {
final File parent = getFileForDocId(parentDocumentId);
final MatrixCursor result = new DirectoryCursor(
resolveProjection(projection), parentDocumentId, parent);
if (parent.isDirectory()) {
//重點是這行
for (File file : FileUtils.listFilesOrEmpty(parent)) {
if (filter.test(file)) {
includeFile(result, null, file);
}
}
} else {
Log.w(TAG, "parentDocumentId '" + parentDocumentId + "' is not Directory");
}
return result;
}
......
}
/** {@hide} */
public static @NonNull File[] listFilesOrEmpty(@Nullable File dir) {
return (dir != null) ? ArrayUtils.defeatNullable(dir.listFiles())
: ArrayUtils.EMPTY_FILE;
}
至此设塔,第一個問題凄吏,已經(jīng)理清:
SAF的ExternalStorageProvider最終也是通過File API來訪問文件系統(tǒng)的
那么第二個問題,就很自然的來了壹置,都是File API操作竞思,為何我們的APP就不能訪問呢?
SAF為何能訪問Android/data目錄
既然钞护,SAF和我們的APP都是File API操作盖喷,那我們就去看看com.android.externalstorage屬于哪些用戶組。
adb shell 查查com.android.externalstorage進程的用戶組
#查進程ID
generic_x86_arm:/ $ ps -A|grep com.android.external
u0_a64 16233 296 1256792 85960 0 0 S com.android.externalstorage
#查進程所屬的用戶組
generic_x86_arm:/ $ cat /proc/16233/status
Name: externalstorage
Umask: 0077
State: S (sleeping)
Tgid: 16233
Ngid: 0
Pid: 16233
PPid: 296
TracerPid: 0
Uid: 10064 10064 10064 10064
Gid: 10064 10064 10064 10064
FDSize: 64
#重點關(guān)注這行輸出
Groups: 1015 1077 1078 1079 9997 20064 50064
拿著這些神秘的GID在前面介紹的網(wǎng)址中一搜难咕,就會很容易的發(fā)現(xiàn)GID的定義類
android_filesystem_config.h
#define AID_SDCARD_RW 1015 /* external storage write access */
#define AID_EXTERNAL_STORAGE 1077 /* Full external storage access including USB OTG volumes */
#define AID_EXT_DATA_RW 1078 /* GID for app-private data directories on external storage */
#define AID_EXT_OBB_RW 1079 /* GID for OBB directories on external storage */
#define AID_EVERYBODY 9997 /* shared between all apps in the same profile */
其中1078和1079分別對應(yīng)Android/data和Android/obb的訪問權(quán)限
如果我們APP能通過某種方式獲取到1078和1079的用戶組權(quán)限课梳,豈不妙哉?
遺憾的是余佃,對于三方APP這是不可能的暮刃,除非是手機廠商的預(yù)置的系統(tǒng)APP
總結(jié)
- Android R上可通過SAF獲得訪問Android/data和Android/obb目錄的權(quán)限,前提是APP targetsdkversion 小于30
- SAF的底層實現(xiàn)ExternalStorageProvider也是通過File API來訪問文件系統(tǒng)的
- SAF之所以能訪問Android/data和Android/obb是因為ExternalStorageProvider
進程具有GID 1078 和1079爆土,三方APP是不可能擁有這些GID的