Split & update apks by ABI

This commit is contained in:
Damontecres 2025-11-16 17:04:02 -05:00
parent 5c12e03b6d
commit 62ab9b8398
No known key found for this signature in database
5 changed files with 87 additions and 49 deletions

View file

@ -15,8 +15,6 @@ concurrency:
cancel-in-progress: true
env:
APK_SHORT_NAME: Wholphin-release.apk
APK_DEBUG_SHORT_NAME: Wholphin-debug.apk
BRANCH_NAME: ${{ github.ref_name }}
jobs:
@ -58,23 +56,26 @@ jobs:
SIGNING_KEY: "${{ secrets.SIGNING_KEY }}"
run: |
./gradlew clean assembleRelease assembleDebug
echo "apk=$(ls app/build/outputs/apk/release/Wholphin-release*.apk)" >> "$GITHUB_OUTPUT"
echo "apk_debug=$(ls app/build/outputs/apk/debug/Wholphin-debug*.apk)" >> "$GITHUB_OUTPUT"
- name: Verify signature
echo "apks=$(find app/build/outputs/apk -name '*.apk' | sort)" >> "$GITHUB_OUTPUT"
- name: Verify signatures
run: |
echo "Verify release APK"
${{env.ANDROID_SDK_ROOT}}/build-tools/${{ env.BUILD_TOOLS_VERSION }}/apksigner verify --verbose --print-certs "${{ steps.build.outputs.apk }}"
echo "Verify debug APK"
${{env.ANDROID_SDK_ROOT}}/build-tools/${{ env.BUILD_TOOLS_VERSION }}/apksigner verify --verbose --print-certs "${{ steps.build.outputs.apk_debug }}"
- name: Copy APK to ${{ env.APK_SHORT_NAME }}
echo "Verify APK signatures"
while IFS= read -r line; do
${{env.ANDROID_SDK_ROOT}}/build-tools/${{ env.BUILD_TOOLS_VERSION }}/apksigner verify --verbose --print-certs "$line"
done <<< "${{ steps.build.outputs.apks }}"
- name: Copy APK to shorter names
id: apks
run: |
cp "${{ steps.build.outputs.apk }}" "${{ env.APK_SHORT_NAME }}"
cp "${{ steps.build.outputs.apk_debug }}" "${{ env.APK_DEBUG_SHORT_NAME }}"
while IFS= read -r line; do
# Wholphin-debug-0.2.9-62-g5c12e03-16-arm64-v8a.apk => Wholphin-debug-arm64-v8a.apk
short_name="$(echo $line | sed -E 's/-[0-9]+\.[0-9]+\.[0-9]+-[0-9]+-g[a-fA-F0-9]+-[0-9]+//')"
echo "$line => $short_name"
cp "$line" "$short_name"
done <<< "${{ steps.build.outputs.apks }}"
- name: Checksums
run: |
echo "SHA256 checksums:"
sha256sum "${{ steps.build.outputs.apk }}" "${{ env.APK_SHORT_NAME }}"
sha256sum "${{ steps.build.outputs.apk_debug }}" "${{ env.APK_DEBUG_SHORT_NAME }}"
find app/build/outputs/apk -name '*.apk' | xargs sha256sum
- name: Delete ${{ env.TAG_NAME }} tag and release
uses: dev-drprasad/delete-tag-and-release@v1.1
with:
@ -99,7 +100,7 @@ jobs:
with:
allowUpdates: true
artifactErrorsFailBuild: true
artifacts: "${{ steps.build.outputs.apk_debug }},${{ steps.build.outputs.apk }},${{ env.APK_DEBUG_SHORT_NAME }},${{ env.APK_SHORT_NAME }}"
artifacts: "${{ steps.apks.outputs.apks }}"
draft: false
generateReleaseNotes: false
makeLatest: false

View file

@ -37,21 +37,31 @@ jobs:
SIGNING_KEY: "${{ secrets.SIGNING_KEY }}"
run: |
./gradlew clean assembleRelease
echo "apk=$(ls app/build/outputs/apk/release/Wholphin-release*.apk)" >> "$GITHUB_OUTPUT"
- name: Verify signature
echo "apks=$(find app/build/outputs/apk -name '*.apk' | sort)" >> "$GITHUB_OUTPUT"
- name: Verify signatures
run: |
${{env.ANDROID_SDK_ROOT}}/build-tools/${{ env.BUILD_TOOLS_VERSION }}/apksigner verify --verbose --print-certs "${{ steps.buildapp.outputs.apk }}"
echo "Verify APK signatures"
while IFS= read -r line; do
${{env.ANDROID_SDK_ROOT}}/build-tools/${{ env.BUILD_TOOLS_VERSION }}/apksigner verify --verbose --print-certs "$line"
done <<< "${{ steps.build.outputs.apks }}"
- name: Copy APK to ${{ env.APK_SHORT_NAME }}
id: apks
run: |
cp "${{ steps.buildapp.outputs.apk }}" "${{ env.APK_SHORT_NAME }}"
while IFS= read -r line; do
short_name="$(echo $line | sed -E 's/-release-[0-9]+\.[0-9]+\.[0-9]+-[0-9]+-g[a-fA-F0-9]+-[0-9]+//')"
echo "$line $short_name"
cp "$line" "$short_name"
done <<< "${{ steps.build.outputs.apks }}"
echo "apks=$(find app/build/outputs/apk -name '*.apk' -print0 | tr '\0' ',' | sed 's/,$//')" >> "$GITHUB_OUTPUT"
- name: Checksums
run: |
echo "SHA256 checksums:"
sha256sum "${{ steps.buildapp.outputs.apk }}" "${{ env.APK_SHORT_NAME }}"
find app/build/outputs/apk -name '*.apk' | xargs sha256sum
- name: Create GitHub release
uses: ncipollo/release-action@v1
with:
artifacts: "${{ steps.buildapp.outputs.apk }},${{ env.APK_SHORT_NAME }}"
artifacts: "${{ steps.apks.outputs.apks }}"
makeLatest: true
prerelease: false
generateReleaseNotes: false

View file

@ -139,8 +139,10 @@ android {
variant.outputs
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
.forEach { output ->
val abi = output.getFilter("ABI").let { if (it != null) "-$it" else "" }
val outputFileName =
"Wholphin-${variant.baseName}-${variant.versionName}-${variant.versionCode}.apk"
"Wholphin-${variant.baseName}-${variant.versionName}-${variant.versionCode}$abi.apk"
println(outputFileName)
output.outputFileName = outputFileName
}
}
@ -148,7 +150,7 @@ android {
splits {
abi {
isEnable = false
isEnable = true
reset()
include("armeabi-v7a", "arm64-v8a")
isUniversalApk = true

View file

@ -24,6 +24,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
@ -51,9 +53,8 @@ class UpdateChecker
) {
companion object {
// TODO apk names
private const val ASSET_NAME = "Wholphin.apk"
private const val DEBUG_ASSET_NAME = "Wholphin-debug.apk"
private const val RELEASE_ASSET_NAME = "Wholphin-release.apk"
private const val ASSET_NAME = "Wholphin"
private const val APK_NAME = "$ASSET_NAME.apk"
private const val APK_MIME_TYPE = "application/vnd.android.package-archive"
@ -116,15 +117,10 @@ class UpdateChecker
suspend fun getLatestRelease(updateUrl: String): Release? {
return withContext(Dispatchers.IO) {
val preferredAsset =
if (PreferenceManager
.getDefaultSharedPreferences(context)
.getBoolean("updatePreferRelease", true)
) {
RELEASE_ASSET_NAME
} else {
DEBUG_ASSET_NAME
}
val preferRelease =
PreferenceManager
.getDefaultSharedPreferences(context)
.getBoolean("updatePreferRelease", true)
val request =
Request
@ -136,21 +132,15 @@ class UpdateChecker
if (it.isSuccessful && it.body != null) {
val result = Json.parseToJsonElement(it.body!!.string())
val name = result.jsonObject["name"]?.jsonPrimitive?.contentOrNull
val version = Version.Companion.tryFromString(name)
val version = Version.tryFromString(name)
val publishedAt =
result.jsonObject["published_at"]?.jsonPrimitive?.contentOrNull
val body = result.jsonObject["body"]?.jsonPrimitive?.contentOrNull
val downloadUrl =
result.jsonObject["assets"]
?.jsonArray
?.firstOrNull { asset ->
val assetName =
asset.jsonObject["name"]?.jsonPrimitive?.contentOrNull
assetName == ASSET_NAME || assetName == preferredAsset
}?.jsonObject
?.get("browser_download_url")
?.jsonPrimitive
?.contentOrNull
?.let { assets -> getDownloadUrl(assets, preferRelease) }
Timber.v("version=$version, downloadUrl=$downloadUrl")
if (version != null) {
val notes =
if (body.isNotNullOrBlank()) {
@ -174,6 +164,35 @@ class UpdateChecker
}
}
private fun getDownloadUrl(
assets: JsonArray,
preferRelease: Boolean,
): String? {
val abiSuffix = Build.SUPPORTED_ABIS.firstOrNull().let { if (it != null) "-$it" else "" }
val releaseSuffix = if (preferRelease) "-release" else "-debug"
val preferredNames =
listOf(
"$ASSET_NAME${releaseSuffix}$abiSuffix.apk",
"$ASSET_NAME$releaseSuffix.apk",
"$ASSET_NAME.apk",
)
var preferredAsset: JsonObject? = null
outer@ for (name in preferredNames) {
for (asset in assets) {
val assetName =
asset.jsonObject["name"]?.jsonPrimitive?.contentOrNull
if (name == assetName) {
preferredAsset = asset.jsonObject
break@outer
}
}
}
return preferredAsset
?.get("browser_download_url")
?.jsonPrimitive
?.contentOrNull
}
suspend fun installRelease(
release: Release,
callback: DownloadCallback,
@ -195,7 +214,7 @@ class UpdateChecker
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentValues =
ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, ASSET_NAME)
put(MediaStore.MediaColumns.DISPLAY_NAME, APK_NAME)
put(MediaStore.MediaColumns.MIME_TYPE, APK_MIME_TYPE)
put(
MediaStore.MediaColumns.RELATIVE_PATH,
@ -267,7 +286,7 @@ class UpdateChecker
val downloadDir =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
downloadDir.mkdirs()
val targetFile = File(downloadDir, ASSET_NAME)
val targetFile = File(downloadDir, APK_NAME)
targetFile.outputStream().use { output ->
response.body!!.byteStream().use { input ->
copyTo(input, output, callback = callback)
@ -320,7 +339,7 @@ class UpdateChecker
} else {
val downloadDir =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val targetFile = File(downloadDir, ASSET_NAME)
val targetFile = File(downloadDir, APK_NAME)
if (targetFile.exists()) {
targetFile.delete()
}

View file

@ -1,6 +1,7 @@
package com.github.damontecres.wholphin.ui.detail
import android.content.Context
import android.os.Build
import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.focusable
@ -224,6 +225,11 @@ fun DebugPage(
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = "ABIs: ${Build.SUPPORTED_ABIS.toList()}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
item {
@ -237,12 +243,12 @@ fun DebugPage(
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = "Current server: ${viewModel.serverRepository.currentServer}",
text = "Current server: ${viewModel.serverRepository.currentServer.value}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = "Current user: ${viewModel.serverRepository.currentUser}",
text = "Current user: ${viewModel.serverRepository.currentUser.value}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
)