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

后端 未结 8 1238
北恋
北恋 2020-12-09 02:50

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 coul

相关标签:
8条回答
  • 2020-12-09 03:24

    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.

    0 讨论(0)
  • 2020-12-09 03:24

    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> ...
    
    0 讨论(0)
  • 2020-12-09 03:26

    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
        }
    }
    
    0 讨论(0)
  • 2020-12-09 03:32

    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()
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-09 03:34

    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

    0 讨论(0)
  • 2020-12-09 03:37
    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 install as in the old days.
    0 讨论(0)
提交回复
热议问题