前言:
????????本篇文章是对系统包安装流程的总结,基于Android12 上 com.android.packageinstaller
源码的分析,第三方应用商城(华为商城,小米商城,应用宝,豌豆荚,酷安等)下载安装应
用,在普通安装和静默安装app两种方式下,对代码流程的梳理和讲解。
触发安装:
? ? ? ? 当你在商城界面中点击安装按钮,应用会自动下载,下载完成后就会调起系统安装应用的界面,此种触发方式一般是通过Intent 隐式调用的,我们先阅读如下代码:
????????Android7.0之前的跳转:
Uri uri = Uri.fromFile(file);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(uri ,"application/vnd.android.package-archive");
startActivity(intent);
? ? ? ? 但是在Android7.0 之后,直接调用的话会报Caused by:android.os.FileUriExposedExceptiony异常,原因是,安卓官方为了提高私有文件的安全性,在?Android 7.0 或更高版本的应用私有目录被限制访问,如果要访问私有文件的话,官方推荐使用FileProvider机制,系统源码文件管理(com.android.documentsui)中有参考例子,我们可以拿这个来讲解一下
? ? ? ? 1. 首先在AndroidManifest.xml 定义一个FileProvder, 可以自定义,也可以直接用系统的,下面的代码就是直接用androidx.core.content.FileProvider 这个类:
<manifest>
...
<application>
...
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.android.documentsui.fileprovider"
android:exported="false"
android:grantUriPermissions="true"
tools:replace="android:authorities">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
...
</application>
</manifest>
????????如果自定义FileProvider类的话,就需要继承FileProvider,如下:
#1. 继承FileProvider
public class MyFileProvider extends FileProvider {
public MyFileProvider() {
super(R.xml.file_paths)
}
}
#2. AndroidManifest.xml中组件声明:
<manifest>
...
<application>
...
<provider
android:name="com.sample.MyFileProvider"
android:authorities="com.mydomain.fileprovider"
android:exported="false" #FileProvider 是不需要公开的
android:grantUriPermissions="true"> #允许您授予对文件的临时访问权限
...
</provider>
...
</application>
</manifest>
? ? ? ? 2.配置file_paths.xml文件后,FileProvider 就能为预先指定的目录中的文件生成可以被访问的内容URI,在源码FileProvider.java 文件中有配置的说明
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!--external-path 对应 Environment.getExternalStorageDirectory()-->
<external-path name="name" path="." />
<!--files-path对应Context.getFilesDir()-->
<files-path name="name" path="." />
<!--cache-path对应Context.getCacheDir()-->
<cache-path name="name" path="." />
<!--external-files-path对Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)-->
<external-files-path name="name" path="path" />
<!--external-cache-path对应Context.getExternalCacheDir()-->
<external-cache-path name="name" path="path" />
<!--cache-path对应Context.getExternalMediaDirs()(API21+)-->
<external-media-path name="name" path="path" />
</paths>
? ? ? ? 3. 那么你定了FilerProvider目的是什么呢? 就是可以通过Content URI 与另外一个应用程序共享此文件,这句话我的理解:比如文管中的apk,通过创建Content Uri,然后通过intent调用起包安装应用程序进行安装,这不就是相当于共享了此文件。那怎样为文件生成Content URI呢? 可以通过FileProvider.getUriForFile()方法,好了,到这里,通过如下代码,在来理解一下在应用商城中下载应用后会调用起系统安装应用界面的场景:
#第二个参数,就是定义FileProvider 中 android:authorities: 标签内容
public static Uri getUriForFile(@NonNull Context context, @NonNull String authority,
@NonNull File file) {
}
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri uri;
# 高于或等于版本Android7.0
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
#赋予临时权限给Uri
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
#生成Content Uri
uri=FileProvider.getUriForFile(this,"android:authorities标签内容",file);
}else{
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
uri = Uri.fromFile(file);
}
#设置intent的data和type参数
intent.setDataAndType(uri ,"application/vnd.android.package-archive");
startActivity(intent);
#Android7.0可以不用加 Android8.0 以上还需要在AndroidManifest.xml 加上此权限
//申请未知来源权限
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
? ? ? ? 4. 配置好intent的启动参数后,调用startActivity(intent),那我们看看是启动了一个什么界面,这里就跳转到包安装(com.android.packageinstaller)的InstallStart这个界面,理由呢?代码即事实:
<activity android:name=".InstallStart"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:exported="true"
android:excludeFromRecents="true">
#通过这个intent-filter匹配条件 启动了此Activity
<intent-filter android:priority="1">
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.INSTALL_PACKAGE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="content" />
<data android:mimeType="application/vnd.android.package-archive" />
</intent-filter>
<intent-filter android:priority="1">
<action android:name="android.intent.action.INSTALL_PACKAGE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="package" />
<data android:scheme="content" />
</intent-filter>
<intent-filter android:priority="1">
<action android:name="android.content.pm.action.CONFIRM_INSTALL" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
启动安装?:
? ? ? ? 通过上面代码,这个时候我们已经调用起开始安装InstallStart这个界面, 具体看看这个Activity的代码:
? ? ? ? 1. 如果为静默安装方式,则直接跳转到PackageInstallerActivity.java 这个安装界面
? ? ? ? 2. 如果为普通安装方式,解析packageUri 的android:scheme标签,是"package" 还是"content", 在本文章中 触发安装 中intent的uri为Content Uri?? 所以此处解析出来的android:scheme为 content,根据判断条件就跳转到InstallStaging.java 界面。
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
#是否为静默安装方式,目前三方应用商城中,除了华为应用商城是静默安装外,其他大部分都是普通安装
final boolean isSessionInstall =
PackageInstaller.ACTION_CONFIRM_INSTALL.equals(intent.getAction());
if (isSessionInstall) {
nextActivity.setClass(this, PackageInstallerActivity.class);
} else {
Uri packageUri = intent.getData();
if (packageUri != null && packageUri.getScheme().equals(
ContentResolver.SCHEME_CONTENT)) {
// [IMPORTANT] This path is deprecated, but should still work. Only necessary
// features should be added.
// Copy file to prevent it from being changed underneath this process
nextActivity.setClass(this, InstallStaging.class);
} else if (packageUri != null && packageUri.getScheme().equals(
PackageInstallerActivity.SCHEME_PACKAGE)) {
nextActivity.setClass(this, PackageInstallerActivity.class);
} else {
Intent result = new Intent();
result.putExtra(Intent.EXTRA_INSTALL_RESULT,
PackageManager.INSTALL_FAILED_INVALID_URI);
setResult(RESULT_FIRST_USER, result);
nextActivity = null;
}
}
}
?看看InstallStaging文件中代码的功能:
? ? ? ? 1. 在onResume方法中,执行StagingAsyncTask 这个异步任务,通过阅读代码,我们可以得出,通过Content Uri 地址,把apk源文件 通过io流,拷贝到(com.android.packageinstaller)应用程序中,文件为mStagedFile?
@Override
protected void onResume() {
super.onResume();
......
#创建临时文件
mStagedFile = TemporaryFileManager.getStagedFile(this);
mStagingTask = new StagingAsyncTask();
mStagingTask.execute(getIntent().getData());
}
}
private final class StagingAsyncTask extends AsyncTask<Uri, Void, Boolean> {
@Override
protected Boolean doInBackground(Uri... params) {
if (params == null || params.length <= 0) {
return false;
}
Uri packageUri = params[0];
try (InputStream in = getContentResolver().openInputStream(packageUri)) {
// Despite the comments in ContentResolver#openInputStream the returned stream can
// be null.
if (in == null) {
return false;
}
try (OutputStream out = new FileOutputStream(mStagedFile)) {
byte[] buffer = new byte[1024 * 1024];
int bytesRead;
while ((bytesRead = in.read(buffer)) >= 0) {
// Be nice and respond to a cancellation
if (isCancelled()) {
return false;
}
out.write(buffer, 0, bytesRead);
}
}
} catch (IOException | SecurityException | IllegalStateException e) {
Log.w(LOG_TAG, "Error staging apk from content URI", e);
return false;
}
return true;
}
@Override
protected void onPostExecute(Boolean success) {
if (success) {
// Now start the installation again from a file
Intent installIntent = new Intent(getIntent());
installIntent.setClass(InstallStaging.this, DeleteStagedFileOnResult.class);
installIntent.setData(Uri.fromFile(mStagedFile));
if (installIntent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) {
installIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
}
installIntent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(installIntent);
InstallStaging.this.finish();
} else {
showError();
}
}
? ? ? ? 接下来就跳转到?DeleteStagedFileOnResult.java 这个界面,然后就直接跳转到PackageInstallerActivity.java 界面中
public class DeleteStagedFileOnResult extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
Intent installIntent = new Intent(getIntent());
installIntent.setClass(this, PackageInstallerActivity.class);
installIntent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivityForResult(installIntent, 0);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
File sourceFile = new File(getIntent().getData().getPath());
sourceFile.delete();
setResult(resultCode, data);
finish();
}
}
在来看看?PackageInstallerActivity.java 这个类,阅读代码发现有如下功能:
1. 可以获取应用的安装来源,比如是通过华为商城,应用宝,小米商城等,代码实现如下:
#这个uid非常重要,判断安装来源的重要变量
private int mOriginatingUid = PackageInstaller.SessionParams.UID_UNKNOWN;
#安装来源的应用包名:
mOriginatingPackage = (mOriginatingUid != PackageInstaller.SessionParams.UID_UNKNOWN)
? getPackageNameForUid(mOriginatingUid) : null;
#通过uid来获取应用的包名
private String getPackageNameForUid(int sourceUid) {
String[] packagesForUid = mPm.getPackagesForUid(sourceUid);
if (packagesForUid == null) {
return null;
}
if (packagesForUid.length > 1) {
if (mCallingPackage != null) {
for (String packageName : packagesForUid) {
if (packageName.equals(mCallingPackage)) {
return packageName;
}
}
}
Log.i(TAG, "Multiple packages found for source uid " + sourceUid);
}
return packagesForUid[0];
}
#通过包名来获取应用名称
????????
? ? ? ??
2. 对同一个app,如果已经安装了此app,当再安装一个高版本时,会提示更新安装
3. 还有一些异常场景的判断,比如内部不够,安装包解析错误,安装过程中报错,如下代码:
private DialogFragment createDialog(int id) {
switch (id) {
case DLG_PACKAGE_ERROR:
return SimpleErrorDialog.newInstance(R.string.Parse_error_dlg_text);
case DLG_OUT_OF_SPACE:
return OutOfSpaceDialog.newInstance(
mPm.getApplicationLabel(mPkgInfo.applicationInfo));
case DLG_INSTALL_ERROR:
return InstallErrorDialog.newInstance(
mPm.getApplicationLabel(mPkgInfo.applicationInfo));
case DLG_NOT_SUPPORTED_ON_WEAR:
return NotSupportedOnWearDialog.newInstance();
case DLG_INSTALL_APPS_RESTRICTED_FOR_USER:
return SimpleErrorDialog.newInstance(
R.string.install_apps_user_restriction_dlg_text);
case DLG_UNKNOWN_SOURCES_RESTRICTED_FOR_USER:
return SimpleErrorDialog.newInstance(
R.string.unknown_apps_user_restriction_dlg_text);
case DLG_EXTERNAL_SOURCE_BLOCKED:
return ExternalSourcesBlockedDialog.newInstance(mOriginatingPackage);
case DLG_ANONYMOUS_SOURCE:
return AnonymousSourceDialog.newInstance();
}
return null;
}
|