Silent installation, startup of App and silent uninstallation in Android 11.0 system

Silent installation, startup of App and silent uninstallation in Android 11.0 system

  • Silently install and launch apps and silently uninstall
    • Modify PackageInstaller
    • Third-party App requests silent installation

Silent installation and startup of the App and silent uninstallation

This article describes how to modify the system PackageInstaller application in Android 11 to support silent installation of apps and start the installed apps.

Modify PackageInstaller

PackageInstaller is an App specifically responsible for app installation in the system. Silent installation logic is added to this application. The path where the application is located is /frameworks/base/packages/PackageInstaller/src/com/android/packageinstaller/

  1. Add SilenceInstallManager, the path is /frameworks/base/packages/PackageInstaller/src/com/android/packageinstaller/SilenceInstallManager.java;
package com.android.packageinstaller;

import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.pm.PackageParser;
import android.content.pm.ResolveInfo;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Message;
import android.util.ArrayMap;
import android.util.Log;

import androidx.annotation.NonNull;

import com.android.internal.content.PackageHelper;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;

import android.content.pm.IPackageDeleteObserver;


final class SilenceInstallManager {<!-- -->
    private static final String TAG = "SilenceInstallManager";

    private static final int MSG_WHAT_INSTALL_FINISH_SUCCESS = 0;
    private static final int MSG_WHAT_INSTALL_FINISH_FAIL = 1;
    private static final int MSG_WHAT_UNINSTALL_COMPLETE = 2;

    private Context mContext;

    @SuppressLint("NewApi")
    private ArrayMap<Integer, InstallAppInfo> InstallAppInfoMap = new ArrayMap<>();


    private static volatile SilenceInstallManager INSTANCE;

    private SilenceInstallManager(Context context) {<!-- -->
        mContext = context;
    }

    public static SilenceInstallManager getInstance(Context context) {<!-- -->
        if (null == INSTANCE) {<!-- -->
            synchronized (SilenceInstallManager.class) {<!-- -->
                if (null == INSTANCE) {<!-- -->
                    INSTANCE = new SilenceInstallManager(context.getApplicationContext());
                }
            }
        }
        return INSTANCE;
    }


    @SuppressLint("NewApi")
    private PackageInstaller.SessionCallback mSessionCallback = new PackageInstaller.SessionCallback() {<!-- -->
        @Override
        public void onCreated(int sessionId) {<!-- -->
            Log.d(TAG, "onCreated---->" + sessionId);
        }

        @Override
        public void onBadgingChanged(int sessionId) {<!-- -->
// Log.w(TAG, "SilenceInstallReceiver onBadgingChanged---->" + sessionId);
        }

        @Override
        public void onActiveChanged(int sessionId, boolean active) {<!-- -->
// Log.w(TAG, "SilenceInstallReceiver onActiveChanged---->" + sessionId + " active--->" + active);
        }

        @Override
        public void onProgressChanged(int sessionId, float progress) {<!-- -->
// Log.w(TAG, "SilenceInstallReceiver onProgressChanged---->" + sessionId + " progress--->" + progress);
        }

        @Override
        public void onFinished(int sessionId, boolean success) {<!-- -->
            Log.d(TAG, "onFinished---->" + sessionId + " success--->" + success);
            Message msg = Message.obtain();
            msg.what = MSG_WHAT_INSTALL_FINISH_SUCCESS;
            msg.arg1 = sessionId;
            msg.obj = success;
            mHandler.sendMessage(msg);
        }
    };

    @SuppressLint("HandlerLeak")
    private Handler mHandler = new Handler() {<!-- -->
        @Override
        public void dispatchMessage(@NonNull Message msg) {<!-- -->
            mContext.getPackageManager().getPackageInstaller().unregisterSessionCallback(mSessionCallback);
            if (msg.what == MSG_WHAT_INSTALL_FINISH_SUCCESS) {<!-- -->
                boolean result = (boolean) msg.obj;
                int sessionId = msg.arg1;
                InstallAppInfo info = InstallAppInfoMap.remove(sessionId);
                if (result) {<!-- -->
                    Log.d(TAG, "install success");
                    if (null != info) {<!-- -->
                        if (info.isLaunch & amp; & amp; null != info.info & amp; & amp; null != info.info.packageName & amp; & amp; !"".equals(info.info.packageName )) {<!-- -->
                            launchApp(info.info.packageName);
                        }

                        File f = new File(info.filePath);
                        if (f.exists()) {<!-- -->
                            f.delete();
                        }
                    }
                } else {<!-- -->
                    Log.d(TAG, "install fail");
                }
            } else if (msg.what == MSG_WHAT_INSTALL_FINISH_FAIL) {<!-- -->
                int sessionId = msg.arg1;
                if (sessionId != -1) {<!-- -->
                    InstallAppInfoMap.remove(sessionId);
                }
                Log.d(TAG, "install fail");
            } else if (msg.what == MSG_WHAT_UNINSTALL_COMPLETE) {<!-- -->
                Log.d(TAG, "uninstall complete--->" + msg.arg1);
                if (msg.arg1 == PackageManager.DELETE_SUCCEEDED) {<!-- -->
                    Log.d(TAG, "delete succeeded");
                } else {<!-- -->
                    Log.d(TAG, "delete fail");
                }
            }
        }
    };

    public void silenceInstall(String appFilePath, boolean launch) {<!-- -->
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {<!-- -->
            mContext.getPackageManager().getPackageInstaller().registerSessionCallback(mSessionCallback);

            PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
                    PackageInstaller.SessionParams.MODE_FULL_INSTALL);
            params.setInstallAsInstantApp(false);
            params.setInstallReason(PackageManager.INSTALL_REASON_USER);

            File file = new File(appFilePath);
            if (!file.exists()) {<!-- -->
                sendFailMsg(-1);
                return;
            }

            try {<!-- -->
                PackageParser.PackageLite pkg = PackageParser.parsePackageLite(file, 0);
                params.setAppPackageName(pkg.packageName);
                params.setInstallLocation(pkg.installLocation);
                params.setSize(
                        PackageHelper.calculateInstalledSize(pkg, false, params.abiOverride));
            } catch (PackageParser.PackageParserException e) {<!-- -->
                Log.e(TAG, "Cannot parse package " + file + ". Assuming defaults.");
                Log.e(TAG,
                        "Cannot calculate installed size " + file + ". Try only apk size.");
                params.setSize(file.length());
            } catch (IOException e) {<!-- -->
                Log.e(TAG,
                        "Cannot calculate installed size " + file + ". Try only apk size.");
                params.setSize(file.length());
            }

            try {<!-- -->
                PackageInfo mPkgInfo = PackageUtil.getPackageInfo(mContext, file, PackageManager.GET_PERMISSIONS);
                int mSessionId = mContext.getPackageManager().getPackageInstaller().createSession(params);
                InstallAppInfo installAppInfo = new InstallAppInfo(mSessionId, appFilePath, mPkgInfo, launch);
                InstallAppInfoMap.put(mSessionId, installAppInfo);

                InstallingAsyncTask mInstallingTask = new InstallingAsyncTask(mContext, appFilePath, mSessionId);
                mInstallingTask.execute();
            } catch (IOException e) {<!-- -->
                e.printStackTrace();
                sendFailMsg(-1);
            }
        }
    }

    private void sendFailMsg(int sessionId) {<!-- -->
        Message msg = Message.obtain();
        msg.what = MSG_WHAT_INSTALL_FINISH_FAIL;
        msg.arg1 = sessionId;
        mHandler.sendMessage(msg);
    }


    @SuppressLint("NewApi")
    private final class InstallingAsyncTask extends AsyncTask<Void, Void,
            PackageInstaller.Session> {<!-- -->

        private Context mContext;
        private String mAppPath;
        private int mSessionId;

        public InstallingAsyncTask(Context context, String appPath, int sessionId) {<!-- -->
            mContext = context;
            mAppPath = appPath;
            mSessionId = sessionId;
        }

        @Override
        protected PackageInstaller.Session doInBackground(Void... params) {<!-- -->
            PackageInstaller.Session session;
            try {<!-- -->
                session = mContext.getPackageManager().getPackageInstaller().openSession(mSessionId);
            } catch (IOException e) {<!-- -->
                return null;
            }

            session.setStagingProgress(0);

            try {<!-- -->
                File file = new File(mAppPath);

                try (InputStream in = new FileInputStream(file)) {<!-- -->
                    long sizeBytes = file.length();
                    try (OutputStream out = session
                            .openWrite("PackageInstaller", 0, sizeBytes)) {<!-- -->
                        byte[] buffer = new byte[1024 * 1024];
                        while (true) {<!-- -->
                            int numRead = in.read(buffer);

                            if (numRead == -1) {<!-- -->
                                session.fsync(out);
                                break;
                            }

                            if (isCancelled()) {<!-- -->
                                session.close();
                                break;
                            }

                            out.write(buffer, 0, numRead);
                            if (sizeBytes > 0) {<!-- -->
                                float fraction = ((float) numRead / (float) sizeBytes);
                                session.addProgress(fraction);
                            }
                        }
                    }
                }

                return session;
            } catch (IOException | SecurityException e) {
                Log.e(TAG, "Could not write package", e);
                session.close();
                return null;
            }
        }

        @Override
        protected void onPostExecute(PackageInstaller.Session session) {
            if (session != null) {
                Intent broadcastIntent = new Intent();
                PendingIntent pendingIntent = PendingIntent.getBroadcast(
                        mContext,
                        1,
                        broadcastIntent,
                        PendingIntent.FLAG_UPDATE_CURRENT);

                session.commit(pendingIntent.getIntentSender());

                session.close();

                Log.d(TAG, "send install PendingIntent----->");
            } else {<!-- -->
                mContext.getPackageManager().getPackageInstaller().abandonSession(mSessionId);
                sendFailMsg(mSessionId);

                File f = new File(mAppPath);
                if (f.exists()) {<!-- -->
                    f.delete();
                }

                Log.e(TAG, "copy fail delete file----->");
            }

            mContext = null;
            mAppPath = "";
            mSessionId = -1;
        }
    }


    private class InstallAppInfo {<!-- -->
        private int sessionId;
        private String filePath;
        privatePackageInfo info;
        private boolean isLaunch;

        public InstallAppInfo(int sessionId, String filePath, PackageInfo info, boolean isLaunch) {<!-- -->
            this.sessionId = sessionId;
            this.filePath = filePath;
            this.info = info;
            this.isLaunch = isLaunch;
        }

        public int getSessionId() {<!-- -->
            return sessionId;
        }

        public String getFilePath() {<!-- -->
            return filePath;
        }

        public PackageInfo getInfo() {<!-- -->
            return info;
        }

        public boolean isLaunch() {<!-- -->
            return isLaunch;
        }
    }


    private void launchApp(String appPackageName) {<!-- -->
        Intent mLaunchIntent = mContext.getPackageManager().getLaunchIntentForPackage(appPackageName);
        if (mLaunchIntent != null) {<!-- -->
            List<ResolveInfo> list = mContext.getPackageManager().queryIntentActivities(mLaunchIntent, 0);
            if (list != null & amp; & amp; list.size() > 0) {<!-- -->
                Log.d(TAG, "launch app--->");
                mContext.startActivity(mLaunchIntent);
            }
        }
    }


    public void silenceUninstall(String packageName) {<!-- -->
        Log.i(TAG, "silenceUninstall--->" + packageName);
        PackageDeleteObserver observer = new PackageDeleteObserver();
        mContext.getPackageManager().deletePackage(packageName, observer, PackageManager.DELETE_ALL_USERS);
    }

    private class PackageDeleteObserver extends IPackageDeleteObserver.Stub {<!-- -->
        public void packageDeleted(String packageName, int returnCode) {<!-- -->
            Message msg = mHandler.obtainMessage(MSG_WHAT_UNINSTALL_COMPLETE);
            msg.arg1 = returnCode;
            msg.obj = packageName;
            mHandler.sendMessage(msg);
        }
    }
}

  1. Add SilenceInstallReceiver, the path is /frameworks/base/packages/PackageInstaller/src/com/android/packageinstaller/SilenceInstallReceiver.java;
package com.android.packageinstaller;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;


public class SilenceInstallReceiver extends BroadcastReceiver {<!-- -->
    public static final String SILENCE_INSTALL_APP = "com.android.packageinstaller.ACTION_SILENCE_INSTALL";
    public static final String SILENCE_INSTALL_KEY = "silence_install";
    public static final String IS_LAUNCH_KEY = "is_launch";
    public static final String APP_URI_KEY = "app_uri";


    @Override
    public void onReceive(Context context, Intent intent) {<!-- -->
        if (SILENCE_INSTALL_APP.equals(intent.getAction())) {<!-- -->
            Uri uri = intent.getParcelableExtra(APP_URI_KEY);
            boolean isLaunch = intent.getBooleanExtra(IS_LAUNCH_KEY, false);
            SilenceInstallManager.getInstance(context).silenceInstall(uri.getPath(), isLaunch);
        }

    }

}

  1. InstallStart is the entry activity for the PackageInstaller program to install apps. Modify InstallStart and add a silent installation logical branch. The path is /frameworks/base/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java;
//********Omit code******

  protected void onCreate(@Nullable Bundle savedInstanceState) {<!-- -->

        //********Omit code******
        if (isSessionInstall) {<!-- -->
            nextActivity.setClass(this, PackageInstallerActivity.class);
        } else {<!-- -->
            Uri packageUri = intent.getData();

            if (packageUri != null & amp; & amp; 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 & amp; & amp; 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;
            }
        }

       //If the installation request contains the silent installation flag, perform the silent installation operation.
        if (intent.getBooleanExtra(SilenceInstallReceiver.SILENCE_INSTALL_KEY, false)) {<!-- -->
            StagingAsyncAppTask mStagingTask = new StagingAsyncAppTask(intent.getBooleanExtra(SilenceInstallReceiver.IS_LAUNCH_KEY, false));
            mStagingTask.execute(getIntent().getData());
            return;
        }

        if (nextActivity != null) {<!-- -->
            startActivity(nextActivity);
        }
        finish();
}


    @SuppressLint("NewApi")
    private final class StagingAsyncAppTask extends AsyncTask<Uri, Void, File> {<!-- -->

        private boolean mIsLaunch;

        public StagingAsyncAppTask(boolean isLaunch){<!-- -->
            mIsLaunch = isLaunch;
        }


        @Override
        protected File doInBackground(Uri... params) {<!-- -->
            Log.d(LOG_TAG, "copy file from user app start");
            if (params == null || params.length <= 0) {<!-- -->
                return null;
            }
            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 null;
                }

                File mStagedFile = TemporaryFileManager.getStagedFile(InstallStart.this);

                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
                        out.write(buffer, 0, bytesRead);
                    }
                }
                return mStagedFile;
            } catch (IOException | SecurityException | IllegalStateException e) {<!-- -->
                Log.w(LOG_TAG, "Error staging apk from content URI", e);
            }
            return null;
        }

        @Override
        protected void onPostExecute(File installFile) {<!-- -->
            if (null != installFile) {<!-- -->
                // Now start the installation again from a file
                Log.d(LOG_TAG, "copy file from user app finish");

                Intent installIntent = new Intent(SilenceInstallReceiver.SILENCE_INSTALL_APP);
                installIntent.putExtra(SilenceInstallReceiver.APP_URI_KEY, Uri.fromFile(installFile));
                installIntent.putExtra(SilenceInstallReceiver.IS_LAUNCH_KEY, mIsLaunch);
                installIntent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
                installIntent.setPackage("com.android.packageinstaller");
                sendBroadcast(installIntent);

                Log.d(LOG_TAG, "send to install");
            } else {<!-- -->
                Log.d(LOG_TAG, "copy file from user app fail");
            }

            finish();
        }
    }

//********Omit code******
  1. UninstallerActivity is the entry activity for the PackageInstaller program to uninstall apps. Modify UninstallerActivity and add a silent uninstallation logical branch. The path is /frameworks/base/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java;
//********Omit code******
public void onCreate(Bundle icicle){<!-- -->
         //********Omit code******
        
        //Add silent uninstall logic
        if (intent.getBooleanExtra(SilenceInstallReceiver.SILENCE_INSTALL_KEY, false)) {<!-- -->
            SilenceInstallManager.getInstance(this).silenceUninstall(mPackageName);
            return;
        }

        showConfirmationDialog();
}

//********Omit code******
  1. Modify PackageInstallerApplication, the path is /frameworks/base/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerApplication.java;
//********Omit code******

public class PackageInstallerApplication extends Application {<!-- -->
    @Override
    public void onCreate() {<!-- -->
        super.onCreate();
        PackageItemInfo.forceSafeLabels();

//Add management class initialization
        SilenceInstallManager.getInstance(this);
    }
}

//********Omit code******
  1. Modify AndroidManifest.xml, the path is /frameworks/base/packages/PackageInstaller/AndroidManifest.xml;
//********Omit code******

//Add background startup activity permissions
<uses-permission android:name="android.permission.START_ACTIVITIES_FROM_BACKGROUND" />

//********Omit code******

<receiver android:name=".SilenceInstallReceiver"
            android:exported="false">
            <intent-filter android:priority="1">
                <action android:name="com.android.packageinstaller.ACTION_SILENCE_INSTALL" />
            </intent-filter>
        </receiver>

//********Omit code******
  1. Starting from Android 10, the system restricts starting Activity in the background without user interaction, and the following information will be output.
Background activity start [callingPackage: com.android.packageinstaller; callingUid: 10069; isCallingUidForeground: false; callingUidHasAnyVisibleWindow: false; callingUidProcState: CACHED_EMPTY; isCallingUidPersistentSystemProcess: false; realCallingUid: 10069; isRealCallingUidForeground: false; realCallingUidHasAnyVisible Window: false; realCallingUidProcState: CACHED_EMPTY; isRealCallingUidPersistentSystemProcess: false; originatingPendingIntent: null; isBgStartWhitelisted: false; intent: Intent {<!-- --> act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 pkg=com.qiyi. video cmp=com.qiyi.video/.WelcomeActivity }; callerApp: ProcessRecord{<!-- -->ba2b2ca 2471:com.android.packageinstaller/u0a69}]

Abort background activity starts from 10069

When the developed App is switched to the background, application installation requests cannot be made. The shouldAbortBackgroundActivityStart() method in ActivityStarter.java determines whether the activity can be started. Here, add modification logic processing. The path is /frameworks/base/services/core/ java/com/android/server/wm/ActivityStarter.java;

//********Omit code******

boolean shouldAbortBackgroundActivityStart(int callingUid, int callingPid,
            final String callingPackage, int realCallingUid, int realCallingPid,
            WindowProcessController callerApp, PendingIntentRecord originatingPendingIntent,
            boolean allowBackgroundActivityStart, Intent intent) {<!-- -->

//********Omit code******

        // don't abort if the callingUid has SYSTEM_ALERT_WINDOW permission
        if (mService.hasSystemAlertWindowPermission(callingUid, callingPid, callingPackage)) {<!-- -->
            Slog.w(TAG, "Background activity start for " + callingPackage
                     + " allowed because SYSTEM_ALERT_WINDOW permission is granted.");
            return false;
        }
        
        //Add judgment logic to determine whether it can be started through the allowed_Background logo
        if (intent.getBooleanExtra("allowed_Background", false)) {<!-- -->
            Slog.w(TAG, "allowed_Background.");
            return false;
        }
    
       //********Omit code******
}

//********Omit code******

Third-party App requests silent installation

  1. Use the following method in the developed app to request installation
    private void installTest() {<!-- -->
        String appPath = getExternalFilesDir(null).getAbsolutePath() + File.separator + "aiqiyi.apk";

        File appFile = new File(appPath);
        if (!appFile.exists()) {<!-- -->
            showToast("Please place the upgrade file in" + getExternalFilesDir(null).getAbsolutePath() + File.separator + "directory");
            return;
        }

        Uri uri = FileProvider.getUriForFile(getApplicationContext(), getPackageName() + ".file_provider", appFile);
        CTKManager.getInstance(InstallActivity.this).getApi().silenceInstall(InstallActivity.this, uri, true);


        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {<!-- -->
            Intent installApkIntent = new Intent();
            installApkIntent.setAction(Intent.ACTION_VIEW);
            installApkIntent.addCategory(Intent.CATEGORY_DEFAULT);
            installApkIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            installApkIntent.setDataAndType(uri, "application/vnd.android.package-archive");
            installApkIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            //Set the silent installation flag
            installApkIntent.putExtra("silence_install", true);
            //Set whether to start the logo after the installation is completed
            installApkIntent.putExtra("is_launch", true);
            //Set the activity logo to start in the background
            installApkIntent.putExtra("allowed_Background", true);
            if (getPackageManager().queryIntentActivities(installApkIntent, 0).size() > 0) {<!-- -->
                startActivity(installApkIntent);
            }
        }
    }