How to share split APKs created while using instant-run, within Android itself?

后端 未结 3 1135
挽巷
挽巷 2020-12-13 17:38

Background

I have an app (here) that, among other features, allows to share APK files.

In order to do so, it reaches the file by accessing the path of pack

3条回答
  •  难免孤独
    2020-12-13 18:30

    To merge multiple split apks to an single apk might be a little complicated.

    Here is a suggestion to share the split apks directly and let the system to handle the merge and installation.

    This might not be an answer to the question, since it's a little long, I post here as an 'answer'.

    Framework new API PackageInstaller can handle monolithic apk or split apk.

    In development environment

    • for monolithic apk, using adb install single_apk

    • for split apk, using adb install-multiple a_list_of_apks

    You can see these two modes above from android studio Run output depends on your project has Instant run enable or disable.

    For the command adb install-multiple, we can see the source code here, it will call the function install_multiple_app.

    And then perform the following procedures

    pm install-create # create a install session
    pm install-write  # write a list of apk to session
    pm install-commit # perform the merge and install
    

    What the pm actually do is call the framework api PackageInstaller, we can see the source code here

    runInstallCreate
    runInstallWrite
    runInstallCommit
    

    It's not mysterious at all, I just copied some methods or function here.

    The following script can be invoked from adb shell environment to install all split apks to device, like adb install-multiple. I think it might work programmatically with Runtime.exec if your device is rooted.

    #!/system/bin/sh
    
    # get the total size in byte
    total=0
    for apk in *.apk
    do
        o=( $(ls -l $apk) )
        let total=$total+${o[3]}
    done
    
    echo "pm install-create total size $total"
    
    create=$(pm install-create -S $total)
    sid=$(echo $create |grep -E -o '[0-9]+')
    
    echo "pm install-create session id $sid"
    
    for apk in *.apk
    do
        _ls_out=( $(ls -l $apk) )
        echo "write $apk to $sid"
        cat $apk | pm install-write -S ${_ls_out[3]} $sid $apk -
    done
    
    pm install-commit $sid
    

    I my example, the split apks include (I got the list from android studio Run output)

    app/build/output/app-debug.apk
    app/build/intermediates/split-apk/debug/dependencies.apk
    and all apks under app/build/intermediates/split-apk/debug/slices/slice[0-9].apk
    

    Using adb push all the apks and the script above to a public writable directory, e.g /data/local/tmp/slices, and run the install script, it will install to your device just like adb install-multiple.

    The code below is just another variant of the script above, if your app has platform signature or device is rooted, I think it will be ok. I didn't have the environment to test.

    private static void installMultipleCmd() {
        File[] apks = new File("/data/local/tmp/slices/slices").listFiles(new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                return pathname.getAbsolutePath().endsWith(".apk");
            }
        });
        long total = 0;
        for (File apk : apks) {
            total += apk.length();
        }
    
        Log.d(TAG, "installMultipleCmd: total apk size " + total);
        long sessionID = 0;
        try {
            Process pmInstallCreateProcess = Runtime.getRuntime().exec("/system/bin/sh\n");
            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(pmInstallCreateProcess.getOutputStream()));
            writer.write("pm install-create\n");
            writer.flush();
            writer.close();
    
            int ret = pmInstallCreateProcess.waitFor();
            Log.d(TAG, "installMultipleCmd: pm install-create return " + ret);
    
            BufferedReader pmCreateReader = new BufferedReader(new InputStreamReader(pmInstallCreateProcess.getInputStream()));
            String l;
            Pattern sessionIDPattern = Pattern.compile(".*(\\[\\d+\\])");
            while ((l = pmCreateReader.readLine()) != null) {
                Matcher matcher = sessionIDPattern.matcher(l);
                if (matcher.matches()) {
                    sessionID = Long.parseLong(matcher.group(1));
                }
            }
            Log.d(TAG, "installMultipleCmd: pm install-create sessionID " + sessionID);
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    
        StringBuilder pmInstallWriteBuilder = new StringBuilder();
        for (File apk : apks) {
            pmInstallWriteBuilder.append("cat " + apk.getAbsolutePath() + " | " +
                    "pm install-write -S " + apk.length() + " " + sessionID + " " + apk.getName() + " -");
            pmInstallWriteBuilder.append("\n");
        }
    
        Log.d(TAG, "installMultipleCmd: will perform pm install write \n" + pmInstallWriteBuilder.toString());
    
        try {
            Process pmInstallWriteProcess = Runtime.getRuntime().exec("/system/bin/sh\n");
            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(pmInstallWriteProcess.getOutputStream()));
            // writer.write("pm\n");
            writer.write(pmInstallWriteBuilder.toString());
            writer.flush();
            writer.close();
    
            int ret = pmInstallWriteProcess.waitFor();
            Log.d(TAG, "installMultipleCmd: pm install-write return " + ret);
            checkShouldShowError(ret, pmInstallWriteProcess);
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    
        try {
            Process pmInstallCommitProcess = Runtime.getRuntime().exec("/system/bin/sh\n");
            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(pmInstallCommitProcess.getOutputStream()));
            writer.write("pm install-commit " + sessionID);
            writer.flush();
            writer.close();
    
            int ret = pmInstallCommitProcess.waitFor();
            Log.d(TAG, "installMultipleCmd: pm install-commit return " + ret);
            checkShouldShowError(ret, pmInstallCommitProcess);
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    private static void checkShouldShowError(int ret, Process process) {
        if (process != null && ret != 0) {
            BufferedReader reader = null;
            try {
                reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
                String l;
                while ((l = reader.readLine()) != null) {
                    Log.d(TAG, "checkShouldShowError: " + l);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    

    Meanwhile, the simple way, you can try the framework api. Like the sample code above, it might work if the device is rooted or your app has platform signature, but I didn't get a workable environment to test it.

    private static void installMultiple(Context context) {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
            PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller();
    
            PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);
    
            try {
                final int sessionId = packageInstaller.createSession(sessionParams);
                Log.d(TAG, "installMultiple: sessionId " + sessionId);
    
                PackageInstaller.Session session = packageInstaller.openSession(sessionId);
    
                File[] apks = new File("/data/local/tmp/slices/slices").listFiles(new FileFilter() {
                    @Override
                    public boolean accept(File pathname) {
                        return pathname.getAbsolutePath().endsWith(".apk");
                    }
                });
    
                for (File apk : apks) {
                    InputStream inputStream = new FileInputStream(apk);
    
                    OutputStream outputStream = session.openWrite(apk.getName(), 0, apk.length());
                    byte[] buffer = new byte[65536];
                    int count;
                    while ((count = inputStream.read(buffer)) != -1) {
                        outputStream.write(buffer, 0, count);
                    }
    
                    session.fsync(outputStream);
                    outputStream.close();
                    inputStream.close();
                    Log.d(TAG, "installMultiple: write file to session " + sessionId + " " + apk.length());
                }
    
                try {
                    IIntentSender target = new IIntentSender.Stub() {
    
                        @Override
                        public int send(int i, Intent intent, String s, IIntentReceiver iIntentReceiver, String s1) throws RemoteException {
                            int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE);
                            Log.d(TAG, "send: status " + status);
                            return 0;
                        }
                    };
                    session.commit(IntentSender.class.getConstructor(IIntentSender.class).newInstance(target));
                } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
                    e.printStackTrace();
                }
                session.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    In order to use the hidden api IIntentSender, I add the jar library android-hidden-api as the provided dependency.

提交回复
热议问题