Install Apk 相容性問題
問題描述
現象
程式碼執行安裝Apk,出現系統彈框解析錯誤,解析包時出現錯誤
場景
在華為P20 Android 8.0 手機上,下載Apk並使用通知欄進度條顯示,開啟應用鎖屏通知許可權,下載過程在鎖屏情況下進行,下載完成後自動執行安裝Apk,在解鎖後出現系統彈框,解析包出現錯誤。
解決之前安裝Apk的方法
首先在AndroidManifest中宣告fileProvider
<provider android:name="android.support.v4.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_path" /> </provider>
provider屬性說明
屬性 | 說明 |
---|---|
name | android V4 包中的類FileProvider |
authorities | 你的檔案的Uri的域名一般以包名.fileprovider的格式,防止重名 |
exported | 設定不允許匯出,我們的FileProvider應該是私有的 |
grantUriPermissions | 允許獲取檔案的臨時訪問許可權 |
resourse | 設定FileProvider訪問的檔案路徑 |
res包下建立 file_path.xml
<?xml version="1.0" encoding="utf-8"?> <paths xmlns:android="http://schemas.android.com/apk/res/android"> <external-path name="external_path" path="test" /> <cache-path name="internal_path" path="test" /> 這裡可以建立很多個paths,但是每個paths的name不能一樣 </paths>
path 說明
<files-path name="*name*" path="*path*" />對應的是:Context.getFileDir()的路徑地址 對應路徑:Context.getFileDir()+"/${path}/" 得到路徑:content://${applicationId}/&{name}/ <cache-path name="*name*" path="*path*" /> 對應路徑:Context.getCacheFir()+"/${path}/" 得到路徑:content://${applicationId}/&{name}/ <external-path name="*name*" path="*path*" /> 對應路徑:Environment.getExternalStorageDirectory()+"/${path}/" 得到路徑:content://${applicationId}/&{name}/ <external-files-path name="*name*" path="*path*" /> 對應路徑:Context.getExternalStorageDirectory()+"/${path}/" 得到路徑:content://${applicationId}/&{name}/ <external-cache-path name="*name*" path="*path*" /> 對應路徑: Context.getExternalCacheDir()+"/${path}/" 得到路徑:content://${applicationId}/&{name}/
舉個例子說明:
path做如下宣告 <paths xmlns:android="http://schemas.android.com/apk/res/android"> <files-path name="my_images" path="images/"/> </paths> File imagePath = new File(Context.getFilesDir(), "images"); File newFile = new File(imagePath, "default_image.jpg"); Uri contentUri = getUriForFile(getContext(), "com.mydomain.fileprovider", newFile); contentUri值為:content://com.mydomain.fileprovider/my_images/default_image.jpg
安裝apk的方法(7.0版本相容問題)
public static void installApk(Context context,File apkFile){ try { Intent intent = new Intent(Intent.ACTION_VIEW); Uri apkUri = null; //判斷版本是否是 7.0 及 7.0 以上 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { apkUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", apkFile); //新增對目標應用臨時授權該Uri所代表的檔案 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); } else { apkUri = Uri.fromFile(apkFile); } intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setDataAndType(apkUri, "application/vnd.android.package-archive"); context.startActivity(intent); } catch (Exception e) { e.printStackTrace(); } }
Android 8.0系統需要宣告許可權
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGE" />
OK,以上就是大家普遍解決7.0,以及8.0版本相容問題的方法。
但是,在上文描述的場景中依然報出了錯誤:
java.lang.SecurityException: Permission Denial: opening provider android.support.v4.content.FileProvider from ProcessRecord{cc3ad2316425: com.android.packageinstaller/u0a21} (pid=16425, uid=10021) that is not exported from uid 10340

懵逼.jpg
問題定位
經過短暫的懵逼後,開始通過各種方式,探索問題的原因。
根據系統log分析,猜測在鎖屏時,用於安裝Apk的service處於休眠或者不可用的狀態,導致通過 intent.addflags
方式賦予的臨時許可權失效了。於是,再次仔細看了官方文件後,發現還有一個方法,可以生成許可權且在主動呼叫方法或者手機重啟後才會失效。
改進後的程式碼
public static void installApk(Context context,File apkFile){ try { Intent intent = new Intent(Intent.ACTION_VIEW); Uri apkUri = null; //判斷版本是否是 7.0 及 7.0 以上 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { apkUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", apkFile); //新增對目標應用臨時授權該Uri所代表的檔案 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); } else { apkUri = Uri.fromFile(apkFile); } intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setDataAndType(apkUri, "application/vnd.android.package-archive"); //查詢所有符合 intent 跳轉目標應用型別的應用,注意此方法必須放置setDataAndType的方法之後 List<ResolveInfo> resInfoList = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); //然後全部授權 for (ResolveInfo resolveInfo : resInfoList) { String packageName = resolveInfo.activityInfo.packageName; context.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); } context.startActivity(intent); } catch (Exception e) { e.printStackTrace(); } }
再次嘗試,此問題再沒有出現。