Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2041a98
feat: add SOS Emergency feature with sender/receiver integration, aud…
MatthieuTexier Mar 26, 2026
81c952d
merge: resolve conflict with upstream/main
MatthieuTexier Mar 29, 2026
1a86992
fix: add missing identityRepository param to MapViewModelTest
MatthieuTexier Mar 29, 2026
39595be
Revert "fix: add missing identityRepository param to MapViewModelTest"
MatthieuTexier Mar 29, 2026
2d2ed29
fix: cancel triggerJob in deactivate() to prevent orphaned coroutines
MatthieuTexier Mar 29, 2026
f4f230b
fix: delete SOS trail on cancellation to prevent stale restore
MatthieuTexier Mar 29, 2026
117cab5
fix: critical race conditions and cancellation bugs in SOS
MatthieuTexier Mar 29, 2026
a0da494
fix: address remaining SOS concurrency and robustness issues
MatthieuTexier Mar 29, 2026
c65352d
fix: replace Mutex with AtomicBoolean to prevent silent trigger drops
MatthieuTexier Mar 29, 2026
122f006
fix: SOS GPS from FIELD_TELEMETRY, tracker restore on restart, audio ANR
MatthieuTexier Mar 29, 2026
1b4082f
fix: unpack FIELD_TELEMETRY to dict in fields serialization
MatthieuTexier Mar 29, 2026
6527603
fix: reduce condition complexity in parseSosLocation for detekt
MatthieuTexier Mar 29, 2026
9a418ad
fix: avoid sensor cycling on countdown ticks + generation-guard trigg…
MatthieuTexier Mar 30, 2026
82c7cf3
fix: remaining concurrency, serialization, and robustness issues
MatthieuTexier Mar 30, 2026
2638e01
fix: separate try blocks for stop/release in stopRecorder
MatthieuTexier Mar 30, 2026
4dcedb7
fix: re-verify Active state before launching periodic/audio jobs
MatthieuTexier Mar 30, 2026
855ed8f
fix: MediaRecorder thread safety, poll path fields, audio state check
MatthieuTexier Mar 30, 2026
fafd478
fix: memoize SOS checks in Compose, audio on IO, atomic restore guard
MatthieuTexier Mar 30, 2026
c5ae81b
merge: resolve conflicts with upstream/main
MatthieuTexier Mar 30, 2026
c7b1d84
fix: update test for json.dumps in poll_received_messages fields
MatthieuTexier Mar 30, 2026
b04df3e
ci: re-trigger after flaky ApkSharingViewModelTest
MatthieuTexier Mar 30, 2026
4ffa4dc
fix: PIN bypass window, missing index, poll path SOS state extraction
MatthieuTexier Mar 31, 2026
115b3a4
fix: type-safe combine snapshot + state guard in startPeriodicUpdates
MatthieuTexier Mar 31, 2026
9664e24
fix: replace runBlocking PIN fallback with eager scope init
MatthieuTexier Mar 31, 2026
9353e7e
fix: shake detector inflated accumulator through brief sub-threshold …
MatthieuTexier Apr 14, 2026
eea3803
fix: recalibrate shake sensitivity slider so full range is usable
MatthieuTexier Apr 14, 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
15 changes: 8 additions & 7 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -258,13 +258,14 @@ android {
val total = project.findProperty("testShardTotal")?.toString()?.toIntOrNull()
if (shard != null && total != null && total > 1) {
val testSourceDir = project.file("src/test/java")
val allTestClasses = project.fileTree(testSourceDir) {
include("**/*Test.kt")
}.files.map { f ->
f.relativeTo(testSourceDir)
.path.replace(File.separatorChar, '.')
.removeSuffix(".kt")
}.sorted()
val allTestClasses =
project.fileTree(testSourceDir) {
include("**/*Test.kt")
}.files.map { f ->
f.relativeTo(testSourceDir)
.path.replace(File.separatorChar, '.')
.removeSuffix(".kt")
}.sorted()
val shardClasses = allTestClasses.filterIndexed { i, _ -> i % total == shard }
shardClasses.forEach { cls -> it.filter.includeTestsMatching(cls) }
}
Expand Down
28 changes: 28 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />

<!-- Notification permission (Android 13+) -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
Expand All @@ -68,12 +69,16 @@
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<!-- Covers both ReticulumService and SosTriggerService (app-wide, not per-service) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<!-- Required to show incoming call screen over other apps when phone is unlocked -->
<!-- and app is closed (Android 10+ background activity launch restriction) -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-feature android:name="android.hardware.microphone" android:required="false" />

<!-- Auto-start after device boot -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

<!-- Companion Device Manager (Android 12+) for RNode association -->
<uses-feature android:name="android.software.companion_device_setup" android:required="false" />
<uses-permission android:name="android.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND" />
Expand Down Expand Up @@ -157,6 +162,19 @@
android:resource="@xml/usb_device_filter" />
</activity>

<!-- SOS Trigger Detection Service -->
<!-- Foreground service that keeps the process alive for accelerometer-based -->
<!-- SOS gesture detection (shake/tap) when the app is in the background -->
<service
android:name=".service.SosTriggerService"
android:enabled="true"
android:exported="false"
Comment thread
greptile-apps[bot] marked this conversation as resolved.
android:foregroundServiceType="specialUse|microphone">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="Emergency SOS gesture detection via accelerometer" />
</service>

<!-- Reticulum Background Service -->
<!-- Runs in separate process to allow clean restarts when applying config changes -->
<!-- BLE functionality is now integrated via KotlinBLEBridge (no separate service needed) -->
Expand All @@ -180,6 +198,16 @@
</intent-filter>
</service>

<!-- Auto-start services after device boot -->
<receiver
android:name=".receiver.BootReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>

<!-- FileProvider for sharing identity files -->
<provider
android:name="androidx.core.content.FileProvider"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ interface IReticulumService {
* @param iconBgColor Optional icon background color hex string (3 bytes RGB, e.g., "1E88E5")
* @return JSON string with result: {"success": true, "message_hash": "...", "delivery_method": "..."}
*/
String sendLxmfMessageWithMethod(in byte[] destHash, String content, in byte[] sourceIdentityPrivateKey, String deliveryMethod, boolean tryPropagationOnFail, in byte[] imageData, String imageFormat, String imageDataPath, in Map fileAttachments, in Map fileAttachmentPaths, String replyToMessageId, String iconName, String iconFgColor, String iconBgColor);
String sendLxmfMessageWithMethod(in byte[] destHash, String content, in byte[] sourceIdentityPrivateKey, String deliveryMethod, boolean tryPropagationOnFail, in byte[] imageData, String imageFormat, String imageDataPath, in Map fileAttachments, in Map fileAttachmentPaths, String replyToMessageId, String iconName, String iconFgColor, String iconBgColor, String telemetryJson, in byte[] audioData, String audioDataPath, String sosState);

/**
* Provide an alternative relay for message retry.
Expand Down
32 changes: 32 additions & 0 deletions app/src/main/java/com/lxmf/messenger/ColumbaApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import com.lxmf.messenger.reticulum.protocol.ServiceReticulumProtocol
import com.lxmf.messenger.service.IdentityResolutionManager
import com.lxmf.messenger.service.MessageCollector
import com.lxmf.messenger.service.PropagationNodeManager
import com.lxmf.messenger.service.SosActiveTracker
import com.lxmf.messenger.service.SosManager
import com.lxmf.messenger.service.SosTriggerDetector
import com.lxmf.messenger.service.TelemetryCollectorManager
import com.lxmf.messenger.startup.ConfigApplyFlagManager
import com.lxmf.messenger.startup.ServiceIdentityVerifier
Expand Down Expand Up @@ -88,6 +91,15 @@ class ColumbaApplication : Application() {
@Inject
lateinit var telemetryCollectorManager: TelemetryCollectorManager

@Inject
lateinit var sosManager: SosManager

@Inject
lateinit var sosTriggerDetector: SosTriggerDetector

@Inject
lateinit var receivedLocationDao: com.lxmf.messenger.data.db.dao.ReceivedLocationDao

// Application-level coroutine scope for app-wide operations
// Uses Dispatchers.Default for background initialization (no main-thread work needed)
// SupervisorJob ensures failures don't crash the entire app
Expand Down Expand Up @@ -137,6 +149,26 @@ class ColumbaApplication : Application() {

android.util.Log.d("ColumbaApplication", "Main app process detected ($processName) - proceeding with auto-initialization")

// Start SOS trigger detector (observes settings, starts/stops accelerometer listener)
sosTriggerDetector.startObserving()
// Restore SOS active state if app was restarted while SOS was active
sosManager.restoreIfActive()
// Restore SOS active tracker from recent trail data (receiver side)
applicationScope.launch {
try {
val recentSenders =
receivedLocationDao.getRecentSosTrailSenders(
sinceTimestamp = System.currentTimeMillis() - 24 * 3600_000L,
)
if (recentSenders.isNotEmpty()) {
SosActiveTracker.restoreFromSenders(recentSenders.toSet())
Comment thread
MatthieuTexier marked this conversation as resolved.
android.util.Log.d("ColumbaApplication", "Restored ${recentSenders.size} SOS active senders")
}
} catch (e: Exception) {
android.util.Log.w("ColumbaApplication", "Failed to restore SOS active senders", e)
}
}

// Preload theme preference into DataStore's in-memory cache
// This eliminates theme flash on app startup by ensuring the theme is cached
// before MainActivity renders. Combined with SplashScreen API for zero-flash UX.
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/java/com/lxmf/messenger/IncomingCallActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ class IncomingCallActivity : ComponentActivity() {
return
}

// SOS silent auto-answer: if SOS is active and setting is enabled, answer immediately
val app = applicationContext as? ColumbaApplication
if (app?.sosManager?.shouldAutoAnswer() == true) {
Log.i(TAG, "SOS silent auto-answer: answering call automatically")
answerCall()
currentIdentityHash.value?.let { navigateToActiveCall(it) }
return
}

// Show over lock screen and turn screen on
configureWindowForIncomingCall()

Expand Down
Loading
Loading