web-dev-qa-db-fra.com

Comment partager des fichiers APK fractionnés créés lors de l'exécution instantanée, dans Android lui-même?

Contexte

J'ai une application ( ici ) qui, entre autres fonctionnalités, permet de partager des fichiers APK.

Pour ce faire, il atteint le fichier en accédant au chemin de packageInfo.applicationInfo.sourceDir (lien docs ( ici ) , et partage simplement le fichier (en utilisant ContentProvider si nécessaire, comme je l'ai utilisé ici ).

Le problème

Cela fonctionne très bien dans la plupart des cas, en particulier lors de l'installation de fichiers APK à partir du Play Store ou d'un fichier APK autonome, mais lorsque j'installe une application à l'aide d'Android-Studio lui-même, je vois plusieurs fichiers APK sur ce chemin, et aucun d'entre eux n'est ceux valides qui peuvent être installés et exécutés sans aucun problème.

Voici une capture d'écran du contenu de ce dossier, après avoir essayé un échantillon de repo "Alerter" github :

enter image description here

Je ne sais pas quand ce problème a commencé, mais il se produit au moins sur mon Nexus 5x avec Android 7.1.2. Peut-être même avant.

Ce que j'ai trouvé

Cela semble provenir uniquement du fait que l'exécution instantanée est activée sur l'IDE, de sorte qu'il pourrait aider à mettre à jour l'application sans avoir besoin de tout reconstruire ensemble:

enter image description here

Après l'avoir désactivé, je peux voir qu'il y a un seul fichier APK, comme c'était le cas auparavant:

enter image description here

Vous pouvez voir la différence de taille de fichier entre l'APK correct et celui divisé.

En outre, il semble qu'il existe une API pour obtenir les chemins d'accès à tous les fichiers APK fractionnés:

https://developer.Android.com/reference/Android/content/pm/ApplicationInfo.html#splitPublicSourceDirs

La question

Quelle devrait être la façon la plus simple de partager un APK qui doit être divisé en plusieurs?

Est-il vraiment nécessaire de les fusionner d'une manière ou d'une autre?

Il semble que cela soit possible selon ( la documentation :

Chemins complets vers zéro ou plusieurs fichiers APK fractionnés qui, lorsqu'ils sont combinés avec le fichier APK de base défini dans sourceDir, forment une application complète.

Mais quelle est la bonne façon de le faire, et existe-t-il un moyen rapide et efficace de le faire? Peut-être sans vraiment créer de fichier?

Existe-t-il peut-être une API pour obtenir un fichier APK fusionné de tous les fichiers fractionnés? Ou peut-être qu'un tel APK existe déjà de toute façon dans une autre voie, et qu'il n'est pas nécessaire de fusionner?

EDIT: je viens de remarquer que toutes les applications tierces que j'ai essayées sont censées partager l'APK d'une application installée ne le font pas dans ce cas.

42
android developer

Je suis le responsable technique @Google pour le Android plugin Gradle, permettez-moi d'essayer de répondre à votre question en supposant que je comprends votre cas d'utilisation.

Tout d'abord, certains utilisateurs ont mentionné que vous ne devriez pas partager votre build compatible InstantRun et ils sont corrects. Instant Run s'appuie sur une application est hautement personnalisé pour l'image de périphérique/émulateur actuelle sur laquelle vous déployez. Donc, en gros, disons que vous générez une version compatible IR de votre application pour un appareil particulier exécutant 21, cela échouera misérablement si vous essayez d'utiliser exactement les mêmes APK sur un appareil exécutant 23. Je peux aller dans une explication beaucoup plus approfondie si nécessaire mais il suffit de dire que nous générons des codes octets personnalisés sur les API trouvées dans Android.jar (qui est bien sûr spécifique à la version).

Je ne pense donc pas que le partage de ces fichiers APK ait un sens, vous devez soit utiliser une version désactivée par infrarouge, soit une version.

Maintenant, pour certains détails, chaque tranche APK contient 1+ fichier (s) dex, donc en théorie, rien ne vous empêche de décompresser tous ces fichiers APK de tranche, de prendre tous les fichiers dex et de les replacer dans le fichier base.apk/rezip/resign et ça devrait juste marcher. Cependant, ce sera toujours une application compatible IR, donc il démarrera le petit serveur pour écouter IDE requêtes, etc, etc ... Je ne peux pas imaginer une bonne raison pour le faire.

J'espère que cela t'aides.

18
Jerome Dochez

Fusionner plusieurs apk divisés en un seul apk peut être un peu compliqué.

Voici une suggestion pour partager les apks divisés directement et laisser le système gérer la fusion et l'installation.

Ce n'est peut-être pas une réponse à la question, car c'est un peu long, je poste ici comme une "réponse".

La nouvelle API du framework PackageInstaller peut gérer monolithic apk ou split apk.

En environnement de développement

  • pour monolithic apk, en utilisant adb install single_apk

  • pour split apk, en utilisant adb install-multiple a_list_of_apks

Vous pouvez voir ces deux modes ci-dessus à partir de Android studio Run la sortie dépend de votre projet a Instant run Activer ou désactiver.

Pour la commande adb install-multiple, on peut voir le code source ici , il appellera la fonction install_multiple_app.

Et puis effectuez les procédures suivantes

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

Ce que le pm fait, c'est appeler le framework api PackageInstaller, nous pouvons voir le code source ici

runInstallCreate
runInstallWrite
runInstallCommit

Ce n'est pas mystérieux du tout, je viens de copier quelques méthodes ou fonctions ici.

Le script suivant peut être appelé à partir de adb Shell environnement pour installer tout split apks vers l'appareil, comme adb install-multiple. Je pense que cela pourrait fonctionner par programme avec Runtime.exec si votre appareil est rooté.

#!/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

Je mon exemple, les apks split comprennent (j'ai obtenu la liste de 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

En utilisant adb Push toutes les apks et le script ci-dessus dans un répertoire accessible en écriture, par exemple /data/local/tmp/slices, et exécutez le script d'installation, il s'installera sur votre appareil comme adb install-multiple.

Le code ci-dessous n'est qu'une autre variante du script ci-dessus, si votre application a une signature de plate-forme ou un appareil est enraciné, je pense que ce sera correct. Je n'avais pas l'environnement à tester.

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();
                }
            }
        }
    }
}

En attendant, de manière simple, vous pouvez essayer l'api du framework. Comme l'exemple de code ci-dessus, cela pourrait fonctionner si l'appareil est enraciné ou si votre application a la signature de la plate-forme, mais je n'ai pas eu d'environnement de travail pour le tester.

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();
        }
    }
}

Afin d'utiliser l'api caché IIntentSender, j'ajoute la bibliothèque jar Android-hidden-api comme dépendance provided.

1
alijandro