mirror of
https://github.com/meowarex/rl-mobile.git
synced 2026-06-17 21:13:11 +10:00
Manager: full split-APK merge support
- CI now uses APKEditor's merge command for proper resources.arsc + split merging (previous ad-hoc Java merge missed split-only resource entries like notification_icon, causing runtime Resources$NotFoundException) - ManifestPatcher: dynamically detect the original package name and rewrite every reference to it (permissions/provider authorities); drop split-only attributes (isSplitRequired, requiredSplitTypes, splitTypes) so the installer doesn't reject the merged APK with "Missing split" - SmaliPatchStep: extract extension/**/*.smali entries from patches.zip into the smali dir before assembly, so new helper classes (radiant/SparkleButton etc) actually end up in the patched dex - PMIntentReceiver / PMInstallerError: surface PackageInstaller EXTRA_STATUS_MESSAGE so install failures show Android's real reason instead of the generic enum label Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -56,19 +56,11 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [[ "$tidal_src" == *.apkm ]]; then
|
if [[ "$tidal_src" == *.apkm ]]; then
|
||||||
echo "Extracting & merging splits from $tidal_src"
|
echo "Merging splits from $tidal_src via APKEditor"
|
||||||
workdir=$(mktemp -d)
|
curl -sLo /tmp/APKEditor.jar https://github.com/REAndroid/APKEditor/releases/download/V1.4.3/APKEditor-1.4.3.jar
|
||||||
unzip -q "$tidal_src" -d "$workdir"
|
java -jar /tmp/APKEditor.jar m -i "$tidal_src" -o ./dist/tidal-stock.apk
|
||||||
ls "$workdir"
|
|
||||||
dist_abs=$(realpath ./dist)/tidal-stock.apk
|
|
||||||
|
|
||||||
javac scripts/MergeApk.java -d scripts
|
|
||||||
splits=("$workdir"/split_*.apk)
|
|
||||||
java -cp scripts MergeApk "$dist_abs" "$workdir/base.apk" "${splits[@]}"
|
|
||||||
|
|
||||||
rm -rf "$workdir"
|
|
||||||
echo "Merged tidal-stock.apk:"
|
echo "Merged tidal-stock.apk:"
|
||||||
ls -la "$dist_abs"
|
ls -la ./dist/tidal-stock.apk
|
||||||
else
|
else
|
||||||
cp "$tidal_src" ./dist/tidal-stock.apk
|
cp "$tidal_src" ./dist/tidal-stock.apk
|
||||||
fi
|
fi
|
||||||
|
|||||||
+13
-10
@@ -12,16 +12,19 @@ import kotlinx.parcelize.Parcelize
|
|||||||
* that is captured by a receiver into something human readable.
|
* that is captured by a receiver into something human readable.
|
||||||
*/
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class PMInstallerError(val status: Int) : InstallerResult.Error(), Parcelable {
|
data class PMInstallerError(val status: Int, val message: String? = null) : InstallerResult.Error(), Parcelable {
|
||||||
override fun getDebugReason() = when (status) {
|
override fun getDebugReason(): String {
|
||||||
PackageInstaller.STATUS_FAILURE -> "Unknown failure"
|
val reason = when (status) {
|
||||||
PackageInstaller.STATUS_FAILURE_BLOCKED -> "Blocked"
|
PackageInstaller.STATUS_FAILURE -> "Unknown failure"
|
||||||
PackageInstaller.STATUS_FAILURE_INVALID -> "Invalid package"
|
PackageInstaller.STATUS_FAILURE_BLOCKED -> "Blocked"
|
||||||
PackageInstaller.STATUS_FAILURE_CONFLICT -> "Package conflict"
|
PackageInstaller.STATUS_FAILURE_INVALID -> "Invalid package"
|
||||||
PackageInstaller.STATUS_FAILURE_STORAGE -> "Storage error"
|
PackageInstaller.STATUS_FAILURE_CONFLICT -> "Package conflict"
|
||||||
PackageInstaller.STATUS_FAILURE_INCOMPATIBLE -> "Device incompatibility"
|
PackageInstaller.STATUS_FAILURE_STORAGE -> "Storage error"
|
||||||
/* PackageInstaller.STATUS_FAILURE_TIMEOUT */ 8 -> "Operation timeout"
|
PackageInstaller.STATUS_FAILURE_INCOMPATIBLE -> "Device incompatibility"
|
||||||
else -> "Unknown code ($status)"
|
/* PackageInstaller.STATUS_FAILURE_TIMEOUT */ 8 -> "Operation timeout"
|
||||||
|
else -> "Unknown code ($status)"
|
||||||
|
}
|
||||||
|
return if (message != null) "$reason: $message" else reason
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLocalizedReason(context: Context): String {
|
override fun getLocalizedReason(context: Context): String {
|
||||||
|
|||||||
@@ -60,13 +60,14 @@ class PMIntentReceiver : BroadcastReceiver() {
|
|||||||
PackageInstaller.STATUS_FAILURE_ABORTED -> InstallerResult.Cancelled(systemTriggered = true)
|
PackageInstaller.STATUS_FAILURE_ABORTED -> InstallerResult.Cancelled(systemTriggered = true)
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Log.w(BuildConfig.TAG, "PM failed with error code $status")
|
val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
||||||
|
Log.w(BuildConfig.TAG, "PM failed with error code $status: $message")
|
||||||
|
|
||||||
if (status <= PackageInstaller.STATUS_SUCCESS) {
|
if (status <= PackageInstaller.STATUS_SUCCESS) {
|
||||||
// Unknown status code (not an error)
|
// Unknown status code (not an error)
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
PMInstallerError(status).also {
|
PMInstallerError(status, message).also {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
/* context = */ context,
|
/* context = */ context,
|
||||||
/* text = */ it.getLocalizedReason(context),
|
/* text = */ it.getLocalizedReason(context),
|
||||||
|
|||||||
+15
-1
@@ -38,11 +38,25 @@ class SmaliPatchStep : Step(), IDexProvider, KoinComponent {
|
|||||||
|
|
||||||
val patches = mutableListOf<LoadedPatch>()
|
val patches = mutableListOf<LoadedPatch>()
|
||||||
|
|
||||||
// Load and parse all the patches from the smali patch archive
|
// Load and parse all the patches from the smali patch archive.
|
||||||
|
// Extension classes (extension/**/*.smali) are extracted into smaliDir
|
||||||
|
// so they get assembled into the new dex alongside patched classes.
|
||||||
container.log("Loading patches from smali patch archive: ${patchesZip.absolutePath}")
|
container.log("Loading patches from smali patch archive: ${patchesZip.absolutePath}")
|
||||||
|
smaliDir.mkdirs()
|
||||||
ZipReader(patchesZip).use { zip ->
|
ZipReader(patchesZip).use { zip ->
|
||||||
for (patchFile in zip.entryNames) {
|
for (patchFile in zip.entryNames) {
|
||||||
container.log("Parsing patch file $patchFile")
|
container.log("Parsing patch file $patchFile")
|
||||||
|
if (patchFile.endsWith("/")) continue
|
||||||
|
|
||||||
|
if (patchFile.endsWith(".smali") && patchFile.startsWith("extension/")) {
|
||||||
|
val relative = patchFile.removePrefix("extension/")
|
||||||
|
val out = smaliDir.resolve(relative)
|
||||||
|
out.parentFile?.mkdirs()
|
||||||
|
out.writeBytes(zip.openEntry(patchFile)!!.read())
|
||||||
|
container.log("Extracted extension smali: $relative")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if (!patchFile.endsWith(".patch")) continue
|
if (!patchFile.endsWith(".patch")) continue
|
||||||
|
|
||||||
val lines = zip.openEntry(patchFile)!!.read()
|
val lines = zip.openEntry(patchFile)!!.read()
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ object ManifestPatcher {
|
|||||||
private const val PACKAGE = "package"
|
private const val PACKAGE = "package"
|
||||||
private const val COMPILE_SDK_VERSION = "compileSdkVersion"
|
private const val COMPILE_SDK_VERSION = "compileSdkVersion"
|
||||||
private const val COMPILE_SDK_VERSION_CODENAME = "compileSdkVersionCodename"
|
private const val COMPILE_SDK_VERSION_CODENAME = "compileSdkVersionCodename"
|
||||||
|
private const val IS_SPLIT_REQUIRED = "isSplitRequired"
|
||||||
|
private const val REQUIRED_SPLIT_TYPES = "requiredSplitTypes"
|
||||||
|
private const val SPLIT_TYPES = "splitTypes"
|
||||||
|
|
||||||
fun patchManifest(
|
fun patchManifest(
|
||||||
manifestBytes: ByteArray,
|
manifestBytes: ByteArray,
|
||||||
@@ -22,6 +25,18 @@ object ManifestPatcher {
|
|||||||
appName: String,
|
appName: String,
|
||||||
debuggable: Boolean,
|
debuggable: Boolean,
|
||||||
): ByteArray {
|
): ByteArray {
|
||||||
|
// Extract original package name so we can rewrite every reference to it
|
||||||
|
// (permissions, provider authorities) to the new packageName.
|
||||||
|
var originalPackage: String? = null
|
||||||
|
AxmlReader(manifestBytes).accept(object : AxmlVisitor() {
|
||||||
|
override fun child(ns: String?, name: String?) = object : NodeVisitor() {
|
||||||
|
override fun attr(ns: String?, name: String?, resourceId: Int, type: Int, value: Any?) {
|
||||||
|
if (name == PACKAGE && value is String) originalPackage = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
val origPkg = originalPackage ?: "com.aspiro.tidal"
|
||||||
|
|
||||||
val reader = AxmlReader(manifestBytes)
|
val reader = AxmlReader(manifestBytes)
|
||||||
val writer = AxmlWriter()
|
val writer = AxmlWriter()
|
||||||
|
|
||||||
@@ -42,6 +57,11 @@ object ManifestPatcher {
|
|||||||
COMPILE_SDK_VERSION_CODENAME to "6.0-2438415"
|
COMPILE_SDK_VERSION_CODENAME to "6.0-2438415"
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
// Drop split-only manifest attributes — we merged all splits into one APK
|
||||||
|
override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) {
|
||||||
|
if (name == IS_SPLIT_REQUIRED || name == REQUIRED_SPLIT_TYPES || name == SPLIT_TYPES) return
|
||||||
|
super.attr(ns, name, resourceId, type, value)
|
||||||
|
}
|
||||||
private var addExternalStoragePerm = false
|
private var addExternalStoragePerm = false
|
||||||
|
|
||||||
override fun child(ns: String?, name: String): NodeVisitor {
|
override fun child(ns: String?, name: String): NodeVisitor {
|
||||||
@@ -84,7 +104,7 @@ object ManifestPatcher {
|
|||||||
super.attr(
|
super.attr(
|
||||||
ns, name, resourceId, type,
|
ns, name, resourceId, type,
|
||||||
when (name) {
|
when (name) {
|
||||||
"name" -> (value as String).replace("com.tidal.android", packageName)
|
"name" -> (value as String).replace(origPkg, packageName)
|
||||||
else -> value
|
else -> value
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -135,7 +155,7 @@ object ManifestPatcher {
|
|||||||
super.attr(
|
super.attr(
|
||||||
ns, name, resourceId, type,
|
ns, name, resourceId, type,
|
||||||
if (name == "authorities") {
|
if (name == "authorities") {
|
||||||
(value as String).replace("com.tidal.android", packageName)
|
(value as String).replace(origPkg, packageName)
|
||||||
} else {
|
} else {
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user