首页 > 其他分享 >存储访问框架SAF

存储访问框架SAF

时间:2024-06-02 14:43:58浏览次数:24  
标签:存储 java err 框架 SAF 3032 uri android com

存储访问框架SAF简析(Storage Access Framework)

1.简介

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的存储访问框架主要是用于访问非应用本身专属的文件(应用专属的文件包括如/data/data/下面应用报名目录中的文件和/sdcard(/storage/emulated/0/)下面Android/目录下data或media等目录下应用报名目录中的文件,如/storage/emulated/0/Android/data/包名/),具体可先看下上面三个开发者网站的资料,然后看个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;
    }
}

这里在外部存储/sdcard目录下创建了个My目录,然后在该目录下保存了一个文件testfile1.txt,查看上述demo运行对于该文件的访问权限

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

其中关键打印的如下

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的几种访问方式中,简单的file或uri直接去获取文件的方式是无法得到文件的,而只有通过action为ACTION_OPEN_DOCUMENT的intent去选择的方式在选择返回后才有文件的权限,能够得到文件

2.应用分享文件

https://developer.android.google.cn/training/secure-file-sharing/setup-sharing

https://developer.android.google.cn/training/secure-file-sharing/share-file

在这里建议先看下应用分享文件的介绍,因为其和SAF访问逻辑大同小异,理解了分享文件,后面SAF逻辑就比较容易理解了

这里只简要介绍下

首先,要分享一个应用的专属文件(如),需要声明一个ContentProvider:如(一般可以直接使用androidx.core.FileProvider,也可以自己实现,这里是简单的将androidx.core.FileProvider中的代码复制挪到本地创建的FileProvider中以便调试修改)

<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>

这里必须要有grantUriPermissions为true,或者exported为true(直接使用androidx.core.FileProvider会抛异常,可注释调抛异常的部分(attachInfo方法中)),不然其他应用无法通过uri访问文件,这里的meta-data部分主要是在androidx.core.FileProvider中会读取filepaths文件中配置用于根据文件路径生成对应的uri,这里本地使用外部存储目录下的文件,所以filepaths文件中配置了external-files-path标签,具体配置和路径对应情况可查看androidx.core.FileProvider中代码,这里的意思即对应/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的时候,另一个应用需要通过uri访问该应用的文件,则可以启动该应用的activity,然后在该应用的activity中选择了文件后setResult然后回到另一个应用,如:

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();
}

这里主要是通过将文件路径转为provider对应的uri,然后将uri设置到intent中,并添加对应的flag,如Intent.FLAG_GRANT_READ_URI_PERMISSION,然后将其通过setResult进行设置,这样,在另一应用的onActivityResult回调中就可以获取到该uri,并暂时能通过该uri访问对应文件

打印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

可见其生成uri为content://com.example.android.myprovider/documents/testfile.txt

而此时,另一应用就可以通过该uri访问该应用的文件了

这里的逻辑主要涉及UriGrantsManagerService,(以android10的代码查看)在结束所写的Provider的应用的Activity时其会调到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;
}

这里有这样的代码调用:

mService.mUgmInternal.grantUriPermissionFromIntent(r.info.applicationInfo.uid,
                    resultTo.packageName, resultData,
                    resultTo.getUriPermissionsLocked(), resultTo.mUserId);

这里最后会调用到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);
}

这里会将查询对应应用是否有对应uri权限,如果没有则尝试创建(这里会检查相关权限和配置,创建会保存一些uri、应用包名、uid、模式等变量信息),可使用adb shell dumpsys activity permissions来dump当前的权限信息(即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.启动action为ACTION_OPEN_DOCUMENT的intent的activity后的逻辑和uri返回

结合启动的activity和对应intent的action等信息,可知其就是启动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>

其在选择文件后会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();
}

这里逻辑与分享文件的逻辑基本一致,另外一个关键即是uri,这里uri是查询数据库来获取的,但基本上并不是DocumentsUI(com.android.documentsui)的数据库,其在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应用中定义的,查看其AndroidManifest.xml中相关定义

<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应用中定义的,查看其AndroidManifest.xml中相关定义

<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应用中定义的,查看其AndroidManifest.xml中相关定义

<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应用中定义的,查看其AndroidManifest.xml中相关定义

<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>

当然可能还有其他的provider,到这里也可以看到其逻辑原理与应用分享文件的逻辑基本一致,不同的是这里使用DocumentsUI来统一管理,但实际provider并不一定在DocumentsUI,当然DocumentsUI有对应provider的权限

而且参考这种方式应用也可以实现自己的provider添加到SAF框架中,可参考创建自定义文档提供程序 | Android 开发者 | Android Developers (google.cn)

标签:存储,java,err,框架,SAF,3032,uri,android,com
From: https://www.cnblogs.com/luoliang13/p/18227118

相关文章

  • [DotNetGuide]C#/.NET/.NET Core优秀项目和框架精选
    前言注意:排名不分先后,都是十分优秀的开源项目和框架,每周定期更新分享(欢迎关注公众号:追逐时光者,第一时间获取每周精选分享资讯......
  • LangChain学习圣经:从0到1精通LLM大模型应用开发的基础框架
    文章很长,且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录博客园版为您奉上珍贵的学习资源:免费赠送:《尼恩Java面试宝典》持续更新+史上最全+面试必备2000页+面试必备+大厂必备+涨薪必备免费赠送:《尼恩技术圣经+高并发系列PDF》,帮你实现技术自由,完成职业升级,薪......
  • Redis数据存储和读写
    今天工作群里,有小伙伴问了一个问题,从Redis获取的数据,一会是0,一会是OK。这引起了我们对Redis数据存储和读写的疑问。以下是整理的一些技术研究内容。在Redis中,所有的数据存储都是基于字符串的。无论你插入的是String、int还是DateTime类型的数据,最终都会以字符串的形式存......
  • 初步搭建一个自己的对象存储服务---Minio
    docker安装1、拉取镜像dockerpullminio/minio2、启动镜像dockerrun-p9000:9000-p9001:9001--nameminio-d--restart=always-e"MINIO_ACCESS_KEY=admin"-e"MINIO_SECRET_KEY=admin123456"-v/home/data:/data-v/home/config:/root/.miniominio/m......
  • 【Python爬虫--scrapy+selenium框架】超详细的Python爬虫scrapy+selenium框架学习笔记
    六,selenium想要下载PDF或者md格式的笔记请点击以下链接获取python爬虫学习笔记点击我获取Scrapy+selenium详细学习笔记点我获取Python超详细的学习笔记共21万字点我获取1,下载配置##安装:pipinstallselenium##它与其他库不同的地方是他要启动你电脑上的浏览器......
  • Vue3的自动化测试怎么做?详细说明一下常用的测试工具和框架
    随着前端技术的飞速发展,Vue3作为一个优秀的前端框架,已经广泛应用于各类项目中。在开发过程中,为了提升代码质量、减少运维成本,自动化测试变得尤为重要。不仅可以提高开发效率,还能确保产品的稳定性。那么,Vue3自动化测试该怎么做呢?本文将详细介绍常用的测试工具和框架。为......
  • MyPRC 框架设计与实现
    MyPRC框架设计与实现框架概述框架具体实现MyRPC框架核心类关系简图服务启动流程Service.hService.cpp信号处理相关代码:signalhandler.hpp事件分发流程MainReactorSubReactorreactor.cppeventdispatch.hppcoroutinelocal.hpptimer.hpp服务器端请求处理流程事件监听状态......
  • Caliburn.Micro框架学习笔记——Action的参数传递机制
    据此篇文章,我们继续来谈谈Caliburn.Mirco的Action参数传递机制。因此程序结构都是默认MVVM的形式。基本机制它的机制是——Caliburn.Micro的智能对象参数绑定机制通过约定和反射使得视图和视图模型之间的交互变得更加直观和简洁。通过cal:Message.Attach语法(附加属性的......
  • web前端三大主流框架详细介绍
    1.AngularAngular是一个由Google开发的用于构建Web应用的开源JavaScript框架。Angular使用TypeScript语言编写,它是一种由Microsoft开发的JavaScript超集,可以提供更丰富的功能和更严格的类型检查。Angular是MVC(Model-View-Controller)框架,它提供了一种结构化的方法来开发Web应用......
  • 在iPhone上恢复已删除的Safari历史记录的最佳方法
    您是否正在寻找恢复iPhone上已删除的Safari历史记录的最佳方法?好吧,这篇文章提供了4种在有/无备份的情况下恢复iPhone上已删除的Safari历史记录的最佳方法。现在按照分步指南进行操作。iPhone上的Safari历史记录会被永久删除吗?当您选择清除Safari历史记录时,它......