是否可以在没有根目录的Android设备本身上合并/安装拆分的APK文件(又称为“应用程序捆绑包”)?

and*_*per 17 android apk

背景

过去,我曾在此处询问过有关应用程序捆绑/拆分apk文件的共享或备份的问题

这似乎是几乎不可能完成的任务,我只能找出如何安装拆分后的APK文件,即使如此,也只能通过adb进行:

adb install-multiple apk1 apk2 ...
Run Code Online (Sandbox Code Playgroud)

问题

有人告诉我,实际上应该可以将多个拆分的APK文件合并到一个我可以安装的文件中(在此处),但是没有给出如何做的信息。

这对于将其保存以备后用(备份)可能很有用,因为当前无法在设备内安装split-apk文件。

实际上,这是一个重大问题,我不知道有任何备份应用程序可以处理拆分的APK文件(应用程序捆绑包),其中包括Titanium应用程序。

我发现了什么

我拍摄了一个使用应用程序捆绑的示例应用程序,称为“ AirBnb”。

查看文件中包含的内容,这些就是Play商店决定下载的文件:

在此处输入图片说明

因此,我尝试输入每个。“基础”是主要的基础,因此我略过了其他基础。对我来说,似乎所有这些文件都包含在其中:

  • “ META-INF”
  • “ resources.arsc”
  • “ AndroidManifest.xml”
  • 如果是带有“ xxxhdpi”的文件,我还会得到“ res”文件夹。

事实是,由于这些都存在于多个地方,所以我不知道如何合并它们。

问题

  1. 将这些全部合并为一个APK文件的方式是什么?

  2. 是否可以在没有root用户且没有PC的情况下安装拆分的APK文件?过去在备份应用程序(例如Titanium)上可以做到这一点,但只能在普通的APK文件上使用,而不能在应用程序捆绑包(拆分式APK)上使用。


编辑:我设置了赏金。请,如果您知道解决方案,请显示它。显示您经过测试可以正常工作的内容。合并拆分的APK文件或安装它们,这些文件在设备上都没有root权限。


编辑:遗憾的是,无论有没有root,这里的所有解决方案都不起作用,即使我找到了一个成功完成它(有和没有root)的应用程序,它叫做“ SAI(Split APKs Installer)”(我认为存储库在这里,在我悬赏后找到)。

我要设一个新的赏金。请发布新答案的人证明其在有根或无根的情况下均有效。如果需要,请在Github上显示(这里只是重要内容)。我知道这个应用程序无论如何都是开源的,但是对我来说重要的是如何在这里做它并与他人共享,因为目前这里显示的内容是行不通的,并且即使不是真正需要它也需要root用户。

这次,在我看到确实有用的东西之前,我不会给予任何赏金(以前我时间很短,并且把它授予了我认为应该起作用的答案)。

nka*_*123 8

请检查这个。当我们发送

adb install-multiple apk1 apk2 ...
Run Code Online (Sandbox Code Playgroud)

它调用此代码 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]);
    }
Run Code Online (Sandbox Code Playgroud)

依次调用 Pm.java 或执行 PackageManagerService 代码的新方式,两者相似

我尝试将该代码集成到我的应用程序中,我遇到的问题是无法完成 apk 安装,这是由于应用程序需要的原因。

<uses-permission android:name="android.permission.INSTALL_PACKAGES"/>
Run Code Online (Sandbox Code Playgroud)

但它只提供给系统私有应用程序。当我从 adb shell apk 安装成功执行这些步骤时,当我创建我的应用程序时,系统 priv-app apk 安装成功。

调用PackageManager的新apis的代码,主要是从Pm.java中复制的安装split apks的步骤

  1. 创建一个带有参数 -S 的会话,返回会话 ID。

    (install-create, -S, 52488426) 52488426 -- apk 的总大小。

  2. 在该会话中使用大小、名称和路径编写拆分的 apk

    (安装-写入, -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. 使用会话 ID 提交会话

    (安装提交,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
Run Code Online (Sandbox Code Playgroud)

并调用函数来安装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;
}
Run Code Online (Sandbox Code Playgroud)

这是我们在执行 adb install-multiple 时在 Pm.java 中实际收到的命令列表

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]]
Run Code Online (Sandbox Code Playgroud)

因此,对于不是系统私有应用程序的应用程序,我不知道他们如何安装拆分的 apk。作为系统私有应用程序的 Play 商店可以使用这些 apis 并安装拆分 apks 没有任何问题。


nka*_*123 7

不需要 root 实现检查这个 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());
Run Code Online (Sandbox Code Playgroud)

编辑:由于该解决方案有效,但并未真正在此处发布,因此我决定在将其标记为正确解决方案之前先编写它。这是代码:

显现

<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>
Run Code Online (Sandbox Code Playgroud)

APK安装服务

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
    }
}
Run Code Online (Sandbox Code Playgroud)

主要活动

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()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


Pie*_*rre 1

从 Android App Bundle 中,您可以使用带有标志的bundletool build-apks 命令生成“通用 APK” --mode=universal。这将生成一个与所有设备(您的应用程序支持)兼容的“胖”APK。

我知道这并不能严格回答您的问题,但尝试合并 APK 不仅是一项复杂的任务,而且会导致很多情况下出现错误。