1.簡(jiǎn)介
https://developer.android.google.cn/guide/topics/providers/document-provider
https://developer.android.google.cn/training/data-storage/shared/documents-files
https://developer.android.google.cn/training/data-storage/use-cases
Android的存儲(chǔ)訪問(wèn)框架主要是用于訪問(wèn)非應(yīng)用本身專屬的文件(應(yīng)用專屬的文件包括如/data/data/下面應(yīng)用報(bào)名目錄中的文件和/sdcard(/storage/emulated/0/)下面Android/目錄下data或media等目錄下應(yīng)用報(bào)名目錄中的文件,如/storage/emulated/0/Android/data/包名/),具體可先看下上面三個(gè)開(kāi)發(fā)者網(wǎng)站的資料,然后看個(gè)demo:
public class SAFDemoActivity extends AppCompatActivity {
private final static String TAG="SAFDemoActivity";
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode==PICK_FILE){
if(resultCode==RESULT_OK){
Uri uri = null;
if (data != null) {
uri = data.getData();
// Perform operations on the document using its URI.
int result=checkCallingOrSelfUriPermission(uri,Intent.FLAG_GRANT_READ_URI_PERMISSION);
Log.i(TAG,"onActivityResult:uri="+uri+":av="+isUriAvailable(uri)+":result="+result);
}
}else{
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_safdemo);
}
public void startOpen(View view) {
File file=new File("/sdcard/My/testfile1.txt");
Log.i(TAG,"startOpen:exists="+file.exists()+":canRead="+file.canRead());
Uri uri=Uri.fromFile(file);
int result=checkCallingOrSelfUriPermission(uri,Intent.FLAG_GRANT_READ_URI_PERMISSION);
Log.i(TAG,"startOpen:uri="+uri+":av="+isUriAvailable(uri)+":result="+result);
try {
MediaScannerConnection.scanFile(getApplicationContext(), new String[]{file.getCanonicalPath()},
new String[]{"text/plain"}, new MediaScannerConnection.OnScanCompletedListener() {
@Override
public void onScanCompleted(String path, Uri uri) {
int result=checkCallingOrSelfUriPermission(uri,Intent.FLAG_GRANT_READ_URI_PERMISSION);
Log.i(TAG,"startOpen:path="+path+":uri="+uri+":av="+isUriAvailable(uri)+":result="+result);
}
});
} catch (IOException e) {
e.printStackTrace();
}
openFile();
}
private static final int PICK_FILE = 2;
private void openFile() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("text/plain");
startActivityForResult(intent, PICK_FILE);
}
private boolean isUriAvailable(Uri uri){
try {
AssetFileDescriptor afd=getContentResolver().openAssetFileDescriptor(uri,"r");
long length=afd.getLength();
Log.i(TAG,"isUriAvailable:test:length="+length);
return true;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
}
這里在外部存儲(chǔ)/sdcard目錄下創(chuàng)建了個(gè)My目錄,然后在該目錄下保存了一個(gè)文件testfile1.txt,查看上述demo運(yùn)行對(duì)于該文件的訪問(wèn)權(quán)限
3032 3032 I SAFDemoActivity: startOpen:exists=true:canRead=false
3032 3032 W System.err: java.io.FileNotFoundException: open failed: EACCES (Permission denied)
3032 3032 W System.err: at android.os.ParcelFileDescriptor.openInternal(ParcelFileDescriptor.java:315)
3032 3032 W System.err: at android.os.ParcelFileDescriptor.open(ParcelFileDescriptor.java:220)
3032 3032 W System.err: at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:1498)
3032 3032 W System.err: at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:1420)
3032 3032 W System.err: at com.example.android.mydemos.SAFDemoActivity.isUriAvailable(SAFDemoActivity.java:79)
3032 3032 W System.err: at com.example.android.mydemos.SAFDemoActivity.startOpen(SAFDemoActivity.java:50)
3032 3032 W System.err: at java.lang.reflect.Method.invoke(Native Method)
3032 3032 W System.err: at androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:397)
3032 3032 W System.err: at android.view.View.performClick(View.java:7140)
3032 3032 W System.err: at android.view.View.performClickInternal(View.java:7117)
3032 3032 W System.err: at android.view.View.access$3500(View.java:801)
3032 3032 W System.err: at android.view.View$PerformClick.run(View.java:27351)
3032 3032 W System.err: at android.os.Handler.handleCallback(Handler.java:883)
3032 3032 W System.err: at android.os.Handler.dispatchMessage(Handler.java:100)
3032 3032 W System.err: at android.os.Looper.loop(Looper.java:214)
3032 3032 W System.err: at android.app.ActivityThread.main(ActivityThread.java:7356)
3032 3032 W System.err: at java.lang.reflect.Method.invoke(Native Method)
3032 3032 W System.err: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
3032 3032 W System.err: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
3032 3032 I SAFDemoActivity: startOpen:uri=file:///sdcard/My/testfile1.txt:av=false:result=-1
0979 3092 D MediaProvider: Scanned /storage/emulated/0/My/testfile1.txt as /storage/emulated/0/My/testfile1.txt for content://media/external_primary/file/346
0979 30999 E DatabaseUtils: Writing exception to parcel
0979 30999 E DatabaseUtils: java.lang.SecurityException: com.example.android.mydemos has no access to content://media/external_primary/file/346
0979 30999 E DatabaseUtils: at com.android.providers.media.MediaProvider.enforceCallingPermissionInternal(MediaProvider.java:5707)
0979 30999 E DatabaseUtils: at com.android.providers.media.MediaProvider.enforceCallingPermission(MediaProvider.java:5630)
0979 30999 E DatabaseUtils: at com.android.providers.media.MediaProvider.checkAccess(MediaProvider.java:5727)
0979 30999 E DatabaseUtils: at com.android.providers.media.MediaProvider.openFileAndEnforcePathPermissionsHelper(MediaProvider.java:5357)
0979 30999 E DatabaseUtils: at com.android.providers.media.MediaProvider.openFileCommon(MediaProvider.java:5167)
0979 30999 E DatabaseUtils: at com.android.providers.media.MediaProvider.openFile(MediaProvider.java:5117)
0979 30999 E DatabaseUtils: at android.content.ContentProvider.openAssetFile(ContentProvider.java:1712)
0979 30999 E DatabaseUtils: at android.content.ContentProvider.openAssetFile(ContentProvider.java:1776)
0979 30999 E DatabaseUtils: at android.content.ContentProvider$Transport.openAssetFile(ContentProvider.java:459)
0979 30999 E DatabaseUtils: at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:255)
0979 30999 E DatabaseUtils: at android.os.Binder.execTransactInternal(Binder.java:1021)
0979 30999 E DatabaseUtils: at android.os.Binder.execTransact(Binder.java:994)
3032 3051 W System.err: java.lang.SecurityException: com.example.android.mydemos has no access to content://media/external_primary/file/346
3032 3051 W System.err: at android.os.Parcel.createException(Parcel.java:2071)
3032 3051 W System.err: at android.os.Parcel.readException(Parcel.java:2039)
3032 3051 W System.err: at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:188)
3032 3051 W System.err: at android.database.DatabaseUtils.readExceptionWithFileNotFoundExceptionFromParcel(DatabaseUtils.java:151)
3032 3051 W System.err: at android.content.ContentProviderProxy.openAssetFile(ContentProviderNative.java:631)
3032 3051 W System.err: at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:1521)
3032 3051 W System.err: at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:1420)
3032 3051 W System.err: at com.example.android.mydemos.SAFDemoActivity.isUriAvailable(SAFDemoActivity.java:79)
3032 3051 W System.err: at com.example.android.mydemos.SAFDemoActivity.access$000(SAFDemoActivity.java:18)
3032 3051 W System.err: at com.example.android.mydemos.SAFDemoActivity$1.onScanCompleted(SAFDemoActivity.java:58)
3032 3051 W System.err: at android.media.MediaScannerConnection$ClientProxy.onScanCompleted(MediaScannerConnection.java:204)
3032 3051 W System.err: at android.media.MediaScannerConnection$1.scanCompleted(MediaScannerConnection.java:53)
3032 3051 W System.err: at android.media.IMediaScannerListener$Stub.onTransact(IMediaScannerListener.java:97)
3032 3051 W System.err: at android.os.Binder.execTransactInternal(Binder.java:1021)
3032 3051 W System.err: at android.os.Binder.execTransact(Binder.java:994)
3032 3051 I SAFDemoActivity: startOpen:path=/storage/emulated/0/My/testfile1.txt:uri=content://media/external_primary/file/346:av=false:result=-1
3032 3032 I SAFDemoActivity: isUriAvailable:test:length=6
3032 3032 I SAFDemoActivity: onActivityResult:uri=content://com.android.externalstorage.documents/document/primary%3AMy%2Ftestfile1.txt:av=true:result=0
其中關(guān)鍵打印的如下
3032 3032 I SAFDemoActivity: startOpen:exists=true:canRead=false
3032 3032 I SAFDemoActivity: startOpen:uri=file:///sdcard/My/testfile1.txt:av=false:result=-1
3032 3051 I SAFDemoActivity: startOpen:path=/storage/emulated/0/My/testfile1.txt:uri=content://media/external_primary/file/346:av=false:result=-1
3032 3032 I SAFDemoActivity: isUriAvailable:test:length=6
3032 3032 I SAFDemoActivity: onActivityResult:uri=content://com.android.externalstorage.documents/document/primary%3AMy%2Ftestfile1.txt:av=true:result=0
顯然在demo的幾種訪問(wèn)方式中,簡(jiǎn)單的file或uri直接去獲取文件的方式是無(wú)法得到文件的,而只有通過(guò)action為ACTION_OPEN_DOCUMENT的intent去選擇的方式在選擇返回后才有文件的權(quán)限,能夠得到文件
2.應(yīng)用分享文件
https://developer.android.google.cn/training/secure-file-sharing/setup-sharing
https://developer.android.google.cn/training/secure-file-sharing/share-file
在這里建議先看下應(yīng)用分享文件的介紹,因?yàn)槠浜蚐AF訪問(wèn)邏輯大同小異,理解了分享文件,后面SAF邏輯就比較容易理解了
這里只簡(jiǎn)要介紹下
首先,要分享一個(gè)應(yīng)用的專屬文件(如),需要聲明一個(gè)ContentProvider:如(一般可以直接使用androidx.core.FileProvider,也可以自己實(shí)現(xiàn),這里是簡(jiǎn)單的將androidx.core.FileProvider中的代碼復(fù)制挪到本地創(chuàng)建的FileProvider中以便調(diào)試修改)
<provider
android:name=".FileProvider"
android:authorities="com.example.android.myprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
這里必須要有g(shù)rantUriPermissions為true,或者exported為true(直接使用androidx.core.FileProvider會(huì)拋異常,可注釋調(diào)拋異常的部分(attachInfo方法中)),不然其他應(yīng)用無(wú)法通過(guò)uri訪問(wèn)文件,這里的meta-data部分主要是在androidx.core.FileProvider中會(huì)讀取filepaths文件中配置用于根據(jù)文件路徑生成對(duì)應(yīng)的uri,這里本地使用外部存儲(chǔ)目錄下的文件,所以filepaths文件中配置了external-files-path標(biāo)簽,具體配置和路徑對(duì)應(yīng)情況可查看androidx.core.FileProvider中代碼,這里的意思即對(duì)應(yīng)/storage/emulated/0/Android/data/com.example.android.myprovider/files/Documents/下的文件,生成的uri的path目錄為documents
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-files-path
name="documents"
path="Documents"/>
</paths>
如果在grantUriPermissions為true,exported為false的時(shí)候,另一個(gè)應(yīng)用需要通過(guò)uri訪問(wèn)該應(yīng)用的文件,則可以啟動(dòng)該應(yīng)用的activity,然后在該應(yīng)用的activity中選擇了文件后setResult然后回到另一個(gè)應(yīng)用,如:
public void onFileClick(View view) {
File file=new File(getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS),"testfile.txt");
Log.i(TAG,"onFileClick:file="+file+":exists="+file.exists()+":canRead="+file.canRead());
if(file.exists()&&file.canRead()){
Uri uri=FileProvider.getUriForFile(getApplicationContext(),AUTH,file);
Log.i(TAG,"onFileClick:uri="+uri);
Intent intent=new Intent();
intent.setData(uri);
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);
setResult(RESULT_OK,intent);
}else{
setResult(RESULT_OK);
}
finish();
}
這里主要是通過(guò)將文件路徑轉(zhuǎn)為provider對(duì)應(yīng)的uri,然后將uri設(shè)置到intent中,并添加對(duì)應(yīng)的flag,如Intent.FLAG_GRANT_READ_URI_PERMISSION,然后將其通過(guò)setResult進(jìn)行設(shè)置,這樣,在另一應(yīng)用的onActivityResult回調(diào)中就可以獲取到該uri,并暫時(shí)能通過(guò)該uri訪問(wèn)對(duì)應(yīng)文件
打印log如下
8510 8510 I SAFDemoActivity: startOpen2:exists=true:canRead=false
8510 8510 I SAFDemoActivity: startOpen2:uri=file:///storage/emulated/0/Android/data/com.example.android.myprovider/files/Documents/testfile.txt:av=false:result=-1
8510 8510 I SAFDemoActivity: startOpen2:uri2=content://com.example.android.myprovider/documents/testfile.txt:av=false:result2=-1
8559 8559 I MyProvider: onFileClick:file=/storage/emulated/0/Android/data/com.example.android.myprovider/files/Documents/testfile.txt:exists=true:canRead=true
8559 8559 I MyProvider: onFileClick:uri=content://com.example.android.myprovider/documents/testfile.txt
8510 8510 I SAFDemoActivity: isUriAvailable:test:length=0
8510 8510 I SAFDemoActivity: onActivityResult:uri=content://com.example.android.myprovider/documents/testfile.txt:av=true:result=0
可見(jiàn)其生成uri為content://com.example.android.myprovider/documents/testfile.txt
而此時(shí),另一應(yīng)用就可以通過(guò)該uri訪問(wèn)該應(yīng)用的文件了
這里的邏輯主要涉及UriGrantsManagerService,(以android10的代碼查看)在結(jié)束所寫的Provider的應(yīng)用的Activity時(shí)其會(huì)調(diào)到ActivityStack的finishActivityResultsLocked方法
private void finishActivityResultsLocked(ActivityRecord r, int resultCode, Intent resultData) {
// send the result
ActivityRecord resultTo = r.resultTo;
if (resultTo != null) {
if (DEBUG_RESULTS) Slog.v(TAG_RESULTS, "Adding result to " + resultTo
+ " who=" + r.resultWho + " req=" + r.requestCode
+ " res=" + resultCode + " data=" + resultData);
if (resultTo.mUserId != r.mUserId) {
if (resultData != null) {
resultData.prepareToLeaveUser(r.mUserId);
}
}
if (r.info.applicationInfo.uid > 0) {
mService.mUgmInternal.grantUriPermissionFromIntent(r.info.applicationInfo.uid,
resultTo.packageName, resultData,
resultTo.getUriPermissionsLocked(), resultTo.mUserId);
}
resultTo.addResultLocked(r, r.resultWho, r.requestCode, resultCode, resultData);
r.resultTo = null;
}
else if (DEBUG_RESULTS) Slog.v(TAG_RESULTS, "No result destination from " + r);
// Make sure this HistoryRecord is not holding on to other resources,
// because clients have remote IPC references to this object so we
// can't assume that will go away and want to avoid circular IPC refs.
r.results = null;
r.pendingResults = null;
r.newIntents = null;
r.icicle = null;
}
這里有這樣的代碼調(diào)用:
mService.mUgmInternal.grantUriPermissionFromIntent(r.info.applicationInfo.uid,
resultTo.packageName, resultData,
resultTo.getUriPermissionsLocked(), resultTo.mUserId);
這里最后會(huì)調(diào)用到UriGrantsManagerService的grantUriPermissionFromIntent方法
void grantUriPermissionFromIntent(int callingUid,
String targetPkg, Intent intent, UriPermissionOwner owner, int targetUserId) {
NeededUriGrants needed = checkGrantUriPermissionFromIntent(callingUid, targetPkg,
intent, intent != null ? intent.getFlags() : 0, null, targetUserId);
if (needed == null) {
return;
}
grantUriPermissionUncheckedFromIntent(needed, owner);
}
這里會(huì)將查詢對(duì)應(yīng)應(yīng)用是否有對(duì)應(yīng)uri權(quán)限,如果沒(méi)有則嘗試創(chuàng)建(這里會(huì)檢查相關(guān)權(quán)限和配置,創(chuàng)建會(huì)保存一些uri、應(yīng)用包名、uid、模式等變量信息),可使用adb shell dumpsys activity permissions來(lái)dump當(dāng)前的權(quán)限信息(即UriGrantsManagerService持有的mGrantedUriPermissions信息)如:
ACTIVITY MANAGER URI PERMISSIONS (dumpsys activity permissions)
Granted Uri Permissions:
* UID 10219 holds:
UriPermission{bfeaca0 content://com.example.android.myprovider/documents/testfile.txt [user 0] [prefix]}
targetUserId=0 sourcePkg=com.example.android.myprovider targetPkg=com.example.android.mydemos
mode=0x3 owned=0x3 global=0x0 persistable=0x3 persisted=0x0
readOwners:
* ActivityRecord{ece1a65 u0 com.example.android.mydemos/.SAFDemoActivity t1452}
writeOwners:
* ActivityRecord{ece1a65 u0 com.example.android.mydemos/.SAFDemoActivity t1452}
3.啟動(dòng)action為ACTION_OPEN_DOCUMENT的intent的activity后的邏輯和uri返回
結(jié)合啟動(dòng)的activity和對(duì)應(yīng)intent的action等信息,可知其就是啟動(dòng)com.android.documentsui/com.android.documentsui.picker.PickActivity,查看其AndroidManifest.xml文件中PickActivity信息:
<activity
android:name=".picker.PickActivity"
android:theme="@style/DocumentsTheme"
android:visibleToInstantApps="true">
<intent-filter>
<action android:name="android.intent.action.OPEN_DOCUMENT" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.OPENABLE" />
<data android:mimeType="*/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.CREATE_DOCUMENT" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.OPENABLE" />
<data android:mimeType="*/*" />
</intent-filter>
<intent-filter android:priority="100">
<action android:name="android.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.OPENABLE" />
<data android:mimeType="*/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.OPEN_DOCUMENT_TREE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
其在選擇文件后會(huì)setResult然后finish退出返回
private void onPickFinished(Uri... uris) {
if (DEBUG) {
Log.d(TAG, "onFinished() " + Arrays.toString(uris));
}
final Intent intent = new Intent();
if (uris.length == 1) {
intent.setData(uris[0]);
} else if (uris.length > 1) {
final ClipData clipData = new ClipData(
null, mState.acceptMimes, new ClipData.Item(uris[0]));
for (int i = 1; i < uris.length; i++) {
clipData.addItem(new ClipData.Item(uris[i]));
}
intent.setClipData(clipData);
}
updatePickResult(
intent, mSearchMgr.isSearching(), Metrics.sanitizeRoot(mState.stack.getRoot()));
// TODO: Separate this piece of logic per action.
// We don't instantiate different objects for different actions at the first place, so it's
// not a easy task to separate this logic cleanly.
// Maybe we can add an ActionPolicy class for IoC and provide various behaviors through its
// inheritance structure.
if (mState.action == ACTION_GET_CONTENT) {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
} else if (mState.action == ACTION_OPEN_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);
} else if (mState.action == ACTION_PICK_COPY_DESTINATION) {
// Picking a copy destination is only used internally by us, so we
// don't need to extend permissions to the caller.
intent.putExtra(Shared.EXTRA_STACK, (Parcelable) mState.stack);
intent.putExtra(FileOperationService.EXTRA_OPERATION_TYPE, mState.copyOperationSubType);
} else {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
}
mActivity.setResult(FragmentActivity.RESULT_OK, intent, 0);
mActivity.finish();
}
這里邏輯與分享文件的邏輯基本一致,另外一個(gè)關(guān)鍵即是uri,這里uri是查詢數(shù)據(jù)庫(kù)來(lái)獲取的,但基本上并不是DocumentsUI(com.android.documentsui)的數(shù)據(jù)庫(kù),其在Providers類中有定義幾種authority
public static final String AUTHORITY_STORAGE = "com.android.externalstorage.documents";
public static final String ROOT_ID_DEVICE = "primary";
public static final String ROOT_ID_HOME = "home";
public static final String AUTHORITY_DOWNLOADS = "com.android.providers.downloads.documents";
public static final String ROOT_ID_DOWNLOADS = "downloads";
public static final String AUTHORITY_MEDIA = "com.android.providers.media.documents";
public static final String ROOT_ID_IMAGES = "images_root";
public static final String ROOT_ID_VIDEOS = "videos_root";
public static final String ROOT_ID_AUDIO = "audio_root";
public static final String AUTHORITY_MTP = "com.android.mtp.documents";
其中com.android.externalstorage.documents是在ExternalStorageProvider應(yīng)用中定義的,查看其AndroidManifest.xml中相關(guān)定義
<provider
android:name=".ExternalStorageProvider"
android:label="@string/storage_description"
android:authorities="com.android.externalstorage.documents"
android:grantUriPermissions="true"
android:exported="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
<!-- Stub that allows MediaProvider to make incoming calls -->
<path-permission
android:path="/media_internal"
android:permission="android.permission.WRITE_MEDIA_STORAGE" />
</provider>
其中com.android.providers.downloads.documents是在DownloadProvider應(yīng)用中定義的,查看其AndroidManifest.xml中相關(guān)定義
<provider
android:name=".DownloadStorageProvider"
android:label="@string/storage_description"
android:authorities="com.android.providers.downloads.documents"
android:grantUriPermissions="true"
android:exported="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
其中com.android.providers.media.documents是在MediaProvider應(yīng)用中定義的,查看其AndroidManifest.xml中相關(guān)定義
<provider
android:name=".MediaDocumentsProvider"
android:label="@string/storage_description"
android:authorities="com.android.providers.media.documents"
android:grantUriPermissions="true"
android:exported="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
其中com.android.mtp.documents是在MtpDocumentsProvider應(yīng)用中定義的,查看其AndroidManifest.xml中相關(guān)定義
<provider
android:name=".MtpDocumentsProvider"
android:authorities="com.android.mtp.documents"
android:grantUriPermissions="true"
android:exported="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
當(dāng)然可能還有其他的provider,到這里也可以看到其邏輯原理與應(yīng)用分享文件的邏輯基本一致,不同的是這里使用DocumentsUI來(lái)統(tǒng)一管理,但實(shí)際provider并不一定在DocumentsUI,當(dāng)然DocumentsUI有對(duì)應(yīng)provider的權(quán)限
而且參考這種方式應(yīng)用也可以實(shí)現(xiàn)自己的provider添加到SAF框架中,可參考創(chuàng)建自定義文檔提供程序 | Android 開(kāi)發(fā)者 | Android Developers (google.cn)