diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml new file mode 100644 index 0000000..5474fba --- /dev/null +++ b/.forgejo/workflows/release.yml @@ -0,0 +1,41 @@ +name: Release + +on: + push: + branches: [main] + +jobs: + deploy-playstore: + name: Build & Deploy to Play Store + runs-on: self-hosted + if: github.ref == 'refs/heads/main' + + steps: + - uses: actions/checkout@v4 + + - name: Prepare Keystore + env: + ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + run: | + if [ -n "$ANDROID_KEYSTORE_BASE64" ]; then + echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > android/app/upload-keystore.jks + else + echo "Error: ANDROID_KEYSTORE_BASE64 secret is not set." + exit 1 + fi + + - name: Build App Bundle + env: + ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + run: nix develop --command task build-android-bundle + + # Placeholder for Play Store upload. + # Once you have the Service Account JSON and everything is ready, + # you can uncomment this or use a specialized action. + # - name: Upload to Play Store + # env: + # PLAY_STORE_CONFIG_JSON: ${{ secrets.PLAY_STORE_CONFIG_JSON }} + # run: | + # echo "$PLAY_STORE_CONFIG_JSON" > play-store-key.json + # # Example using a tool like 'fastlane supply' or a dedicated flutter package + # # nix develop --command fvm flutter pub run ... diff --git a/README.md b/README.md index ffa6f32..fe0bb6a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ IMAP/SMTP email client written in [Flutter](https://flutter.dev). -Targets **Android, iOS, and Desktop** (Linux done; macOS, Windows, Android, iOS scaffolded). +Targets **Android, iOS, and Desktop** (Linux done; macOS, Windows, Android, iOS scaffolded). Supports **multiple accounts** — each synced independently via IMAP IDLE. ## Design philosophy: offline-first @@ -41,9 +41,9 @@ The UI never touches the network. The sync engine runs in the background and wri ## For users -Download the latest release from the [Releases page](https://github.com/guettli/sharedinbox3/releases) *(not yet published)*. - -Run the app, tap **+**, and enter your IMAP/SMTP server details. The app syncs your INBOX in the background using IMAP IDLE and works offline — the network is only needed during initial sync and when sending mail. +Run the app, tap **+**, and enter your IMAP/SMTP server details. The app syncs your INBOX in the +background using IMAP IDLE and works offline — the network is only needed during initial sync and +when sending mail. --- diff --git a/Taskfile.yml b/Taskfile.yml index d6b17aa..e134fd1 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -268,6 +268,19 @@ tasks: cmds: - ANDROID_HOME=${ANDROID_HOME:-$HOME/Android/Sdk} fvm flutter build apk --release --no-pub | grep -Ev "was tree-shaken|Tree-shaking can be disabled" + build-android-bundle: + desc: Build a release App Bundle (AAB) + deps: [_preflight, _android-sdk-check, _pub-get, generate-changelog] + method: timestamp + sources: + - lib/**/*.dart + - android/**/* + - pubspec.yaml + generates: + - build/app/outputs/bundle/release/app-release.aab + cmds: + - ANDROID_HOME=${ANDROID_HOME:-$HOME/Android/Sdk} fvm flutter build appbundle --release --no-pub | grep -Ev "was tree-shaken|Tree-shaking can be disabled" + deploy-android: desc: Build release APK and upload via scp to $ANDROID_APK_SCP_USER@$ANDROID_APK_SCP_HOST:$ANDROID_APK_SCP_PATH deps: [check, build-android] diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 078a2d0..984cce3 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } android { - namespace = "com.example.sharedinbox" + namespace = "de.sharedinbox.mua" compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion @@ -19,9 +19,20 @@ android { jvmTarget = JavaVersion.VERSION_17.toString() } + signingConfigs { + create("release") { + // Hardcoded alias matching t.sh + keyAlias = "upload" + // Use the same password for both key and keystore + val pass = System.getenv("ANDROID_KEYSTORE_PASSWORD") + storePassword = pass + keyPassword = pass + storeFile = file("upload-keystore.jks") + } + } + defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "com.example.sharedinbox" + applicationId = "de.sharedinbox.mua" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion @@ -32,11 +43,14 @@ android { buildTypes { release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig = signingConfigs.getByName("debug") - // Minification is disabled: R8 strips plugin classes (flutter_secure_storage, - // sqlite3) that are needed at runtime, causing an immediate crash on launch. + // Use the signing config defined above for release builds. + // If the keystore file exists (e.g. in CI or manually placed), sign it. + signingConfig = if (signingConfigs.getByName("release").storeFile?.exists() == true) { + signingConfigs.getByName("release") + } else { + signingConfigs.getByName("debug") + } + isMinifyEnabled = false isShrinkResources = false } diff --git a/android/app/src/main/kotlin/com/example/sharedinbox/MainActivity.kt b/android/app/src/main/kotlin/de/sharedinbox/mua/MainActivity.kt similarity index 74% rename from android/app/src/main/kotlin/com/example/sharedinbox/MainActivity.kt rename to android/app/src/main/kotlin/de/sharedinbox/mua/MainActivity.kt index ad859fe..3f87244 100644 --- a/android/app/src/main/kotlin/com/example/sharedinbox/MainActivity.kt +++ b/android/app/src/main/kotlin/de/sharedinbox/mua/MainActivity.kt @@ -1,4 +1,4 @@ -package com.example.sharedinbox +package de.sharedinbox.mua import io.flutter.embedding.android.FlutterActivity diff --git a/stalwart-dev/integration_android_test.sh b/stalwart-dev/integration_android_test.sh index 04e53c1..a1c9847 100755 --- a/stalwart-dev/integration_android_test.sh +++ b/stalwart-dev/integration_android_test.sh @@ -135,10 +135,10 @@ cd "$ROOT" # Clear any leftover app state from previous runs (stale DB, cached APK process). # Only run if the package is installed — that way any failure is a real error. -if "$ADB" -s "$EMULATOR_ID" shell pm list packages | grep -qF "com.example.sharedinbox"; then - "$ADB" -s "$EMULATOR_ID" shell am force-stop com.example.sharedinbox - "$ADB" -s "$EMULATOR_ID" shell pm clear com.example.sharedinbox - "$ADB" -s "$EMULATOR_ID" uninstall com.example.sharedinbox +if "$ADB" -s "$EMULATOR_ID" shell pm list packages | grep -qF "de.sharedinbox.mua"; then + "$ADB" -s "$EMULATOR_ID" shell am force-stop de.sharedinbox.mua + "$ADB" -s "$EMULATOR_ID" shell pm clear de.sharedinbox.mua + "$ADB" -s "$EMULATOR_ID" uninstall de.sharedinbox.mua fi ts "flutter test start"