feat: improve logs screen scroll

Add logs screen share
This commit is contained in:
Zane Schepke
2024-10-13 01:44:21 -04:00
parent 1306fdc8b2
commit fea31437cd
11 changed files with 375 additions and 318 deletions
@@ -3,7 +3,7 @@ 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.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.util.ReleaseTree
@@ -23,7 +23,7 @@ class WireGuardAutoTunnel : Application() {
lateinit var applicationScope: CoroutineScope
@Inject
lateinit var localLogCollector: LocalLogCollector
lateinit var logReader: LogReader
@Inject
@IoDispatcher
@@ -47,7 +47,7 @@ class WireGuardAutoTunnel : Application() {
}
if (!isRunningOnTv()) {
applicationScope.launch(ioDispatcher) {
localLogCollector.start()
logReader.start()
}
}
}
@@ -1,8 +1,8 @@
package com.zaneschepke.wireguardautotunnel.module
import android.content.Context
import com.zaneschepke.logcatter.LocalLogCollector
import com.zaneschepke.logcatter.LogcatUtil
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.logcatter.LogcatCollector
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -24,7 +24,7 @@ class AppModule {
@Singleton
@Provides
fun provideLogCollect(@ApplicationContext context: Context): LocalLogCollector {
return LogcatUtil.init(context = context)
fun provideLogCollect(@ApplicationContext context: Context): LogReader {
return LogcatCollector.init(context = context)
}
}
@@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.logs
import android.annotation.SuppressLint
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
@@ -13,7 +12,7 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Save
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -21,8 +20,12 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -35,9 +38,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.logcatter.model.LogMessage
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel
import kotlinx.coroutines.launch
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
@@ -45,35 +46,52 @@ fun LogsScreen(viewModel: LogsViewModel = hiltViewModel()) {
val logs = viewModel.logs
val context = LocalContext.current
val clipboardManager: ClipboardManager = LocalClipboardManager.current
val lazyColumnListState = rememberLazyListState()
val clipboardManager: ClipboardManager = LocalClipboardManager.current
val scope = rememberCoroutineScope()
var isAutoScrolling by remember { mutableStateOf(true) }
var lastScrollPosition by remember { mutableIntStateOf(0) }
LaunchedEffect(logs.size) {
scope.launch {
LaunchedEffect(isAutoScrolling) {
if (isAutoScrolling) {
lazyColumnListState.animateScrollToItem(logs.size)
}
}
LaunchedEffect(logs.size) {
if (isAutoScrolling) {
lazyColumnListState.animateScrollToItem(logs.size)
}
}
LaunchedEffect(lazyColumnListState) {
snapshotFlow { lazyColumnListState.firstVisibleItemIndex }
.collect { currentScrollPosition ->
if (currentScrollPosition < lastScrollPosition && isAutoScrolling) {
isAutoScrolling = false
}
val visible = lazyColumnListState.layoutInfo.visibleItemsInfo
if (visible.isNotEmpty()) {
if (visible.last().index
== lazyColumnListState.layoutInfo.totalItemsCount - 1 && !isAutoScrolling
) {
isAutoScrolling = true
}
}
lastScrollPosition = currentScrollPosition
}
}
Scaffold(
floatingActionButton = {
FloatingActionButton(
onClick = {
scope.launch {
viewModel.saveLogsToFile().onSuccess {
Toast.makeText(
context,
context.getString(R.string.logs_saved),
Toast.LENGTH_SHORT,
).show()
}
}
viewModel.shareLogs(context)
},
shape = RoundedCornerShape(16.dp),
containerColor = MaterialTheme.colorScheme.primary,
) {
val icon = Icons.Filled.Save
val icon = Icons.Filled.Share
Icon(
imageVector = icon,
contentDescription = icon.name,
@@ -1,19 +1,25 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.logs
import android.content.Context
import androidx.compose.runtime.mutableStateListOf
import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.zaneschepke.logcatter.LocalLogCollector
import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.logcatter.model.LogMessage
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.MainDispatcher
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.FileUtils
import com.zaneschepke.wireguardautotunnel.util.extensions.chunked
import com.zaneschepke.wireguardautotunnel.util.extensions.launchShareFile
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job
import com.zaneschepke.wireguardautotunnel.R
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
import java.time.Duration
import java.time.Instant
import javax.inject.Inject
@@ -22,8 +28,7 @@ import javax.inject.Inject
class LogsViewModel
@Inject
constructor(
private val localLogCollector: LocalLogCollector,
private val fileUtils: FileUtils,
private val localLogCollector: LogReader,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@MainDispatcher private val mainDispatcher: CoroutineDispatcher,
) : ViewModel() {
@@ -44,13 +49,19 @@ constructor(
}
}
suspend fun saveLogsToFile(): Result<Unit> {
val file =
localLogCollector.getLogFile().getOrElse {
return Result.failure(it)
}
val fileContent = fileUtils.readBytesFromFile(file)
val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt"
return fileUtils.saveByteArrayToDownloads(fileContent, fileName)
fun shareLogs(context: Context): Job = viewModelScope.launch(ioDispatcher) {
runCatching {
val sharePath = File(context.filesDir, "external_files")
if (sharePath.exists()) sharePath.delete()
sharePath.mkdir()
val file = File("${sharePath.path + "/" + Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.zip")
if (file.exists()) file.delete()
file.createNewFile()
localLogCollector.zipLogFiles(file.absolutePath)
val uri = FileProvider.getUriForFile(context, context.getString(R.string.provider), file)
context.launchShareFile(uri)
}.onFailure {
Timber.e(it)
}
}
}
@@ -26,6 +26,16 @@ fun Context.openWebUrl(url: String): Result<Unit> {
}
}
fun Context.launchShareFile(file: Uri) {
val shareIntent = Intent().apply {
setAction(Intent.ACTION_SEND)
setType("*/*")
putExtra(Intent.EXTRA_STREAM, file)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
this.startActivity(Intent.createChooser(shareIntent, ""))
}
fun Context.showToast(resId: Int) {
Toast.makeText(
this,