fix: tile control and kernel sync (#320)

increase auto tunnel delay to 3 seconds

optimize stats job by killing it when app is backgrounded

fix tunnel launch from background

add restart of services and tunnels after update
This commit is contained in:
Zane Schepke 2024-08-17 21:29:31 -04:00 committed by GitHub
parent 3f4673b2a7
commit 30851a7d7b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 235 additions and 109 deletions

View File

@ -167,6 +167,16 @@
android:stopWithTask="false"
tools:node="merge" />
<service
android:name=".service.foreground.TunnelBackgroundService"
android:exported="false"
android:foregroundServiceType="systemExempted"
android:permission="android.permission.BIND_VPN_SERVICE">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
<receiver
android:name=".receiver.BootReceiver"
android:enabled="true"
@ -184,6 +194,13 @@
android:name=".receiver.BackgroundActionReceiver"
android:enabled="true"
android:exported="false"/>
<receiver
android:name=".receiver.AppUpdateReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
<receiver
android:name=".receiver.KernelReceiver"
android:exported="false"

View File

@ -3,10 +3,15 @@ package com.zaneschepke.wireguardautotunnel
import android.app.Application
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy
import com.zaneschepke.logcatter.LocalLogCollector
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ -17,6 +22,13 @@ class WireGuardAutoTunnel : Application() {
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var localLogCollector: LocalLogCollector
@Inject
@IoDispatcher
lateinit var ioDispatcher: CoroutineDispatcher
override fun onCreate() {
super.onCreate()
instance = this
@ -33,6 +45,11 @@ class WireGuardAutoTunnel : Application() {
} else {
Timber.plant(ReleaseTree())
}
if (!isRunningOnTv()) {
applicationScope.launch(ioDispatcher) {
localLogCollector.start()
}
}
}
companion object {

View File

@ -0,0 +1,47 @@
package com.zaneschepke.wireguardautotunnel.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class AppUpdateReceiver : BroadcastReceiver() {
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var appDataRepository: AppDataRepository
@Inject
lateinit var serviceManager: ServiceManager
@Inject
lateinit var tunnelService: TunnelService
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return
applicationScope.launch {
val settings = appDataRepository.settings.getSettings()
if (settings.isAutoTunnelEnabled) {
Timber.i("Restarting services after upgrade")
serviceManager.startWatcherServiceForeground(context)
}
if (!settings.isAutoTunnelEnabled || settings.isAutoTunnelPaused) {
val tunnels = appDataRepository.tunnels.getAll().filter { it.isActive }
if (tunnels.isNotEmpty()) context.startTunnelBackground(tunnels.first().id)
}
}
}
}

View File

@ -5,10 +5,12 @@ import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Provider
@ -25,14 +27,19 @@ class BackgroundActionReceiver : BroadcastReceiver() {
@Inject
lateinit var tunnelConfigRepository: TunnelConfigRepository
@Inject
lateinit var serviceManager: ServiceManager
override fun onReceive(context: Context, intent: Intent) {
val id = intent.getIntExtra(TUNNEL_ID_EXTRA_KEY, 0)
if (id == 0) return
when (intent.action) {
ACTION_CONNECT -> {
Timber.d("Connect actions")
applicationScope.launch {
val tunnel = tunnelConfigRepository.getById(id)
tunnel?.let {
serviceManager.startTunnelBackgroundService(context)
tunnelService.get().startTunnel(it)
}
}
@ -41,6 +48,7 @@ class BackgroundActionReceiver : BroadcastReceiver() {
applicationScope.launch {
val tunnel = tunnelConfigRepository.getById(id)
tunnel?.let {
serviceManager.stopTunnelBackgroundService(context)
tunnelService.get().stopTunnel(it)
}
}

View File

@ -7,6 +7,7 @@ import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -35,7 +36,7 @@ class BootReceiver : BroadcastReceiver() {
val settings = appDataRepository.settings.getSettings()
if (settings.isRestoreOnBootEnabled) {
appDataRepository.getStartTunnelConfig()?.let {
tunnelService.get().startTunnel(it)
context.startTunnelBackground(it.id)
}
}
if (settings.isAutoTunnelEnabled) {

View File

@ -50,4 +50,20 @@ class ServiceManager {
AutoTunnelService::class.java,
)
}
fun startTunnelBackgroundService(context: Context) {
actionOnService(
Action.START_FOREGROUND,
context,
TunnelBackgroundService::class.java,
)
}
fun stopTunnelBackgroundService(context: Context) {
actionOnService(
Action.STOP,
context,
TunnelBackgroundService::class.java,
)
}
}

View File

@ -0,0 +1,41 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.Notification
import android.os.Bundle
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class TunnelBackgroundService : ForegroundService() {
@Inject
lateinit var notificationService: NotificationService
private val foregroundId = 123
override fun onCreate() {
super.onCreate()
startForeground(foregroundId, createNotification())
}
override fun startService(extras: Bundle?) {
super.startService(extras)
startForeground(foregroundId, createNotification())
}
override fun stopService() {
super.stopService()
stopForeground(STOP_FOREGROUND_REMOVE)
}
private fun createNotification(): Notification {
return notificationService.createNotification(
getString(R.string.vpn_channel_id),
getString(R.string.vpn_channel_name),
getString(R.string.tunnel_start_text),
description = "",
)
}
}

View File

@ -7,6 +7,8 @@ import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
import com.zaneschepke.wireguardautotunnel.util.extensions.stopTunnelBackground
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -42,8 +44,8 @@ class ShortcutsActivity : ComponentActivity() {
Timber.d("Shortcut action on name: ${tunnelConfig?.name}")
tunnelConfig?.let {
when (intent.action) {
Action.START.name -> tunnelService.get().startTunnel(it)
Action.STOP.name -> tunnelService.get().stopTunnel(it)
Action.START.name -> this@ShortcutsActivity.startTunnelBackground(it.id)
Action.STOP.name -> this@ShortcutsActivity.stopTunnelBackground(it.id)
else -> Unit
}
}

View File

@ -68,6 +68,7 @@ class TunnelControlTile : TileService(), LifecycleOwner {
override fun onClick() {
super.onClick()
unlockAndRun {
Timber.d("Click")
lifecycleScope.launch {
val context = this@TunnelControlTile
val lastActive = appDataRepository.getStartTunnelConfig()

View File

@ -14,4 +14,7 @@ interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel {
suspend fun runningTunnelNames(): Set<String>
suspend fun getState(): TunnelState
fun cancelStatsJob()
fun startStatsJob()
}

View File

@ -14,7 +14,6 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStati
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
@ -153,6 +152,14 @@ constructor(
}
}
override fun cancelStatsJob() {
statsJob?.cancel()
}
override fun startStatsJob() {
statsJob = startTunnelStatisticsJob()
}
override fun getName(): String {
return _vpnState.value.tunnelConfig?.name ?: ""
}
@ -164,15 +171,9 @@ constructor(
private fun handleStateChange(state: TunnelState) {
emitTunnelState(state)
WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
if (state == TunnelState.UP) {
statsJob = startTunnelStatisticsJob()
}
if (state == TunnelState.DOWN) {
try {
statsJob?.cancel()
} catch (e: CancellationException) {
Timber.i("Stats job cancelled")
}
when (state) {
TunnelState.UP -> startStatsJob()
else -> cancelStatsJob()
}
}

View File

@ -39,8 +39,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
import com.zaneschepke.wireguardautotunnel.ui.common.prompt.CustomSnackBar
import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigScreen
@ -65,10 +64,7 @@ class MainActivity : AppCompatActivity() {
lateinit var appStateRepository: AppStateRepository
@Inject
lateinit var settingsRepository: SettingsRepository
@Inject
lateinit var serviceManager: ServiceManager
lateinit var tunnelService: TunnelService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -77,13 +73,6 @@ class MainActivity : AppCompatActivity() {
enableEdgeToEdge(navigationBarStyle = SystemBarStyle.dark(Color.Transparent.toArgb()))
lifecycleScope.launch {
val settings = settingsRepository.getSettings()
if (settings.isAutoTunnelEnabled) {
serviceManager.startWatcherService(application.applicationContext)
}
}
setContent {
val appViewModel = hiltViewModel<AppViewModel>()
val appUiState by appViewModel.appUiState.collectAsStateWithLifecycle()
@ -241,4 +230,9 @@ class MainActivity : AppCompatActivity() {
}
}
}
override fun onDestroy() {
super.onDestroy()
tunnelService.cancelStatsJob()
}
}

View File

@ -9,21 +9,16 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.zaneschepke.logcatter.LocalLogCollector
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
import xyz.teamgravity.pin_lock_compose.PinManager
import javax.inject.Inject
import javax.inject.Provider
@ -41,11 +36,7 @@ class SplashActivity : ComponentActivity() {
lateinit var tunnelService: Provider<TunnelService>
@Inject
lateinit var localLogCollector: LocalLogCollector
@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope
lateinit var serviceManager: ServiceManager
override fun onCreate(savedInstanceState: Bundle?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@ -54,29 +45,17 @@ class SplashActivity : ComponentActivity() {
}
super.onCreate(savedInstanceState)
applicationScope.launch {
if (!this@SplashActivity.isRunningOnTv()) localLogCollector.start()
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
val pinLockEnabled = appStateRepository.isPinLockEnabled()
if (pinLockEnabled) {
PinManager.initialize(WireGuardAutoTunnel.instance)
}
// TODO eventually make this support multi-tunnel
Timber.d("Check for active tunnels")
val settings = appDataRepository.settings.getSettings()
if (settings.isKernelEnabled) {
// delay in case state change is underway while app is opened
delay(Constants.FOCUS_REQUEST_DELAY)
val activeTunnels = appDataRepository.tunnels.getActive()
Timber.d("Kernel mode enabled, seeing if we need to start a tunnel")
activeTunnels.firstOrNull()?.let {
Timber.d("Trying to start active kernel tunnel: ${it.name}")
tunnelService.get().startTunnel(it)
}
}
if (settings.isAutoTunnelEnabled) serviceManager.startWatcherService(application.applicationContext)
if (tunnelService.get().getState() == TunnelState.UP) tunnelService.get().startStatsJob()
val tunnels = appDataRepository.tunnels.getActive()
if (tunnels.isNotEmpty() && tunnelService.get().getState() == TunnelState.DOWN) tunnelService.get().startTunnel(tunnels.first())
requestTunnelTileServiceStateUpdate()
requestAutoTunnelTileServiceUpdate()

View File

@ -22,7 +22,7 @@ fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavIte
showBottomBar = bottomNavItems.firstOrNull { navBackStackEntry?.destination?.route?.contains(it.route) == true } != null
if(showBottomBar) {
if (showBottomBar) {
NavigationBar(
containerColor = MaterialTheme.colorScheme.surface,
) {
@ -32,7 +32,7 @@ fun BottomNavBar(navController: NavController, bottomNavItems: List<BottomNavIte
NavigationBarItem(
selected = selected,
onClick = {
if(navBackStackEntry?.destination?.route == item.route) return@NavigationBarItem
if (navBackStackEntry?.destination?.route == item.route) return@NavigationBarItem
navController.navigate(item.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations

View File

@ -12,7 +12,6 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
@ -67,7 +66,6 @@ import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Screen
@ -84,9 +82,7 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.launchAppSettings
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
import kotlinx.coroutines.launch
import timber.log.Timber
import xyz.teamgravity.pin_lock_compose.PinManager
import java.io.File
@OptIn(
ExperimentalPermissionsApi::class,
@ -155,43 +151,6 @@ fun SettingsScreen(
},
)
fun exportAllConfigs() {
try {
val wgFiles =
uiState.tunnels.map { config ->
val file = File(context.cacheDir, "${config.name}-wg.conf")
file.outputStream().use {
it.write(config.wgQuick.toByteArray())
}
file
}
val amFiles =
uiState.tunnels.mapNotNull { config ->
if (config.amQuick != TunnelConfig.AM_QUICK_DEFAULT) {
val file = File(context.cacheDir, "${config.name}-am.conf")
file.outputStream().use {
it.write(config.amQuick.toByteArray())
}
file
} else {
null
}
}
scope.launch {
viewModel.onExportTunnels(wgFiles + amFiles).onFailure {
appViewModel.showSnackbarMessage(it.getMessage(context))
}.onSuccess {
didExportFiles = true
appViewModel.showSnackbarMessage(
context.getString(R.string.exported_configs_message),
)
}
}
} catch (e: Exception) {
Timber.e(e)
}
}
fun isBatteryOptimizationsDisabled(): Boolean {
val pm = context.getSystemService(POWER_SERVICE) as PowerManager
return pm.isIgnoringBatteryOptimizations(context.packageName)
@ -300,7 +259,13 @@ fun SettingsScreen(
AuthorizationPrompt(
onSuccess = {
showAuthPrompt = false
exportAllConfigs()
scope.launch {
viewModel.exportAllConfigs().onSuccess {
appViewModel.showSnackbarMessage(context.getString(R.string.exported_configs_message))
}.onFailure {
appViewModel.showSnackbarMessage(context.getString(R.string.export_configs_failed))
}
}
},
onError = { _ ->
showAuthPrompt = false
@ -379,7 +344,7 @@ fun SettingsScreen(
.focusRequester(focusRequester)
},
)
AnimatedVisibility(visible = uiState.settings.isTunnelOnWifiEnabled) {
if (uiState.settings.isTunnelOnWifiEnabled) {
Column {
FlowRow(
modifier =

View File

@ -248,4 +248,13 @@ constructor(
onSuccess()
}
}
suspend fun exportAllConfigs(): Result<Unit> {
return kotlin.runCatching {
val tunnels = appDataRepository.tunnels.getAll()
val wgFiles = fileUtils.createWgFiles(tunnels)
val amFiles = fileUtils.createAmFiles(tunnels)
onExportTunnels(wgFiles + amFiles)
}
}
}

View File

@ -7,7 +7,7 @@ object Constants {
const val MANUAL_TUNNEL_CONFIG_ID = "0"
const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1_000L // 10 minutes
const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L
const val WATCHER_COLLECTION_DELAY = 1_000L
const val WATCHER_COLLECTION_DELAY = 3_000L
const val CONF_FILE_EXTENSION = ".conf"
const val ZIP_FILE_EXTENSION = ".zip"
const val URI_CONTENT_SCHEME = "content"

View File

@ -6,6 +6,8 @@ import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.provider.MediaStore.MediaColumns
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import timber.log.Timber
@ -39,6 +41,26 @@ class FileUtils(
}
}
fun createWgFiles(tunnels: TunnelConfigs): List<File> {
return tunnels.map { config ->
val file = File(context.cacheDir, "${config.name}-wg.conf")
file.outputStream().use {
it.write(config.wgQuick.toByteArray())
}
file
}
}
fun createAmFiles(tunnels: TunnelConfigs): List<File> {
return tunnels.filter { it.amQuick != TunnelConfig.AM_QUICK_DEFAULT }.map { config ->
val file = File(context.cacheDir, "${config.name}-am.conf")
file.outputStream().use {
it.write(config.amQuick.toByteArray())
}
file
}
}
suspend fun saveByteArrayToDownloads(content: ByteArray, fileName: String): Result<Unit> {
return withContext(ioDispatcher) {
try {

View File

@ -1,7 +1,7 @@
object Constants {
const val VERSION_NAME = "3.5.0"
const val VERSION_NAME = "3.5.1"
const val JVM_TARGET = "17"
const val VERSION_CODE = 35001
const val VERSION_CODE = 35100
const val TARGET_SDK = 34
const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel"

View File

@ -0,0 +1,5 @@
What's new:
- Fixes for tunnels not launching from background
- Add support for restart services after update
- UI animation speed improvements
- Other optimizations

View File

@ -5,7 +5,7 @@ import kotlinx.coroutines.flow.Flow
import java.io.File
interface LocalLogCollector {
fun start(onLogMessage: ((message: LogMessage) -> Unit)? = null)
suspend fun start(onLogMessage: ((message: LogMessage) -> Unit)? = null)
fun stop()

View File

@ -69,7 +69,7 @@ object LogcatUtil {
internal object Logcat : LocalLogCollector {
private var logcatReader: LogcatReader? = null
override fun start(onLogMessage: ((message: LogMessage) -> Unit)?) {
override suspend fun start(onLogMessage: ((message: LogMessage) -> Unit)?) {
logcatReader ?: run {
logcatReader =
LogcatReader(
@ -78,9 +78,7 @@ object LogcatUtil {
onLogMessage,
)
}
logcatReader?.let { logReader ->
if (!logReader.isAlive) logReader.start()
}
logcatReader?.run()
}
override fun stop() {
@ -142,7 +140,7 @@ object LogcatUtil {
pID: String,
private val logcatPath: String,
private val callback: ((input: LogMessage) -> Unit)?,
) : Thread() {
) {
private var logcatProc: Process? = null
private var reader: BufferedReader? = null
private var mRunning = true
@ -177,7 +175,7 @@ object LogcatUtil {
}.let { last -> findIpv4AddressRegex.replace(last, "<ipv4-address>") }
}
override fun run() {
fun run() {
if (outputStream == null) return
try {
clear()