1. 程式人生 > >安裝新版本的apk:android.os.FileUriExposedException

安裝新版本的apk:android.os.FileUriExposedException

最近在做app更新升級功能的時候,又碰到因為android 7.0 引起的相容問題了。

android.os.FileUriExposedException: file:///storage/emulated/0/Download/appName-1.0.3.apk exposed beyond app through Intent.getData()

1. 原因:

  • Android 7.0 以後,Google 移除掉了容易被濫用的“允許位置來源”應用的開關,取消了“允許未知來源”的檢查框,如果想安裝一些第三方商店的應用,需要手動開啟應用的“允許安裝未知來源程式”的許可權;
  • 另外,7.0系統之後,Android不再允許在app中把file://Uri暴露給其他app(包括安裝app),包括但不侷限於通過Intent或ClipData 等方法。原因在於使用file://Uri會有一些風險,比如:
    a. 檔案是私有的,接收file://Uri的app無法訪問該檔案。

    b. 在Android6.0之後引入執行時許可權,如果接收file://Uri的app沒有申請
    c. READ_EXTERNAL_STORAGE許可權,在讀取檔案時會引發崩潰。

因此,google提供了FileProvider,使用它可以生成content://Uri來替代file://Uri。

2. 解決方法

  • 增加相應的Provider元件庫依賴:
implementation 'com.android.support:support-v4:26.1.0'
  • 在AndroidManifest.xml檔案中配置Provider元件
    <application>
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="$application_id.provider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths"/>
        </provider>
    </application>

註釋:

  1. 「authorities」:一般採用「包名」+".provider"的方式命名,避免與其他的Provider衝突;
  2. 「exported」:必須是「false」,否則會報錯的;
  3. 「grantUriPermission」: 「true」,表示授予 URI 臨時訪問許可權;
  4. 「meta-data」:設定需要授予URI許可權的檔案路徑。
  • 在AndroidManifest.xml檔案增加相應的安裝以及檔案讀取許可權
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"
        tools:ignore="ProtectedPermissions"/>
  • 增加xml資原始檔「provider_paths」
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="external_Files" path="."/>
</paths>

註釋:
「name」:uri路徑名稱,按照你的命名習慣,隨便起就可以。。。此值的子目錄名包含在路徑屬性中。
「path」:所共享的子目錄。注意是目錄,不是檔案!如“demo/apk”,“xxx”等,表示的是下的“demo/apk”,“xxx”子目錄。Attention:path=“.”表示所有子目錄。

1. 「external-path」:表示Environment.getExternalStorageDirectory()目錄

<external-path name="name" path="path" />

2. 「files-path」:表示Context.getFileDir()目錄

<files-path name="name" path="path" />

3. 「cache-path」:表示getCacheDir()目錄

<cache-path name="name" path="path" />

4. 「external-files-path」:表示Context#getExternalFilesDir(String) 和Context.getExternalFilesDir(null)目錄

<external-files-path name="name" path="path" />

5. 「external-cache-path」:表示Context.getExternalCacheDir()目錄

<external-cache-path name="name" path="path" />
  • 最後,執行安裝過程
    /**
     * 安裝apk
     */
    private fun installApk(apkFile: File) {
        val intent = Intent()
        //執行動作
        intent.action = Intent.ACTION_VIEW
        val apkUri: Uri?
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //7.0以上版本,需要配置許可權才能安裝未知來源的程式:本程式碼的處理是使用FileProvider讀取Uri資源
            //引數1-上下文, 引數2-Provider地址(與AndroidManifest.xml檔案中保持一致)   引數3-apk檔案
            apkUri = FileProvider.getUriForFile(mContext, "demo.com.xxx.provider", apkFile)
            //新增這一句表示對目標應用臨時授權該Uri所代表的檔案
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
        } else {
            apkUri = Uri.fromFile(apkFile)
        }
        intent.setDataAndType(apkUri, "application/vnd.android.package-archive")
        mContext.startActivity(intent)
        mContext.finish()
        android.os.Process.killProcess(android.os.Process.myPid())
    }

OK,解決完成!