/*
* 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.content.Context
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.startService
import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.scopes.ActivityScoped
import kotlinx.coroutines.Job
import org.meshtastic.core.service.IMeshService
import org.meshtastic.core.service.ServiceRepository
import timber.log.Timber
import javax.inject.Inject
/** A Activity-lifecycle-aware [ServiceClient] that binds [MeshService] once the Activity is started. */
@ActivityScoped
class MeshServiceClient
@Inject
constructor(
@ActivityContext private val context: Context,
private val serviceRepository: ServiceRepository,
) : ServiceClient(IMeshService.Stub::asInterface),
DefaultLifecycleObserver {
private val lifecycleOwner: LifecycleOwner = context as LifecycleOwner
// TODO Inject this for ease of testing
private var serviceSetupJob: Job? = null
init {
Timber.d("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)
Timber.d("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)
Timber.d("Lifecycle: ON_START")
try {
bindMeshService()
} catch (ex: BindFailedException) {
Timber.e("Bind of MeshService failed: ${ex.message}")
}
}
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
Timber.d("Lifecycle: ON_DESTROY")
owner.lifecycle.removeObserver(this)
Timber.d("Removed self as LifecycleObserver to $lifecycleOwner")
}
// endregion
@Suppress("TooGenericExceptionCaught")
private fun bindMeshService() {
Timber.d("Binding to mesh service!")
try {
MeshService.startService(context)
} catch (ex: Exception) {
Timber.e("Failed to start service from activity - but ignoring because bind will work: ${ex.message}")
}
connect(context, MeshService.createIntent(context), BIND_AUTO_CREATE + BIND_ABOVE_CLIENT)
}
}