Android R 如何訪問Android/data目錄?

前言

Android R上分區存儲的限制得到進一步加強,無論APP的targetsdkversion是多少,都將無法訪問Android/data和Android/obb這二個應用私有目錄。這無疑對會部分APP的業務場景及用戶體驗造成沖擊,典型的如下

  • 文件管理類軟件:微信、QQ傳輸的文件無法展示給用戶以便捷使用
  • 垃圾清理類軟件:清理緩存功能受阻

“你有你的張良計,我有我的過墻梯”,現市面上文件管理類軟件(如MT管理器)已解決上述系統限制,本文將淺析其實現方案,并主要分析以下2個問題:

  • SAF是通過何種方式訪問文件系統的,MediaStore API ? File API ? Native Code ?
  • SAF為何能訪問Android/data目錄

實現方案

其實現方案很簡單,就是通過Intent ACTION_OPEN_DOCUMENT_TREE,啟動SAF讓用戶授權訪問Android/data目錄,屬于官方公開的方法。
前提是APP的targetsdkversion要小于30

摘自官方文檔
摘自官方文檔

文檔鏈接:
文檔訪問限制
授予對目錄內容的訪問權限

基本使用

  1. 通過Intent啟動SAF授權界面,注意URI的百分號編解碼(%3A和%2F),別隨意替換,否則SAF無法導航到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看實際業務需要可再補充
            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";
授權申請
  1. 在用戶同意授權后,持久化uri權限(否則關機重啟或授權界面finish后,APP就無權限訪問了),并只能通過DocumentFile進行業務操作,File API操作是無效的,此授權只是授權uri操作,并未授權文件系統,后續章節有說明。
 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();
                   //補充說明下授權文件夾后,文件夾中的子文件的uri格式如下,可自行按格式拼接直接訪問子文件:
                   //content://com.android.externalstorage.documents/tree/primary%3ATest%2Ftest/document/primary%3ATest%2Ftest%2F666.mp3
                    ......
                }
                break;
            default:
                break;
        }
    }
  1. 注意這個授權用戶是可以撤回的,通過點擊應用信息界面的存儲,就會看到撤回界面,所以業務需要去動態判斷
 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;
    }
授權撤回

拓展

通過前面二個章節,已經介紹了實現方案的基本使用,下面就該分析本文的亮點內容了

  • SAF是通過何種方式訪問文件系統的,MediaStore API ? File API ? Native Code ?
  • SAF為何能訪問Android/data目錄
存儲訪問框架(SAF)簡介

為方便后續講解,先簡單回顧下SAF

SAF架構

APP:
com.example.photos就是我們自己的APP

System UI:
com.google.android.documentsui,一般稱作DoucmentUI,就是上文中啟動的授權界面APP,它只是個UI殼子

DocumentProvider:
DocumentUI中數據的提供者,這個Provider可以有很多
com.android.externalstorage,是本地文件系統的Provider

關于SAF更詳細介紹,請參考官方存儲訪問框架
經過SAF的簡單介紹,分析目標很明確,那就是com.android.externalstorage

SAF是通過何種方式訪問文件系統的

先安利幾個AOSP源碼查看網址:
官方的Android Code Search
國內的AOSP XREF

PS:后文源碼鏈接都用的是XREF,方便國內查看

從DocumentFile#listFile入手,經過源碼跟蹤會發現最終會調用 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的實現類
ExternalStorageProvider
frameworks/base/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java

import com.android.internal.content.FileSystemProvider;
public class ExternalStorageProvider extends FileSystemProvider 

queryChildDocuments的實現位于其父類 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;
    }
 ......
}

FileUtils#listFilesOrEmpty

    /** {@hide} */
    public static @NonNull File[] listFilesOrEmpty(@Nullable File dir) {
        return (dir != null) ? ArrayUtils.defeatNullable(dir.listFiles())
                : ArrayUtils.EMPTY_FILE;
    }

至此,第一個問題,已經理清:
SAF的ExternalStorageProvider最終也是通過File API來訪問文件系統的

那么第二個問題,就很自然的來了,都是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
#重點關注這行輸出
Groups: 1015 1077 1078 1079 9997 20064 50064

拿著這些神秘的GID在前面介紹的網址中一搜,就會很容易的發現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分別對應Android/data和Android/obb的訪問權限
如果我們APP能通過某種方式獲取到1078和1079的用戶組權限,豈不妙哉?
遺憾的是,對于三方APP這是不可能的,除非是手機廠商的預置的系統APP

總結

  • Android R上可通過SAF獲得訪問Android/data和Android/obb目錄的權限,前提是APP targetsdkversion 小于30
  • SAF的底層實現ExternalStorageProvider也是通過File API來訪問文件系統的
  • SAF之所以能訪問Android/data和Android/obb是因為ExternalStorageProvider
    進程具有GID 1078 和1079,三方APP是不可能擁有這些GID的
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容