42

Background

In the past, I've asked about sharing or backup of app-bundle / split apk files, here .

This seems like an almost impossible task, which I could only figure out how to install the split APK files, and even then it's only via adb:

adb install-multiple apk1 apk2 ...

The problem

I was told that it should be actually possible to merge multiple split APK files into one that I could install (here), but wasn't given of how to do it.

This could be useful for saving it for later (backup), and because currently there is no way to install split-apk files within the device.

In fact, this is such a major issue, that I don't know of any backup app that can handle split APK files (app bundle), and this include Titanium app.

What I've found

I took a sample app that uses app-bundles, called "AirBnb".

Looking at the files it has, those are what the Play Store decided to download:

enter image description here

So I tried to enter each. The "base" is the main one, so I skipped it to look at the others. To me it seems that all have these files within:

  • "META-INF"
  • "resources.arsc"
  • "AndroidManifest.xml"
  • in the case of the one with the "xxxhdpi", I also get "res" folder.

Thing is, since those all exist in multiple places, I don't get how could I merge them.

The questions

  1. What is the way to merge those all into one APK file?

  2. Is it possible to install split APK files without root and without PC ? This was possible in the past on backup apps such as Titanium, but only on normal APK files, and not app bundle (split apk).


EDIT: I've set a bounty. Please, if you know of a solution, show it. Show something that you've tested to work. Either of merging split APK files, or installing them , all without root and right on the device.


EDIT: Sadly all solutions here didn't work, with or without root, and that's even though I've found an app that succeeded doing it (with and without root), called "SAI (Split APKs Installer)" (I think its repository is here, found after I've put a bounty).

I'm putting a new bounty. Please, whoever publishes a new answer, show that it works with and without root. Show on Github if needed (and here just the important stuff). I know this app is open sourced anyway, but it's important for me how to do it here, and share with others, as currently what's shown here isn't working, and requires root, even though it's not really needed.

This time I won't grant the bounty till I see something that indeed works (previously I was short on time and granted it to the answer I thought should work).

android developer
  • 114,585
  • 152
  • 739
  • 1,270
  • "Sharing" play store applications doesn't really sound like something you are *authorized* to do, and there are platform level solutions for backup. – Chris Stratton Mar 17 '19 at 23:24
  • What do you mean installing APKs from within the device? The PackageManager surely allows this. – Pierre Mar 18 '19 at 00:35
  • 1
    @Pierre It doesn't. I've already tried to install each of those APK files. Installing the base one works, but the rest won't be allowed. Also, there is a very bug chance that the installed app won't work correctly at all (because of missing resources) – android developer Mar 18 '19 at 12:52
  • @ChrisStratton Why would it be a problem for me to share my backup of the app to Google Drive? What you wrote doesn't make sense. I've bought/downloaded the app, so I should be able to do it again in the future, with or without Internet connection. Plus, this is a technical question. Many backup apps now fail to backup app-bundles because of this issue. – android developer Mar 18 '19 at 12:56
  • The APKs should not be installed through multiple install calls. They have to be installed in a single session. Look at the [PackageInstaller API](https://android.googlesource.com/platform/frameworks/base/+/20e0c50/core/java/android/content/pm/PackageInstaller.java), there's even a section in the javadoc about split APKs. – Pierre Mar 18 '19 at 18:41
  • @Pierre So it's possible to install them all using official API, without root and without a PC ? If so, please write an answer of how to do it (working code please), and I will accept it. – android developer Mar 19 '19 at 08:04
  • As much i know bro, i have used an app named Anti Split which converts Split into APK to be installed easily. Yeah it actually merges them into 1 single APK. Thats impressive – DiLDoST Nov 25 '21 at 16:23
  • @DiLDoSTWahag Will it keep the same signature of the original? – android developer Nov 27 '21 at 07:48
  • @android-developer am not sure, i guess no because it modifies the base app and adds the split files or merge them together. And as because of changes and replacements the app gets resigned. Btw i saw an option about not removing the sign on the settings of the app. Maybe thats used for keeping the original signature (not sure). – DiLDoST Nov 28 '21 at 18:46

8 Answers8

14

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.

nkalra0123
  • 2,281
  • 16
  • 17
  • So there is no official way to install split apks without being a system app, but it's ok to install a normal one? It actually worked for you only when being a system app? How did you convert the app to be a system app, and back to be a user app? What if I have root ? Would that help to install the split-apks? – android developer Apr 01 '19 at 13:54
  • 1
    no official way to install split apks without being a system app --- Not sure, will update if I find a way. It actually worked for you only when being a system app? --- Yes, after installing as /system/priv-app How did you convert the app to be a system app, --- Place the app in /system/priv-app and install from this location What if I have root ? Would that help to install the split-apks? -- Yes, basically the app needs android.permission.INSTALL_PACKAGES, if you are root you can grant this permission, Or you can execute pm commands from Runtime.exec with root user – nkalra0123 Apr 02 '19 at 06:50
  • Can you show how to convert an app (including currently installed app) to be system app, and back to user app (using root) ? If I have root, I don't need to have the app to be a system app? Can you show what to run in case I have root? For now, because you're showing effort, I mark this as the correct answer. Will grant the bounty when all of these solutions are clear and understandable. – android developer Apr 02 '19 at 12:14
8

No root required implementation Check this git hub link: https://github.com/nkalra0123/splitapkinstall

We have to create a service and pass that handle in session.commit()

 Intent callbackIntent = new Intent(getApplicationContext(), APKInstallService.class);
 PendingIntent pendingIntent = PendingIntent.getService(getApplicationContext(), 0, callbackIntent, 0);
 session.commit(pendingIntent.getIntentSender());

EDIT: Since the solution works, but not really published here, I've decided to write it before marking it as correct solution. Here's the code:

manifest

<manifest package="com.nitin.apkinstaller" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
  <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
  <application
    android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true"
    android:theme="@style/AppTheme" tools:ignore="AllowBackup,GoogleAppIndexingWarning">
    <activity
      android:name=".MainActivity" android:label="@string/app_name" android:theme="@style/AppTheme.NoActionBar">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>

    <service android:name=".APKInstallService"/>
  </application>
</manifest>

APKInstallService

class APKInstallService : Service() {
    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
        when (if (intent.hasExtra(PackageInstaller.EXTRA_STATUS)) null else intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0)) {
            PackageInstaller.STATUS_PENDING_USER_ACTION -> {
                Log.d("AppLog", "Requesting user confirmation for installation")
                val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
                confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                try {
                    startActivity(confirmationIntent)
                } catch (e: Exception) {
                }
            }
            PackageInstaller.STATUS_SUCCESS -> Log.d("AppLog", "Installation succeed")
            else -> Log.d("AppLog", "Installation failed")
        }
        stopSelf()
        return START_NOT_STICKY
    }

    override fun onBind(intent: Intent): IBinder? {
        return null
    }
}

MainActivity

class MainActivity : AppCompatActivity() {
    private lateinit var packageInstaller: PackageInstaller

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val toolbar = findViewById<Toolbar>(R.id.toolbar)
        setSupportActionBar(toolbar)
        val fab = findViewById<FloatingActionButton>(R.id.fab)
        fab.setOnClickListener {
            packageInstaller = packageManager.packageInstaller
            val ret = installApk("/storage/emulated/0/Download/split/")
            Log.d("AppLog", "onClick: return value is $ret")
        }

    }

    private fun installApk(apkFolderPath: String): Int {
        val nameSizeMap = HashMap<String, Long>()
        var totalSize: Long = 0
        var sessionId = 0
        val folder = File(apkFolderPath)
        val listOfFiles = folder.listFiles()
        try {
            for (listOfFile in listOfFiles) {
                if (listOfFile.isFile) {
                    Log.d("AppLog", "installApk: " + listOfFile.name)
                    nameSizeMap[listOfFile.name] = listOfFile.length()
                    totalSize += listOfFile.length()
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
            return -1
        }
        val installParams = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
        installParams.setSize(totalSize)
        try {
            sessionId = packageInstaller.createSession(installParams)
            Log.d("AppLog","Success: created install session [$sessionId]")
            for ((key, value) in nameSizeMap) {
                doWriteSession(sessionId, apkFolderPath + key, value, key)
            }
            doCommitSession(sessionId)
            Log.d("AppLog","Success")
        } catch (e: IOException) {
            e.printStackTrace()
        }
        return sessionId
    }

    private fun doWriteSession(sessionId: Int, inPath: String?, sizeBytes: Long, splitName: String): Int {
        var inPathToUse = inPath
        var sizeBytesToUse = sizeBytes
        if ("-" == inPathToUse) {
            inPathToUse = null
        } else if (inPathToUse != null) {
            val file = File(inPathToUse)
            if (file.isFile)
                sizeBytesToUse = file.length()
        }
        var session: PackageInstaller.Session? = null
        var inputStream: InputStream? = null
        var out: OutputStream? = null
        try {
            session = packageInstaller.openSession(sessionId)
            if (inPathToUse != null) {
                inputStream = FileInputStream(inPathToUse)
            }
            out = session!!.openWrite(splitName, 0, sizeBytesToUse)
            var total = 0
            val buffer = ByteArray(65536)
            var c: Int
            while (true) {
                c = inputStream!!.read(buffer)
                if (c == -1)
                    break
                total += c
                out!!.write(buffer, 0, c)
            }
            session.fsync(out!!)
            Log.d("AppLog", "Success: streamed $total bytes")
            return PackageInstaller.STATUS_SUCCESS
        } catch (e: IOException) {
            Log.e("AppLog", "Error: failed to write; " + e.message)
            return PackageInstaller.STATUS_FAILURE
        } finally {
            try {
                out?.close()
                inputStream?.close()
                session?.close()
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
    }

    private fun doCommitSession(sessionId: Int) {
        var session: PackageInstaller.Session? = null
        try {
            try {
                session = packageInstaller.openSession(sessionId)
                val callbackIntent = Intent(applicationContext, APKInstallService::class.java)
                val pendingIntent = PendingIntent.getService(applicationContext, 0, callbackIntent, 0)
                session!!.commit(pendingIntent.intentSender)
                session.close()
                Log.d("AppLog", "install request sent")
                Log.d("AppLog", "doCommitSession: " + packageInstaller.mySessions)
                Log.d("AppLog", "doCommitSession: after session commit ")
            } catch (e: IOException) {
                e.printStackTrace()
            }

        } finally {
            session!!.close()
        }
    }
}
android developer
  • 114,585
  • 152
  • 739
  • 1,270
nkalra0123
  • 2,281
  • 16
  • 17
  • Where is the relevant code here? All I see is an Intent... Where are the files being handled? And how can it be done using root (the question was with and without root) ? – android developer Apr 14 '19 at 06:46
  • Open the github link it has all the relevant code for rootless approach. I have already given you the code for root approach. It worked for me. – nkalra0123 Apr 14 '19 at 11:10
  • Why did you post as a new answer? Anyway, please publish the one with root. – android developer Apr 14 '19 at 14:19
  • OK I've updated your answer with the code, as it works fine, but the rooted method still doesn't work for me. Can you please publish it on Github too? I've granted you the bounty for both solutions, but the rooted solutions actually doesn't work... – android developer Apr 20 '19 at 09:50
  • Thanks, Rooted solution worked for me, I tried on a rooted emulator, I will try on a rooted phone. Will update you, if i find something, Have you tried to debug, at which stage it is failing, There are three stages first is creating a session "install-create" then second is wrting split apks to session with "install-write" and then committing the session with "install-commit" . To check first step, check if you get valid session id or by executing "adb shell dumpsys package" and search for "Historical install sessions:" or "Active install sessions:" – nkalra0123 Apr 20 '19 at 15:42
  • To debug 2nd stage, enter rooted shell and go to "/data/app" and check this temporary directory "vmdl1984641550.tmp" name might differ on your device, other directories are package name of other apks. If you see all the splits copied here, this means 2nd step is correct. To debug 3rd step, check logcat output, you might see something there, logs from package manager, like may be apk is corrupt or something else. – nkalra0123 Apr 20 '19 at 15:49
  • The sessionId is some random number, so it's valid, no? About "install-write" I don't see it reaching the exception. All seem fine, as I wrote before... I don't see any tmp file on "/data/app" path though. Not in the end, and not during any step. – android developer Apr 21 '19 at 10:18
  • It really saved my day... Thanks. – chain Jul 15 '19 at 12:01
4

It is possible to merge split APKs into a single APK, both manually and automatically, but forced to use a made up signing key means the APK can’t be installed as an update to the genuine app and in case the app may checks itself for tempering

A detailed guide how to merge split APKs manually: https://platinmods.com/threads/how-to-turn-a-split-apk-into-a-normal-non-split-apk.76683/

A PC software to merge split APKs automatically: https://www.andnixsh.com/2020/06/sap-split-apks-packer-by-kirlif-windows.html

  • 1
    Why does it need to be re-signed? The APKs are already signed. Is it because you need to extract the content? – android developer Jul 03 '20 at 23:40
  • 2
    Yes, modifying an APK would need to be re-signed due to a security feature on the system –  Jul 04 '20 at 09:41
  • 1
    I see. OK thank you. So you say there is no way to really merge the files while also keeping it working exactly the same, right? – android developer Jul 04 '20 at 14:44
  • 2
    Yep, no way to keep it working excatly the same. Only the app developer who have the right signature can do it. Most of the cases, the app is working fine even the signature is different. –  Jul 04 '20 at 14:57
  • 2
    Too bad. Was hoping for an easy way for this. Thank you. You get +1 for the effort. The other answer wasn't quite about this, but answered the other question I had, of installing the split APK files. – android developer Jul 05 '20 at 07:15
2
  1. Run bundletool with --mode=universal
  2. This will generate an APKS file, rename extension to zip
  3. Unzip
  4. You will find a fat universal.apk file which can be installed as in the old days.
Tekmology
  • 41
  • 5
  • This looks promising. On what exactly to run on step 1? Meaning what is the input? On the entire list of APK files? How? Can you please show the full command? Does it work on Android, or only for PC ? Can you please find how to do it within Android? – android developer Apr 02 '20 at 08:39
  • 1
    For this to work, the input has to aab file not apks(which is what op is asking) – Cerlin May 27 '20 at 11:18
  • Thanks so much. This should be the correct answer but you should provide more detail – RoShan Shan Jun 06 '22 at 15:50
1

From an Android App Bundle, you can generate a "universal APK" using bundletool build-apks command with the --mode=universal flag. This will generate a single "fat" APK that is compatible with all devices (that your app supports).

I know this isn't strictly answering your question, but trying to merge the APKs is not only a complex task, but will result in a lot of cases in something incorrect.

Pierre
  • 15,865
  • 4
  • 36
  • 50
  • Can you please show how to do it within the device itself (meaning without PC) ? What's the code I should call ? Also, do you have any other possible solution for the backup of the split APKs that will allow to also restore them, without root and without a PC ? It's just that it was possible with a single APK in the past, but now it's not, and many (if not all) backup apps now fail to backup such apps because of this (including Titanium Backup app). – android developer Mar 18 '19 at 12:54
  • Already possible. See my other comment about the PackageInstaller API. – Pierre Mar 18 '19 at 18:41
  • If it's possible, please show how to do it, with a working example, and I will accept it. If merging isn't possible, this is better than nothing. – android developer Mar 19 '19 at 08:05
  • is there a way to merge to single apk without bundle file? –  Aug 16 '19 at 15:46
1

If you have root, you can use this code.

Please get the read/write sdcard permission.(via runtime permissions or permission granted from settings app) before executing this code. airbnb apk was successfully installed after running this code.

Calling this function with args "/split-apks/" , I have placed the airbnb split apks in a directory in /sdcard/split-apks/.

installApk("/split-apks/");


 public void installApk(String apkFolderPath)
{
    PackageInstaller packageInstaller =  getPackageManager().getPackageInstaller();
    HashMap<String, Long> nameSizeMap = new HashMap<>();
    long totalSize = 0;

    File folder = new File(Environment.getExternalStorageDirectory().getPath()+ apkFolderPath);
    File[] listOfFiles = folder.listFiles();
    for (int i = 0; i < listOfFiles.length; i++) {
        if (listOfFiles[i].isFile()) {
            System.out.println("File " + listOfFiles[i].getName());
            nameSizeMap.put(listOfFiles[i].getName(),listOfFiles[i].length());
            totalSize += listOfFiles[i].length();
        }
    }

    String su = "/system/xbin/su";


    final String[] pm_install_create = new String[]{su, "-c", "pm" ,"install-create", "-S", Long.toString(totalSize) };
    execute(null, pm_install_create);

    List<PackageInstaller.SessionInfo> sessions = packageInstaller.getAllSessions();

    int sessId = sessions.get(0).getSessionId();

    String sessionId = Integer.toString(sessId);


    for(Map.Entry<String,Long> entry : nameSizeMap.entrySet())
    {
        String[] pm_install_write = new String[]{su, "-c", "pm" ,"install-write", "-S", Long.toString(entry.getValue()),sessionId, entry.getKey(), Environment.getExternalStorageDirectory().getPath()+apkFolderPath+ entry.getKey()};

        execute(null,pm_install_write);

    }

    String[] pm_install_commit  = new String[]{su, "-c", "pm" ,"install-commit", sessionId};


    execute(null, pm_install_commit);

}
public String execute(Map<String, String> environvenmentVars, String[] cmd) {

    boolean DEBUG = true;
    if (DEBUG)
        Log.d("log","command is " + Arrays.toString(cmd));

    try {
        Process process = Runtime.getRuntime().exec(cmd);
        if (DEBUG)
            Log.d("log", "process is " + process);

        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
        if (DEBUG)
            Log.d("log", "bufferreader is " + reader);

        if (DEBUG)
            Log.d("log", "readline " + reader.readLine());
        StringBuffer output = new StringBuffer();

        char[] buffer = new char[4096];
        int read;

        while ((read = reader.read(buffer)) > 0) {
            output.append(buffer, 0, read);
        }

        reader.close();

        process.waitFor();
        if (DEBUG)
            Log.d("log", output.toString());

        return output.toString();

    }

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

    return null;

}

EDIT: same code, but in Kotlin, as it's shorter:

sample usage:

Foo.installApk(context,fullPathToSplitApksFolder)

Example:

        AsyncTask.execute {
            Foo.installApk(this@MainActivity,"/storage/emulated/0/Download/split")
        }

Code:

object Foo {
    @WorkerThread
    @JvmStatic
    fun installApk(context: Context, apkFolderPath: String) {
        val packageInstaller = context.packageManager.packageInstaller
        val nameSizeMap = HashMap<File, Long>()
        var totalSize: Long = 0
        val folder = File(apkFolderPath)
        val listOfFiles = folder.listFiles().filter { it.isFile && it.name.endsWith(".apk") }
        for (file in listOfFiles) {
            Log.d("AppLog", "File " + file.name)
            nameSizeMap[file] = file.length()
            totalSize += file.length()
        }
        val su = "su"
        val pmInstallCreate = arrayOf(su, "-c", "pm", "install-create", "-S", totalSize.toString())
        execute(pmInstallCreate)
        val sessions = packageInstaller.allSessions
        val sessionId = Integer.toString(sessions[0].sessionId)
        for ((file, value) in nameSizeMap) {
            val pmInstallWrite = arrayOf(su, "-c", "pm", "install-write", "-S", value.toString(), sessionId, file.name, file.absolutePath)
            execute(pmInstallWrite)
        }
        val pmInstallCommit = arrayOf(su, "-c", "pm", "install-commit", sessionId)
        execute(pmInstallCommit)
    }

    @WorkerThread
    @JvmStatic
    private fun execute(cmd: Array<String>): String? {
        Log.d("AppLog", "command is " + Arrays.toString(cmd))
        try {
            val process = Runtime.getRuntime().exec(cmd)
            Log.d("AppLog", "process is $process")
            val reader = BufferedReader(InputStreamReader(process.inputStream))
            Log.d("AppLog", "bufferreader is $reader")
            Log.d("AppLog", "readline " + reader.readLine())
            val output = StringBuilder()
            val buffer = CharArray(4096)
            var read: Int
            while (true) {
                read = reader.read(buffer)
                if (read <= 0)
                    break
                output.append(buffer, 0, read)
            }
            reader.close()
            process.waitFor()
            Log.d("AppLog", output.toString())
            return output.toString()
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return null
    }
}
android developer
  • 114,585
  • 152
  • 739
  • 1,270
nkalra0123
  • 2,281
  • 16
  • 17
  • What are the various numbers? How did you know what to put for them? I'm talking about : "52488426" , "44334187", "1262034", "266117", "6626088" . And, what are the strings are "sessiongId" ? How did you think about what to put for them? I'm talking about "1_base.apk", "2_split_config.en.apk", "3_split_config.hdpi.apk" , "4_split_config.x86.apk" . – android developer Apr 03 '19 at 06:39
  • I have downloaded the x86 version of airbnb app, It has 4 parts, 1. base.apk of size 44334187 bytes, 2. split_config.en.apk of size 1262034 bytes 3. split_config.hdpi.apk of size 266117 bytes, 4. split_config.x86.apk of size 6626088 bytes and 52488426 is the sum of all these(total size) So you have to find the sizes of all the splits and add in the command Basically, you are telling the system that you want to create a session in which you need , then with install-write to write bytes from to – nkalra0123 Apr 03 '19 at 07:35
  • You need session id that you created with "pm" ,"install-create", "-S", "52488426" String sessiongId = Integer.toString(sessionId); is just the string representation of sessionId that we just created, it is required to be passed in next commands of install-write and install-commit – nkalra0123 Apr 03 '19 at 07:36
  • I used names like 1_base.apk, because when you do adb install-multiple apk1 apk2 ... android system is using the same names, appending _originalName.apk I have already added "This is the list of commands that are actually received in Pm.java when we do adb install-multiple" – nkalra0123 Apr 03 '19 at 07:41
  • About the numbers, you should have put variables or function calls instead. It's very confusing to see such numbers, which aren't solving a more general question. About the names of the APK files, I don't understand what is the purpose of those. Are those going to be the final files names after being installed? – android developer Apr 03 '19 at 13:02
  • 1
    @androiddeveloper , Please check now, I have removed all the hardcoding of sizes and names now. Just pass the correct directory name to the function installApk – nkalra0123 Apr 03 '19 at 14:04
  • Sadly I'm having some root issues today. You've succeded to install and run the installed app? All went well? Can you share how to do it without root, yet having root for moving the app to be a system app (and also how to restore it to be a user app) ? For now I give the bounty, but I still want some answers. – android developer Apr 03 '19 at 22:49
  • You've succeeded to install and run the installed app? -- yes . Yes, everything went well. Can you share how to do it without root -- I don't know as of now.. I will update if i find the solution. code for moving the app to be a system app with relevant logs -- https://docs.google.com/document/d/1WP76ty2-wpSUI0pRz1UP5VKlevDiob6ah61QV3M8Ss0/edit?usp=sharing The code is a bit hacky, I have tried on a nexus 4 running KK, steps for newer Android versions will require some changes. – nkalra0123 Apr 04 '19 at 08:01
  • This is a bit weird to see in a document. Can you please share it as a working app in Github instead? This way I could try the functionalities : convert current/other app as system app, convert back to user app, install split-apk ... – android developer Apr 04 '19 at 08:09
  • 1
    Ok, I will do that on the weekend. – nkalra0123 Apr 04 '19 at 08:11
  • Wow. Thank you very much for this. – android developer Apr 04 '19 at 08:30
  • I tried your code, and it didn't do anything on my device (rooted) . It didn't crash, and it went through all of the steps, but in the end the app didn't get installed. BTW, I think it's better to use a full file path, and just "su" instead of "/system/xbin/su" – android developer Apr 13 '19 at 18:50
  • I've updated your answer with shorter, Kotlin code, with the suggestions of the IDE applied. It's the same as you wrote, in general. Sadly both don't work for me. Please check... – android developer Apr 13 '19 at 20:08
  • Also, I've found an app called "SAI (Split APKs Installer) ", which can install split APK files even without root (though it has a way to do it with root too). Please show how to make it work. – android developer Apr 13 '19 at 23:22
  • SAI source code is present on git : https://github.com/Aefyr/SAI/blob/master/app/src/main/java/com/aefyr/sai/installer/rooted/RootedSAIPackageInstaller.java I have checked the code written above, it is working fine on a nougat rooted device also. So you will have to debug, on which step it is failing. – nkalra0123 May 21 '19 at 07:55
  • Can you please put it in the sample you've made, to have both kinds: https://github.com/nkalra0123/splitapkinstall – android developer May 21 '19 at 20:19
0

Well I don't know the coding part much, as I have not learnt android, but I can suggest something which you can try. If the task is just to make the split apka into one, what I do is

  1. Install the app using SAI, or Lucky patcher (since it started creating apks instead of apk, and so is able to install them)
  2. Extract as a single apk using apk extractor (the first appearing in the search, by Meher)
  3. Optional - Uninstall the app, if you only needed apk

So you can look at their source code (if they are open source, otherwise something similar), and then try to make a single app to do all these processes (if you know android).

Hope it helps, and please provide your app link, if you manage to create one.

Thanks and cheers

Utpal1234
  • 46
  • 4
  • 1
    Sadly not quite. Reasons: 1. APKS, APKM, XAPK files are not official for Android. Each needs an app to run them. 2. You could have it in less steps than you wrote: you can just zip the APK files and rename the file extension to ".apks" and that's it. No need for a new app. Could even be done via PC. 3. My app already handles installation of all of these types now (directly via other apps, such as file managers, link here: https://play.google.com/store/apps/details?id=com.lb.app_manager ) , and I will probably offer to generate APKS , even though it's not really official. – android developer Sep 10 '20 at 18:55
  • @androiddeveloper Thanks, I didn't know it was that simple to create APKS. But here, the OP wanted to know how to get one APK file from the APKS. So that's why I put these steps. – Utpal1234 Nov 17 '21 at 07:10
  • Wait, are you saying these apps can merge the multiple APK files inside the APKS file, into a single APK file? – android developer Nov 17 '21 at 08:13
-1

What is the way to merge those all into one APK file?

After installing (see question 2), use eg TotalCommander to copy the apk from 'installed apps'

Is it possible to install split APK files without root and without PC ?

Use any terminal app, then:

 pm install <split1> <split2> ...
Bhargav Rao
  • 50,140
  • 28
  • 121
  • 140
jer194
  • 11
  • 2
    You really tried to use "pm install " without PC and root, and it worked for you? As for Total Commander, it doesn't really copy all of the APKs. Just the main one. You should copy them more manually, from "/data/app" – android developer Sep 05 '19 at 06:58