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

View file

@ -37,21 +37,31 @@ jobs:
SIGNING_KEY: "${{ secrets.SIGNING_KEY }}" SIGNING_KEY: "${{ secrets.SIGNING_KEY }}"
run: | run: |
./gradlew clean assembleRelease ./gradlew clean assembleRelease
echo "apk=$(ls app/build/outputs/apk/release/Wholphin-release*.apk)" >> "$GITHUB_OUTPUT" echo "apks=$(find app/build/outputs/apk -name '*.apk' | sort)" >> "$GITHUB_OUTPUT"
- name: Verify signature - name: Verify signatures
run: | 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 }} - name: Copy APK to ${{ env.APK_SHORT_NAME }}
id: apks
run: | 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 - name: Checksums
run: | run: |
echo "SHA256 checksums:" 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 - name: Create GitHub release
uses: ncipollo/release-action@v1 uses: ncipollo/release-action@v1
with: with:
artifacts: "${{ steps.buildapp.outputs.apk }},${{ env.APK_SHORT_NAME }}" artifacts: "${{ steps.apks.outputs.apks }}"
makeLatest: true makeLatest: true
prerelease: false prerelease: false
generateReleaseNotes: false generateReleaseNotes: false

View file

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

View file

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

View file

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