Skip to content
Merged
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
79dfdd6
persistence step 1
callebtc Oct 29, 2025
6de1c15
fix build
callebtc Oct 29, 2025
9bc7a8b
Merge branch 'main' into background-persistence
callebtc Oct 29, 2025
235f511
messages in the background work, notifications not yet
callebtc Oct 30, 2025
a5cdc4f
app state store
callebtc Oct 30, 2025
0f059b3
DM icon shows up
callebtc Oct 30, 2025
86edebc
notification launches when app is closed!
callebtc Oct 30, 2025
2625495
keep ui updated
callebtc Oct 30, 2025
53c8e2c
lifecycle fixes
callebtc Oct 30, 2025
4fc1f5b
extensive logging, maybe revert later
callebtc Oct 30, 2025
863bb45
Merge branch 'main' into background-persistence
callebtc Oct 30, 2025
71331f0
send nickname in announcement
callebtc Oct 31, 2025
17b24f2
quit in notification
callebtc Oct 31, 2025
0853ec0
setting in about sheet
callebtc Nov 2, 2025
bd22d84
fix quit bitchat
callebtc Nov 2, 2025
f18dbe4
lifecycle fixes
callebtc Nov 2, 2025
91b2d90
power mode based on background state
callebtc Nov 2, 2025
8e05a4d
stats for both direciotns
callebtc Nov 3, 2025
10d9c3d
fix graph persistence
callebtc Nov 3, 2025
5a26c8d
better counting
callebtc Nov 3, 2025
156b83c
count per device
callebtc Nov 3, 2025
f584bc1
only compute when debug sheet is open? untested
callebtc Nov 3, 2025
fe27f99
fix read receipts
callebtc Nov 3, 2025
24f4947
fix read receipts fully
callebtc Nov 3, 2025
71f03e8
fix unread badge if messages have been read in focus
callebtc Nov 3, 2025
4c58547
foreground promotion fix
callebtc Nov 3, 2025
43621d9
fix app kill in notification
callebtc Nov 3, 2025
cd6487d
Merge branch 'main' into background-persistence
callebtc Nov 4, 2025
5e04d87
Merge branch 'main' into background-persistence
callebtc Dec 12, 2025
efe16ee
adjust to new tor
callebtc Dec 12, 2025
266403d
nice
callebtc Dec 12, 2025
66ef3af
about sheet design
callebtc Dec 12, 2025
0f91958
Merge branch 'main' into background-persistence
callebtc Dec 13, 2025
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
36 changes: 36 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@

<!-- Notification permissions -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Foreground service and boot permissions for long-running background mesh -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- Connected device foreground service type for BLE operations (API 34+) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<!-- Data sync foreground service type (required when declaring dataSync) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

<!-- Microphone for voice notes -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
Expand Down Expand Up @@ -77,5 +84,34 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<!-- Persistent foreground service to run the mesh in background -->
<service
android:name=".service.MeshForegroundService"
android:exported="false"
android:foregroundServiceType="connectedDevice|dataSync"
tools:ignore="DataExtractionRules">
</service>

<!-- Listen for in-app broadcast when POST_NOTIFICATIONS is granted -->
<receiver
android:name=".service.NotificationPermissionChangedReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.bitchat.android.action.NOTIFICATION_PERMISSION_GRANTED" />
</intent-filter>
</receiver>

<!-- Auto-start mesh service after boot if enabled -->
<receiver
android:name=".service.BootCompletedReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
</manifest>
6 changes: 6 additions & 0 deletions app/src/main/java/com/bitchat/android/BitchatApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ class BitchatApplication : Application() {
// Initialize debug preference manager (persists debug toggles)
try { com.bitchat.android.ui.debug.DebugPreferenceManager.init(this) } catch (_: Exception) { }

// Initialize mesh service preferences
try { com.bitchat.android.service.MeshServicePreferences.init(this) } catch (_: Exception) { }

// Proactively start the foreground service to keep mesh alive
try { com.bitchat.android.service.MeshForegroundService.start(this) } catch (_: Exception) { }

// TorManager already initialized above
}
}
39 changes: 26 additions & 13 deletions app/src/main/java/com/bitchat/android/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class MainActivity : OrientationAwareActivity() {
private lateinit var locationStatusManager: LocationStatusManager
private lateinit var batteryOptimizationManager: BatteryOptimizationManager

// Core mesh service - managed at app level
// Core mesh service - provided by the foreground service holder
private lateinit var meshService: BluetoothMeshService
private val mainViewModel: MainViewModel by viewModels()
private val chatViewModel: ChatViewModel by viewModels {
Expand All @@ -66,13 +66,21 @@ class MainActivity : OrientationAwareActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// Check if this is a quit request from the notification
if (intent.getBooleanExtra("ACTION_QUIT_APP", false)) {
android.util.Log.d("MainActivity", "Quit request received in onCreate, finishing activity")
finish()
return
}

// Enable edge-to-edge display for modern Android look
enableEdgeToEdge()

// Initialize permission management
permissionManager = PermissionManager(this)
// Initialize core mesh service first
meshService = BluetoothMeshService(this)
// Ensure foreground service is running and get mesh instance from holder
try { com.bitchat.android.service.MeshForegroundService.start(applicationContext) } catch (_: Exception) { }
meshService = com.bitchat.android.service.MeshServiceHolder.getOrCreate(applicationContext)
bluetoothStatusManager = BluetoothStatusManager(
activity = this,
context = this,
Expand Down Expand Up @@ -630,6 +638,15 @@ class MainActivity : OrientationAwareActivity() {

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)

// Check if this is a quit request from the notification
if (intent.getBooleanExtra("ACTION_QUIT_APP", false)) {
android.util.Log.d("MainActivity", "Quit request received, finishing activity")
finish()
return
}

// Handle notification intents when app is already running
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
handleNotificationIntent(intent)
Expand All @@ -640,6 +657,8 @@ class MainActivity : OrientationAwareActivity() {
super.onResume()
// Check Bluetooth and Location status on resume and handle accordingly
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
// Reattach mesh delegate to new ChatViewModel instance after Activity recreation
try { meshService.delegate = chatViewModel } catch (_: Exception) { }
// Set app foreground state
meshService.connectionManager.setAppBackgroundState(false)
chatViewModel.setAppBackgroundState(false)
Expand All @@ -665,13 +684,15 @@ class MainActivity : OrientationAwareActivity() {
}
}

override fun onPause() {
override fun onPause() {
super.onPause()
// Only set background state if app is fully initialized
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
// Set app background state
meshService.connectionManager.setAppBackgroundState(true)
chatViewModel.setAppBackgroundState(true)
// Detach UI delegate so the foreground service can own DM notifications while UI is closed
try { meshService.delegate = null } catch (_: Exception) { }
}
}

Expand Down Expand Up @@ -746,14 +767,6 @@ class MainActivity : OrientationAwareActivity() {
Log.w("MainActivity", "Error cleaning up location status manager: ${e.message}")
}

// Stop mesh services if app was fully initialized
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
try {
meshService.stopServices()
Log.d("MainActivity", "Mesh services stopped successfully")
} catch (e: Exception) {
Log.w("MainActivity", "Error stopping mesh services in onDestroy: ${e.message}")
}
}
// Do not stop mesh here; ForegroundService owns lifecycle for background reliability
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ class BluetoothConnectionManager(

try {
isActive = true
Log.d(TAG, "ConnectionManager activated (permissions and adapter OK)")

// set the adapter's name to our 8-character peerID for iOS privacy, TODO: Make this configurable
// try {
Expand Down Expand Up @@ -179,6 +180,7 @@ class BluetoothConnectionManager(
[email protected] = false
return@launch
}
Log.d(TAG, "GATT Server started")
} else {
Log.i(TAG, "GATT Server disabled by debug settings; not starting")
}
Expand All @@ -189,6 +191,7 @@ class BluetoothConnectionManager(
[email protected] = false
return@launch
}
Log.d(TAG, "GATT Client started")
} else {
Log.i(TAG, "GATT Client disabled by debug settings; not starting")
}
Expand All @@ -214,6 +217,7 @@ class BluetoothConnectionManager(
isActive = false

connectionScope.launch {
Log.d(TAG, "Stopping client/server and power components...")
// Stop component managers
clientManager.stop()
serverManager.stop()
Expand All @@ -230,6 +234,18 @@ class BluetoothConnectionManager(
Log.i(TAG, "All Bluetooth services stopped")
}
}

/**
* Indicates whether this instance can be safely reused for a future start.
* Returns false if its coroutine scope has been cancelled.
*/
fun isReusable(): Boolean {
val active = connectionScope.isActive
if (!active) {
Log.d(TAG, "BluetoothConnectionManager isReusable=false (scope cancelled)")
}
return active
}

/**
* Set app background state for power optimization
Expand Down
Loading
Loading