Is it possible to merge/install split APK files (AKA “app bundle”), on Android device itself, without root?

Please check this.
when we send

adb install-multiple apk1 apk2 ...

it calls this code
install-multiple

 std::string install_cmd;
    if (_use_legacy_install()) {
        install_cmd = "exec:pm";
    } else {
        install_cmd = "exec:cmd package";
    }

    std::string cmd = android::base::StringPrintf("%s install-create -S %" PRIu64, install_cmd.c_str(), total_size);
    for (i = 1; i < first_apk; i++) {
        cmd += " " + escape_arg(argv[i]);
    }

which in turn calls Pm.java or a new way of executing PackageManagerService code, both are similar

I tried to integrate that code in my app, The problem which I faced, apk installation was not able to complete, it is due to the reason that the app needs.

<uses-permission android:name="android.permission.INSTALL_PACKAGES"/>

But it is only given to system-priv apps. When I executed these steps from adb shell apk installation was successful and when I created my app a system priv-app apk install was successfull.

code to call new apis of PackageManager, mostly copied from Pm.java
Steps in installing split apks

  1. Create a session with argument -S , return session id.

    (install-create, -S, 52488426)
    52488426 — total size of apks.

  2. Write split apks in that session with size , name and path

    (install-write, -S, 44334187, 824704264, 1_base.apk, -)

    (install-write, -S, 1262034, 824704264, 2_split_config.en.apk, -)

    (install-write, -S, 266117, 824704264, 3_split_config.hdpi.apk, -)

    (install-write, -S, 6626088, 824704264, 4_split_config.x86.apk, -)

  3. commit the session with session id

    (install-commit, 824704264)

I have placed airbnb apk in my sdcard.

OnePlus5:/sdcard/com.airbnb.android-1 $ ll
total 51264
-rw-rw---- 1 root sdcard_rw 44334187 2019-04-01 14:20 base.apk
-rw-rw---- 1 root sdcard_rw  1262034 2019-04-01 14:20 split_config.en.apk
-rw-rw---- 1 root sdcard_rw   266117 2019-04-01 14:20 split_config.hdpi.apk
-rw-rw---- 1 root sdcard_rw  6626088 2019-04-01 14:20 split_config.x86.apk

and calling functions to install apk.

final InstallParams installParams = makeInstallParams(52488426l);

            try {
                int sessionId = runInstallCreate(installParams);

                runInstallWrite(44334187,sessionId, "1_base.apk", "/sdcard/com.airbnb.android-1/base.apk");

                runInstallWrite(1262034,sessionId, "2_split_config.en.apk", "/sdcard/com.airbnb.android-1/split_config.en.apk");

                runInstallWrite(266117,sessionId, "3_split_config.hdpi.apk", "/sdcard/com.airbnb.android-1/split_config.hdpi.apk");

                runInstallWrite(6626088,sessionId, "4_split_config.x86.apk", "/sdcard/com.airbnb.android-1/split_config.x86.apk");


                if (doCommitSession(sessionId, false )
                        != PackageInstaller.STATUS_SUCCESS) {
                }
                System.out.println("Success");

            } catch (RemoteException e) {
                e.printStackTrace();
            }

private int runInstallCreate(InstallParams installParams) throws RemoteException {
    final int sessionId = doCreateSession(installParams.sessionParams);
    System.out.println("Success: created install session [" + sessionId + "]");
    return sessionId;
}

private int doCreateSession(PackageInstaller.SessionParams params)
        throws RemoteException {

    int sessionId = 0 ;
    try {
        sessionId = packageInstaller.createSession(params);
    } catch (IOException e) {
        e.printStackTrace();
    }
    return sessionId;
}

private int runInstallWrite(long size, int sessionId , String splitName ,String path ) throws RemoteException {
    long sizeBytes = -1;

    String opt;
    sizeBytes = size;
    return doWriteSession(sessionId, path, sizeBytes, splitName, true /*logSuccess*/);
}


private int doWriteSession(int sessionId, String inPath, long sizeBytes, String splitName,
                           boolean logSuccess) throws RemoteException {
    if ("-".equals(inPath)) {
        inPath = null;
    } else if (inPath != null) {
        final File file = new File(inPath);
        if (file.isFile()) {
            sizeBytes = file.length();
        }
    }

    final PackageInstaller.SessionInfo info = packageInstaller.getSessionInfo(sessionId);

    PackageInstaller.Session session = null;
    InputStream in = null;
    OutputStream out = null;
    try {
        session = packageInstaller.openSession(sessionId);

        if (inPath != null) {
            in = new FileInputStream(inPath);
        }

        out = session.openWrite(splitName, 0, sizeBytes);

        int total = 0;
        byte[] buffer = new byte[65536];
        int c;
        while ((c = in.read(buffer)) != -1) {
            total += c;
            out.write(buffer, 0, c);
        }
        session.fsync(out);

        if (logSuccess) {
            System.out.println("Success: streamed " + total + " bytes");
        }
        return PackageInstaller.STATUS_SUCCESS;
    } catch (IOException e) {
        System.err.println("Error: failed to write; " + e.getMessage());
        return PackageInstaller.STATUS_FAILURE;
    } finally {
        try {
            out.close();
            in.close();
            session.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}


private int doCommitSession(int sessionId, boolean logSuccess) throws RemoteException {
    PackageInstaller.Session session = null;
    try {
        try {
            session = packageInstaller.openSession(sessionId);
        } catch (IOException e) {
            e.printStackTrace();
        }
        session.commit(PendingIntent.getBroadcast(getApplicationContext(), sessionId,
                new Intent("android.intent.action.MAIN"), 0).getIntentSender());
        System.out.println("install request sent");

        Log.d(TAG, "doCommitSession: " + packageInstaller.getMySessions());

        Log.d(TAG, "doCommitSession: after session commit ");

        return 1;
    } finally {
        session.close();
    }
}



private static class InstallParams {
    PackageInstaller.SessionParams sessionParams;
}

private InstallParams makeInstallParams(long totalSize ) {
    final PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);
    final InstallParams params = new InstallParams();
    params.sessionParams = sessionParams;
    String opt;
    sessionParams.setSize(totalSize);
    return params;
}

This is the list of commands that are actually received in Pm.java when we do adb install-multiple

04-01 16:04:40.626  4886  4886 D Pm      : run() called with: args = [[install-create, -S, 52488426]]
04-01 16:04:41.862  4897  4897 D Pm      : run() called with: args = [[install-write, -S, 44334187, 824704264, 1_base.apk, -]]
04-01 16:04:56.036  4912  4912 D Pm      : run() called with: args = [[install-write, -S, 1262034, 824704264, 2_split_config.en.apk, -]]
04-01 16:04:57.584  4924  4924 D Pm      : run() called with: args = [[install-write, -S, 266117, 824704264, 3_split_config.hdpi.apk, -]]
04-01 16:04:58.842  4936  4936 D Pm      : run() called with: args = [[install-write, -S, 6626088, 824704264, 4_split_config.x86.apk, -]]
04-01 16:05:01.304  4948  4948 D Pm      : run() called with: args = [[install-commit, 824704264]]

So for apps which are not system priv-app, I don’t know how can they can install split apks.
Play store being a system priv-app can use these apis and install split apks without any issues.

Leave a Comment

Hata!: SQLSTATE[HY000] [1045] Access denied for user 'divattrend_liink'@'localhost' (using password: YES)