1. 程式人生 > >Android APP自動升級安裝失敗.md

Android APP自動升級安裝失敗.md

##Android APP自動升級安裝失敗

###概述

自動升級在APP中是一個非常常見的功能,當你的應用有更新時,可以提醒使用者升級甚至在必要時可強制使用者升級。但隨著系統版本的更新,安裝apk的許可權也在收緊,導致一些APP在高版本的機器上升級失敗。這時就有必要了解一下如何處理這樣的問題了。

###許可權機制

以下是官網的譯文(用google翻譯的網頁)

許可權更改

Android 7.0包含可能會影響您的應用的許可權更改。

檔案系統許可權更改

為了提高私人檔案的安全性,針對Android 7.0或更高版本的應用的私人目錄限制了訪問許可權(0700)。此設定可防止私有檔案的元資料洩漏,例如其大小或存在。此許可權更改有多種副作用:

所有者不應再放寬私有檔案的檔案許可權,並且使用MODE_WORLD_READABLE和/或 嘗試執行此操作 MODE_WORLD_WRITEABLE將觸發a SecurityException。

注意:截至目前,此限制尚未完全執行。應用程式仍可使用本機API或FileAPI 修改其私人目錄的許可權。但是,我們強烈建議不要放寬對私人目錄的許可權。

file://在包域外 傳遞URI可能會使接收者無法訪問路徑。因此,嘗試傳遞 file://URI觸發器a FileUriExposedException。共享私有檔案內容的推薦方法是使用FileProvider。

該DownloadManager可以通過檔名不再私下共享儲存的檔案。傳統應用程式在訪問時可能會以無法訪問的路徑結束COLUMN_LOCAL_FILENAME。針對Android 7.0或更高版本的應用會SecurityException在嘗試訪問時 觸發COLUMN_LOCAL_FILENAME。通過使用DownloadManager.Request.setDestinationInExternalFilesDir()或 DownloadManager.Request.setDestinationInExternalPublicDir() 仍然可以訪問路徑 來將下載位置設定為公共位置的舊應用程式 COLUMN_LOCAL_FILENAME,但強烈建議不要使用此方法。訪問由檔案公開的檔案的首選方法DownloadManager是使用 ContentResolver.openFileDescriptor()。

在應用之間共享檔案

對於定位到Android 7.0的應用,Android框架會強制執行StrictModeAPI策略,禁止file://在應用外部公開URI。如果包含檔案URI的intent離開您的應用程式,則該應用程式將失敗並顯示FileUriExposedException異常。

要在應用程式之間共享檔案,您應該傳送content://URI並授予URI臨時訪問許可權。授予此許可權的最簡單方法是使用FileProvider該類。有關許可權和共享檔案的詳細資訊,請參閱共享檔案。

大意就是說檔案的訪問許可權提高了,不能直接使用file://的方式來共享檔案了,應該使用 content://URI的方式來共享檔案,並使用FileProvider類來授權。 ###應對方法

老程式碼一般都是用下面這樣的程式碼來安裝下載下來的apk的:

 public static void installAPk(Context context, File apkFile) {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        Uri uri = Uri.fromFile(apkFile);
        intent.setDataAndType(uri, "application/vnd.android.package-archive");
        context.startActivity(intent);
    }

但7.0以上要求使用FileProvider來授權訪問檔案。

  1. 定義FileProvider
  2. 指定可用檔案
  3. 生成檔案的URI
  4. 授予URI臨時許可權
  5. 向另一個應用程式提供內容URI

下面來一一介紹一下這幾個步驟:

一、定義FileProvider

在AndroidManifest.xml檔案中註冊provider

<provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="com.xbd.file.provider"
        android:exported="false"
        android:grantUriPermissions="true">
        <!-- 元資料 -->
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/update_apk_paths" />
</provider>

解釋一下幾個引數的含義:

android:name 檔案提供者的類名,固定為"android.support.v4.content.FileProvider",如果你很牛逼也可以自己寫一個類並繼承"android.support.v4.content.FileProvider",然後實現一些擴充套件的功能。

android:authorities 許可權的名字,用於標識provider提供的內容,可以有多個名字,各名字之間用“;”隔開。為了不和其它名字衝突一般使用域名的形式來描述

android:exported 內容提供者是否可供其他應用程式使用,在這裡不需要,所以填false

android:grantUriPermissions 是否授權給那些本來無許可權訪問的人臨時訪問內容提供者提供的內容,這裡填true,不然就沒法訪問到這個檔案了。

二、指定可用的檔案

為了指定需要訪問的檔案,需要在一個xml檔案中指定被訪問檔案的儲存路徑。 在res目錄下新建一個xml資料夾,然後新建一個檔案:update_apk_paths.xml(檔名自己隨意起),內容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<paths>
    <!--
    files-path:          該方式提供在應用的內部儲存區的檔案/子目錄的檔案。
                          它對應Context.getFilesDir返回的路徑:eg:”/data/data/com.jph.simple/files”。

    cache-path:          該方式提供在應用的內部儲存區的快取子目錄的檔案。
                          它對應getCacheDir返回的路徑:eg:“/data/data/com.jph.simple/cache”;

    external-path:       該方式提供在外部儲存區域根目錄下的檔案。
                          它對應Environment.getExternalStorageDirectory返回的路徑:

    external-cache-path: 該方式提供在應用的外部儲存區根目錄的下的檔案。
                          它對應Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)
                          返回的路徑。eg:”/storage/emulated/0/Android/data/com.jph.simple/files”
    -->
    <cache-path name="update" path="" />
</paths>

paths元素下可以有很多子元素,如files-path、cache-path、external-path等,意義在上面的註解中都說明了。這裡我使用了 cache-path,也就是說我的apk檔案存放在了內部儲存區的快取目錄中。

name=“update” 相當於下面的path的別名,為了把真實的路徑隱藏起來,這樣就只能看到別名,如果按照這個別名路徑去找檔案的話肯定是找不到的。這個別名自己隨便取,我把它叫做“update”

path="" 代表你要分享的真實的子目錄名,空字串代表根目錄,注意該值必須是一個子目錄,不能是檔名

綜合來講,以上配置表明:我要分享一個目錄供其它人訪問,這個目錄就是內部儲存區的快取目錄的根目錄,即 getCacheDir()的返回值。所有根目錄及其子目錄下的檔案都可以被訪問,同時我為這快取目錄取了一個別名叫“update”,以混淆視聽。

然後將上面的update_apk_paths.xml檔案連結到AndroidManifest.xml中定義的provider中,也就是定義中的“元資料”的內容

<!-- 元資料 -->
<meta-data
    android:name="android.support.FILE_PROVIDER_PATHS"
    android:resource="@xml/update_apk_paths" />	

android:name 代表資源的型別,此處為固定值"android.support.FILE_PROVIDER_PATHS"

android:resource 代表資原始檔,即update_apk_paths.xml,但是不要字尾名

三、生成檔案的URI

用以下方式生成檔案的Uri:

Uri apkUri = FileProvider.getUriForFile(context, "com.xbd.file.provider", apkFile);

其中,第二個引數"com.xbd.file.provider"是在AndroidManifest.xml檔案中宣告的provider中 android:authorities元素的值,第三個引數apkFile就是下載下來的儲存在快取目錄下的apk檔案

四、授予URI臨時許可權

授權有很多種方式:這裡只說一種,就是通過Intent addFlags()方法,如:

intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

五、向另一個應用程式提供內容URI

用startActivity(intent)啟動一個應用就可以了,被啟動的應用就有許可權訪問你提供的檔案了,但要注意必須新增這句:

intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

###實用程式碼

綜合以上分析,可將原來安裝apk的程式碼改成以下的樣子:

public static void installAPk(Context context, File apkFile) {
    Intent intent = new Intent(Intent.ACTION_VIEW);
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        // 7.0 以上
        Uri apkUri = FileProvider.getUriForFile(context, "com.xbd.file.provider", apkFile);
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        // [6.0 ~ 7.0)
        Uri uri = FileProvider.getUriForFile(context, context.getPackageName() + ".update" +
            ".provider", apkFile);
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        intent.setDataAndType(uri, "application/vnd.android.package-archive");
    } else {
        // 6.0以下
        Uri uri = Uri.fromFile(apkFile);
        intent.setDataAndType(uri, "application/vnd.android.package-archive");
    }
}

當然,還要有前面介紹的配置一起配合使用。 由於水平有限,如果文中存在錯誤之處,請大家批評指正,歡迎大家一起來分享、探討!

微信:724360018