Можно ли объединить/установить разделенные APK файлы (AKA "комплект приложений") на самом устройстве Android без рута?

Фон

Раньше я спрашивал о совместном использовании или резервном копировании apk файлов приложения/пакета, здесь.

Это кажется почти невыполнимой задачей, которую я мог только выяснить, как установить разделенные файлы APK, и даже тогда это только через adb:

adb install-multiple apk1 apk2 ...

Проблема

Мне сказали, что на самом деле должно быть возможно объединить несколько разбитых APK файлов в один, который я мог бы установить (здесь), но мне не сказали, как это сделать.

Это может быть полезно для сохранения его на более поздний срок (резервного копирования), а также потому, что в настоящее время нет способа установить файлы split-apk внутри устройства.

На самом деле это настолько серьезная проблема, что я не знаю ни одного приложения для резервного копирования, которое может обрабатывать разделенные файлы APK (комплект приложений), в том числе приложение Titanium.

Что я нашел

Я взял пример приложения, которое использует наборы приложений, под названием "AirBnb".

Что касается файлов, которые он имеет, то Play Store решил загрузить:

enter image description here

Поэтому я попытался ввести каждый. "База" является основной, поэтому я пропустил ее, чтобы посмотреть на остальные. Мне кажется, что у всех есть эти файлы внутри:

  • "META-INF"
  • "Resources.arsc"
  • "AndroidManifest.xml"в случае с "xxxhdpi" я также получаю папку "res".

Дело в том, что, поскольку все они существуют в нескольких местах, я не понимаю, как их объединить.

Вопросы

  1. Как объединить все эти файлы в один APK файл?

  2. Можно ли установить сплит APK файлы без рута и без ПК? В прошлом это было возможно для приложений резервного копирования, таких как Titanium, но только для обычных файлов APK, а не для пакета приложений (split apk).


ОБНОВЛЕНИЕ: я назначил награду. Пожалуйста, если вы знаете решение, покажите его. Покажите то, что вы тестировали на работу. Либо слияние разделенных файлов APK, либо установка их без прав root и прав на устройстве.


ОБНОВЛЕНИЕ: К сожалению, все решения здесь не работали, с или без рута, и что, хотя я нашел приложение, которое преуспело в этом (с и без рута), под названием "SAI (Split APKs Installer)" (я думаю, что это репозиторий здесь, найден после того, как я положил награду).

Я ставлю новую награду. Пожалуйста, кто бы ни опубликовал новый ответ, покажите, что он работает с рутом и без него. Покажите на Github, если нужно (и здесь только важные вещи). Я знаю, что это приложение в любом случае с открытым исходным кодом, но для меня важно, как это сделать здесь и поделиться с другими, поскольку в настоящее время показанное здесь не работает и требует root, даже если оно не очень нужно.

На этот раз я не буду предоставлять награду, пока не увижу что-то, что действительно работает (ранее у меня было мало времени и я дал ответ, который, как я думал, должен работать).

Ответы

Ответ 1

Реализация не требуется. Проверьте ссылку git hub: https://github.com/nkalra0123/splitapkinstall.

Мы должны создать сервис и передать этот дескриптор в session.commit()

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

РЕДАКТИРОВАТЬ: Поскольку решение работает, но не опубликовано здесь, я решил написать его, прежде чем пометить как правильное решение. Вот код:

манифест

<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
    }
}

Основная деятельность

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

Ответ 2

Из пакета приложений для Android вы можете сгенерировать "универсальный APK" с помощью команды bundletool build-apks с --mode=universal флагом --mode=universal. Это создаст один "толстый" APK, который совместим со всеми устройствами (которые поддерживает ваше приложение).

Я знаю, что это не совсем ответ на ваш вопрос, но попытка объединить APK не только сложная задача, но во многих случаях приведет к чему-то неправильному.

Ответ 3

Пожалуйста, проверьте это. когда мы отправим

adb install-multiple apk1 apk2 ...

он вызывает этот код установить несколько

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

который в свою очередь вызывает Pm.java или новый способ выполнения кода PackageManagerService, оба схожи

Я пытался интегрировать этот код в мое приложение. Проблема, с которой я столкнулся, не удалось завершить установку apk, это связано с необходимостью приложения.

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

Но это только для системных приложений. Когда я выполнил эти шаги из adb shell, установка apk прошла успешно, а когда я создала свое приложение, системная установка priv-app apk прошла успешно.

код для вызова нового API-интерфейса PackageManager, в основном скопированного из Pm.java. Шаги по установке сплит файлов

  1. Создайте сеанс с аргументом -S, верните идентификатор сеанса.

    (install-create, -S, 52488426) 52488426 - общий размер apks.

  2. Напишите разделенные apks в этом сеансе с размером, именем и путем

    (установить-записать, -S, 44334187, 824704264, 1_base.apk, -)

    (установить-записать, -S, 1262034, 824704264, 2_split_config.en.apk, -)

    (установить-записать, -S, 266117, 824704264, 3_split_config.hdpi.apk, -)

    (установить-записать, -S, 6626088, 824704264, 4_split_config.x86.apk, -)

  3. зафиксировать сеанс с идентификатором сеанса

    (install-commit, 824704264)

Я поместил airbnb apk в мою 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

и вызывая функции для установки 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;
}

Это список команд, которые фактически принимаются в Pm.java, когда мы выполняем 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]]

Так что для приложений, которые не являются системными priv-app, я не знаю, как они могут установить сплит apks. Play store как системное priv-приложение может без проблем использовать эти apis и устанавливать сплит apk.

Ответ 4

Bundletool - это обычный инструмент CLI для выполнения этих задач; однако для запуска обычного Java- jar потребуется обычная Java JVM. Тем не менее, добавление его в качестве зависимости в проект приложения Android может сработать - с другой стороны, все параметры командной строки необходимо будет указывать по-разному, что потребует частичной перезаписи. Не существует "простого" решения проблемы.

Ответ 5

Если у вас есть root, вы можете использовать этот код.

Перед выполнением этого кода, пожалуйста, получите разрешение на чтение/запись SD-карты (через разрешения времени выполнения или разрешение, предоставленное приложением настроек). airbnb apk был успешно установлен после запуска этого кода.

Вызывая эту функцию с аргументами "/split-apks/", я поместил разделенные apk airbnb в каталог в /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;

}

РЕДАКТИРОВАТЬ: тот же код, но в Kotlin, так как он короче:

пример использования:

Foo.installApk(context,fullPathToSplitApksFolder)

Пример:

        AsyncTask.execute {
            Foo.installApk([email protected],"/storage/emulated/0/Download/split")
        }

Код:

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
    }
}

Ответ 6

  Как объединить все эти файлы в один APK файл?

После установки (см. вопрос 2) используйте, например, TotalCommander, чтобы скопировать APK из  "установленные приложения"

Можно ли установить сплит APK файлы без рута и без ПК?

Используйте любое приложение терминала, а затем:

 pm install <split1> <split2> ...