本文基于Android 10源码分析
前言
本篇文章是对系统包安装流程的总结,基于com.android.packageinstaller源码的分析,第三方应用商城(华为商城,小米商城,应用宝,豌豆荚,酷安等)下载安装应用,在普通安装和静默安装app两种方式下,对代码流程的梳理和讲解。
1.触发安装
当你在商城界面中点击安装按钮,应用会自动下载,下载完成后就会调起系统安装应用的界面,此种触发方式一般是通过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)中有参考例子,我们可以拿这个来讲解一下。(Android 10及以上的版本都不再支持FileProvider机制,而是引入了新的API和机制)
(1)首先在AndroidManifest.xml定义一个FileProvider, 可以自定义,也可以直接用系统的,下面的代码就是直接用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 与另外一个应用程序共享此文件,到这里,通过如下代码,在来理解一下在应用商城中下载应用后会调用起系统安装应用界面的场景:
//#第二个参数,就是定义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这个界面,代码如下:
<!-- frameworks/base/packages/PackageInstaller/AndroidManifest.xml -->
<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>
2.启动安装
通过上面代码,已经调起开始安装InstallStart这个界面,具体看看这个Activity的代码:
-
如果为静默安装方式,则直接跳转到PackageInstallerActivity.java这个安装界面;
-
如果为普通安装方式,解析packageUri的android:scheme标签,是"package" 还是"content",在本文章中 《1.触发安装》中intent的uri为Content Uri 所以此处解析出来的android:scheme为content,根据判断条件就跳转到InstallStaging.java界面。
// frameworks/base/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
......
//是否为静默安装方式,目前三方应用商城中,除了华为应用商城是静默安装外,其他大部分都是普通安装
final boolean isSessionInstall =
PackageInstaller.ACTION_CONFIRM_INSTALL.equals(intent.getAction());
......
if (isSessionInstall) {
// 1.根据intent的action判断为静默安装
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
// 2.根据intent的data判断为普通安装: packageUri为content
nextActivity.setClass(this, InstallStaging.class);
} else if (packageUri != null && packageUri.getScheme().equals(
PackageInstallerActivity.SCHEME_PACKAGE)) {
// 3.根据intent的data判断为静默安装: packageUri为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;
}
}
if (nextActivity != null) {
// 4.启动Activity
startActivity(nextActivity);
}
finish();
}
这里可以看到InstallStart这个Activity中,会根据不同安装方式选择去启动不同的下一步的Activity,分别是静默安装的PackageInstallerActivity和普通安装的InstallStaging,下面就对着两者分别分析。
2.1 InstallStaging
普通安装方式时,会启动InstallStaging。下面看看InstallStaging文件中代码的功能:
(1)在onResume方法中,执行StagingAsyncTask这个异步任务,通过Content Uri地址,把apk源文件通过io流,拷贝到(com.android.packageinstaller)应用程序中,文件为mStagedFile。
// frameworks/base/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java
protected void 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;
}
// 将apk读取到mStagedFile文件中
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);
// 跳转到DeleteStagedFileOnResult中
startActivity(installIntent);
InstallStaging.this.finish(); // 结束当前的Activity
} else {
showError();
}
}
}
(2)接下来就跳转到DeleteStagedFileOnResult.java这个界面,然后就直接跳转到PackageInstallerActivity.java界面中:
// frameworks/base/packages/PackageInstaller/src/com/android/packageinstaller/DeleteStagedFileOnResult.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);
// 又是直接跳转到PackageInstallerActivity
startActivityForResult(installIntent, 0);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
setResult(resultCode, data);
finish();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (isFinishing()) {
File sourceFile = new File(getIntent().getData().getPath());
new Thread(sourceFile::delete).start();
}
}
}
2.2 PackageInstallerActivity
既然静默安装方式和普通安装方式最后都会用到PackageInstallerActivity,这里就对该Activity进行分析。
(1)可以获取应用的安装来源,比如是通过华为商城,应用宝,小米商城等,代码实现如下:
// frameworks/base/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
// 这个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,当再安装一个高版本时,会提示更新安装。
// frameworks/base/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
private void startInstallConfirm() {
View viewToEnable;
if (mAppInfo != null) {
// 如果设备中已经存在了mAppInfo,则提示更新新版本
viewToEnable = requireViewById(R.id.install_confirm_question_update);
mOk.setText(R.string.update);
} else {
// This is a new application with no permissions.
viewToEnable = requireViewById(R.id.install_confirm_question);
}
viewToEnable.setVisibility(View.VISIBLE);
mEnableOk = true;
mOk.setEnabled(true);
mOk.setFilterTouchesWhenObscured(true);
}
(3)还有一些异常场景的判断,比如内部不够,安装包解析错误,安装过程中报错,如下代码:
// frameworks/base/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
private DialogFragment createDialog(int id) {
if (mLocalLOGV) Log.i(TAG, "createDialog(" + 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;
}
网友评论