積跬步至千里系列之六--安裝與解除安裝應用程式(PackageInstaller)(一)
PackageInstaller屬於framework層的一個系統應用,其位置位於:原始碼目錄/package/apps/PackageInstaller.所有的系統預製應用全都放在apps/目錄下,比如Setting, Launcher等都在此目錄下.
我是第一次分析系統應用,需要按照步驟來進行分析,一般需要注意的幾點主要有:1)尋找一個程式的入口,java,C,C#都是從main函式開始執行的,分析原始碼程式時也要先照一個入口 (2)先弄清楚程式實現的主要功能是什麼 (3)關注流程和邏輯,不要過於關注細節。要學會抓大放小。
* 一、尋找PackageInstaller的入口*
至目前我們已經知道了PackageInstaller也是一個apk程式,其主要的功能就是實現應用的安裝和解除安裝功能。自然的,也就是安裝應用和解除安裝應用時會使用到PackageInstaller程式。
在Android中,呼叫某個元件或者新開一個介面都是通過Intent來實現的,有顯示的呼叫和隱示呼叫兩種情況,一般都是指定其Action,然後執行startActivity開啟介面。對於開啟packageInstaller的安裝程式介面,會用到如下呼叫:
Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
//指定要安裝的apk的檔案路徑
intent.setData(Uri.parse("file:/sdcard/qq.apk"));
startActivity(intent);
執行如上intent呼叫後,就會彈出提示安裝qq應用的提示視窗.如上執行的intent包含了一個Action為ACTION_INSTALL_PACKAGE的意圖呼叫,接下來,就去PackageInstaller的清單配置檔案中搜索包含Action為ACTION_INSTALL_PACKAGE的Activity,然後進行確定具體呼叫的那個Activity.
通過瀏覽mainfest.xml檔案,可以總結一些資訊:
1.共定義了兩對四個視窗。其中PackageInstallerActivity和InstallAppProgress用於安裝應用程式;UninstallerActivity和UninstallAppProgress用於解除安裝應用程式.
2.intent-filter包含android.intent.action.MAIN的Activity Action的Activity會在系統程式列表中列出相應的應用圖示。PackageInstaller中的Activity並沒有註冊MAIN的Action,圖示不會列在應用程式列表中
3.PackageInstallerActivity包含了兩個Intent Filter,也就是支援兩種方式開啟該Activity。第一種方式為:
Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
intent.setDataAndType(Uri.fromFile(new File("/sdcard/qq.apk")),"application/vnd.android.package-archive");
startActivity(intent);
第二種方式:
Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
intent.setData(Uri.fromFile(new File("/sdcard/qq.apk" )));
startActivity(intent);
特別說明的是:如果通過指定Package的方式安裝Android應用,需要該Android應用已安裝。
3.解除安裝應用程式:解除安裝某個應用程式時,也是通過傳送Intent的相對應的Action來實現的。
第一種刪除呼叫:
Intent intent = new Intent(Intent.ACTION_DELETE);
intent.setData(Uri.parse("package:com.tencent.mobileqq"));
startActivity(intent);
第二種解除安裝呼叫:
Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE);
intent.setData(Uri.parse("package:com.tencent.mobileqq"));
startActivity(intent);
安裝Android應用前的校驗
上面我們知道安裝應用時會呼叫PackageInstallerActivity彈出安裝介面,在安裝時,我們可以留意到,還會將要安裝的應用所用到的許可權以列表的形式進行展示,應用名稱,應用圖示等資訊也會一一進行展示,所以我們要看看PckageInstallerActivity中是如何獲取到這些資訊的。在PackageInstallerActivity中主要完成的工作如下:
1).從Intent中獲取Package URI,Scheme等資訊。
2).對從Intent物件獲取的資訊進行校驗。主要校驗Scheme
3).根據Scheme的具體值(file或者package)進行相應的處理
4).獲取ApplicationInfo物件,該物件包含與Android應用相關的資訊,如應用名稱,應用圖示,應用許可權等
5).初始化用於顯示名稱和應用圖示的控制元件
6).校驗當前Android系統是否允許“未知來源”的應用被安裝
7).進行安裝前的準備工作,顯示校驗視窗
PackageInstallerActivity.java中onCreate方法如下:
//建立PackageManager物件
mPm = getPackageManager();
mInstaller = mPm.getPackageInstaller();
mUserManager = (UserManager) getSystemService(Context.USER_SERVICE);
final Intent intent = getIntent();
//獲取待安裝Android應用的路徑或Package
mPackageURI = intent.getData();
mOriginatingURI = intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI);
mReferrerURI = intent.getParcelableExtra(Intent.EXTRA_REFERRER);
}
final boolean unknownSourcesAllowedByAdmin = isUnknownSourcesAllowedByAdmin();
final boolean unknownSourcesAllowedByUser = isUnknownSourcesEnabled();
boolean requestFromUnknownSource = isInstallRequestFromUnknownSource(intent);
mInstallFlowAnalytics = new InstallFlowAnalytics();
mInstallFlowAnalytics.setContext(this);
mInstallFlowAnalytics.setStartTimestampMillis(SystemClock.elapsedRealtime());
mInstallFlowAnalytics.setInstallsFromUnknownSourcesPermitted(unknownSourcesAllowedByAdmin
&& unknownSourcesAllowedByUser);
mInstallFlowAnalytics.setInstallRequestFromUnknownSource(requestFromUnknownSource);
mInstallFlowAnalytics.setVerifyAppsEnabled(isVerifyAppsEnabled());
mInstallFlowAnalytics.setAppVerifierInstalled(isAppVerifierInstalled());
mInstallFlowAnalytics.setPackageUri(mPackageURI.toString());
//獲取scheme:file或者package
final String scheme = mPackageURI.getScheme();
//從此處可以看到,scheme只有兩個值:file或package
if (scheme != null && !"file".equals(scheme) && !"package".equals(scheme)) {
Log.w(TAG, "Unsupported scheme " + scheme);
setPmResult(PackageManager.INSTALL_FAILED_INVALID_URI);
mInstallFlowAnalytics.setFlowFinished(
InstallFlowAnalytics.RESULT_FAILED_UNSUPPORTED_SCHEME);
finish();
return;
}
final PackageUtil.AppSnippet as;
//scheme是package時
if ("package".equals(mPackageURI.getScheme())) {
mInstallFlowAnalytics.setFileUri(false);
try {
//獲取與package對應的Android應用的資訊,包含應用名稱,許可權列表,應用圖示等資訊
mPkgInfo = mPm.getPackageInfo(mPackageURI.getSchemeSpecificPart(),
PackageManager.GET_PERMISSIONS | PackageManager.GET_UNINSTALLED_PACKAGES);
} catch (NameNotFoundException e) {
}
if (mPkgInfo == null) {
Log.w(TAG, "Requested package " + mPackageURI.getScheme()
+ " not available. Discontinuing installation");
showDialogInner(DLG_PACKAGE_ERROR);
setPmResult(PackageManager.INSTALL_FAILED_INVALID_APK);
mInstallFlowAnalytics.setPackageInfoObtained();
mInstallFlowAnalytics.setFlowFinished(
InstallFlowAnalytics.RESULT_FAILED_PACKAGE_MISSING);
return;
}
//建立AppSnippet物件。該物件封裝了用於待安裝Android應用的標題和圖示
as = new PackageUtil.AppSnippet(mPm.getApplicationLabel(mPkgInfo.applicationInfo),
mPm.getApplicationIcon(mPkgInfo.applicationInfo));
} else {//scheme為file的情況,及從apk檔案安裝程式
mInstallFlowAnalytics.setFileUri(true);
//獲取APK檔案的絕對路徑
final File sourceFile = new File(mPackageURI.getPath());
//建立APK檔案的分析器
PackageParser.Package parsed = PackageUtil.getPackageInfo(sourceFile);
// Check for parse errors 出錯直接返回
if (parsed == null) {
Log.w(TAG, "Parse error when parsing manifest. Discontinuing installation");
showDialogInner(DLG_PACKAGE_ERROR);
setPmResult(PackageManager.INSTALL_FAILED_INVALID_APK);
mInstallFlowAnalytics.setPackageInfoObtained();
mInstallFlowAnalytics.setFlowFinished(
InstallFlowAnalytics.RESULT_FAILED_TO_GET_PACKAGE_INFO);
return;
}
mPkgInfo = PackageParser.generatePackageInfo(parsed, null,
PackageManager.GET_PERMISSIONS, 0, 0, null,
new PackageUserState());
mPkgDigest = parsed.manifestDigest;
as = PackageUtil.getAppSnippet(this, mPkgInfo.applicationInfo, sourceFile);
}
mInstallFlowAnalytics.setPackageInfoObtained();
//set view
setContentView(R.layout.install_start);
mInstallConfirm = findViewById(R.id.install_confirm_panel);
mInstallConfirm.setVisibility(View.INVISIBLE);
PackageUtil.initSnippetForNewApp(this, as, R.id.app_snippet);
mOriginatingUid = getOriginatingUid(intent);
// Block the install attempt on the Unknown Sources setting if necessary.
if (!requestFromUnknownSource) {
initiateInstall();
return;
}
// If the admin prohibits it, or we're running in a managed profile, just show error
// and exit. Otherwise show an option to take the user to Settings to change the setting.
final boolean isManagedProfile = mUserManager.isManagedProfile();
if (!unknownSourcesAllowedByAdmin
|| (!unknownSourcesAllowedByUser && isManagedProfile)) {
showDialogInner(DLG_ADMIN_RESTRICTS_UNKNOWN_SOURCES);
mInstallFlowAnalytics.setFlowFinished(
InstallFlowAnalytics.RESULT_BLOCKED_BY_UNKNOWN_SOURCES_SETTING);
} else if (!unknownSourcesAllowedByUser) {
// Ask user to enable setting first
showDialogInner(DLG_UNKNOWN_SOURCES);
mInstallFlowAnalytics.setFlowFinished(
InstallFlowAnalytics.RESULT_BLOCKED_BY_UNKNOWN_SOURCES_SETTING);
} else {
//為安裝應用做一些準備工作
initiateInstall();
}
關於獲取ApplicationInfo物件和顯示校驗視窗的問題,需要一個方法isInstallingUnknownAppsAllowed方法。該方法如下:
private boolean isInstallRequestFromUnknownSource(Intent intent) {
String callerPackage = getCallingPackage();
if (callerPackage != null && intent.getBooleanExtra(
Intent.EXTRA_NOT_UNKNOWN_SOURCE, false)) {
try {
mSourceInfo = mPm.getApplicationInfo(callerPackage, 0);
if (mSourceInfo != null) {
if ((mSourceInfo.flags & ApplicationInfo.FLAG_PRIVILEGED) != 0) {
// Privileged apps are not considered an unknown source.
return false;
}
}
} catch (NameNotFoundException e) {
}
}
return true;
}
上述方法通過Content Provider獲取設定中的“未知來源”是否選中。如果選中返回true,否則返回false。執行isInstallingUnknownAppsAllowed方法並不需要再Manifest.xml檔案中設定任何許可權,但必須對APK檔案進行系統簽名。也就是說使用isInstallingUnknownAppsallowed方法的ape程式必須與Android原始碼一起編譯,或使用mm/mmm命令單獨進行編譯。在普通的Android應用中儘管可以編譯通過,但執行isInstallingUnknownAppsAllowed方法時會丟擲異常。
接下里就分析具體是如何獲取許可權列表的,這個在放在下一篇來說。