Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ce8eaf1
Add iPhone relay health monitoring
Xare123 Jul 24, 2026
5db2ec5
Harden iPhone relay health monitoring
Xare123 Jul 24, 2026
991b805
Show relay health only for iPhone relays
Xare123 Jul 24, 2026
ab9293a
Clean up stale UI listeners
Xare123 Jul 24, 2026
cf7e519
Fix UI lifecycle and relay retry edge cases
Xare123 Jul 24, 2026
b576167
Avoid eager full-resolution image decoding
Xare123 Jul 24, 2026
7478ceb
Improve Android control accessibility
Xare123 Jul 24, 2026
9cf1034
Build resilient Alpha profile APK
Xare123 Jul 24, 2026
95ef277
Prioritize Alpha profile artifact
Xare123 Jul 24, 2026
ad26128
Remove workflow trailing whitespace
Xare123 Jul 24, 2026
e6e284c
Compile resilient anisette provisioning
Xare123 Jul 24, 2026
018bd01
Retry anisette transport failures
Xare123 Jul 24, 2026
1fc63f0
Harden message refresh and receive diagnostics
Xare123 Jul 24, 2026
b2c8184
Use fresh message service on transcript refresh
Xare123 Jul 24, 2026
c01e6b7
Fix nullable reaction preview fallback
Xare123 Jul 24, 2026
dc2bd90
Run focused message helper test
Xare123 Jul 24, 2026
f8fe0e2
docs: add Android development and diagnostics guide
Xare123 Jul 24, 2026
95c08b5
docs: define delivery and performance verification plan
Xare123 Jul 24, 2026
b7e9b98
Harden background routing and reconnect lifecycle
Xare123 Jul 24, 2026
d2e8c6b
Back off terminal APS poll panics
Xare123 Jul 24, 2026
d332a41
Register socket callbacks before connecting
Xare123 Jul 24, 2026
696461d
fix: preserve incoming message delivery integrity
Xare123 Jul 24, 2026
18c8cdb
Fix avatar resource leaks and push acknowledgement ordering
Xare123 Jul 24, 2026
2f324f2
fix: harden group participant and reaction handling
Xare123 Jul 24, 2026
e7623f9
Refresh group avatars without background controller churn
Xare123 Jul 24, 2026
7791de6
Harden partial widget initialization and profile loading
Xare123 Jul 25, 2026
cdac24b
Use upstream RustPush provisioning dependency
Xare123 Jul 25, 2026
27a2d33
Close remaining UI lifecycle races
Xare123 Jul 25, 2026
1da494a
Use HTTPS for public submodules
Xare123 Jul 25, 2026
92eeedb
Fix reply navigation and message lifecycle races
Xare123 Jul 25, 2026
d31edcf
Fix recycled message controller remounts
Xare123 Jul 25, 2026
d6eba36
Make conversation keyboard dismissal reliable
Xare123 Jul 25, 2026
6028e16
Load complete reply threads with originators
Xare123 Jul 25, 2026
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
20 changes: 15 additions & 5 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,22 @@ jobs:
cp rustpush/certs/legacy-fairplay/fairplay.crt rustpush/certs/fairplay/$name.crt
done

- name: Run focused message helper tests
run: flutter test test/helpers/message_helper_test.dart

# First run is expected to fail until ffmpeg_kit_flutter_new is fixed.
- name: Run Build Script
run: |
flutter build apk --flavor alpha --debug --target-platform android-arm64

- name: Build Alpha Profile APK
run: flutter build apk --flavor alpha --profile --target-platform android-arm64

- uses: actions/upload-artifact@v4
with:
name: Alpha Profile APK
path: build/app/outputs/flutter-apk/app-alpha-profile.apk

- name: Build Alpha Debug APK
run: flutter build apk --flavor alpha --debug --target-platform android-arm64

- uses: actions/upload-artifact@v4
with:
name: Alpha Debug APK
path: build/app/outputs/flutter-apk/app-alpha-debug.apk
path: build/app/outputs/flutter-apk/app-alpha-debug.apk
4 changes: 2 additions & 2 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[submodule "rustpush"]
path = rustpush
url = git@github.com:OpenBubbles/rustpush.git
url = https://github.com/OpenBubbles/rustpush.git
[submodule "telephony_plus"]
path = telephony_plus
url = git@github.com:OpenBubbles/telephony_plus.git
url = https://github.com/OpenBubbles/telephony_plus.git
7 changes: 6 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ We encourage all contributions to this project! All we ask are you follow these
Please make sure you have completed the following pre-requisites:

* Install Git: [download](https://git-scm.com/downloads)
* Install Java: [download](https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html)
* Install Java 21: [download](https://adoptium.net/temurin/releases/?version=21)
* Install Flutter: [guide/download](https://flutter.dev/docs/get-started/install)
* Install Android Studio [download](https://developer.android.com/studio)
- Also install the Flutter & Dart Plugins via the Plugin Manager
Expand All @@ -26,6 +26,11 @@ Once you have a code editor installed, remember to install all of the required p
* Flutter
* Intellisense/Intellicode

Before opening a pull request, read the repository-specific
[development and diagnostics notes](docs/DEVELOPMENT.md). Do not commit relay
registration codes, Apple credentials, phone numbers, message text, generated
signing files, or `.env` values.

## Forking the Repository

In order to start contributing, follow these steps:
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,11 @@ If you need help setting up the app, have any issues or feature requests, or jus
## Getting Started

[Quickstart](https://openbubbles.app/quickstart.html)

## Contributor documentation

The repository-specific development and diagnostic notes are in
[`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) and
[`docs/DIAGNOSTICS.md`](docs/DIAGNOSTICS.md). They document the Android build
matrix, the iMessage/relay versus SMS/MMS/RCS boundary, safe log collection,
and the current known limitations.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import android.content.Context
import android.content.Intent
import android.util.Log
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import com.bluebubbles.messaging.Constants
Expand Down Expand Up @@ -47,6 +49,9 @@ class SocketIOForegroundService : Service() {
private var isBeingDestroyed: Boolean = false

private var hasStarted: Boolean = false
private val reconnectHandler = Handler(Looper.getMainLooper())
private var reconnectRunnable: Runnable? = null
private var reconnectAttempt: Int = 0

private val eventBlacklist: Array<String> = arrayOf(
"typing-indicator",
Expand Down Expand Up @@ -102,6 +107,9 @@ class SocketIOForegroundService : Service() {
Log.d(Constants.logTag, "Foreground Service is connecting to: $serverUrl")

val opts = IO.Options()
// Reconnects are scheduled by this service so the Socket.IO manager
// cannot race a second retry loop with our URL/service lifecycle.
opts.reconnection = false

try {
// Read the custom headers JSON string from preferences and parse it into a map
Expand All @@ -124,17 +132,20 @@ class SocketIOForegroundService : Service() {
val encodedPw = URLEncoder.encode(storedPassword, "UTF-8")
opts.query = "password=$encodedPw"
mSocket = IO.socket(serverUrl, opts)
mSocket!!.connect()

mSocket!!.on(Socket.EVENT_CONNECT) {
Log.d(Constants.logTag, "Socket.io connected to your server!")
reconnectAttempt = 0
reconnectRunnable?.let { reconnectHandler.removeCallbacks(it) }
reconnectRunnable = null
updateNotification(CONNECTED)
}

mSocket!!.on(Socket.EVENT_CONNECT_ERROR) { args ->
val error = args[0] as Exception
Log.d(Constants.logTag, "Socket.io failed to connect to $serverUrl! Error: ${error.message}")
updateNotification(CONNECT_FAILED + error.message)
tryReconnect()
}

// with reason, details args
Expand All @@ -148,6 +159,7 @@ class SocketIOForegroundService : Service() {
val details = args.getOrNull(1)
Log.d(Constants.logTag, "Socket.io disconnected from server! Reason: $reason, Details: $details")
updateNotification(DISCONNECTED + reason)
tryReconnect()
}

mSocket!!.on("reconnecting") {
Expand All @@ -165,15 +177,17 @@ class SocketIOForegroundService : Service() {
val event = args[0] as String
val message = args[1] as JSONObject

Log.d(Constants.logTag, "Received event of type $event from Socket.io...")
if (!eventBlacklist.contains(event)) {
Log.d(Constants.logTag, "Received event of type $event from Socket.io...")
DartWorkManager.createWorker(applicationContext, "socket-event", hashMapOf("event" to event, "data" to message.toString())) {}
} else {
Log.d(Constants.logTag, "Ignored event of type $event from Socket.io...")
}
}
}

// Register every callback before opening the transport so an
// immediate connect or event cannot race listener setup.
mSocket!!.connect()
} catch (e: Exception) {
if (isBeingDestroyed) {
return
Expand All @@ -190,11 +204,18 @@ class SocketIOForegroundService : Service() {

private fun tryReconnect() {
if (mSocket != null && !mSocket!!.connected()) {
Log.e(Constants.logTag, "Waiting 30 seconds before reconnecting...")

// Sleep for 30 seconds before attempting to reconnect
Thread.sleep(30000)
mSocket!!.connect()
if (reconnectRunnable != null) return
val delaySeconds = 30L * (1L shl reconnectAttempt.coerceAtMost(3))
reconnectAttempt = (reconnectAttempt + 1).coerceAtMost(3)
Log.e(Constants.logTag, "Scheduling reconnect in ${delaySeconds}s...")
val runnable = Runnable {
reconnectRunnable = null
if (!isBeingDestroyed && mSocket != null && !mSocket!!.connected()) {
mSocket!!.connect()
}
}
reconnectRunnable = runnable
reconnectHandler.postDelayed(runnable, delaySeconds * 1000L)
}
}

Expand Down Expand Up @@ -261,6 +282,8 @@ class SocketIOForegroundService : Service() {
override fun onDestroy() {
isBeingDestroyed = true
hasStarted = false
reconnectRunnable?.let { reconnectHandler.removeCallbacks(it) }
reconnectRunnable = null
Log.d(Constants.logTag, "BlueBubbles Service is being destroyed!")

super.onDestroy()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,10 @@ class APNService : Service(), MsgReceiver {
override fun receievedMsg(ptr: ULong, retry: ULong) {
Handler(Looper.getMainLooper()).post {
if (MainActivity.engine != null) {
Log.i("ugh running", "here $ptr $retry")
// app is alive, deliver directly there
MethodCallHandler.invokeMethod("APNMsg", mapOf("pointer" to ptr.toString(), "retry" to retry.toString()))
return@post
}
Log.i("ugh running", "backend $ptr $retry")
CoroutineScope(Dispatchers.Main).launch {
DartWorker.callMethod(this@APNService, "APNMsg", mapOf("pointer" to ptr.toString(), "retry" to retry.toString()))
}
Expand Down
111 changes: 111 additions & 0 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# OpenBubbles Android development

This document describes the supported local workflow for the Flutter Android
client. It is intentionally separate from the end-user setup guide: a local
build is useful for testing, but it does not replace a trusted relay, Apple
device, or production signing configuration.

## Scope and message routing

OpenBubbles is the iMessage client. The relay or Mac/iPhone side handles the
Apple service connection; the Android app renders conversations, sends user
actions, persists local state, and receives relay events.

For a predictable test environment, keep the routing boundary explicit:

- iMessage traffic stays in OpenBubbles and its configured relay.
- SMS, MMS, and RCS stay in the device's default Google Messages app unless a
test specifically targets forwarding.
- Do not enable Google Messages and OpenBubbles SMS forwarding at the same time
during a delivery test. Two active paths can create duplicates, reorder
messages, or make a successful delivery look lost.

This boundary is a diagnostic control, not a claim that every carrier or relay
configuration behaves identically.

## Toolchain

The current CI workflow is the source of truth for the tested build matrix:

- Flutter 3.24.0, stable channel
- Dart SDK supplied by that Flutter release
- Rust stable for the Rust bridge and RustPush components
- Java 21 (Temurin in CI)
- Android SDK and command-line tools
- Protobuf compiler (`protoc`)

The older Java 8 link in historical contribution notes is not the CI target.
Use the same major Java version as CI when diagnosing Gradle failures.

## Local checkout and build

Clone the repository with submodules, then install dependencies:

```bash
git clone --recurse-submodules <your-fork-url>
cd openbubbles-app
flutter pub get
```

Run a focused test before a full build:

```bash
flutter test test/helpers/message_helper_test.dart
```

The CI workflow builds unsigned arm64 Alpha artifacts. The equivalent local
commands are:

```bash
flutter build apk --flavor alpha --profile --target-platform android-arm64
flutter build apk --flavor alpha --debug --target-platform android-arm64
```

Use a release build for performance measurements. Do not compare a debug build
with a store release and attribute every frame difference to application code.
Generated files, signing keys, `.env` values, relay registration codes, and
Apple credentials must not be committed.

## Change and review workflow

1. Start from a clean branch based on the intended upstream branch.
2. Make one narrow change per branch where practical.
3. Add or update a focused test for message parsing, routing, or state changes.
4. Run the focused test and the relevant Flutter analyzer/build locally.
5. Describe the user-visible behavior, failure mode, and test evidence in the PR.
6. Keep performance claims tied to a reproducible device, build mode, and test
scenario.

Changes that affect both the Android client and ValidationRelay should be
reviewed as a coordinated pair. The Android client must tolerate relay
disconnects and malformed responses; the relay must not log or persist secrets.

## Safe diagnostics

Enable the app's Developer Mode only for a controlled reproduction. Capture a
short window around one send or receive operation, then redact or remove the
capture before sharing it publicly. See [`DIAGNOSTICS.md`](DIAGNOSTICS.md) for
the collection checklist and known failure classes.

Never include registration codes, registration secrets, Apple IDs, phone
numbers, message text, attachment URLs, auth tokens, or full device identifiers
in an issue or pull request. A short-lived hash or local incident ID is enough
to correlate events.

## Known limitations

- Android background execution, Doze, OEM battery policies, and network path
changes can delay relay delivery even when the app code is healthy.
- CloudKit/Apple plist payloads can contain types that are not present in every
historical message. Decoding must fail closed and preserve the rest of the
sync rather than crashing the UI.
- Notifications can arrive with incomplete contact/group metadata. UI and
notification code must use a generic avatar fallback instead of throwing.
- Reaction events may race message persistence. A missing target should be
retried or ignored with bounded logging, not trigger an unbounded lookup loop.
- A green WebSocket connection only proves transport connectivity. It does not
prove that registration, validation, or message persistence succeeded.

These are test boundaries, not promises of feature support. Record the exact
device, Android version, build variant, relay, and network when reporting a
failure.
Loading