背景
在过去,我问过关于应用程序捆绑/拆分apk文件的共享或备份的问题,。
这似乎是一个几乎不可能完成的任务,我只能弄清楚如何安装拆分的APK文件,即使这样也只能通过adb:
adb install-multiple apk1 apk2 ...
问题所在
有人告诉我,实际上应该可以将多个拆分的APK文件合并成一个我可以安装的文件(),但没有告诉我怎么做。
这对于保存它以便以后(备份)很有用,因为目前没有办法在设备中安装split-apk文件。
事实上,这是一个很大的问题,以至于我不知道有任何备份应用程序可以处理拆分的APK文件(应用程序包),这包括钛应用程序。
我发现了什么
我使用了一个使用应用程序包的示例应用程序,名为"AirBnb“。
看看它拥有的文件,这些是Play Store决定下载的文件:
所以我试着进入每一个。"base“是主要的一个,所以我跳过它来看看其他的。在我看来,所有的文件都包含这些文件:
对于带有"xxxhdpi“的文件夹,我也得到了”
问题是,由于这些都存在于多个地方,我不知道如何合并它们。
问题
编辑:我设置了一笔赏金。如果你知道一个解决方案,请展示出来。展示一些你已经测试过的东西。要么合并拆分的APK文件,要么安装它们,都不需要根目录就可以直接在设备上使用。
编辑:遗憾的是,这里的所有解决方案都不起作用,不管有没有根目录,尽管我已经找到了一个成功做到这一点的应用程序(有根目录和没有根目录),名为"SAI (拆分APK安装程序)“(我认为它的存储库是,在我放了一笔赏金之后找到的)。
我要放一笔新的赏金。无论谁发布了新的答案,请证明它在有根和没有根的情况下都可以工作。如果需要,在Github上显示(这里只显示重要的内容)。我知道这个应用程序无论如何都是开源的,但它对我来说很重要,如何在这里做到这一点,并与其他人分享,因为目前这里显示的内容不起作用,需要root,即使它并不是真正需要的。
这一次,我不会给予赏金,直到我看到一些确实有效的东西(以前我时间很短,并将其授予我认为应该有效的答案)。
发布于 2019-04-01 21:36:30
请检查一下这个。当我们发送
adb install-multiple apk1 apk2 ...
它将此代码称为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]);
}
它反过来调用Pm.java或一种执行PackageManagerService代码的新方法,两者是相似的
我尝试将这些代码集成到我的应用程序中,我遇到的问题是,apk安装无法完成,这是由于应用程序需要的原因。
<uses-permission android:name="android.permission.INSTALL_PACKAGES"/>
但它只提供给系统priv应用程序。当我从adb shell执行这些步骤时,apk安装是成功的,当我创建应用程序时,系统priv-app apk安装是成功的。
调用PackageManager新apis的代码,大部分是从安装拆分apis的Pm.java步骤中复制的
(install-create,-S,52488426) 52488426 -- apks的总大小。
(安装-写入,-S,44334187,824704264,1_base.apk,-)
(安装-写入,-S,1262034,824704264,2_split_config.en.apk,-)
(安装-写入,-S,266117,824704264,3_split_config.hdpi.apk,-)
(install-write,-S,6626088,824704264,4_split_config.x86.apk,-)
(install-commit,824704264)
我已经把airbnb apk放在我的SD卡里了。
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;
}
这是我们执行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]]
所以对于那些不是system priv-app的应用,我不知道他们怎么才能安装拆分的apks。Play store是一个系统priv-app可以使用这些apis并安装拆分的apis而不会出现任何问题。
发布于 2019-04-14 13:56:45
不需要根目录的实现检查此git集线器链接: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
}
}
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()
}
}
}
发布于 2020-07-03 18:29:34
可以手动和自动将拆分的APK合并为单个APK,但强制使用伪造的签名密钥意味着APK不能作为正版应用程序的更新安装,以防应用程序可能会检查自己是否进行了篡改
如何手动合并拆分的APK的详细指南:https://platinmods.com/threads/how-to-turn-a-split-apk-into-a-normal-non-split-apk.76683/
自动合并拆分APK的PC软件:https://www.andnixsh.com/2020/06/sap-split-apks-packer-by-kirlif-windows.html
https://stackoverflow.com/questions/55212788
复制相似问题