diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index 15da8e36c..6aca2709e 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -45,18 +45,11 @@ import androidx.compose.ui.platform.LocalView import androidx.core.content.edit import androidx.core.net.toUri import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import androidx.lifecycle.lifecycleScope -import com.geeksville.mesh.android.BindFailedException import com.geeksville.mesh.android.GeeksvilleApplication import com.geeksville.mesh.android.Logging -import com.geeksville.mesh.android.ServiceClient -import com.geeksville.mesh.concurrent.handledLaunch import com.geeksville.mesh.model.BluetoothViewModel import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.navigation.DEEP_LINK_BASE_URI -import com.geeksville.mesh.service.MeshService -import com.geeksville.mesh.service.ServiceRepository -import com.geeksville.mesh.service.startService import com.geeksville.mesh.ui.MainMenuAction import com.geeksville.mesh.ui.MainScreen import com.geeksville.mesh.ui.common.theme.AppTheme @@ -66,7 +59,6 @@ import com.geeksville.mesh.ui.sharing.toSharedContact import com.geeksville.mesh.util.LanguageUtils import com.geeksville.mesh.util.getPackageInfoCompat import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.Job import javax.inject.Inject @AndroidEntryPoint @@ -76,7 +68,8 @@ class MainActivity : private val bluetoothViewModel: BluetoothViewModel by viewModels() private val model: UIViewModel by viewModels() - @Inject internal lateinit var serviceRepository: ServiceRepository + // This is aware of the Activity lifecycle and handles binding to the mesh service. + @Inject internal lateinit var meshServiceClient: MeshServiceClient private var showAppIntro by mutableStateOf(false) @@ -225,46 +218,6 @@ class MainActivity : } } - private var serviceSetupJob: Job? = null - - private val mesh = - object : ServiceClient(IMeshService.Stub::asInterface) { - override fun onConnected(service: IMeshService) { - serviceSetupJob?.cancel() - serviceSetupJob = - lifecycleScope.handledLaunch { - serviceRepository.setMeshService(service) - debug("connected to mesh service, connectionState=${model.connectionState.value}") - } - } - - override fun onDisconnected() { - serviceSetupJob?.cancel() - serviceRepository.setMeshService(null) - } - } - - private fun bindMeshService() { - debug("Binding to mesh service!") - try { - MeshService.startService(this) - } catch (ex: Exception) { - errormsg("Failed to start service from activity - but ignoring because bind will work ${ex.message}") - } - - mesh.connect(this, MeshService.createIntent(), BIND_AUTO_CREATE + BIND_ABOVE_CLIENT) - } - - override fun onStart() { - super.onStart() - - try { - bindMeshService() - } catch (ex: BindFailedException) { - errormsg("Bind of MeshService failed${ex.message}") - } - } - private fun showSettingsPage() { createSettingsIntent().send() } diff --git a/app/src/main/java/com/geeksville/mesh/MeshServiceClient.kt b/app/src/main/java/com/geeksville/mesh/MeshServiceClient.kt new file mode 100644 index 000000000..88e68729c --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/MeshServiceClient.kt @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2025 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.geeksville.mesh + +import android.app.Activity +import androidx.appcompat.app.AppCompatActivity.BIND_ABOVE_CLIENT +import androidx.appcompat.app.AppCompatActivity.BIND_AUTO_CREATE +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.geeksville.mesh.android.BindFailedException +import com.geeksville.mesh.android.ServiceClient +import com.geeksville.mesh.concurrent.handledLaunch +import com.geeksville.mesh.service.MeshService +import com.geeksville.mesh.service.ServiceRepository +import com.geeksville.mesh.service.startService +import dagger.hilt.android.scopes.ActivityScoped +import kotlinx.coroutines.Job +import javax.inject.Inject + +/** A Activity-lifecycle-aware [ServiceClient] that binds [MeshService] once the Activity is started. */ +@ActivityScoped +class MeshServiceClient +@Inject +constructor( + /** + * Ideally, this would be broken up into Context and LifecycleOwner. However, ApplicationModule defines its own + * LifecycleOwner which overrides the default binding for @ActivityScoped. The solution to this is to add a + * qualifier to the LifecycleOwner provider in ApplicationModule. + */ + private val activity: Activity, + private val serviceRepository: ServiceRepository, +) : ServiceClient(IMeshService.Stub::asInterface), + DefaultLifecycleObserver { + + // TODO Use the default binding for @ActivityScoped + private val lifecycleOwner: LifecycleOwner = activity as LifecycleOwner + + // TODO Inject this for ease of testing + private var serviceSetupJob: Job? = null + + init { + debug("Adding self as LifecycleObserver for $lifecycleOwner") + lifecycleOwner.lifecycle.addObserver(this) + } + + // region ServiceClient overrides + + override fun onConnected(service: IMeshService) { + serviceSetupJob?.cancel() + serviceSetupJob = + lifecycleOwner.lifecycleScope.handledLaunch { + serviceRepository.setMeshService(service) + debug("connected to mesh service, connectionState=${serviceRepository.connectionState.value}") + } + } + + override fun onDisconnected() { + serviceSetupJob?.cancel() + serviceRepository.setMeshService(null) + } + + // endregion + + // region DefaultLifecycleObserver overrides + + override fun onStart(owner: LifecycleOwner) { + super.onStart(owner) + debug("Lifecycle: ON_START") + + try { + bindMeshService() + } catch (ex: BindFailedException) { + errormsg("Bind of MeshService failed: ${ex.message}") + } + } + + override fun onDestroy(owner: LifecycleOwner) { + super.onDestroy(owner) + debug("Lifecycle: ON_DESTROY") + + owner.lifecycle.removeObserver(this) + debug("Removed self as LifecycleObserver to $lifecycleOwner") + } + + // endregion + + @Suppress("TooGenericExceptionCaught") + private fun bindMeshService() { + debug("Binding to mesh service!") + try { + MeshService.startService(activity) + } catch (ex: Exception) { + errormsg("Failed to start service from activity - but ignoring because bind will work: ${ex.message}") + } + + connect(activity, MeshService.createIntent(), BIND_AUTO_CREATE + BIND_ABOVE_CLIENT) + } +}