1. 程式人生 > >積跬步至千里系列之六--安裝與解除安裝應用程式(PackageInstaller)(一)

積跬步至千里系列之六--安裝與解除安裝應用程式(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方法時會丟擲異常。
接下里就分析具體是如何獲取許可權列表的,這個在放在下一篇來說。