fix: stop tunnel regression

Fixes regression where tunnel is stuck in on state after x amount of toggles. Closes #163

Adds obfuscation of potentially sensitive data from logs.
Closes #160

Adds hiding of FAB on scroll to allow users to toggle tunnels when they have many tunnel configs.
Closes #161
This commit is contained in:
Zane Schepke 2024-04-16 00:14:06 -04:00
parent a2b8eb5b0b
commit 5447ec73f7
18 changed files with 108 additions and 61 deletions

View File

@ -97,7 +97,7 @@ jobs:
- name: Get checksum - name: Get checksum
id: checksum id: checksum
run: echo "checksum=$(apksigner verify -print-certs ${{ steps.apk-path.outputs.path }} | grep -Po "(?<=SHA-256 digest:) .*")" | awk '{$1=$1};1' >> $GITHUB_OUTPUT run: echo "checksum=$(apksigner verify -print-certs ${{ steps.apk-path.outputs.path }} | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
- name: Append checksum - name: Append checksum
id: append_checksum id: append_checksum
@ -105,8 +105,9 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
body: > body: |
<br /> SHA256 fingerprint: <br /> ```${{ steps.checksum.outputs.checksum }}``` SHA256 fingerprint:
```${{ steps.checksum.outputs.checksum }}```
tag_name: ${{ github.ref_name }} tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }} name: ${{ github.ref_name }}
draft: false draft: false

View File

@ -103,7 +103,7 @@ jobs:
- name: Get checksum - name: Get checksum
id: checksum id: checksum
run: echo "checksum=$(apksigner verify -print-certs ${{ steps.apk-path.outputs.path }} | grep -Po "(?<=SHA-256 digest:) .*")" | awk '{$1=$1};1' >> $GITHUB_OUTPUT run: echo "checksum=$(apksigner verify -print-certs ${{ steps.apk-path.outputs.path }} | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
- name: Append checksum - name: Append checksum
id: append_checksum id: append_checksum
@ -111,8 +111,9 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
body: > body: |
<br /> SHA256 fingerprint: <br /> ```${{ steps.checksum.outputs.checksum }}``` SHA256 fingerprint:
```${{ steps.checksum.outputs.checksum }}```
tag_name: ${{ github.ref_name }} tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }} name: ${{ github.ref_name }}
draft: false draft: false

View File

@ -24,7 +24,7 @@ class NotificationActionReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) = goAsync { override fun onReceive(context: Context, intent: Intent?) = goAsync {
try { try {
//TODO fix for manual start changes when enabled //TODO fix for manual start changes when enabled
serviceManager.stopVpnService(context) serviceManager.stopVpnServiceForeground(context)
delay(Constants.TOGGLE_TUNNEL_DELAY) delay(Constants.TOGGLE_TUNNEL_DELAY)
serviceManager.startVpnServiceForeground(context) serviceManager.startVpnServiceForeground(context)
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -3,5 +3,6 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
enum class Action { enum class Action {
START, START,
START_FOREGROUND, START_FOREGROUND,
STOP STOP,
STOP_FOREGROUND
} }

View File

@ -24,7 +24,7 @@ open class ForegroundService : LifecycleService() {
when (action) { when (action) {
Action.START.name, Action.START.name,
Action.START_FOREGROUND.name -> startService(intent.extras) Action.START_FOREGROUND.name -> startService(intent.extras)
Action.STOP.name, Action.STOP_FOREGROUND.name -> stopService()
Constants.ALWAYS_ON_VPN_ACTION -> { Constants.ALWAYS_ON_VPN_ACTION -> {
Timber.i("Always-on VPN starting service") Timber.i("Always-on VPN starting service")
startService(intent.extras) startService(intent.extras)
@ -37,16 +37,9 @@ open class ForegroundService : LifecycleService() {
"with a null intent. It has been probably restarted by the system.", "with a null intent. It has been probably restarted by the system.",
) )
} }
// by returning this we make sure the service is restarted if the system kills the service
return START_STICKY return START_STICKY
} }
override fun onDestroy() {
super.onDestroy()
Timber.d("The service has been destroyed")
stopService()
}
protected open fun startService(extras: Bundle?) { protected open fun startService(extras: Bundle?) {
if (isServiceStarted) return if (isServiceStarted) return
Timber.d("Starting ${this.javaClass.simpleName}") Timber.d("Starting ${this.javaClass.simpleName}")
@ -55,12 +48,8 @@ open class ForegroundService : LifecycleService() {
protected open fun stopService() { protected open fun stopService() {
Timber.d("Stopping ${this.javaClass.simpleName}") Timber.d("Stopping ${this.javaClass.simpleName}")
try {
stopForeground(STOP_FOREGROUND_REMOVE) stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf() stopSelf()
} catch (e: Exception) {
Timber.e(e)
}
isServiceStarted = false isServiceStarted = false
} }
} }

View File

@ -23,9 +23,8 @@ class ServiceManager(private val appDataRepository: AppDataRepository) {
intent.component?.javaClass intent.component?.javaClass
try { try {
when (action) { when (action) {
Action.START_FOREGROUND -> context.startForegroundService(intent) Action.START_FOREGROUND, Action.STOP_FOREGROUND -> context.startForegroundService(intent)
Action.START -> context.startService(intent) Action.START, Action.STOP -> context.startService(intent)
Action.STOP -> context.stopService(intent)
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e.message) Timber.e(e.message)
@ -46,6 +45,16 @@ class ServiceManager(private val appDataRepository: AppDataRepository) {
) )
} }
suspend fun stopVpnServiceForeground(context: Context, isManualStop: Boolean = false) {
if (isManualStop) onManualStop()
Timber.i("Stopping vpn service")
actionOnService(
Action.STOP_FOREGROUND,
context,
WireGuardTunnelService::class.java,
)
}
suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) { suspend fun stopVpnService(context: Context, isManualStop: Boolean = false) {
if (isManualStop) onManualStop() if (isManualStop) onManualStop()
Timber.i("Stopping vpn service") Timber.i("Stopping vpn service")

View File

@ -18,6 +18,7 @@ import com.zaneschepke.wireguardautotunnel.service.notification.NotificationServ
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -56,7 +57,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
private val networkEventsFlow = MutableStateFlow(WatcherState()) private val networkEventsFlow = MutableStateFlow(WatcherState())
private lateinit var watcherJob: Job private var watcherJob: Job? = null
private var wakeLock: PowerManager.WakeLock? = null private var wakeLock: PowerManager.WakeLock? = null
private val tag = this.javaClass.name private val tag = this.javaClass.name
@ -74,11 +75,6 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
} }
override fun onDestroy() {
super.onDestroy()
stopService()
}
override fun startService(extras: Bundle?) { override fun startService(extras: Bundle?) {
super.startService(extras) super.startService(extras)
try { try {
@ -139,8 +135,10 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
private fun cancelWatcherJob() { private fun cancelWatcherJob() {
if (this::watcherJob.isInitialized) { try {
watcherJob.cancel() watcherJob?.cancel()
} catch (e : CancellationException) {
Timber.i("Watcher job cancelled")
} }
} }
@ -236,7 +234,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
} }
if (results.contains(false)) { if (results.contains(false)) {
Timber.i("Restarting VPN for ping failure") Timber.i("Restarting VPN for ping failure")
serviceManager.stopVpnService(this) serviceManager.stopVpnServiceForeground(this)
delay(Constants.VPN_RESTART_DELAY) delay(Constants.VPN_RESTART_DELAY)
serviceManager.startVpnServiceForeground(this) serviceManager.startVpnServiceForeground(this)
delay(Constants.PING_COOLDOWN) delay(Constants.PING_COOLDOWN)
@ -324,7 +322,9 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
) )
val ssid = wifiService.getNetworkName(it.networkCapabilities) val ssid = wifiService.getNetworkName(it.networkCapabilities)
ssid?.let { ssid?.let {
Timber.i("Detected SSID: $ssid") if(it.contains(Constants.UNREADABLE_SSID)) {
Timber.w("SSID unreadable: missing permissions")
} else Timber.i("Detected valid SSID")
appDataRepository.appState.setCurrentSsid(ssid) appDataRepository.appState.setCurrentSsid(ssid)
networkEventsFlow.value = networkEventsFlow.value =
networkEventsFlow.value.copy( networkEventsFlow.value.copy(
@ -381,7 +381,7 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
watcherState.isTunnelOffOnMobileDataConditionMet() -> { watcherState.isTunnelOffOnMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off") Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
serviceManager.stopVpnService(this) serviceManager.stopVpnServiceForeground(this)
} }
watcherState.isTunnelNotWifiNamePreferredMet(watcherState.currentNetworkSSID) -> { watcherState.isTunnelNotWifiNamePreferredMet(watcherState.currentNetworkSSID) -> {
@ -407,17 +407,17 @@ class WireGuardConnectivityWatcherService : ForegroundService() {
watcherState.isTrustedWifiConditionMet() -> { watcherState.isTrustedWifiConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off") Timber.i("$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off")
serviceManager.stopVpnService(this) serviceManager.stopVpnServiceForeground(this)
} }
watcherState.isTunnelOffOnWifiConditionMet() -> { watcherState.isTunnelOffOnWifiConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off") Timber.i("$autoTunnel - tunnel off on wifi condition met, turning vpn off")
serviceManager.stopVpnService(this) serviceManager.stopVpnServiceForeground(this)
} }
watcherState.isTunnelOffOnNoConnectivityMet() -> { watcherState.isTunnelOffOnNoConnectivityMet() -> {
Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off") Timber.i("$autoTunnel - tunnel off on no connectivity met, turning vpn off")
serviceManager.stopVpnService(this) serviceManager.stopVpnServiceForeground(this)
} }
else -> { else -> {

View File

@ -16,10 +16,12 @@ import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.handshakeStatus import com.zaneschepke.wireguardautotunnel.util.handshakeStatus
import com.zaneschepke.wireguardautotunnel.util.mapPeerStats import com.zaneschepke.wireguardautotunnel.util.mapPeerStats
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -35,7 +37,7 @@ class WireGuardTunnelService : ForegroundService() {
@Inject @Inject
lateinit var notificationService: NotificationService lateinit var notificationService: NotificationService
private lateinit var job: Job private var job: Job? = null
private var didShowConnected = false private var didShowConnected = false
@ -49,11 +51,6 @@ class WireGuardTunnelService : ForegroundService() {
} }
} }
override fun onDestroy() {
super.onDestroy()
}
override fun startService(extras: Bundle?) { override fun startService(extras: Bundle?) {
super.startService(extras) super.startService(extras)
cancelJob() cancelJob()
@ -182,8 +179,10 @@ class WireGuardTunnelService : ForegroundService() {
} }
private fun cancelJob() { private fun cancelJob() {
if (this::job.isInitialized) { try {
job.cancel() job?.cancel()
} catch (e : CancellationException) {
Timber.i("Tunnel job cancelled")
} }
} }
} }

View File

@ -40,7 +40,7 @@ class ShortcutsActivity : ComponentActivity() {
this@ShortcutsActivity, tunnelConfig?.id, isManualStart = true, this@ShortcutsActivity, tunnelConfig?.id, isManualStart = true,
) )
Action.STOP.name -> serviceManager.stopVpnService( Action.STOP.name -> serviceManager.stopVpnServiceForeground(
this@ShortcutsActivity, this@ShortcutsActivity,
isManualStop = true, isManualStop = true,
) )

View File

@ -80,7 +80,7 @@ class TunnelControlTile : TileService() {
scope.launch { scope.launch {
try { try {
if (vpnService.getState() == Tunnel.State.UP) { if (vpnService.getState() == Tunnel.State.UP) {
serviceManager.stopVpnService( serviceManager.stopVpnServiceForeground(
this@TunnelControlTile, this@TunnelControlTile,
isManualStop = true, isManualStop = true,
) )

View File

@ -10,6 +10,7 @@ import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.Kernel import com.zaneschepke.wireguardautotunnel.module.Kernel
import com.zaneschepke.wireguardautotunnel.module.Userspace import com.zaneschepke.wireguardautotunnel.module.Userspace
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -33,7 +34,7 @@ constructor(
private val scope = CoroutineScope(Dispatchers.IO) private val scope = CoroutineScope(Dispatchers.IO)
private lateinit var statsJob: Job private var statsJob: Job? = null
private var backend: Backend = userspaceBackend private var backend: Backend = userspaceBackend
@ -134,8 +135,10 @@ constructor(
} }
} }
if (state == State.DOWN) { if (state == State.DOWN) {
if (this::statsJob.isInitialized) { try {
statsJob.cancel() statsJob?.cancel()
} catch (e : CancellationException) {
Timber.i("Stats job cancelled")
} }
} }
} }

View File

@ -118,12 +118,12 @@ constructor(
fun readLogCatOutput() = fun readLogCatOutput() =
viewModelScope.launch(viewModelScope.coroutineContext + Dispatchers.IO) { viewModelScope.launch(viewModelScope.coroutineContext + Dispatchers.IO) {
launch { launch {
Logcatter.logs { Logcatter.logs(callback = {
logs.add(it) logs.add(it)
if (logs.size > Constants.LOG_BUFFER_SIZE) { if (logs.size > Constants.LOG_BUFFER_SIZE) {
logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt()) logs.removeRange(0, (logs.size - Constants.LOG_BUFFER_SIZE).toInt())
} }
} })
} }
} }

View File

@ -71,8 +71,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
@ -124,6 +128,25 @@ fun MainScreen(
val sheetState = rememberModalBottomSheetState() val sheetState = rememberModalBottomSheetState()
var showBottomSheet by remember { mutableStateOf(false) } var showBottomSheet by remember { mutableStateOf(false) }
// Nested scroll for control FAB
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
// Hide FAB
if (available.y < -1) {
isVisible.value = false
}
// Show FAB
if (available.y > 1) {
isVisible.value = true
}
return Offset.Zero
}
}
}
var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) } var showDeleteTunnelAlertDialog by remember { mutableStateOf(false) }
var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) } var selectedTunnel by remember { mutableStateOf<TunnelConfig?>(null) }
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
@ -377,8 +400,8 @@ fun MainScreen(
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxSize()
.overscroll(ScrollableDefaults.overscrollEffect()), .overscroll(ScrollableDefaults.overscrollEffect()).nestedScroll(nestedScrollConnection),
state = rememberLazyListState(0, uiState.tunnels.count()), state = rememberLazyListState(0, uiState.tunnels.count()),
userScrollEnabled = true, userScrollEnabled = true,
reverseLayout = false, reverseLayout = false,

View File

@ -35,4 +35,6 @@ object Constants {
const val TUNNEL_EXTRA_KEY = "tunnelId" const val TUNNEL_EXTRA_KEY = "tunnelId"
const val UNREADABLE_SSID = "<unknown ssid>"
} }

View File

@ -1,7 +1,7 @@
object Constants { object Constants {
const val VERSION_NAME = "3.4.1" const val VERSION_NAME = "3.4.2"
const val JVM_TARGET = "17" const val JVM_TARGET = "17"
const val VERSION_CODE = 34100 const val VERSION_CODE = 34200
const val TARGET_SDK = 34 const val TARGET_SDK = 34
const val MIN_SDK = 26 const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel" const val APP_ID = "com.zaneschepke.wireguardautotunnel"

View File

@ -0,0 +1,4 @@
What's new:
- Fix stop tunnel regression
- Add logs obfuscation
- Add hide FAB on scroll

View File

@ -20,7 +20,7 @@ pinLockCompose = "1.0.3"
roomVersion = "2.6.1" roomVersion = "2.6.1"
timber = "5.0.1" timber = "5.0.1"
tunnel = "1.0.20230706" tunnel = "1.0.20230706"
androidGradlePlugin = "8.4.0-rc01" androidGradlePlugin = "8.4.0-rc02"
kotlin = "1.9.23" kotlin = "1.9.23"
ksp = "1.9.23-1.0.19" ksp = "1.9.23-1.0.19"
composeBom = "2024.03.00" composeBom = "2024.03.00"

View File

@ -3,16 +3,31 @@ package com.zaneschepke.logcatter
import com.zaneschepke.logcatter.model.LogMessage import com.zaneschepke.logcatter.model.LogMessage
object Logcatter { object Logcatter {
fun logs(callback: (input: LogMessage) -> Unit) {
private val findKeyRegex = """[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=""".toRegex()
private val findIpv6AddressRegex = """(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))""".toRegex()
private val findIpv4AddressRegex = """((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}""".toRegex()
private val findTunnelNameRegex = """(?<=tunnel ).*?(?= UP| DOWN)""".toRegex()
fun logs(callback: (input: LogMessage) -> Unit, obfuscator: (log : String) -> String = { log -> this.obfuscator(log)}){
clear() clear()
Runtime.getRuntime().exec("logcat -v epoch") Runtime.getRuntime().exec("logcat -v epoch")
.inputStream .inputStream
.bufferedReader() .bufferedReader()
.useLines { lines -> .useLines { lines ->
lines.forEach { callback(LogMessage.from(it)) } lines.forEach { callback(LogMessage.from(obfuscator(it))) }
} }
} }
private fun obfuscator(log : String) : String {
return findKeyRegex.replace(log, "<crypto-key>").let { first ->
findIpv6AddressRegex.replace(first, "<ipv6-address>").let { second ->
findTunnelNameRegex.replace(second, "<tunnel>")
}
}.let{ last -> findIpv4AddressRegex.replace(last,"<ipv4-address>") }
}
fun clear() { fun clear() {
Runtime.getRuntime().exec("logcat -c") Runtime.getRuntime().exec("logcat -c")
} }