Merge pull request #82 from permissionlesstech/fix-startup-2

Fix startup 2
This commit is contained in:
callebtc
2025-07-12 17:13:38 +02:00
committed by GitHub
10 changed files with 454 additions and 101 deletions
@@ -17,6 +17,8 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.ViewModelProvider
import com.bitchat.android.mesh.BluetoothMeshService
import com.bitchat.android.onboarding.* import com.bitchat.android.onboarding.*
import com.bitchat.android.ui.ChatScreen import com.bitchat.android.ui.ChatScreen
import com.bitchat.android.ui.ChatViewModel import com.bitchat.android.ui.ChatViewModel
@@ -29,7 +31,17 @@ class MainActivity : ComponentActivity() {
private lateinit var permissionManager: PermissionManager private lateinit var permissionManager: PermissionManager
private lateinit var onboardingCoordinator: OnboardingCoordinator private lateinit var onboardingCoordinator: OnboardingCoordinator
private lateinit var bluetoothStatusManager: BluetoothStatusManager private lateinit var bluetoothStatusManager: BluetoothStatusManager
private val chatViewModel: ChatViewModel by viewModels()
// Core mesh service - managed at app level
private lateinit var meshService: BluetoothMeshService
private val chatViewModel: ChatViewModel by viewModels {
object : ViewModelProvider.Factory {
override fun <T : androidx.lifecycle.ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return ChatViewModel(application, meshService) as T
}
}
}
// UI state for onboarding flow // UI state for onboarding flow
private var onboardingState by mutableStateOf(OnboardingState.CHECKING) private var onboardingState by mutableStateOf(OnboardingState.CHECKING)
@@ -50,6 +62,9 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// Initialize core mesh service first
meshService = BluetoothMeshService(this)
// Initialize permission management // Initialize permission management
permissionManager = PermissionManager(this) permissionManager = PermissionManager(this)
bluetoothStatusManager = BluetoothStatusManager( bluetoothStatusManager = BluetoothStatusManager(
@@ -289,7 +304,7 @@ class MainActivity : ComponentActivity() {
// This solves the issue where app needs restart to work on first install // This solves the issue where app needs restart to work on first install
delay(1000) // Give the system time to process permission grants delay(1000) // Give the system time to process permission grants
android.util.Log.d("MainActivity", "Permissions verified, starting mesh service") android.util.Log.d("MainActivity", "Permissions verified, initializing chat system")
// Ensure all permissions are still granted (user might have revoked in settings) // Ensure all permissions are still granted (user might have revoked in settings)
if (!permissionManager.areAllPermissionsGranted()) { if (!permissionManager.areAllPermissionsGranted()) {
@@ -299,8 +314,11 @@ class MainActivity : ComponentActivity() {
return@launch return@launch
} }
// Initialize chat view model - this will start the mesh service // Set up mesh service delegate and start services
chatViewModel.meshService.startServices() meshService.delegate = chatViewModel
meshService.startServices()
android.util.Log.d("MainActivity", "Mesh service started successfully")
// Handle any notification intent // Handle any notification intent
handleNotificationIntent(intent) handleNotificationIntent(intent)
@@ -330,6 +348,8 @@ class MainActivity : ComponentActivity() {
super.onResume() super.onResume()
// Check Bluetooth status on resume and handle accordingly // Check Bluetooth status on resume and handle accordingly
if (onboardingState == OnboardingState.COMPLETE) { if (onboardingState == OnboardingState.COMPLETE) {
// Set app foreground state
meshService.connectionManager.setAppBackgroundState(false)
chatViewModel.setAppBackgroundState(false) chatViewModel.setAppBackgroundState(false)
// Check if Bluetooth was disabled while app was backgrounded // Check if Bluetooth was disabled while app was backgrounded
@@ -347,6 +367,8 @@ class MainActivity : ComponentActivity() {
super.onPause() super.onPause()
// Only set background state if app is fully initialized // Only set background state if app is fully initialized
if (onboardingState == OnboardingState.COMPLETE) { if (onboardingState == OnboardingState.COMPLETE) {
// Set app background state
meshService.connectionManager.setAppBackgroundState(true)
chatViewModel.setAppBackgroundState(true) chatViewModel.setAppBackgroundState(true)
} }
} }
@@ -376,12 +398,32 @@ class MainActivity : ComponentActivity() {
} }
} }
/**
* Restart mesh services (for debugging/troubleshooting)
*/
fun restartMeshServices() {
if (onboardingState == OnboardingState.COMPLETE) {
lifecycleScope.launch {
try {
android.util.Log.d("MainActivity", "Restarting mesh services")
meshService.stopServices()
delay(1000)
meshService.startServices()
android.util.Log.d("MainActivity", "Mesh services restarted successfully")
} catch (e: Exception) {
android.util.Log.e("MainActivity", "Error restarting mesh services: ${e.message}")
}
}
}
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
// Only stop mesh services if they were started // Stop mesh services if app was fully initialized
if (onboardingState == OnboardingState.COMPLETE) { if (onboardingState == OnboardingState.COMPLETE) {
try { try {
chatViewModel.meshService.stopServices() meshService.stopServices()
android.util.Log.d("MainActivity", "Mesh services stopped successfully")
} catch (e: Exception) { } catch (e: Exception) {
android.util.Log.w("MainActivity", "Error stopping mesh services in onDestroy: ${e.message}") android.util.Log.w("MainActivity", "Error stopping mesh services in onDestroy: ${e.message}")
} }
@@ -129,16 +129,16 @@ class BluetoothConnectionManager(
try { try {
isActive = true isActive = true
// Setup GATT server first
setupGattServer()
// Start power manager and services // Start power manager and services
connectionScope.launch { connectionScope.launch {
powerManager.start() powerManager.start()
delay(300) // Brief delay to ensure GATT server is ready
// Setup GATT server after power manager is ready
setupGattServer()
delay(500) // Ensure GATT server is ready
startAdvertising() startAdvertising()
delay(200) delay(100)
if (powerManager.shouldUseDutyCycle()) { if (powerManager.shouldUseDutyCycle()) {
Log.i(TAG, "Using power-aware duty cycling") Log.i(TAG, "Using power-aware duty cycling")
@@ -410,10 +410,15 @@ class BluetoothConnectionManager(
} }
if (characteristic.uuid == CHARACTERISTIC_UUID) { if (characteristic.uuid == CHARACTERISTIC_UUID) {
Log.d(TAG, "Server: Received packet from ${device.address}, size: ${value.size} bytes")
val packet = BitchatPacket.fromBinaryData(value) val packet = BitchatPacket.fromBinaryData(value)
if (packet != null) { if (packet != null) {
val peerID = String(packet.senderID).replace("\u0000", "") val peerID = String(packet.senderID).replace("\u0000", "")
Log.d(TAG, "Server: Parsed packet type ${packet.type} from $peerID")
delegate?.onPacketReceived(packet, peerID, device) delegate?.onPacketReceived(packet, peerID, device)
} else {
Log.w(TAG, "Server: Failed to parse packet from ${device.address}, size: ${value.size} bytes")
Log.w(TAG, "Server: Packet data: ${value.joinToString(" ") { "%02x".format(it) }}")
} }
if (responseNeeded) { if (responseNeeded) {
@@ -458,48 +463,47 @@ class BluetoothConnectionManager(
// Proper cleanup sequencing to prevent race conditions // Proper cleanup sequencing to prevent race conditions
gattServer?.let { server -> gattServer?.let { server ->
Log.d(TAG, "Cleaning up existing GATT server") Log.d(TAG, "Cleaning up existing GATT server")
connectionScope.launch { try {
// Give time for pending callbacks to complete
delay(100)
server.close() server.close()
} catch (e: Exception) {
Log.w(TAG, "Error closing existing GATT server: ${e.message}")
} }
} }
// Create new server after cleanup delay // Small delay to ensure cleanup is complete
connectionScope.launch { Thread.sleep(100)
delay(200) // Allow previous server to fully close
if (!isActive) {
if (!isActive) { Log.d(TAG, "Service inactive, skipping GATT server creation")
Log.d(TAG, "Service inactive, skipping GATT server creation") return
return@launch
}
gattServer = bluetoothManager.openGattServer(context, serverCallback)
// Create characteristic with notification support
characteristic = BluetoothGattCharacteristic(
CHARACTERISTIC_UUID,
BluetoothGattCharacteristic.PROPERTY_READ or
BluetoothGattCharacteristic.PROPERTY_WRITE or
BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE or
BluetoothGattCharacteristic.PROPERTY_NOTIFY,
BluetoothGattCharacteristic.PERMISSION_READ or
BluetoothGattCharacteristic.PERMISSION_WRITE
)
val descriptor = BluetoothGattDescriptor(
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"),
BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE
)
characteristic?.addDescriptor(descriptor)
val service = BluetoothGattService(SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY)
service.addCharacteristic(characteristic)
gattServer?.addService(service)
Log.i(TAG, "GATT server setup complete")
} }
// Create new server
gattServer = bluetoothManager.openGattServer(context, serverCallback)
// Create characteristic with notification support
characteristic = BluetoothGattCharacteristic(
CHARACTERISTIC_UUID,
BluetoothGattCharacteristic.PROPERTY_READ or
BluetoothGattCharacteristic.PROPERTY_WRITE or
BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE or
BluetoothGattCharacteristic.PROPERTY_NOTIFY,
BluetoothGattCharacteristic.PERMISSION_READ or
BluetoothGattCharacteristic.PERMISSION_WRITE
)
val descriptor = BluetoothGattDescriptor(
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"),
BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE
)
characteristic?.addDescriptor(descriptor)
val service = BluetoothGattService(SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY)
service.addCharacteristic(characteristic)
gattServer?.addService(service)
Log.i(TAG, "GATT server setup complete")
} }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@@ -836,10 +840,15 @@ class BluetoothConnectionManager(
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
val value = characteristic.value val value = characteristic.value
Log.d(TAG, "Client: Received packet from ${gatt.device.address}, size: ${value.size} bytes")
val packet = BitchatPacket.fromBinaryData(value) val packet = BitchatPacket.fromBinaryData(value)
if (packet != null) { if (packet != null) {
val peerID = String(packet.senderID).replace("\u0000", "") val peerID = String(packet.senderID).replace("\u0000", "")
Log.d(TAG, "Client: Parsed packet type ${packet.type} from $peerID")
delegate?.onPacketReceived(packet, peerID, gatt.device) delegate?.onPacketReceived(packet, peerID, gatt.device)
} else {
Log.w(TAG, "Client: Failed to parse packet from ${gatt.device.address}, size: ${value.size} bytes")
Log.w(TAG, "Client: Packet data: ${value.joinToString(" ") { "%02x".format(it) }}")
} }
} }
} }
@@ -69,10 +69,10 @@ class BluetoothMeshService(private val context: Context) {
while (isActive) { while (isActive) {
try { try {
delay(10000) // 10 seconds delay(10000) // 10 seconds
val debugInfo = getDebugStatus() if (isActive) { // Double-check before logging
Log.d(TAG, "=== PERIODIC DEBUG STATUS ===") val debugInfo = getDebugStatus()
Log.d(TAG, debugInfo) Log.d(TAG, "=== PERIODIC DEBUG STATUS ===\n$debugInfo\n=== END DEBUG STATUS ===")
Log.d(TAG, "=== END DEBUG STATUS ===") }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error in periodic debug logging: ${e.message}") Log.e(TAG, "Error in periodic debug logging: ${e.message}")
} }
@@ -282,7 +282,7 @@ class BluetoothMeshService(private val context: Context) {
* Start the mesh service * Start the mesh service
*/ */
fun startServices() { fun startServices() {
// Prevent double starts // Prevent double starts (defensive programming)
if (isActive) { if (isActive) {
Log.w(TAG, "Mesh service already active, ignoring duplicate start request") Log.w(TAG, "Mesh service already active, ignoring duplicate start request")
return return
@@ -297,9 +297,7 @@ class BluetoothMeshService(private val context: Context) {
// Send initial announcements after services are ready // Send initial announcements after services are ready
serviceScope.launch { serviceScope.launch {
delay(1000) delay(1000)
if (isActive) { // Check if still active sendBroadcastAnnounce()
sendBroadcastAnnounce()
}
} }
} else { } else {
Log.e(TAG, "Failed to start Bluetooth services") Log.e(TAG, "Failed to start Bluetooth services")
@@ -0,0 +1,219 @@
package com.bitchat.android.services
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import com.bitchat.android.model.BitchatMessage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.util.*
/**
* Message retention service for saving channel messages locally
* Matches iOS MessageRetentionService functionality
*/
class MessageRetentionService private constructor(private val context: Context) {
companion object {
private const val TAG = "MessageRetentionService"
private const val PREF_NAME = "message_retention"
private const val KEY_FAVORITE_CHANNELS = "favorite_channels"
@Volatile
private var INSTANCE: MessageRetentionService? = null
fun getInstance(context: Context): MessageRetentionService {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: MessageRetentionService(context.applicationContext).also { INSTANCE = it }
}
}
}
private val prefs: SharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
private val retentionDir = File(context.filesDir, "retained_messages")
init {
if (!retentionDir.exists()) {
retentionDir.mkdirs()
}
}
// MARK: - Channel Bookmarking (Favorites)
fun getFavoriteChannels(): Set<String> {
return prefs.getStringSet(KEY_FAVORITE_CHANNELS, emptySet()) ?: emptySet()
}
fun toggleFavoriteChannel(channel: String): Boolean {
val currentFavorites = getFavoriteChannels().toMutableSet()
val wasAdded = if (currentFavorites.contains(channel)) {
currentFavorites.remove(channel)
false
} else {
currentFavorites.add(channel)
true
}
prefs.edit().putStringSet(KEY_FAVORITE_CHANNELS, currentFavorites).apply()
if (!wasAdded) {
// Channel removed from favorites - delete saved messages in background
Thread {
try {
val channelFile = getChannelFile(channel)
if (channelFile.exists()) {
channelFile.delete()
Log.d(TAG, "Deleted saved messages for channel $channel")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to delete messages for channel $channel", e)
}
}.start()
}
Log.d(TAG, "Channel $channel ${if (wasAdded) "bookmarked" else "unbookmarked"}")
return wasAdded
}
fun isChannelBookmarked(channel: String): Boolean {
return getFavoriteChannels().contains(channel)
}
// MARK: - Message Storage
suspend fun saveMessage(message: BitchatMessage, forChannel: String) = withContext(Dispatchers.IO) {
if (!isChannelBookmarked(forChannel)) {
Log.w(TAG, "Attempted to save message for non-bookmarked channel: $forChannel")
return@withContext
}
try {
val channelFile = getChannelFile(forChannel)
val existingMessages = loadMessagesFromFile(channelFile).toMutableList()
// Check if message already exists (by ID)
if (existingMessages.any { it.id == message.id }) {
Log.d(TAG, "Message ${message.id} already saved for channel $forChannel")
return@withContext
}
// Add new message
existingMessages.add(message)
// Sort by timestamp
existingMessages.sortBy { it.timestamp }
// Save back to file
saveMessagesToFile(channelFile, existingMessages)
Log.d(TAG, "Saved message ${message.id} for channel $forChannel")
} catch (e: Exception) {
Log.e(TAG, "Failed to save message for channel $forChannel", e)
}
}
suspend fun loadMessagesForChannel(channel: String): List<BitchatMessage> = withContext(Dispatchers.IO) {
if (!isChannelBookmarked(channel)) {
Log.d(TAG, "Channel $channel not bookmarked, returning empty list")
return@withContext emptyList()
}
try {
val channelFile = getChannelFile(channel)
val messages = loadMessagesFromFile(channelFile)
Log.d(TAG, "Loaded ${messages.size} messages for channel $channel")
return@withContext messages
} catch (e: Exception) {
Log.e(TAG, "Failed to load messages for channel $channel", e)
return@withContext emptyList()
}
}
suspend fun deleteMessagesForChannel(channel: String): Unit = withContext(Dispatchers.IO) {
try {
val channelFile = getChannelFile(channel)
if (channelFile.exists()) {
channelFile.delete()
Log.d(TAG, "Deleted saved messages for channel $channel")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to delete messages for channel $channel", e)
}
}
suspend fun deleteAllStoredMessages(): Unit = withContext(Dispatchers.IO) {
try {
if (retentionDir.exists()) {
retentionDir.listFiles()?.forEach { file ->
file.delete()
}
Log.d(TAG, "Deleted all stored messages")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to delete all stored messages", e)
}
}
// MARK: - File Operations
private fun getChannelFile(channel: String): File {
// Sanitize channel name for filename
val sanitizedChannel = channel.replace("[^a-zA-Z0-9_-]".toRegex(), "_")
return File(retentionDir, "channel_${sanitizedChannel}.dat")
}
private fun loadMessagesFromFile(file: File): List<BitchatMessage> {
if (!file.exists()) {
return emptyList()
}
return try {
FileInputStream(file).use { fis ->
ObjectInputStream(fis).use { ois ->
@Suppress("UNCHECKED_CAST")
ois.readObject() as List<BitchatMessage>
}
}
} catch (e: Exception) {
Log.w(TAG, "Failed to load messages from ${file.name}, returning empty list", e)
emptyList()
}
}
private fun saveMessagesToFile(file: File, messages: List<BitchatMessage>) {
FileOutputStream(file).use { fos ->
ObjectOutputStream(fos).use { oos ->
oos.writeObject(messages)
}
}
}
// MARK: - Statistics
fun getBookmarkedChannelsCount(): Int {
return getFavoriteChannels().size
}
suspend fun getTotalStoredMessagesCount(): Int = withContext(Dispatchers.IO) {
var totalCount = 0
try {
retentionDir.listFiles()?.forEach { file ->
if (file.name.startsWith("channel_") && file.name.endsWith(".dat")) {
val messages = loadMessagesFromFile(file)
totalCount += messages.size
}
}
} catch (e: Exception) {
Log.e(TAG, "Failed to count stored messages", e)
}
totalCount
}
}
@@ -1,5 +1,6 @@
package com.bitchat.android.ui package com.bitchat.android.ui
import android.util.Log
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
@@ -151,11 +152,17 @@ fun ChatHeaderContent(
when { when {
selectedPrivatePeer != null -> { selectedPrivatePeer != null -> {
// Private chat header // Private chat header - ensure state synchronization
val favoritePeers by viewModel.favoritePeers.observeAsState(emptySet())
val fingerprint = viewModel.privateChatManager.getPeerFingerprint(selectedPrivatePeer)
val isFavorite = favoritePeers.contains(fingerprint)
Log.d("ChatHeader", "Header recomposing: peer=$selectedPrivatePeer, fingerprint=$fingerprint, isFav=$isFavorite")
PrivateChatHeader( PrivateChatHeader(
peerID = selectedPrivatePeer, peerID = selectedPrivatePeer,
peerNicknames = viewModel.meshService.getPeerNicknames(), peerNicknames = viewModel.meshService.getPeerNicknames(),
isFavorite = viewModel.isFavorite(selectedPrivatePeer), isFavorite = isFavorite,
onBackClick = onBackClick, onBackClick = onBackClick,
onToggleFavorite = { viewModel.toggleFavorite(selectedPrivatePeer) } onToggleFavorite = { viewModel.toggleFavorite(selectedPrivatePeer) }
) )
@@ -253,7 +260,10 @@ private fun PrivateChatHeader(
// Favorite button - positioned on the right // Favorite button - positioned on the right
IconButton( IconButton(
onClick = onToggleFavorite, onClick = {
Log.d("ChatHeader", "Header toggle favorite: peerID=$peerID, currentFavorite=$isFavorite")
onToggleFavorite()
},
modifier = Modifier.align(Alignment.CenterEnd) modifier = Modifier.align(Alignment.CenterEnd)
) { ) {
Icon( Icon(
@@ -1,5 +1,6 @@
package com.bitchat.android.ui package com.bitchat.android.ui
import android.util.Log
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@@ -188,7 +189,17 @@ class ChatState {
} }
fun setFavoritePeers(favorites: Set<String>) { fun setFavoritePeers(favorites: Set<String>) {
val currentValue = _favoritePeers.value ?: emptySet()
Log.d("ChatState", "setFavoritePeers called with ${favorites.size} favorites: $favorites")
Log.d("ChatState", "Current value: $currentValue")
Log.d("ChatState", "Values equal: ${currentValue == favorites}")
Log.d("ChatState", "Setting on thread: ${Thread.currentThread().name}")
// Always set the value - even if equal, this ensures observers are triggered
_favoritePeers.value = favorites _favoritePeers.value = favorites
Log.d("ChatState", "LiveData value after set: ${_favoritePeers.value}")
Log.d("ChatState", "LiveData has active observers: ${_favoritePeers.hasActiveObservers()}")
} }
} }
@@ -2,6 +2,7 @@ package com.bitchat.android.ui
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.util.Log
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@@ -19,13 +20,13 @@ import kotlin.random.Random
* Refactored ChatViewModel - Main coordinator for bitchat functionality * Refactored ChatViewModel - Main coordinator for bitchat functionality
* Delegates specific responsibilities to specialized managers while maintaining 100% iOS compatibility * Delegates specific responsibilities to specialized managers while maintaining 100% iOS compatibility
*/ */
class ChatViewModel(application: Application) : AndroidViewModel(application), BluetoothMeshDelegate { class ChatViewModel(
application: Application,
val meshService: BluetoothMeshService
) : AndroidViewModel(application), BluetoothMeshDelegate {
private val context: Context = application.applicationContext private val context: Context = application.applicationContext
// Core services
val meshService = BluetoothMeshService(context)
// State management // State management
private val state = ChatState() private val state = ChatState()
@@ -72,7 +73,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
val favoritePeers: LiveData<Set<String>> = state.favoritePeers val favoritePeers: LiveData<Set<String>> = state.favoritePeers
init { init {
meshService.delegate = this // Note: Mesh service delegate is now set by MainActivity
loadAndInitialize() loadAndInitialize()
} }
@@ -100,8 +101,11 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
state.setFavoritePeers(dataManager.favoritePeers) state.setFavoritePeers(dataManager.favoritePeers)
dataManager.loadBlockedUsers() dataManager.loadBlockedUsers()
// Start mesh service // Log all favorites at startup
meshService.startServices() dataManager.logAllFavorites()
logCurrentFavoriteState()
// Note: Mesh service is now started by MainActivity
// Show welcome message if no peers after delay // Show welcome message if no peers after delay
viewModelScope.launch { viewModelScope.launch {
@@ -120,7 +124,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
meshService.stopServices() // Note: Mesh service lifecycle is now managed by MainActivity
} }
// MARK: - Nickname Management // MARK: - Nickname Management
@@ -250,7 +254,19 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
} }
fun toggleFavorite(peerID: String) { fun toggleFavorite(peerID: String) {
Log.d("ChatViewModel", "toggleFavorite called for peerID: $peerID")
privateChatManager.toggleFavorite(peerID) privateChatManager.toggleFavorite(peerID)
// Log current state after toggle
logCurrentFavoriteState()
}
private fun logCurrentFavoriteState() {
Log.i("ChatViewModel", "=== CURRENT FAVORITE STATE ===")
Log.i("ChatViewModel", "LiveData favorite peers: ${favoritePeers.value}")
Log.i("ChatViewModel", "DataManager favorite peers: ${dataManager.favoritePeers}")
Log.i("ChatViewModel", "Peer fingerprints: ${privateChatManager.getAllPeerFingerprints()}")
Log.i("ChatViewModel", "==============================")
} }
// MARK: - Debug and Troubleshooting // MARK: - Debug and Troubleshooting
@@ -259,18 +275,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
return meshService.getDebugStatus() return meshService.getDebugStatus()
} }
fun restartMeshServices() { // Note: Mesh service restart is now handled by MainActivity
viewModelScope.launch { // This function is no longer needed
meshService.stopServices()
delay(1000)
meshService.startServices()
}
}
fun setAppBackgroundState(inBackground: Boolean) { fun setAppBackgroundState(inBackground: Boolean) {
// Forward to connection manager for power optimization
meshService.connectionManager.setAppBackgroundState(inBackground)
// Forward to notification manager for notification logic // Forward to notification manager for notification logic
notificationManager.setAppBackgroundState(inBackground) notificationManager.setAppBackgroundState(inBackground)
} }
@@ -355,13 +363,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
state.setNickname(newNickname) state.setNickname(newNickname)
dataManager.saveNickname(newNickname) dataManager.saveNickname(newNickname)
// Disconnect from mesh // Note: Mesh service restart is now handled by MainActivity
meshService.stopServices() // This method now only clears data, not mesh service lifecycle
// Restart services with new identity
viewModelScope.launch {
delay(500)
meshService.startServices()
}
} }
} }
@@ -2,6 +2,7 @@ package com.bitchat.android.ui
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.util.Log
import com.google.gson.Gson import com.google.gson.Gson
import kotlin.random.Random import kotlin.random.Random
@@ -10,6 +11,10 @@ import kotlin.random.Random
*/ */
class DataManager(private val context: Context) { class DataManager(private val context: Context) {
companion object {
private const val TAG = "DataManager"
}
private val prefs: SharedPreferences = context.getSharedPreferences("bitchat_prefs", Context.MODE_PRIVATE) private val prefs: SharedPreferences = context.getSharedPreferences("bitchat_prefs", Context.MODE_PRIVATE)
private val gson = Gson() private val gson = Gson()
@@ -126,24 +131,41 @@ class DataManager(private val context: Context) {
fun loadFavorites() { fun loadFavorites() {
val savedFavorites = prefs.getStringSet("favorites", emptySet()) ?: emptySet() val savedFavorites = prefs.getStringSet("favorites", emptySet()) ?: emptySet()
_favoritePeers.addAll(savedFavorites) _favoritePeers.addAll(savedFavorites)
Log.d(TAG, "Loaded ${savedFavorites.size} favorite users from storage: $savedFavorites")
} }
fun saveFavorites() { fun saveFavorites() {
prefs.edit().putStringSet("favorites", _favoritePeers).apply() prefs.edit().putStringSet("favorites", _favoritePeers).apply()
Log.d(TAG, "Saved ${_favoritePeers.size} favorite users to storage: $_favoritePeers")
} }
fun addFavorite(fingerprint: String) { fun addFavorite(fingerprint: String) {
_favoritePeers.add(fingerprint) val wasAdded = _favoritePeers.add(fingerprint)
Log.d(TAG, "addFavorite: fingerprint=$fingerprint, wasAdded=$wasAdded")
saveFavorites() saveFavorites()
logAllFavorites()
} }
fun removeFavorite(fingerprint: String) { fun removeFavorite(fingerprint: String) {
_favoritePeers.remove(fingerprint) val wasRemoved = _favoritePeers.remove(fingerprint)
Log.d(TAG, "removeFavorite: fingerprint=$fingerprint, wasRemoved=$wasRemoved")
saveFavorites() saveFavorites()
logAllFavorites()
} }
fun isFavorite(fingerprint: String): Boolean { fun isFavorite(fingerprint: String): Boolean {
return _favoritePeers.contains(fingerprint) val result = _favoritePeers.contains(fingerprint)
Log.d(TAG, "isFavorite check: fingerprint=$fingerprint, result=$result")
return result
}
fun logAllFavorites() {
Log.i(TAG, "=== ALL FAVORITE USERS ===")
Log.i(TAG, "Total favorites: ${_favoritePeers.size}")
_favoritePeers.forEach { fingerprint ->
Log.i(TAG, "Favorite fingerprint: $fingerprint")
}
Log.i(TAG, "========================")
} }
// MARK: - Blocked Users Management // MARK: - Blocked Users Management
@@ -3,6 +3,7 @@ package com.bitchat.android.ui
import com.bitchat.android.model.BitchatMessage import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.DeliveryStatus import com.bitchat.android.model.DeliveryStatus
import java.util.* import java.util.*
import android.util.Log
/** /**
* Handles private chat functionality including peer management and blocking * Handles private chat functionality including peer management and blocking
@@ -13,6 +14,10 @@ class PrivateChatManager(
private val dataManager: DataManager private val dataManager: DataManager
) { ) {
companion object {
private const val TAG = "PrivateChatManager"
}
// Peer identification mapping // Peer identification mapping
private val peerIDToPublicKeyFingerprint = mutableMapOf<String, String>() private val peerIDToPublicKeyFingerprint = mutableMapOf<String, String>()
@@ -99,17 +104,43 @@ class PrivateChatManager(
fun toggleFavorite(peerID: String) { fun toggleFavorite(peerID: String) {
val fingerprint = peerIDToPublicKeyFingerprint[peerID] ?: return val fingerprint = peerIDToPublicKeyFingerprint[peerID] ?: return
if (dataManager.isFavorite(fingerprint)) { Log.d(TAG, "toggleFavorite called for peerID: $peerID, fingerprint: $fingerprint")
val wasFavorite = dataManager.isFavorite(fingerprint)
Log.d(TAG, "Current favorite status: $wasFavorite")
val currentFavorites = state.getFavoritePeersValue()
Log.d(TAG, "Current UI state favorites: $currentFavorites")
if (wasFavorite) {
dataManager.removeFavorite(fingerprint) dataManager.removeFavorite(fingerprint)
Log.d(TAG, "Removed from favorites: $fingerprint")
} else { } else {
dataManager.addFavorite(fingerprint) dataManager.addFavorite(fingerprint)
Log.d(TAG, "Added to favorites: $fingerprint")
} }
state.setFavoritePeers(dataManager.favoritePeers)
// Always update state to trigger UI refresh - create new set to ensure change detection
val newFavorites = dataManager.favoritePeers.toSet()
state.setFavoritePeers(newFavorites)
Log.d(TAG, "Force updated favorite peers state. New favorites: $newFavorites")
Log.d(TAG, "All peer fingerprints: $peerIDToPublicKeyFingerprint")
} }
fun isFavorite(peerID: String): Boolean { fun isFavorite(peerID: String): Boolean {
val fingerprint = peerIDToPublicKeyFingerprint[peerID] ?: return false val fingerprint = peerIDToPublicKeyFingerprint[peerID] ?: return false
return dataManager.isFavorite(fingerprint) val isFav = dataManager.isFavorite(fingerprint)
Log.d(TAG, "isFavorite check: peerID=$peerID, fingerprint=$fingerprint, result=$isFav")
return isFav
}
fun getPeerFingerprint(peerID: String): String? {
return peerIDToPublicKeyFingerprint[peerID]
}
fun getPeerFingerprints(): Map<String, String> {
return peerIDToPublicKeyFingerprint.toMap()
} }
// MARK: - Block/Unblock Operations // MARK: - Block/Unblock Operations
@@ -262,10 +293,6 @@ class PrivateChatManager(
// MARK: - Public Getters // MARK: - Public Getters
fun getPeerFingerprint(peerID: String): String? {
return peerIDToPublicKeyFingerprint[peerID]
}
fun getAllPeerFingerprints(): Map<String, String> { fun getAllPeerFingerprints(): Map<String, String> {
return peerIDToPublicKeyFingerprint.toMap() return peerIDToPublicKeyFingerprint.toMap()
} }
@@ -1,5 +1,6 @@
package com.bitchat.android.ui package com.bitchat.android.ui
import android.util.Log
import androidx.compose.animation.* import androidx.compose.animation.*
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
import androidx.compose.foundation.* import androidx.compose.foundation.*
@@ -263,28 +264,40 @@ fun PeopleSection(
val privateChats by viewModel.privateChats.observeAsState(emptyMap()) val privateChats by viewModel.privateChats.observeAsState(emptyMap())
val favoritePeers by viewModel.favoritePeers.observeAsState(emptySet()) val favoritePeers by viewModel.favoritePeers.observeAsState(emptySet())
// Pre-calculate all favorite states to ensure proper state synchronization
val peerFavoriteStates = remember(favoritePeers, connectedPeers) {
connectedPeers.associateWith { peerID ->
val fingerprint = viewModel.privateChatManager.getPeerFingerprint(peerID)
favoritePeers.contains(fingerprint)
}
}
Log.d("SidebarComponents", "Recomposing with ${favoritePeers.size} favorites, peer states: $peerFavoriteStates")
// Smart sorting: unread DMs first, then by most recent DM, then favorites, then alphabetical // Smart sorting: unread DMs first, then by most recent DM, then favorites, then alphabetical
val sortedPeers = connectedPeers.sortedWith( val sortedPeers = connectedPeers.sortedWith(
compareBy<String> { !hasUnreadPrivateMessages.contains(it) } // Unread DM senders first compareBy<String> { !hasUnreadPrivateMessages.contains(it) } // Unread DM senders first
.thenByDescending { privateChats[it]?.maxByOrNull { msg -> msg.timestamp }?.timestamp?.time ?: 0L } // Most recent DM (convert Date to Long) .thenByDescending { privateChats[it]?.maxByOrNull { msg -> msg.timestamp }?.timestamp?.time ?: 0L } // Most recent DM (convert Date to Long)
.thenBy { .thenBy { !(peerFavoriteStates[it] ?: false) } // Favorites first
val fingerprint = viewModel.privateChatManager.getPeerFingerprint(it)
fingerprint == null || !favoritePeers.contains(fingerprint)
} // Favorites
.thenBy { (if (it == nickname) "You" else (peerNicknames[it] ?: it)).lowercase() } // Alphabetical .thenBy { (if (it == nickname) "You" else (peerNicknames[it] ?: it)).lowercase() } // Alphabetical
) )
sortedPeers.forEach { peerID -> sortedPeers.forEach { peerID ->
val isFavorite = peerFavoriteStates[peerID] ?: false
PeerItem( PeerItem(
peerID = peerID, peerID = peerID,
displayName = if (peerID == nickname) "You" else (peerNicknames[peerID] ?: peerID), displayName = if (peerID == nickname) "You" else (peerNicknames[peerID] ?: peerID),
signalStrength = peerRSSI[peerID] ?: 0, signalStrength = peerRSSI[peerID] ?: 0,
isSelected = peerID == selectedPrivatePeer, isSelected = peerID == selectedPrivatePeer,
isFavorite = favoritePeers.contains(viewModel.privateChatManager.getPeerFingerprint(peerID)), isFavorite = isFavorite,
hasUnreadDM = hasUnreadPrivateMessages.contains(peerID), hasUnreadDM = hasUnreadPrivateMessages.contains(peerID),
colorScheme = colorScheme, colorScheme = colorScheme,
onItemClick = { onPrivateChatStart(peerID) }, onItemClick = { onPrivateChatStart(peerID) },
onToggleFavorite = { viewModel.toggleFavorite(peerID) } onToggleFavorite = {
Log.d("SidebarComponents", "Sidebar toggle favorite: peerID=$peerID, currentFavorite=$isFavorite")
viewModel.toggleFavorite(peerID)
}
) )
} }
} }