FileProvider的原理和使用

爲什麼需要使用FileProvider ?

爲了提高私有目錄的安全性,防止應用信息的泄漏,從 Android 7.0 開始,應用私有目錄的訪問權限被做限制。具體表現爲,開發人員不能夠再簡單地通過 file:// URI 訪問其他應用的私有目錄文件或者讓其他應用訪問自己的私有目錄文件。
同時,也是從 7.0 開始,Android SDK 中的 StrictMode 策略禁止開發人員在應用外部公開 file:// URI。具體表現爲,當我們在應用中使用包含 file:// URI 的 Intent 離開自己的應用時,程序會發生故障。
開發中,如果我們在使用 file:// URI 時忽視了這兩條規定,將導致用戶在 7.0 及更高版本系統的設備中使用到相關功能時,出現 FileUriExposedException 異常,導致應用出現崩潰閃退問題。而這兩個過程的替代解決方案便是使用 FileProvider。

FileProvider的兼容性錯誤
使用FileProvider的時候報告錯誤:Didn't find class "android.support.v4.content.FileProvider" on path:將v4包替換成androidx.core的FileProvider

<provider
    android:name="android.support.v4.content.FileProvider"
	...
</provider>

更改爲:

<provider
	android:name="androidx.core.content.FileProvider"
	...
</provider>

關於更多的錯誤參見:FileProvider 不同版本變化和兼容

FileProvider的使用

1. 聲明FileProvider
AndroidManifest.xmlapplication下面增加provider

<provider
	android:name="androidx.core.content.FileProvider"	
	android:authorities="app包名.fileProvider"
    android:grantUriPermissions="true"
    android:exported="false">
    <meta-data
    	android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

android:authorities需要是唯一的,使用唯一的android:authorities與xml指定的共享目錄進行關聯。一般使用app包名.fileProvider,當然這個名字也可以修改,但是必須唯一。也可以直接寫成android:authorities="${applicationId}.fileProvider"

strat = sCache.get(authority);

2. 編寫FileProvider的xml,指定共享目錄
在res下面創建xml目錄,然後新建file_paths.xml文件,內容如下(tag內容隨自己定義):

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <!--代表的目錄即爲:Environment.getExternalStorageDirectory()/Android/data/包名/-->
    <external-path
        name="files_root"
        path="Android/data/com.example.qsbk/" />

    <!--代表的目錄即爲:Environment.getExternalStorageDirectory()-->
    <external-path
        name="external_storage_root"
        path="." />

    <!--代表的目錄即爲:Environment.getExternalStorageDirectory()/pics -->
    <external-path
        name="external"
        path="pics" />

</paths>

xml的內容格式,一句話總結如下:

<tag name="myname" path="mypath">

此時產生的共享目錄如下:tag代表的目錄+path指定的目錄。保存在以name爲key的hashmap中,所以上述模板會產生一條共享目錄,內容如下:

private final HashMap<String, File> mRoots = new HashMap<String, File>();
mRoots[“myname”] = new File(tag,mypath)

之後調用getUriForFile生成共享Uri的時候,會遍歷mRoots查找最佳的File目錄,而name 屬性則是指定的目錄的一個別名。然後通過Uri.Builder生成 content:// uri。如果沒有找到匹配的目錄,則拋出異常IllegalArgumentException。所以至少得指定一個tag,tag可以是下面的元素之一:

  • <root-path>:設備根目錄/
  • <files-path>:context.getFilesDir()的目錄
  • <cache-path>:context.getCacheDir()的目錄
  • <external-path>:Environment.getExternalStorageDirectory()的目錄
  • <external-files-path>:ContextCompat.getExternalFilesDirs()下標爲0的目錄
  • <external-cache-path>:ContextCompat.getExternalCacheDirs()下標爲0的目錄
  • <external-media-path>:context.getExternalMediaDirs()下標爲0的目錄
    對於上述目錄有疑問的,可以學習android 目錄結構 和 文件存儲。關於Tag是如何生成的共享目錄列表mRoots可以參考FileProvider源碼:
    PathStrategy parsePathStrategy(Context context, String authority);

3. 生成 Content URI
在 Android 7.0 出現之前,我們通常使用 Uri.fromFile() 方法生成一個 File URI。這裏,我們需要使用 FileProvider 類提供的公有靜態方法 getUriForFile 生成 Content URI。比如:

Uri contentUri = FileProvider.getUriForFile(this,
                BuildConfig.APPLICATION_ID + ".fileProvider", myFile);

需要傳遞三個參數,第二個參數就是在AndroidManifest.xml中聲明的android:authorities。第三個參數是我們指定的共享文件,該文件必須在FileProvider的xml指定的共享目錄下(或者子目錄下)否則會拋出異常IllegalArgumentException

String filePath = Environment.getExternalStorageDirectory()+
				"Android/data/com.example.qsbk/imges/temp/1.jpg";
File outputFile = new File(filePath);
if (!outputFile.getParentFile().exists()) {
    outputFile.getParentFile().mkdir();
}
Uri contentUri = FileProvider.getUriForFile(this,
				BuildConfig.APPLICATION_ID + ".fileProvider", outputFile);

生成的 Content URI 是這樣的:

content://com.example.qsbk.fileProvider/files_root/imges/temp/1.jpg

其中Uri:
com.example.qsbk.fileProvider:是provider元素的 authorities 屬性值
files_root:是xml指定共享目錄的,其中name=“files_root”(files_root指定目錄和共享文件路徑最大程度 匹配)
imges/temp/1.jpg:是替換別名之後剩餘沒有匹配的部分路徑。

4. 授予 Content URI 訪問權限
生成 Content URI 對象後,需要對其授權訪問權限。授權方式有兩種:
第一種方式,使用 Context 提供的 grantUriPermission(package, Uri, mode_flags) 方法向其他應用授權訪問 URI 對象。三個參數分別表示授權訪問 URI 對象的其他應用包名,授權訪問的 Uri 對象,和授權類型。其中,授權類型爲 Intent 類提供的讀寫類型常量:

  • FLAG_GRANT_READ_URI_PERMISSION
  • FLAG_GRANT_WRITE_URI_PERMISSION

或者二者同時授權。這種形式的授權方式,有效期截止至發生設備重啓或revokeUriPermission撤銷授權。

第二種方式,配合 Intent 使用。通過 setData 方法向 intent 對象添加 Content URI。然後使用 setFlags 或者 addFlags 方法設置讀寫權限,可選常量值同上。這種形式的授權方式,權限有效期截止至其它應用所處的堆棧銷燬,並且一旦授權給某一個組件後,該應用的其它組件擁有相同的訪問權限。

5. 提供 Content URI 給其它應用

擁有授予權限的 Content URI 後,便可以通過 startActivity或者 setResult 方法啓動其他應用並傳遞授權過的 Content URI 數據。當然,也有其他方式提供服務。

如果你需要一次性傳遞多個 URI 對象,可以使用 intent 對象提供的 setClipData方法,並且 setFlags方法設置的權限適用於所有 Content URIs。

最常用的應用場景,相機拍照或者讀取相冊。

源碼分析:
FileProvider加載的時候會調用attachInfoattachInfo內部通過provider元素的 authorities 獲取緩存中的PathStrategy如果緩存中沒有則生成一個,然後添加到緩存。

@Override
public void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) {
	super.attachInfo(context, info);
	// Sanity check our security
    if (info.exported) {
	    throw new SecurityException("Provider must not be exported");
    }
    if (!info.grantUriPermissions) {
    	throw new SecurityException("Provider must grant uri permissions");
    }

    mStrategy = getPathStrategy(context, info.authority);
}

private static PathStrategy getPathStrategy(Context context, String authority) {
        PathStrategy strat;
        synchronized (sCache) {
            strat = sCache.get(authority);
            if (strat == null) {
                try {
                    strat = parsePathStrategy(context, authority);
                } catch (IOException e) {
                    throw new IllegalArgumentException(
                            "Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
                } catch (XmlPullParserException e) {
                    throw new IllegalArgumentException(
                            "Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
                }
                sCache.put(authority, strat);
            }
        }
        return strat;
    }

生成PathStrategy的時候會解析Provider關聯的xml文件,根據tag生成共享目錄列表HashMap<String, File>

    private static PathStrategy parsePathStrategy(Context context, String authority)
            throws IOException, XmlPullParserException {
        ...
        while ((type = in.next()) != END_DOCUMENT) {
            if (type == START_TAG) {
                final String tag = in.getName();

                final String name = in.getAttributeValue(null, ATTR_NAME);
                String path = in.getAttributeValue(null, ATTR_PATH);

                File target = null;
                if (TAG_ROOT_PATH.equals(tag)) {
                    target = DEVICE_ROOT;
                } else if (TAG_FILES_PATH.equals(tag)) {
                    target = context.getFilesDir();
                } else if (TAG_CACHE_PATH.equals(tag)) {
                    target = context.getCacheDir();
                } else if (TAG_EXTERNAL.equals(tag)) {
                    target = Environment.getExternalStorageDirectory();
                } else if (TAG_EXTERNAL_FILES.equals(tag)) {
                    File[] externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null);
                    if (externalFilesDirs.length > 0) {
                        target = externalFilesDirs[0];
                    }
                } else if (TAG_EXTERNAL_CACHE.equals(tag)) {
                    File[] externalCacheDirs = ContextCompat.getExternalCacheDirs(context);
                    if (externalCacheDirs.length > 0) {
                        target = externalCacheDirs[0];
                    }
                } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
                        && TAG_EXTERNAL_MEDIA.equals(tag)) {
                    File[] externalMediaDirs = context.getExternalMediaDirs();
                    if (externalMediaDirs.length > 0) {
                        target = externalMediaDirs[0];
                    }
                }

                if (target != null) {
                    strat.addRoot(name, buildPath(target, path));
                }
            }
        }

        return strat;
    }

最後使用getUriForFile生成uri

        @Override
        public Uri getUriForFile(File file) {
            String path;
            try {
            	//獲取共享文件的規範化路徑,不懂的可以參考
            	//https://blog.csdn.net/CAir2/article/details/106782930
                path = file.getCanonicalPath();
            } catch (IOException e) {
                throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
            }

            // Find the most-specific root path
            //遍歷xml指定的共享目錄列表,獲取最佳匹配的一項
            Map.Entry<String, File> mostSpecific = null;
            for (Map.Entry<String, File> root : mRoots.entrySet()) {
                final String rootPath = root.getValue().getPath();
                if (path.startsWith(rootPath) && (mostSpecific == null
                        || rootPath.length() > mostSpecific.getValue().getPath().length())) {
                    mostSpecific = root;
                }
            }
			
			//如果沒有找到則拋出異常
            if (mostSpecific == null) {
                throw new IllegalArgumentException(
                        "Failed to find configured root that contains " + path);
            }

            // Start at first char of path under root
            final String rootPath = mostSpecific.getValue().getPath();
            if (rootPath.endsWith("/")) {
                path = path.substring(rootPath.length());
            } else {
                path = path.substring(rootPath.length() + 1);
            }
			
			//使用最匹配的一項,生成content格式的uri
            // Encode the tag and path separately
            path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
            return new Uri.Builder().scheme("content")
                    .authority(mAuthority).encodedPath(path).build();
        }

參考博客:https://blog.csdn.net/growing_tree/article/details/71190741

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章