mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-14 11:30:18 +00:00
android: add google and foss build flavors
This commit is contained in:
@@ -9,11 +9,19 @@ This is the **Kotlin Multiplatform (KMP)** mobile and desktop client for SimpleX
|
||||
## Build Commands
|
||||
|
||||
```bash
|
||||
# Android debug APK
|
||||
./gradlew assembleDebug
|
||||
# Android debug APK, assembleGoogleDebug builds the flavor with the Play Billing dependency
|
||||
./gradlew assembleFossDebug
|
||||
|
||||
# Android release APK
|
||||
./gradlew assembleRelease
|
||||
# Android release APK, distributed via F-Droid and GitHub
|
||||
./gradlew assembleFossRelease
|
||||
|
||||
# Android app bundle, distributed via Google Play, includes Play Billing
|
||||
./gradlew bundleGoogleRelease
|
||||
|
||||
# Always name the flavor for releases. The aggregate tasks (build, assemble, assembleRelease,
|
||||
# bundle, bundleRelease) fail on purpose: they would package a release APK with Play Billing,
|
||||
# or an app bundle without it.
|
||||
# The fdroiddata recipe defaults to assembleRelease and must be changed to assembleFossRelease.
|
||||
|
||||
# Desktop distribution (current OS)
|
||||
./gradlew :desktop:packageDistributionForCurrentOS
|
||||
@@ -22,7 +30,7 @@ This is the **Kotlin Multiplatform (KMP)** mobile and desktop client for SimpleX
|
||||
./gradlew desktopTest
|
||||
|
||||
# Run Android instrumented tests (requires connected device/emulator)
|
||||
./gradlew connectedAndroidTest
|
||||
./gradlew connectedFossDebugAndroidTest
|
||||
|
||||
# Build native libraries for all platforms
|
||||
./gradlew common:cmakeBuild -PcrossCompile
|
||||
|
||||
@@ -35,6 +35,21 @@ android {
|
||||
manifestPlaceholders["extract_native_libs"] = rootProject.extra["compression.level"] as Int != 0
|
||||
}
|
||||
|
||||
// `google` is distributed via Google Play as an app bundle and includes Play Billing.
|
||||
// `foss` is distributed via F-Droid and as APKs on GitHub, without Play dependencies.
|
||||
flavorDimensions += "store"
|
||||
productFlavors {
|
||||
create("google") {
|
||||
dimension = "store"
|
||||
buildConfigField("boolean", "PLAY_STORE", "true")
|
||||
}
|
||||
create("foss") {
|
||||
dimension = "store"
|
||||
isDefault = true
|
||||
buildConfigField("boolean", "PLAY_STORE", "false")
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix = rootProject.extra["application_id.suffix"] as String
|
||||
@@ -128,8 +143,28 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
// The graph is checked rather than the requested task, because every aggregate task
|
||||
// (assemble, assembleRelease, build, bundle, ...) packages these variants too.
|
||||
val projectPath = project.path
|
||||
val apkTasks = setOf("packageFossDebug", "packageGoogleDebug", "packageFossRelease", "packageGoogleRelease")
|
||||
val apkTaskPaths = apkTasks.map { "$projectPath:$it" }.toSet()
|
||||
val bundleTaskPaths = apkTaskPaths.map { it + "Bundle" }.toSet()
|
||||
gradle.taskGraph.whenReady {
|
||||
if (hasTask("$projectPath:packageGoogleRelease")) {
|
||||
throw GradleException("A release apk must not include Play Billing, use assembleFossRelease or bundleGoogleRelease")
|
||||
}
|
||||
if (hasTask("$projectPath:packageFossReleaseBundle")) {
|
||||
throw GradleException("An app bundle must include Play Billing, use bundleGoogleRelease or assembleFossRelease")
|
||||
}
|
||||
// `isBundle` above is derived from the whole invocation, so a bundle in it disables abi splits
|
||||
if (apkTaskPaths.any { hasTask(it) } && bundleTaskPaths.any { hasTask(it) }) {
|
||||
throw GradleException("Build the apks and the bundle in separate invocations, the bundle disables abi splits")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":common"))
|
||||
"googleImplementation"("com.android.billingclient:billing:9.1.0")
|
||||
implementation("androidx.core:core-ktx:1.13.1")
|
||||
//implementation("androidx.compose.ui:ui:${rootProject.extra["compose.version"] as String}")
|
||||
//implementation("androidx.compose.material:material:$compose_version")
|
||||
@@ -160,58 +195,61 @@ dependencies {
|
||||
tasks {
|
||||
val compressApk by creating {
|
||||
doLast {
|
||||
val isRelease = gradle.startParameter.taskNames.find { it.lowercase().contains("release") } != null
|
||||
val buildType: String = if (isRelease) "release" else "debug"
|
||||
val javaHome = System.getProperties()["java.home"] ?: org.gradle.internal.jvm.Jvm.current().javaHome
|
||||
val sdkDir = android.sdkDirectory.absolutePath
|
||||
val keyAlias: String
|
||||
val keyPassword: String
|
||||
val storeFile: String
|
||||
val storePassword: String
|
||||
if (project.properties["android.injected.signing.key.alias"] != null) {
|
||||
keyAlias = project.properties["android.injected.signing.key.alias"] as String
|
||||
keyPassword = project.properties["android.injected.signing.key.password"] as String
|
||||
storeFile = project.properties["android.injected.signing.store.file"] as String
|
||||
storePassword = project.properties["android.injected.signing.store.password"] as String
|
||||
} else {
|
||||
try {
|
||||
val gradleConfig = android.signingConfigs.getByName(buildType)
|
||||
keyAlias = gradleConfig.keyAlias!!
|
||||
keyPassword = gradleConfig.keyPassword!!
|
||||
storeFile = gradleConfig.storeFile!!.absolutePath
|
||||
storePassword = gradleConfig.storePassword!!
|
||||
} catch (e: UnknownDomainObjectException) {
|
||||
// There is no signing config for current build type, can"t sign the apk
|
||||
println("No signing configs for this build type: $buildType")
|
||||
return@doLast
|
||||
// A single invocation can package more than one variant, for example assembleDebug
|
||||
gradle.taskGraph.allTasks.filter { it.path in apkTaskPaths }.forEach { packageTask ->
|
||||
val variant = packageTask.name.removePrefix("package")
|
||||
val buildType: String = if (variant.endsWith("Release")) "release" else "debug"
|
||||
val keyAlias: String
|
||||
val keyPassword: String
|
||||
val storeFile: String
|
||||
val storePassword: String
|
||||
if (project.properties["android.injected.signing.key.alias"] != null) {
|
||||
keyAlias = project.properties["android.injected.signing.key.alias"] as String
|
||||
keyPassword = project.properties["android.injected.signing.key.password"] as String
|
||||
storeFile = project.properties["android.injected.signing.store.file"] as String
|
||||
storePassword = project.properties["android.injected.signing.store.password"] as String
|
||||
} else {
|
||||
try {
|
||||
val gradleConfig = android.signingConfigs.getByName(buildType)
|
||||
keyAlias = gradleConfig.keyAlias!!
|
||||
keyPassword = gradleConfig.keyPassword!!
|
||||
storeFile = gradleConfig.storeFile!!.absolutePath
|
||||
storePassword = gradleConfig.storePassword!!
|
||||
} catch (e: UnknownDomainObjectException) {
|
||||
// There is no signing config for current build type, can"t sign the apk
|
||||
println("No signing configs for this build type: $buildType")
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
val outputDir = packageTask.outputs.files.files.last()
|
||||
exec {
|
||||
workingDir("../../scripts/android")
|
||||
environment = mapOf(
|
||||
"JAVA_HOME" to "$javaHome",
|
||||
"PATH" to "${System.getenv("PATH")}:$javaHome/bin"
|
||||
)
|
||||
commandLine = listOf(
|
||||
"./compress-and-sign-apk.sh",
|
||||
"${rootProject.extra["compression.level"]}",
|
||||
"$outputDir",
|
||||
sdkDir,
|
||||
storeFile,
|
||||
storePassword,
|
||||
keyAlias,
|
||||
keyPassword
|
||||
)
|
||||
}
|
||||
}
|
||||
lateinit var outputDir: File
|
||||
named(if (isRelease) "packageRelease" else "packageDebug") {
|
||||
outputDir = outputs.files.files.last()
|
||||
}
|
||||
exec {
|
||||
workingDir("../../scripts/android")
|
||||
environment = mapOf(
|
||||
"JAVA_HOME" to "$javaHome",
|
||||
"PATH" to "${System.getenv("PATH")}:$javaHome/bin"
|
||||
)
|
||||
commandLine = listOf(
|
||||
"./compress-and-sign-apk.sh",
|
||||
"${rootProject.extra["compression.level"]}",
|
||||
"$outputDir",
|
||||
sdkDir,
|
||||
storeFile,
|
||||
storePassword,
|
||||
keyAlias,
|
||||
keyPassword
|
||||
)
|
||||
}
|
||||
|
||||
if (project.properties["android.injected.signing.key.alias"] != null && buildType == "release") {
|
||||
File(outputDir, "android-release.apk").renameTo(File(outputDir, "simplex.apk"))
|
||||
File(outputDir, "android-armeabi-v7a-release.apk").renameTo(File(outputDir, "simplex-armv7a.apk"))
|
||||
File(outputDir, "android-arm64-v8a-release.apk").renameTo(File(outputDir, "simplex.apk"))
|
||||
if (project.properties["android.injected.signing.key.alias"] != null && buildType == "release") {
|
||||
val flavor = variant.removeSuffix("Release").lowercase()
|
||||
mapOf("arm64-v8a" to "simplex.apk", "armeabi-v7a" to "simplex-armv7a.apk").forEach { (abi, name) ->
|
||||
if (!File(outputDir, "android-$flavor-$abi-release.apk").renameTo(File(outputDir, name))) {
|
||||
logger.warn("No $abi apk to rename to $name")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// View all gradle properties set
|
||||
// project.properties.each { k, v -> println "$k -> $v" }
|
||||
@@ -221,9 +259,7 @@ tasks {
|
||||
// Don"t do anything if no compression is needed
|
||||
if (rootProject.extra["compression.level"] as Int != 0) {
|
||||
whenTaskAdded {
|
||||
if (name == "packageDebug") {
|
||||
finalizedBy(compressApk)
|
||||
} else if (name == "packageRelease") {
|
||||
if (name in apkTasks) {
|
||||
finalizedBy(compressApk)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,6 +370,8 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
override fun androidCreateActiveCallState(): Closeable = ActiveCallState()
|
||||
|
||||
override val androidApiLevel: Int get() = Build.VERSION.SDK_INT
|
||||
|
||||
override val androidIsPlayStoreBuild: Boolean get() = BuildConfig.PLAY_STORE
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -189,7 +189,6 @@ buildConfig {
|
||||
buildConfigField("String", "DESKTOP_VERSION_NAME", "\"${extra["desktop.version_name"]}\"")
|
||||
buildConfigField("int", "DESKTOP_VERSION_CODE", "${extra["desktop.version_code"]}")
|
||||
buildConfigField("String", "DATABASE_BACKEND", "\"${extra["database.backend"]}\"")
|
||||
buildConfigField("Boolean", "ANDROID_BUNDLE", "${extra["android.bundle"]}")
|
||||
buildConfigField("Boolean", "SIMPLEX_ASSETS", "$hasSimplexAssets")
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -30,6 +30,8 @@ interface PlatformInterface {
|
||||
fun androidCreateActiveCallState(): Closeable = Closeable { }
|
||||
fun androidIsXiaomiDevice(): Boolean = false
|
||||
val androidApiLevel: Int? get() = null
|
||||
// The build distributed via Google Play, which has to follow its policies
|
||||
val androidIsPlayStoreBuild: Boolean get() = false
|
||||
@Composable fun androidLockPortraitOrientation() {}
|
||||
suspend fun androidAskToAllowBackgroundCalls(): Boolean = true
|
||||
@Composable fun desktopShowAppUpdateNotice() {}
|
||||
|
||||
+1
-2
@@ -22,7 +22,6 @@ import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.BuildConfigCommon
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
@@ -143,7 +142,7 @@ fun HelpAndSupportView(
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView(stringResource(MR.strings.settings_section_title_support_project)) {
|
||||
if (!BuildConfigCommon.ANDROID_BUNDLE) {
|
||||
if (!platform.androidIsPlayStoreBuild) {
|
||||
ContributeItem(uriHandler)
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
|
||||
@@ -27,8 +27,6 @@ kotlin.jvm.target=11
|
||||
android.version_name=7.0
|
||||
android.version_code=366
|
||||
|
||||
android.bundle=false
|
||||
|
||||
desktop.version_name=7.0
|
||||
desktop.version_code=155
|
||||
|
||||
|
||||
@@ -370,6 +370,7 @@ var platform: PlatformInterface = object : PlatformInterface {}
|
||||
| `androidCreateActiveCallState()` | empty `Closeable` | Create `ActiveCallState` |
|
||||
| `androidIsXiaomiDevice()` | `false` | Check device brand |
|
||||
| `androidApiLevel` | `null` | `Build.VERSION.SDK_INT` |
|
||||
| `androidIsPlayStoreBuild` | `false` | `BuildConfig.PLAY_STORE` |
|
||||
| `androidLockPortraitOrientation()` | no-op | Lock to `SCREEN_ORIENTATION_PORTRAIT` |
|
||||
| `androidAskToAllowBackgroundCalls()` | `true` | Show battery restriction dialog |
|
||||
| `desktopShowAppUpdateNotice()` | no-op | Show update notice (Desktop only) |
|
||||
|
||||
@@ -68,14 +68,15 @@ The project uses several custom forks managed via `cabal.project`:
|
||||
```bash
|
||||
cd apps/multiplatform
|
||||
|
||||
# Build Android debug APK
|
||||
./gradlew assembleDebug
|
||||
# Build Android debug APK; `foss` ships to F-Droid/GitHub, `google` adds Play Billing.
|
||||
# The aggregate tasks fail by design, see apps/multiplatform/README.md
|
||||
./gradlew assembleFossDebug
|
||||
|
||||
# Build desktop
|
||||
./gradlew :desktop:packageDistributionForCurrentOS
|
||||
|
||||
# Run Android tests
|
||||
./gradlew connectedAndroidTest
|
||||
./gradlew connectedFossDebugAndroidTest
|
||||
```
|
||||
|
||||
### iOS
|
||||
|
||||
@@ -23,5 +23,8 @@ unzip -o "$tmp/libsimplex.zip" -d "$tmp/simplex-chat/apps/multiplatform/common/s
|
||||
curl -sSf "$libsup" -o "$tmp/libsupport.zip"
|
||||
unzip -o "$tmp/libsupport.zip" -d "$tmp/simplex-chat/apps/multiplatform/common/src/commonMain/cpp/android/libs/arm64-v8a"
|
||||
|
||||
gradle -p "$tmp/simplex-chat/apps/multiplatform/" -Psimplex.assets.dir=../../assets clean build
|
||||
cp "$tmp/simplex-chat/apps/multiplatform/android/build/outputs/apk/release/android-release-unsigned.apk" "$PWD/simplex-chat.apk"
|
||||
# Build only the arch the libs were downloaded for
|
||||
sed -i.bak 's/include(.*/include("arm64-v8a")/' "$tmp/simplex-chat/apps/multiplatform/android/build.gradle.kts"
|
||||
|
||||
gradle -p "$tmp/simplex-chat/apps/multiplatform/" -Psimplex.assets.dir=../../assets clean :android:assembleFossRelease
|
||||
cp "$tmp/simplex-chat/apps/multiplatform/android/build/outputs/apk/foss/release/android-foss-arm64-v8a-release-unsigned.apk" "$PWD/simplex-chat.apk"
|
||||
|
||||
@@ -101,7 +101,7 @@ build() {
|
||||
sed -i.bak 's/${extract_native_libs}/true/' "$folder/apps/multiplatform/android/src/main/AndroidManifest.xml"
|
||||
sed -i.bak 's/jniLibs.useLegacyPackaging =.*/jniLibs.useLegacyPackaging = true/' "$folder/apps/multiplatform/android/build.gradle.kts"
|
||||
sed -i.bak '/android {/a lint {abortOnError = false}' "$folder/apps/multiplatform/android/build.gradle.kts"
|
||||
sed -i.bak '/tasks/Q' "$folder/apps/multiplatform/android/build.gradle.kts"
|
||||
sed -i.bak '/^tasks {/Q' "$folder/apps/multiplatform/android/build.gradle.kts"
|
||||
sed -i.bak "s/android.version_code=.*/android.version_code=${vercode}/" "$folder/apps/multiplatform/gradle.properties"
|
||||
|
||||
for arch in $arches; do
|
||||
@@ -119,7 +119,7 @@ build() {
|
||||
arch_map "$arch"
|
||||
|
||||
android_tmp_folder="${tmp}/android-${arch}"
|
||||
android_apk_output="${folder}/apps/multiplatform/android/build/outputs/apk/release/android-${android_arch}-release-unsigned.apk"
|
||||
android_apk_output="${folder}/apps/multiplatform/android/build/outputs/apk/foss/release/android-foss-${android_arch}-release-unsigned.apk"
|
||||
android_apk_output_final="simplex-chat-${android_arch}.apk"
|
||||
libs_folder="${folder}/apps/multiplatform/common/src/commonMain/cpp/android/libs"
|
||||
|
||||
@@ -134,7 +134,7 @@ build() {
|
||||
|
||||
# Build only one arch
|
||||
sed -i.bak "s/include(.*/include(\"${android_arch}\")/" "$folder/apps/multiplatform/android/build.gradle.kts"
|
||||
gradle -p "$folder/apps/multiplatform/" -Psimplex.assets.dir=../../assets clean :android:assembleRelease
|
||||
gradle -p "$folder/apps/multiplatform/" -Psimplex.assets.dir=../../assets clean :android:assembleFossRelease
|
||||
|
||||
mkdir -p "$android_tmp_folder"
|
||||
unzip -oqd "$android_tmp_folder" "$android_apk_output"
|
||||
|
||||
@@ -118,7 +118,7 @@ check_apk() {
|
||||
verify_apk() {
|
||||
apk_name="$1"
|
||||
|
||||
# Release APKs are packaged by AGP (gradle :android:assembleRelease; AGP version is
|
||||
# Release APKs are packaged by AGP (gradle :android:assembleFossRelease; AGP version is
|
||||
# gradle.plugin.version in apps/multiplatform/gradle.properties), which zero-pads ZIP
|
||||
# alignment. Do NOT add --pad-like-apksigner (standalone apksigner >= 35.0.0-rc1 uses
|
||||
# the 0xd935 extra-field padding) unless AGP is bumped to a packager that uses it —
|
||||
|
||||
Reference in New Issue
Block a user