mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 09:25:19 +00:00
fix startup
This commit is contained in:
@@ -17,6 +17,8 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.bitchat.android.mesh.BluetoothMeshService
|
||||
import com.bitchat.android.onboarding.*
|
||||
import com.bitchat.android.ui.ChatScreen
|
||||
import com.bitchat.android.ui.ChatViewModel
|
||||
@@ -29,7 +31,17 @@ class MainActivity : ComponentActivity() {
|
||||
private lateinit var permissionManager: PermissionManager
|
||||
private lateinit var onboardingCoordinator: OnboardingCoordinator
|
||||
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
|
||||
private var onboardingState by mutableStateOf(OnboardingState.CHECKING)
|
||||
@@ -50,6 +62,9 @@ class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Initialize core mesh service first
|
||||
meshService = BluetoothMeshService(this)
|
||||
|
||||
// Initialize permission management
|
||||
permissionManager = PermissionManager(this)
|
||||
bluetoothStatusManager = BluetoothStatusManager(
|
||||
@@ -289,7 +304,7 @@ class MainActivity : ComponentActivity() {
|
||||
// This solves the issue where app needs restart to work on first install
|
||||
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)
|
||||
if (!permissionManager.areAllPermissionsGranted()) {
|
||||
@@ -299,8 +314,11 @@ class MainActivity : ComponentActivity() {
|
||||
return@launch
|
||||
}
|
||||
|
||||
// Initialize chat view model - this will start the mesh service
|
||||
chatViewModel.meshService.startServices()
|
||||
// Set up mesh service delegate and start services
|
||||
meshService.delegate = chatViewModel
|
||||
meshService.startServices()
|
||||
|
||||
android.util.Log.d("MainActivity", "Mesh service started successfully")
|
||||
|
||||
// Handle any notification intent
|
||||
handleNotificationIntent(intent)
|
||||
@@ -330,6 +348,8 @@ class MainActivity : ComponentActivity() {
|
||||
super.onResume()
|
||||
// Check Bluetooth status on resume and handle accordingly
|
||||
if (onboardingState == OnboardingState.COMPLETE) {
|
||||
// Set app foreground state
|
||||
meshService.connectionManager.setAppBackgroundState(false)
|
||||
chatViewModel.setAppBackgroundState(false)
|
||||
|
||||
// Check if Bluetooth was disabled while app was backgrounded
|
||||
@@ -347,6 +367,8 @@ class MainActivity : ComponentActivity() {
|
||||
super.onPause()
|
||||
// Only set background state if app is fully initialized
|
||||
if (onboardingState == OnboardingState.COMPLETE) {
|
||||
// Set app background state
|
||||
meshService.connectionManager.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() {
|
||||
super.onDestroy()
|
||||
// Only stop mesh services if they were started
|
||||
// Stop mesh services if app was fully initialized
|
||||
if (onboardingState == OnboardingState.COMPLETE) {
|
||||
try {
|
||||
chatViewModel.meshService.stopServices()
|
||||
meshService.stopServices()
|
||||
android.util.Log.d("MainActivity", "Mesh services stopped successfully")
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.w("MainActivity", "Error stopping mesh services in onDestroy: ${e.message}")
|
||||
}
|
||||
|
||||
@@ -129,16 +129,16 @@ class BluetoothConnectionManager(
|
||||
try {
|
||||
isActive = true
|
||||
|
||||
// Setup GATT server first
|
||||
setupGattServer()
|
||||
|
||||
// Start power manager and services
|
||||
connectionScope.launch {
|
||||
powerManager.start()
|
||||
|
||||
// Setup GATT server after power manager is ready
|
||||
setupGattServer()
|
||||
delay(500) // Ensure GATT server is ready
|
||||
delay(300) // Brief delay to ensure GATT server is ready
|
||||
|
||||
startAdvertising()
|
||||
delay(200)
|
||||
delay(100)
|
||||
|
||||
if (powerManager.shouldUseDutyCycle()) {
|
||||
Log.i(TAG, "Using power-aware duty cycling")
|
||||
@@ -458,48 +458,47 @@ class BluetoothConnectionManager(
|
||||
// Proper cleanup sequencing to prevent race conditions
|
||||
gattServer?.let { server ->
|
||||
Log.d(TAG, "Cleaning up existing GATT server")
|
||||
connectionScope.launch {
|
||||
// Give time for pending callbacks to complete
|
||||
delay(100)
|
||||
try {
|
||||
server.close()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error closing existing GATT server: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// Create new server after cleanup delay
|
||||
connectionScope.launch {
|
||||
delay(200) // Allow previous server to fully close
|
||||
|
||||
if (!isActive) {
|
||||
Log.d(TAG, "Service inactive, skipping GATT server creation")
|
||||
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")
|
||||
// Small delay to ensure cleanup is complete
|
||||
Thread.sleep(100)
|
||||
|
||||
if (!isActive) {
|
||||
Log.d(TAG, "Service inactive, skipping GATT server creation")
|
||||
return
|
||||
}
|
||||
|
||||
// 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")
|
||||
|
||||
@@ -69,10 +69,10 @@ class BluetoothMeshService(private val context: Context) {
|
||||
while (isActive) {
|
||||
try {
|
||||
delay(10000) // 10 seconds
|
||||
val debugInfo = getDebugStatus()
|
||||
Log.d(TAG, "=== PERIODIC DEBUG STATUS ===")
|
||||
Log.d(TAG, debugInfo)
|
||||
Log.d(TAG, "=== END DEBUG STATUS ===")
|
||||
if (isActive) { // Double-check before logging
|
||||
val debugInfo = getDebugStatus()
|
||||
Log.d(TAG, "=== PERIODIC DEBUG STATUS ===\n$debugInfo\n=== END DEBUG STATUS ===")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in periodic debug logging: ${e.message}")
|
||||
}
|
||||
@@ -282,7 +282,7 @@ class BluetoothMeshService(private val context: Context) {
|
||||
* Start the mesh service
|
||||
*/
|
||||
fun startServices() {
|
||||
// Prevent double starts
|
||||
// Prevent double starts (defensive programming)
|
||||
if (isActive) {
|
||||
Log.w(TAG, "Mesh service already active, ignoring duplicate start request")
|
||||
return
|
||||
@@ -297,9 +297,7 @@ class BluetoothMeshService(private val context: Context) {
|
||||
// Send initial announcements after services are ready
|
||||
serviceScope.launch {
|
||||
delay(1000)
|
||||
if (isActive) { // Check if still active
|
||||
sendBroadcastAnnounce()
|
||||
}
|
||||
sendBroadcastAnnounce()
|
||||
}
|
||||
} else {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -19,13 +19,13 @@ import kotlin.random.Random
|
||||
* Refactored ChatViewModel - Main coordinator for bitchat functionality
|
||||
* 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
|
||||
|
||||
// Core services
|
||||
val meshService = BluetoothMeshService(context)
|
||||
|
||||
// State management
|
||||
private val state = ChatState()
|
||||
|
||||
@@ -72,7 +72,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
|
||||
val favoritePeers: LiveData<Set<String>> = state.favoritePeers
|
||||
|
||||
init {
|
||||
meshService.delegate = this
|
||||
// Note: Mesh service delegate is now set by MainActivity
|
||||
loadAndInitialize()
|
||||
}
|
||||
|
||||
@@ -100,8 +100,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
|
||||
state.setFavoritePeers(dataManager.favoritePeers)
|
||||
dataManager.loadBlockedUsers()
|
||||
|
||||
// Start mesh service
|
||||
meshService.startServices()
|
||||
// Note: Mesh service is now started by MainActivity
|
||||
|
||||
// Show welcome message if no peers after delay
|
||||
viewModelScope.launch {
|
||||
@@ -120,7 +119,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
meshService.stopServices()
|
||||
// Note: Mesh service lifecycle is now managed by MainActivity
|
||||
}
|
||||
|
||||
// MARK: - Nickname Management
|
||||
@@ -259,18 +258,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
|
||||
return meshService.getDebugStatus()
|
||||
}
|
||||
|
||||
fun restartMeshServices() {
|
||||
viewModelScope.launch {
|
||||
meshService.stopServices()
|
||||
delay(1000)
|
||||
meshService.startServices()
|
||||
}
|
||||
}
|
||||
// Note: Mesh service restart is now handled by MainActivity
|
||||
// This function is no longer needed
|
||||
|
||||
fun setAppBackgroundState(inBackground: Boolean) {
|
||||
// Forward to connection manager for power optimization
|
||||
meshService.connectionManager.setAppBackgroundState(inBackground)
|
||||
|
||||
// Forward to notification manager for notification logic
|
||||
notificationManager.setAppBackgroundState(inBackground)
|
||||
}
|
||||
@@ -355,13 +346,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
|
||||
state.setNickname(newNickname)
|
||||
dataManager.saveNickname(newNickname)
|
||||
|
||||
// Disconnect from mesh
|
||||
meshService.stopServices()
|
||||
|
||||
// Restart services with new identity
|
||||
viewModelScope.launch {
|
||||
delay(500)
|
||||
meshService.startServices()
|
||||
}
|
||||
// Note: Mesh service restart is now handled by MainActivity
|
||||
// This method now only clears data, not mesh service lifecycle
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user