Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion .github/workflows/android-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
60 changes: 60 additions & 0 deletions fdroid/README.md
Original file line number Diff line number Diff line change
@@ -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<version>` (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.
94 changes: 94 additions & 0 deletions fdroid/in.commandlinecoding.elephant.yml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 0 additions & 4 deletions metadata/en-US/chagelogs/23.txt

This file was deleted.

7 changes: 7 additions & 0 deletions metadata/en-US/changelogs/4003.txt
Original file line number Diff line number Diff line change
@@ -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
27 changes: 27 additions & 0 deletions metadata/en-US/full_description.txt
Original file line number Diff line number Diff line change
@@ -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.

<b>Features</b>
<ul>
<li>End-to-end encrypted one-to-one chats (Signal Protocol: X3DH and Double Ratchet)</li>
<li>Safety numbers with QR codes to verify who you are talking to</li>
<li>Group chats with admin and member roles</li>
<li>Real-time delivery with typing indicators, read receipts and online status</li>
<li>Replies with quoted messages, Markdown formatting, and editing your messages for 15 minutes after sending</li>
<li>Messages stored on your device in an encrypted database (SQLCipher)</li>
<li>Messages written offline are queued and sent when you reconnect</li>
<li>Find people by username and share yours as a QR code</li>
<li>Sign in with a username and password, or optionally with Google or GitHub</li>
<li>Four themes: Frost, Aura, Onyx and Nebula</li>
</ul>

<b>Your server, your data</b>
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.

<b>Good to know</b>
<ul>
<li>Group messages are encrypted in transit (TLS) but are not end-to-end encrypted yet.</li>
<li>Google and GitHub sign-in are optional and open in your browser. A username and password account needs no third-party service.</li>
<li>No ads, analytics or trackers.</li>
</ul>

Source code, server setup and issue tracker: https://github.com/CommandLineCoding/Elephant
13 changes: 0 additions & 13 deletions metadata/en-US/long_description.txt

This file was deleted.

2 changes: 1 addition & 1 deletion metadata/en-US/short_description.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Elephant – a privacy-first, open-source messenger.
Private messenger with end-to-end encrypted chats that you can self-host
84 changes: 39 additions & 45 deletions mobile/android/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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")
Expand All @@ -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
Expand All @@ -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(
Expand All @@ -54,49 +89,8 @@ tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().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 {}
2 changes: 1 addition & 1 deletion mobile/lib/pages/settings/help_about.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Loading
Loading