Android 如何通過代碼安裝 APK?


2019-10-21

關鍵字:Java代碼安裝程序


 

在 APK 開發中,通過 Java 代碼來打開系統的安裝程序以安裝 APK 並不是什么難事,一般的 Android 系統都有開放這一功能。

 

但隨着 Android 系統版本的迭代,其對於權限的把控越來越嚴格,或者說是變得越來越注重安全性。這就導致了以前可以通過很簡單的幾行代碼就能實現的功能,現在要復雜很多。

 

對於通過代碼打開系統安裝程序這一功能的限制,其分水嶺在 Android7.0,即 Android N 上。通常在 Android N 以上的系統使用一種做法,以下則使用另一種做法。

 

這里簡述一下這兩種做法。

 

1、傳統的通過代碼安裝APK的方式

 

傳統的方式就是簡單,寥寥幾行代碼就可以實現需求:

File apk = new File(...);
Uri uri = Uri.fromFile(apk);
Intent intent = new Intent();
intent.setClassName("com.android.packageinstaller", "com.android.packageinstaller.PackageInstallerActivity");
intent.setData(uri);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

這種方法簡單粗暴且實用,只要知曉要安裝的 APK 的位置,並擁有訪問權限即可。

 

但現在市面上主流的 Android 手機系統版本都已經要高於 7.0 了,這一方法幾乎已經沒有用了。

 

2、高版本系統上的通過代碼安裝APK的方式

 

直接上Java代碼:

File apk = new File(...);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Uri uri = FileProvider.getUriForFile(this, "com.apk.demo.fileprovider", apk);
intent.setDataAndType(uri, "application/vnd.android.package-archive");

startActivity(intent);

上面這段代碼看起來似乎和傳統的方式並沒有太大的區別是嗎?

 

確實是,但它真正的區別並沒有在 Java 代碼上體現出來。

 

在高版本系統中,APK 已經不能直接訪問其它 APK 的私有數據了。

 

什么是APK的私有數據?

 

APK在安裝過程中於 data 目錄下創建的專屬目錄自然是其私有數據無疑。另外,只要是在應用程序中封裝的 File 對象,不管這個文件本身是不是由該程序創建的,那這個文件都屬於該程序的“私有數據”。舉個例子來說,假設我們將手機連接到電腦,通過 adb push 的方式往 sdcard 目錄下推了一個 APK 文件進去。然后我們自行編寫了一段代碼,將這個 sdcard 中的安裝包傳到系統的 PackageInstaller 中去安裝,都會報安全錯誤,因為這個位於 sdcard 目錄下文件對我們這段代碼來說是“私有數據”,不允許直接暴露給 PackageInstaller。

 

我知道這有點“蠻橫”,但系統這么做也確實是為了數據的安全性着想,無可厚非,畢竟是有解決的辦法的。

 

下面就來看看在高版本系統中暴露“私有數據”給其它程序的方法。

 

在高版本中,Android7.0 及以上,開放(暴露)私有數據的唯一方式是通過 ContentProvider 來實現。

 

具體的步驟大致如下:

1、配置 AndroidManifest.xml 中的 ContentProvider 信息;

2、配置要開放的 paths 信息;

3、在 Java 代碼中通過 FileProvider 封裝文件信息。

 

1、AndroidManifest.xml 配置

前面說過,高版本系統中其實就是將以前的直接開放變成通過 ContentProvider 來間接開放。因此我們需要在 AndroidManifest.xml 中添加一個 provider 標簽,示例如下:

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

android:name 屬性填寫的是 FileProvider 類的完整名稱。這個類可以填寫兩個值,一個是位於 support(android.support.v4.content.FileProvider) 包下的,另一個是位於 androidx(androidx.core.content.FileProvider) 包下的。這兩種都可以填寫,本質上沒有區別。但是要根據實際情況來決定用哪個,即要看你的工程引的是 androidx 支援包還是 support 支援包。關於 support 與 androidx 的關系本文就不再贅述了。

 

android:authorities 屬性就是和普通的 ContentProvider 一樣的用於訪問文件資源的 uri 標簽頭。值內容根據實際需要來填寫即可。

 

android:exported 與 android:grantUriPermissions 兩個屬性的值照着填就好。它們的含義在這里不太重要,大致是指允許其它應用單次使用自己的 Provider 資源。

 

meta-data 標簽中的內容需要關注的是 android:resource 屬性中的內容。這個屬性的值引向一個自行配置的 xml 文件,這份 xml 文件記載的是設備中的路徑信息,簡單理解就是你想開放哪些目錄中的文件資源給第三方使用的意思。關於這個 xml 的配置請看第 2 步的記載。

 

2、paths 配置

通常的做法是在工程 res 目錄下新建一個 xml 目錄,並在該 xml 目錄下新建一個 xml 文件。文件的名稱必須與第 1 步中 @xml/ 屬性值中配置的一致。

 

根據第 1 步中的示例代碼,我們需要新建一個 file_paths.xml 文件。該文件的內容如下所示:

<?xml version="1.0" encoding="utf-8"?>
<paths>

    <external-path
        name="p1"
        path="/data/dir1/" />

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

    <external-files-path
        name="p3"
        path="/data/dir2" />

    <external-cache-path
        name="p4"
        path="/data/dir3" />

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

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

</paths>

 

在解釋上面的配置信息之前,我們先來看看一個 APK 在運行過程中可以使用的存儲位置,或者說常用的存儲路徑。它們通常有以下五種:

1、程序安裝目錄下的 cache 目錄,即 /data 目錄對應包名下的 cache 目錄;

2、程序安裝目錄下的 files 目錄,同樣是 /data 目錄對應包名下的 files 目錄;

3、sdcard 目錄;

4、sdcard 目錄下對應包名的專屬目錄下的 cache 目錄;

5、sdcard 目錄下對應包名的專屬目錄下的 files 目錄;

 

上述第 3、第 4 、第 5 理應歸為同一種。但我們理解成 Android 為了方便我們的使用特意封裝出了后面兩種也可以。

 

在了解了一個 APK 在設備上可以使用的幾種存儲位置后再來理解上面 xml 中記載的配置信息就很好理解了。上面示例 xml 配置信息中共有 6 個配置項信息,其中前面兩項是同價的,因此其實就相當於只有五種配置信息。

 

我們首先假設我們的 APK 的包名為: com.apk.demo

 

其中,xml 中第 1 、第 2 個配置項信息對應於 APK 存儲位置的第 3 條,即 sdcard 目錄。兩個配置項信息分別代表着不同子路徑位置。其對應的文件絕對路徑為 /storage/emulated/0/data/dir1 或者 /sdcard/data/dir1

 

xml 中第 3 個配置項信息對應於 APK 存儲位置的第 5 條,即 sdcard 下對應包名的 files 目錄。其對應的文件絕對路徑為 /sdcard/Android/data/com.apk.demo/files/data/dir2

 

xml 中第 4 個配置項信息對應於 APK 存儲位置的第 4 條,即 sdcard 下對應包名的 cache 目錄。其對應的文件絕對路徑為 /sdcard/Android/data/com.apk.demo/cache/data/dir3

 

xml 中第 5 個配置項信息對應於 APK 存儲位置的第 1 條,即 /data 下應用包名下的 cache 目錄。其對應的文件絕對路徑為 /data/user/0/com.apk.demo/cache/data 或者 /data/data/com.apk.demo/cache/data

 

xml 中最后一個配置項信息對應於 APK 存儲位置的第 2 條,即 /data 下應用包名下的 files 目錄。其對應的文件絕對路徑為 /data/user/0/com.apk.demo/files/ff 或者 /data/data/com.apk.demo/files/ff

 

簡單來說,就是將你要開放出去的路徑的類型選好,然后填上該類型下的相對路徑即可。

 

我們以第 1 個標簽再詳細說說:

 

這表示我們想開放 sdcard 目錄,然后在 sdcard 目錄下的子路徑是 /data/dir1,組合成絕對路徑就是 /sdcard/data/dir1 。至於 name 標簽則是用於 ContentProvider 標識使用的,一般來講按需設計成不同的值就可以了。

 

哦對了,還是以上圖為例,如果你剛好要訪問 sdcard 根目錄,那么在 path 屬性下直接填一個 '.' 值或者 '/' 值即可,就如上面 xml 示例中的第 2 個配置項那樣。

 

3、Java 代碼配置

Java 代碼的配置就沒什么特別的了,直接以章節首部的代碼來用就可以了。關鍵的代碼其實只有一行:

Uri uri = FileProvider.getUriForFile(this, "com.apk.demo.fileprovider", apk);

 

 

通常我們都會兼顧 Android 高低版本的系統,因此會使用如下所示的“混合型”代碼:

File apk = new File(...);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    Uri uri = FileProvider.getUriForFile(this, "com.apk.demo.fileprovider", apk);
    intent.setDataAndType(uri, "application/vnd.android.package-archive");
}else{
    intent.setDataAndType(Uri.fromFile(apk),"application/vnd.android.package-archive");
}
try {
    startActivity(intent);
}catch(Exception e){
    e.printStackTrace();
}

 


 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM