1. 程式人生 > >Android 7.0: install .apk

Android 7.0: install .apk

最後是試出來了,中間有幾個問題,就是安裝是從內部 cache dir 會有問題,安裝程式會顯示:

there was a problem parsing the package

改成 external cache dir 就OK。所以 xml 檔一定要使用下面這一組:

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

Android Manifest中加入許可權

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

取得 SD Card 許可權:

//Android 6.0以上需要判斷使用者是否願意開啟儲存(WRITE_EXTERNAL_STORAGE)的許可權
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    CheckStoragePermission();
}
private void CheckStoragePermission() {
    //Android 6.0檢查是否開啟儲存(WRITE_EXTERNAL_STORAGE)的許可權,若否,出現詢問視窗
    if (ContextCompat.checkSelfPermission(this,
            Manifest.permission.WRITE_EXTERNAL_STORAGE)
            != PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(this,
            Manifest.permission.READ_EXTERNAL_STORAGE)
            != PackageManager.PERMISSION_GRANTED) {//Can add more as per requirement
        ActivityCompat.requestPermissions(this,
                new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE},
                20);
    }
}

@Override
//Android 6.0以上 接收使用者是否允許使用儲存許可權
public void onRequestPermissionsResult(int requestCode,
                                       String permissions[], int[] grantResults) {
    switch (requestCode) {
        case 20: {
            if (grantResults.length > 0
                    && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            } else {
                CheckStoragePermission();
            }
            return;
        }
    }
}

附註:我測試了多臺Android 6.0 + Android 7.0 裝置,發現提升許可權非必需,完全沒有提升在各裝置都可以正常執行。

附上我在使用中的 install apk function:

public void installApk(Context context, File apkFile) {
    Intent intent = new Intent(Intent.ACTION_VIEW);
    //Log.d(TAG, "filepath:" + apkFile.getAbsolutePath());

    //如果沒有設定SDCard寫許可權,或者沒有sdcard,apk檔案儲存在記憶體中,需要授予許可權才能安裝
    try {
        String[] command = {"chmod", "777", apkFile.toString()};
        ProcessBuilder builder = new ProcessBuilder(command);
        builder.start();
    } catch (IOException ignored) {
    }

    if(Build.VERSION.SDK_INT>=24) {
        intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        Uri uri = FileProvider.getUriForFile(context, "tw.thinkingsoftware.geofence.provider", apkFile);
        //intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
        //intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        intent.setDataAndType(uri, "application/vnd.android.package-archive");
        //Log.d(TAG, "uri:" + uri.getPath());
    } else {
        Uri uri = Uri.fromFile(apkFile);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.setDataAndType(uri, "application/vnd.android.package-archive");
        //Log.d(TAG, "uri:" + uri.getPath());
    }
    context.startActivity(intent);
}

1 manifest 中新增程式碼

<provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="${applicationId}.provider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/provider_paths"/>
    </provider>

2 res/xml/provider_paths.xml

  <?xml version="1.0" encoding="utf-8"?>
    <paths>
        <external-path
            name="publicDir" path="/"/>
    </paths>

3 install

public static void installApk(Context context, File apkFile) {

    Intent intent = new Intent(Intent.ACTION_VIEW);
    Uri uri = null;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        uri = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".provider", apkFile);
        intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
        intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    } else {
        uri = Uri.fromFile(apkFile);
    }
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    intent.setDataAndType(uri, "application/vnd.android.package-archive");
    context.startActivity(intent);
}

you need this url

the code solution my problem. hope help you

使用Intent安裝APK方法(相容Android N)

Android N之前,通過Intent安裝一個APK程式碼差多不是下面這個樣子的:

Intent install = new Intent(Intent.ACTION_VIEW);
install.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
File apkFile = new File(Environment.getExternalStorageDirectory() + "/download/" + "app.apk";
install.setDataAndType(Uri.fromFile(apkFile)), "application/vnd.android.package-archive");
startActivity(install)

上面程式碼在Android N(API 26)之前是沒有問題的,但是從Android 7.0開始,系統修改了安全機制: 限定應用在預設情況下只能訪問自身應用資料。所以當我們想通過File物件訪問其它package資料時,就需要藉助於ContentProvider、FileProvider這些元件,否則會報FileUriExposedException異常。

相應的,顯然上面的程式碼在Android N中會有FileUriExposedException的問題,所以我們需要藉助FileProvider做出下面的修改。

使用FileProvider的步驟

新增Provider到AndroidManifest.xml

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="應用包名.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">

    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths"/>

</provider>

建立@xml/file_paths對應的資原始檔

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path
        name="download"
        path="" />
    <external-files-path
        name="Download"
        path="" />
</paths>

paths裡面用來放你需要訪問的非本應用路徑,只有通過這裡申明後,才能在程式中使用不報錯。

  • files-path 對應 Context.getFilesDir()
  • cache-path 對應 getCacheDir()
  • external-path 對應 Environment.getExternalStorageDirectory()
  • external-files-path 對應 Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)
  • external-cache-path 對應 Context.getExternalCacheDir()

具體其中的path和name就需要你自己來根據需求確定了。

應用程式碼

Intent install = new Intent(Intent.ACTION_VIEW);
install.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
File apkFile = new File(Environment.getExternalStorageDirectory() + "/download/" + "app.apk";

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    install.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    Uri contentUri = FileProvider.getUriForFile(context, "應用報名.fileProvider", apkFile);
    install.setDataAndType(contentUri, "application/vnd.android.package-archive");
} else {
    install.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");
}

startActivity(install);

如何在Android7.0系統下通過Intent安裝apk

Android系統升級到7.0之後,安全性提高了不少,過去我們通常是使用這樣的程式碼進行apk的安裝操作。

1
2
3
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");
context.startActivity(intent);

但是在Android7.0的系統上,執行這段程式碼,會報如下錯誤。

Caused by: android.os.FileUriExposedException

原因是,安卓官方為了提高私有檔案的安全性,面向 Android 7.0 或更高版本的應用私有目錄被限制訪問 (0700)。此設定可防止私有檔案的元資料洩漏,如它們的大小或存在性.

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

1.定義一個FileProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<manifest>
    ...
    <application>
        ...
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.mydomain.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            ...
        </provider>
        ...
    </application>
</manifest>

2.新增可用許可權的檔案目錄

在res目錄下,增加xml資料夾,並新建一個名為 file_paths.xml 的檔案。檔案內容格式如下:

1
2
3
4
<paths xmlns:android="http://schemas.android.com/apk/res/android">
   		<files-path name="name1" path="test1" />
    	...
</paths>

標籤下面必須包含至少包含以下標籤中的一個或者多個。

files-path

1
<files-path name="name1" path="test1" />

表示Context.getFilesDir()目錄或者其子目錄。

示例 : /data/data/com.chen.gradle/files/test1

cache-path

1
<cache-path name="name2" path="test2" />

表示Context.getCacheDir()目錄或者其子目錄。

示例 : /data/data/com.chen.gradle/cache/test2

external-path

1
<external-path name="name3" path="test3" />

表示Environment.getExternalStorageDirectory()目錄或者其子目錄。

示例 : /storage/emulated/0/test3

external-files-path

1
<external-files-path name="name4" path="test4" />

表示Context.getExternalFilesDir(null)目錄或者其子目錄。

示例 : /storage/emulated/0/Android/data/com.chen.gradle/files/test4

external-cache-path

1
<external-cache-path name="name5" path="test5" />

表示Context.getExternalCacheDir()目錄或者其子目錄。

示例 : /storage/emulated/0/Android/data/com.chen.gradle/cache/test5

3.增加到provider

通過<meta-data>標籤將上面的filepath新增到provider當中。

1
2
3
4
5
6
7
8
9
<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.mydomain.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

4.通過provider生成Uri

1
2
3
4
File imagePath = new File(Context.getFilesDir(), "test1");
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = FileProvider.getUriForFile(getContext(), "com.mydomain.fileprovider", newFile);

5.賦予臨時許可權給Uri

1
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

最終安裝apk的程式碼變成這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void installApk(Context context, String apkPath) {
    if (context == null || TextUtils.isEmpty(apkPath)) {
        return;
    }

    
    File file = new File(apkPath);
    Intent intent = new Intent(Intent.ACTION_VIEW);

    //判讀版本是否在7.0以上
    if (Build.VERSION.SDK_INT >= 24) {
        //provider authorities
        Uri apkUri = FileProvider.getUriForFile(context, "com.mydomain.fileprovider", file);
        //Granting Temporary Permissions to a URI
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
    } else {
        intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
    }

    context.startActivity(intent);
}

參考文獻

相關文章: