diff --git a/.github/workflows/android-release.yaml b/.github/workflows/android-release.yaml index 38a80c6..0f186f3 100644 --- a/.github/workflows/android-release.yaml +++ b/.github/workflows/android-release.yaml @@ -26,15 +26,47 @@ jobs: distribution: temurin java-version: '25' + # Pinned so release APKs and F-Droid's build (which reads this line) + # use the same Flutter version. - name: Set up Flutter uses: subosito/flutter-action@v2 with: channel: stable + flutter-version: '3.47.2' cache: true + - name: Check the tag matches pubspec.yaml + working-directory: mobile + run: | + version=$(sed -n -E 's/^version:[[:space:]]*([^+]+)\+.*/\1/p' pubspec.yaml) + if [ "v$version" != "${GITHUB_REF_NAME}" ]; then + echo "::error::Tag ${GITHUB_REF_NAME} doesn't match pubspec.yaml version $version. Bump version (and its +build number) before tagging." + exit 1 + fi + - name: Install Flutter dependencies working-directory: mobile - run: flutter pub get + run: flutter pub get --enforce-lockfile + + - name: Set up release signing + env: + KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + run: | + if [ -z "$KEYSTORE_BASE64" ]; then + echo "::warning::ANDROID_KEYSTORE_BASE64 is not set; APKs are signed with a throwaway debug key and can't update each other." + echo "ELEPHANT_ALLOW_DEBUG_SIGNING=true" >> "$GITHUB_ENV" + exit 0 + fi + echo "$KEYSTORE_BASE64" | base64 --decode > "$RUNNER_TEMP/release.jks" + { + echo "ELEPHANT_KEYSTORE_PATH=$RUNNER_TEMP/release.jks" + echo "ELEPHANT_KEYSTORE_PASSWORD=$KEYSTORE_PASSWORD" + echo "ELEPHANT_KEY_ALIAS=$KEY_ALIAS" + echo "ELEPHANT_KEY_PASSWORD=$KEY_PASSWORD" + } >> "$GITHUB_ENV" - name: Run Flutter analyze working-directory: mobile diff --git a/fdroid/README.md b/fdroid/README.md new file mode 100644 index 0000000..717ad72 --- /dev/null +++ b/fdroid/README.md @@ -0,0 +1,60 @@ +# F-Droid + +[`in.commandlinecoding.elephant.yml`](in.commandlinecoding.elephant.yml) is the build recipe to submit to [fdroiddata](https://gitlab.com/fdroid/fdroiddata). F-Droid builds the app from source with that recipe and signs it with its own key. The store listing (description, changelogs, icon, screenshots) is read from [`metadata/en-US/`](../metadata/en-US/) in this repository. + +## Releasing a new version + +F-Droid checks for new releases by reading `mobile/pubspec.yaml` at each `v*` tag, so the version there must be bumped before tagging. + +1. Bump `version:` in `mobile/pubspec.yaml`. Increase both parts: `0.3.1+3` → `0.3.2+4`. The number after `+` is the versionCode and must always go up. +2. Add a changelog at `metadata/en-US/changelogs/<4000 + versionCode>.txt` (for `+4`, that's `4004.txt`). Keep it under 500 characters. +3. Commit, then tag `v` (e.g. `v0.3.2`) and push the tag. The release workflow fails if the tag doesn't match `pubspec.yaml`. + +F-Droid picks up the new tag automatically within a few days. + +## Version codes + +APKs are built per ABI, and Flutter derives each versionCode from the `pubspec.yaml` build number: + +| ABI | versionCode | +|---|---| +| armeabi-v7a | 1000 + build number | +| arm64-v8a | 2000 + build number | +| x86_64 | 4000 + build number | + +The recipe's `VercodeOperation` mirrors this. If a Flutter upgrade changes this scheme, update the recipe. + +## Flutter version + +F-Droid builds with the Flutter version pinned as `flutter-version` in [`.github/workflows/android-release.yaml`](../.github/workflows/android-release.yaml). To upgrade Flutter, change it there; both the GitHub release and F-Droid follow. + +## Release signing (GitHub releases) + +F-Droid signs its own builds; this only affects the APKs attached to GitHub releases. Add these repository secrets so every release is signed with the same key and users can update from one release to the next: + +| Secret | Value | +|---|---| +| `ANDROID_KEYSTORE_BASE64` | `base64 -w0 release.jks` | +| `ANDROID_KEYSTORE_PASSWORD` | Keystore password | +| `ANDROID_KEY_ALIAS` | Key alias | +| `ANDROID_KEY_PASSWORD` | Key password | + +Without them, the workflow falls back to a throwaway debug key and prints a warning. + +For local release builds, create `mobile/android/key.properties` (git-ignored): + +```properties +storeFile=/absolute/path/to/release.jks +storePassword=... +keyAlias=... +keyPassword=... +``` + +Without `key.properties` or the secrets, release builds are unsigned, which is what F-Droid expects. + +## Submitting to F-Droid + +1. Tag the release (see above). +2. Fork [fdroiddata](https://gitlab.com/fdroid/fdroiddata) and copy the recipe to `metadata/in.commandlinecoding.elephant.yml`. +3. Replace `commit: v0.3.1` in each build with the tag's full commit hash. +4. Run `fdroid lint in.commandlinecoding.elephant` and `fdroid build -v -l in.commandlinecoding.elephant`, then open a merge request. diff --git a/fdroid/in.commandlinecoding.elephant.yml b/fdroid/in.commandlinecoding.elephant.yml new file mode 100644 index 0000000..c19eb8b --- /dev/null +++ b/fdroid/in.commandlinecoding.elephant.yml @@ -0,0 +1,94 @@ +Categories: + - Internet + - Messaging +License: AGPL-3.0-only +AuthorName: CommandLineCoding +WebSite: https://github.com/CommandLineCoding/Elephant +SourceCode: https://github.com/CommandLineCoding/Elephant +IssueTracker: https://github.com/CommandLineCoding/Elephant/issues +Changelog: https://github.com/CommandLineCoding/Elephant/releases + +AutoName: Elephant + +RepoType: git +Repo: https://github.com/CommandLineCoding/Elephant.git + +Builds: + - versionName: 0.3.1 + versionCode: 1003 + commit: v0.3.1 + subdir: mobile + output: build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk + srclibs: + - flutter@stable + rm: + - mobile/ios + prebuild: + - flutterVersion=$(sed -n -E "s/.*flutter-version:\ '(.*)'/\1/p" ../.github/workflows/android-release.yaml) + - '[[ $flutterVersion ]]' + - git -C $$flutter$$ checkout -f $flutterVersion + - export PUB_CACHE=$(pwd)/.pub-cache + - $$flutter$$/bin/flutter config --no-analytics + - $$flutter$$/bin/flutter pub get --enforce-lockfile + scandelete: + - mobile/.pub-cache + build: + - export PUB_CACHE=$(pwd)/.pub-cache + - $$flutter$$/bin/flutter build apk --release --split-per-abi --target-platform="android-arm" + ndk: 28.2.13676358 + + - versionName: 0.3.1 + versionCode: 2003 + commit: v0.3.1 + subdir: mobile + output: build/app/outputs/flutter-apk/app-arm64-v8a-release.apk + srclibs: + - flutter@stable + rm: + - mobile/ios + prebuild: + - flutterVersion=$(sed -n -E "s/.*flutter-version:\ '(.*)'/\1/p" ../.github/workflows/android-release.yaml) + - '[[ $flutterVersion ]]' + - git -C $$flutter$$ checkout -f $flutterVersion + - export PUB_CACHE=$(pwd)/.pub-cache + - $$flutter$$/bin/flutter config --no-analytics + - $$flutter$$/bin/flutter pub get --enforce-lockfile + scandelete: + - mobile/.pub-cache + build: + - export PUB_CACHE=$(pwd)/.pub-cache + - $$flutter$$/bin/flutter build apk --release --split-per-abi --target-platform="android-arm64" + ndk: 28.2.13676358 + + - versionName: 0.3.1 + versionCode: 4003 + commit: v0.3.1 + subdir: mobile + output: build/app/outputs/flutter-apk/app-x86_64-release.apk + srclibs: + - flutter@stable + rm: + - mobile/ios + prebuild: + - flutterVersion=$(sed -n -E "s/.*flutter-version:\ '(.*)'/\1/p" ../.github/workflows/android-release.yaml) + - '[[ $flutterVersion ]]' + - git -C $$flutter$$ checkout -f $flutterVersion + - export PUB_CACHE=$(pwd)/.pub-cache + - $$flutter$$/bin/flutter config --no-analytics + - $$flutter$$/bin/flutter pub get --enforce-lockfile + scandelete: + - mobile/.pub-cache + build: + - export PUB_CACHE=$(pwd)/.pub-cache + - $$flutter$$/bin/flutter build apk --release --split-per-abi --target-platform="android-x64" + ndk: 28.2.13676358 + +AutoUpdateMode: Version +UpdateCheckMode: Tags +VercodeOperation: + - '%c + 1000' + - '%c + 2000' + - '%c + 4000' +UpdateCheckData: mobile/pubspec.yaml|version:\s.+\+(\d+)|.|version:\s(.+)\+ +CurrentVersion: 0.3.1 +CurrentVersionCode: 4003 diff --git a/metadata/en-US/chagelogs/23.txt b/metadata/en-US/chagelogs/23.txt deleted file mode 100644 index 98cfe2d..0000000 --- a/metadata/en-US/chagelogs/23.txt +++ /dev/null @@ -1,4 +0,0 @@ -- Added user authentication with login and registration -- Support for username and password-based accounts -- Added theme customization options -- Initial release on F-Droid \ No newline at end of file diff --git a/metadata/en-US/changelogs/4003.txt b/metadata/en-US/changelogs/4003.txt new file mode 100644 index 0000000..511ac10 --- /dev/null +++ b/metadata/en-US/changelogs/4003.txt @@ -0,0 +1,7 @@ +First release on F-Droid. +- End-to-end encrypted direct messages (Signal Protocol) with safety-number verification +- Group chats with admin roles, replies, message editing, typing indicators and read receipts +- New frosted-glass design with four themes +- Encrypted on-device storage and offline sending +- Username/password, Google or GitHub sign-in +- Connect to Elephant Cloud or your own server diff --git a/metadata/en-US/full_description.txt b/metadata/en-US/full_description.txt new file mode 100644 index 0000000..901a73c --- /dev/null +++ b/metadata/en-US/full_description.txt @@ -0,0 +1,27 @@ +Elephant is a privacy-first, open-source messenger. Direct messages are end-to-end encrypted with the Signal Protocol: your private keys never leave your device, and the server only ever stores public keys and encrypted text. + +Features + + +Your server, your data +Elephant connects to the public server at elephant.commandlinecoding.in by default. The server is also free software (AGPL-3.0), and you can run your own and point the app at it from the sign-in screen. + +Good to know + + +Source code, server setup and issue tracker: https://github.com/CommandLineCoding/Elephant diff --git a/metadata/en-US/long_description.txt b/metadata/en-US/long_description.txt deleted file mode 100644 index c82affa..0000000 --- a/metadata/en-US/long_description.txt +++ /dev/null @@ -1,13 +0,0 @@ -Elephant is a privacy-first, open-source messaging application made with Go and Flutter. - -Features: -- End-to-end encrypted direct messages using the Signal Protocol -- Contact identity verification -- Group chats with admin and member roles -- Real-time delivery over WebSocket, with typing indicators, read receipts and online status -- Replies and message editing -- Sign in with a username and password, Google or GitHub -- Encrypted on-device message storage, with offline message queueing -- Connect to the public server or to your own self-hosted Elephant server - -Note: group messages are not yet end-to-end encrypted. diff --git a/metadata/en-US/short_description.txt b/metadata/en-US/short_description.txt index 61d94bf..5bb4d35 100644 --- a/metadata/en-US/short_description.txt +++ b/metadata/en-US/short_description.txt @@ -1 +1 @@ -Elephant – a privacy-first, open-source messenger. \ No newline at end of file +Private messenger with end-to-end encrypted chats that you can self-host diff --git a/mobile/android/app/build.gradle.kts b/mobile/android/app/build.gradle.kts index b5832bb..fc06d0a 100644 --- a/mobile/android/app/build.gradle.kts +++ b/mobile/android/app/build.gradle.kts @@ -1,9 +1,25 @@ +import java.util.Properties + plugins { id("com.android.application") id("org.jetbrains.kotlin.android") id("dev.flutter.flutter-gradle-plugin") } +// Release signing comes from android/key.properties (never committed) or the +// equivalent ELEPHANT_* environment variables in CI. Without either, release +// builds are unsigned, which is what F-Droid expects: it signs with its own key. +val keystoreProperties = Properties().apply { + val file = rootProject.file("key.properties") + if (file.exists()) file.inputStream().use { load(it) } +} + +fun signingValue(key: String, env: String): String? = + keystoreProperties.getProperty(key) ?: System.getenv(env) + +val releaseStoreFile = signingValue("storeFile", "ELEPHANT_KEYSTORE_PATH") + +// Keep Google Play libraries out of the dependency graph (F-Droid inclusion policy). configurations.all { exclude(group = "com.google.android.play", module = "core") exclude(group = "com.google.android.play", module = "core-common") @@ -23,6 +39,24 @@ android { targetCompatibility = JavaVersion.VERSION_17 } + signingConfigs { + if (releaseStoreFile != null) { + create("release") { + storeFile = file(releaseStoreFile) + storePassword = signingValue("storePassword", "ELEPHANT_KEYSTORE_PASSWORD") + keyAlias = signingValue("keyAlias", "ELEPHANT_KEY_ALIAS") + keyPassword = signingValue("keyPassword", "ELEPHANT_KEY_PASSWORD") + } + } + } + + // AGP otherwise embeds a dependency report encrypted with Google's key, + // which F-Droid's scanner rejects and which breaks reproducible builds. + dependenciesInfo { + includeInApk = false + includeInBundle = false + } + defaultConfig { applicationId = "in.commandlinecoding.elephant" minSdk = flutter.minSdkVersion @@ -33,7 +67,8 @@ android { buildTypes { getByName("release") { - signingConfig = signingConfigs.getByName("debug") + // Kept on one line: F-Droid's build strips `signingConfig` lines. + signingConfig = signingConfigs.findByName("release") ?: signingConfigs.getByName("debug").takeIf { System.getenv("ELEPHANT_ALLOW_DEBUG_SIGNING") == "true" } isMinifyEnabled = true isShrinkResources = true proguardFiles( @@ -54,49 +89,8 @@ tasks.withType().configureEach } } -val abiCodes = mapOf("armeabi-v7a" to 1, "arm64-v8a" to 2, "x86_64" to 3) - -androidComponents { - onVariants { variant -> - variant.outputs.forEach { output -> - val abi = output.filters.find { - it.filterType == com.android.build.api.variant.FilterConfiguration.FilterType.ABI - }?.identifier - - val baseAbiCode = abiCodes[abi] - if (baseAbiCode != null) { - val baseVersionCode = flutter.versionCode - output.versionCode.set(baseVersionCode * 10 + baseAbiCode) - } - } - } -} - -project.afterEvaluate { - tasks.configureEach { - if (name.contains("minifyReleaseWithR8") || name.contains("minifyReleaseWithProguard") || name.contains("dexBuilderRelease")) { - doFirst { - logger.lifecycle("FOSS SANITIZER: Active. Sweeping intermediate files for non-free binaries...") - - val searchDirs = listOf( - File(project.layout.buildDirectory.asFile.get(), "intermediates/classes/release"), - File(project.layout.buildDirectory.asFile.get(), "intermediates/javac/release") - ) - - for (dir in searchDirs) { - if (dir.exists()) { - dir.walkBottomUp().forEach { file -> - val normalizedPath = file.absolutePath.replace('\\', '/') - if (file.isDirectory && normalizedPath.endsWith("com/google/android/play/core")) { - file.deleteRecursively() - logger.lifecycle("Successfully removed vendor-shaded tracking package: $normalizedPath") - } - } - } - } - } - } - } -} +// Per-ABI versionCodes come from Flutter itself with --split-per-abi: +// 1000 + base (armeabi-v7a), 2000 + base (arm64-v8a), 4000 + base (x86_64). +// fdroid/in.commandlinecoding.elephant.yml depends on this scheme. dependencies {} \ No newline at end of file diff --git a/mobile/lib/pages/settings/help_about.dart b/mobile/lib/pages/settings/help_about.dart index 538243a..de3f365 100644 --- a/mobile/lib/pages/settings/help_about.dart +++ b/mobile/lib/pages/settings/help_about.dart @@ -7,7 +7,7 @@ import 'package:url_launcher/url_launcher.dart'; class HelpAboutPage extends StatelessWidget { const HelpAboutPage({super.key}); - static const String appVersion = '0.3.0'; + static const String appVersion = '0.3.1'; static const String githubUrl = 'https://github.com/commandlinecoding/elephant'; diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 5d604d8..82e0957 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -231,7 +231,7 @@ packages: source: hosted version: "4.5.2" flutter_launcher_icons: - dependency: "direct main" + dependency: "direct dev" description: name: flutter_launcher_icons sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" @@ -401,7 +401,7 @@ packages: source: hosted version: "4.12.0" launcher_name: - dependency: "direct main" + dependency: "direct dev" description: name: launcher_name sha256: "5fc9a8b8de9e255d5f21effc33632b5620771c9b2310d459a7710b725353b305" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 031a465..8da1458 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.2.0+2 +version: 0.3.1+3 environment: sdk: ^3.12.0 @@ -33,8 +33,6 @@ dependencies: web_socket_channel: ^3.0.3 provider: ^6.1.5+1 - launcher_name: ^1.0.2 - flutter_launcher_icons: ^0.14.4 flutter_secure_storage: ^9.2.2 dio: ^5.11.0 flutter_markdown_plus: ^1.0.12 @@ -67,6 +65,10 @@ dev_dependencies: flutter_test: sdk: flutter + # Build-time generators for the launcher name and icons (configured above). + launcher_name: ^1.0.2 + flutter_launcher_icons: ^0.14.4 + # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your