Android APP自動升級安裝失敗

Android APP自動升級安裝失敗

概述

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

權限機制

在Android7.0的發佈介紹中提到了一些文件系統權限方面的修改。官網:https://developer.android.google.cn/about/versions/nougat/android-7.0-changes

以下是官網的譯文(用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來授權訪問文件。

根據官方的指引:https://developer.android.google.cn/reference/android/support/v4/content/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>
</resources>

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 {
        // 7.0以下
        Uri uri = Uri.fromFile(apkFile);
        intent.setDataAndType(uri, "application/vnd.android.package-archive");
    }
    context.startActivity(intent);
}

當然,還要有前面介紹的配置一起配合使用。



由於水平有限,如果文中存在錯誤之處,請大家批評指正,歡迎大家一起來分享、探討!

博客:http://blog.csdn.net/MingHuang2017

GitHub:https://github.com/MingHuang1024

Email: [email protected]

微信:724360018

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