diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7f4083b9..41d2782d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -75,6 +75,16 @@
android:exported="true"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/dev/beefers/vendetta/manager/installer/step/download/base/DownloadStep.kt b/app/src/main/java/dev/beefers/vendetta/manager/installer/step/download/base/DownloadStep.kt
index dadb2a54..df7aa6c3 100644
--- a/app/src/main/java/dev/beefers/vendetta/manager/installer/step/download/base/DownloadStep.kt
+++ b/app/src/main/java/dev/beefers/vendetta/manager/installer/step/download/base/DownloadStep.kt
@@ -15,6 +15,7 @@ import dev.beefers.vendetta.manager.installer.step.StepRunner
import dev.beefers.vendetta.manager.installer.step.StepStatus
import dev.beefers.vendetta.manager.utils.mainThread
import dev.beefers.vendetta.manager.utils.showToast
+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.core.component.inject
@@ -123,8 +124,8 @@ abstract class DownloadStep: Step() {
}
is DownloadResult.Cancelled -> {
- runner.logger.e("$fileName download cancelled")
status = StepStatus.UNSUCCESSFUL
+ throw CancellationException("$fileName download cancelled")
}
}
}
diff --git a/app/src/main/java/dev/beefers/vendetta/manager/ui/screen/installer/InstallerScreen.kt b/app/src/main/java/dev/beefers/vendetta/manager/ui/screen/installer/InstallerScreen.kt
index 94ebf56b..7067a689 100644
--- a/app/src/main/java/dev/beefers/vendetta/manager/ui/screen/installer/InstallerScreen.kt
+++ b/app/src/main/java/dev/beefers/vendetta/manager/ui/screen/installer/InstallerScreen.kt
@@ -14,6 +14,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.outlined.Article
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
@@ -37,6 +38,7 @@ import cafe.adriel.voyager.koin.getScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.beefers.vendetta.manager.R
+import dev.beefers.vendetta.manager.domain.manager.PreferenceManager
import dev.beefers.vendetta.manager.installer.step.StepStatus
import dev.beefers.vendetta.manager.ui.viewmodel.installer.InstallerViewModel
import dev.beefers.vendetta.manager.ui.widgets.dialog.BackWarningDialog
@@ -44,6 +46,7 @@ import dev.beefers.vendetta.manager.ui.widgets.dialog.DownloadFailedDialog
import dev.beefers.vendetta.manager.ui.widgets.installer.StepGroupCard
import dev.beefers.vendetta.manager.utils.DiscordVersion
import okhttp3.internal.toImmutableList
+import org.koin.androidx.compose.get
import org.koin.core.parameter.parametersOf
import java.util.UUID
@@ -113,6 +116,7 @@ class InstallerScreen(
Scaffold(
topBar = {
TitleBar(
+ viewModel = viewModel,
onBackClick = {
if(!viewModel.runner.completed)
viewModel.openBackDialog()
@@ -161,11 +165,12 @@ class InstallerScreen(
.fillMaxWidth()
) {
FilledTonalButton(
- onClick = { nav.push(LogViewerScreen(viewModel.runner.logger.logs.toImmutableList())) },
+ onClick = { viewModel.shareLogs(activity!!) },
modifier = Modifier.weight(1f)
) {
- Text(stringResource(R.string.action_view_logs))
+ Text(stringResource(R.string.action_share_logs))
}
+
FilledTonalButton(
onClick = { viewModel.clearCache() },
modifier = Modifier.weight(1f)
@@ -181,8 +186,12 @@ class InstallerScreen(
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun TitleBar(
- onBackClick: () -> Unit
+ onBackClick: () -> Unit,
+ viewModel: InstallerViewModel
) {
+ val prefs: PreferenceManager = get()
+ val nav = LocalNavigator.currentOrThrow
+
TopAppBar(
title = { Text(stringResource(R.string.title_installer)) },
navigationIcon = {
@@ -192,6 +201,16 @@ class InstallerScreen(
contentDescription = stringResource(R.string.action_back)
)
}
+ },
+ actions = {
+ if (prefs.isDeveloper && viewModel.runner.completed) {
+ IconButton(onClick = { nav.push(LogViewerScreen(viewModel.runner.logger.logs.toImmutableList())) }) {
+ Icon(
+ imageVector = Icons.Outlined.Article,
+ contentDescription = stringResource(R.string.action_view_logs)
+ )
+ }
+ }
}
)
}
diff --git a/app/src/main/java/dev/beefers/vendetta/manager/ui/screen/installer/LogViewerScreen.kt b/app/src/main/java/dev/beefers/vendetta/manager/ui/screen/installer/LogViewerScreen.kt
index dcbfd4cb..95cefe0d 100644
--- a/app/src/main/java/dev/beefers/vendetta/manager/ui/screen/installer/LogViewerScreen.kt
+++ b/app/src/main/java/dev/beefers/vendetta/manager/ui/screen/installer/LogViewerScreen.kt
@@ -5,6 +5,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
@@ -14,6 +15,11 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.filled.Share
+import androidx.compose.material.icons.outlined.MoreVert
+import androidx.compose.material.icons.outlined.Save
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -33,11 +39,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import cafe.adriel.voyager.core.screen.Screen
@@ -48,6 +56,7 @@ import dev.beefers.vendetta.manager.R
import dev.beefers.vendetta.manager.installer.util.LogEntry
import dev.beefers.vendetta.manager.ui.viewmodel.installer.LogViewerViewModel
import dev.beefers.vendetta.manager.utils.DimenUtils
+import dev.beefers.vendetta.manager.utils.rememberFileSaveLauncher
import dev.beefers.vendetta.manager.utils.thenIf
import org.koin.core.parameter.parametersOf
@@ -65,7 +74,7 @@ class LogViewerScreen(
}
Scaffold(
- topBar = { Toolbar(scrollBehavior) },
+ topBar = { Toolbar(scrollBehavior, viewModel) },
contentWindowInsets = WindowInsets(0, 0, 0, 0),
modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection)
@@ -143,9 +152,12 @@ class LogViewerScreen(
@Composable
private fun Toolbar(
- scrollBehavior: TopAppBarScrollBehavior
+ scrollBehavior: TopAppBarScrollBehavior,
+ viewModel: LogViewerViewModel
) {
val navigator = LocalNavigator.currentOrThrow
+ val context = LocalContext.current
+ val saveFile = rememberFileSaveLauncher(content = viewModel.logsString)
TopAppBar(
title = { Text(stringResource(R.string.title_logs)) },
@@ -157,8 +169,62 @@ class LogViewerScreen(
)
}
},
+ actions = {
+ var showDropdown by remember {
+ mutableStateOf(false)
+ }
+
+ IconButton(onClick = { saveFile.launch("VD-Manager-${System.currentTimeMillis()}.log") }) {
+ Icon(
+ imageVector = Icons.Outlined.Save,
+ contentDescription = stringResource(R.string.action_save_logs)
+ )
+ }
+
+ IconButton(onClick = { viewModel.shareLogs(context) }) {
+ Icon(
+ imageVector = Icons.Filled.Share,
+ contentDescription = stringResource(R.string.action_share_logs)
+ )
+ }
+
+ IconButton(onClick = { showDropdown = true }) {
+ Icon(
+ imageVector = Icons.Outlined.MoreVert,
+ contentDescription = stringResource(R.string.action_more_options)
+ )
+ }
+
+ Dropdown(
+ viewModel,
+ expanded = showDropdown,
+ onDismiss = { showDropdown = false }
+ )
+ },
scrollBehavior = scrollBehavior
)
}
+ @Composable
+ fun Dropdown(
+ viewModel: LogViewerViewModel,
+ expanded: Boolean,
+ onDismiss: () -> Unit
+ ) {
+ Box {
+ DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = onDismiss,
+ offset = DpOffset(
+ 10.dp, 26.dp
+ )
+ ) {
+ DropdownMenuItem(
+ text = { Text(stringResource(R.string.action_copy_logs)) },
+ onClick = { viewModel.copyLogs() }
+ )
+ }
+ }
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/installer/InstallerViewModel.kt b/app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/installer/InstallerViewModel.kt
index 44d516cd..d99bb479 100644
--- a/app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/installer/InstallerViewModel.kt
+++ b/app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/installer/InstallerViewModel.kt
@@ -2,42 +2,20 @@ package dev.beefers.vendetta.manager.ui.viewmodel.installer
import android.content.Context
import android.content.Intent
-import android.os.Build
-import android.os.Environment
-import android.util.Log
-import androidx.annotation.StringRes
-import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableFloatStateOf
-import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
+import androidx.core.app.ShareCompat
+import androidx.core.content.FileProvider
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
-import com.github.diamondminer88.zip.ZipCompression
-import com.github.diamondminer88.zip.ZipReader
-import com.github.diamondminer88.zip.ZipWriter
import dev.beefers.vendetta.manager.BuildConfig
import dev.beefers.vendetta.manager.R
-import dev.beefers.vendetta.manager.domain.manager.DownloadManager
-import dev.beefers.vendetta.manager.domain.manager.DownloadResult
import dev.beefers.vendetta.manager.domain.manager.InstallManager
-import dev.beefers.vendetta.manager.domain.manager.InstallMethod
-import dev.beefers.vendetta.manager.domain.manager.PreferenceManager
-import dev.beefers.vendetta.manager.installer.Installer
-import dev.beefers.vendetta.manager.installer.session.SessionInstaller
-import dev.beefers.vendetta.manager.installer.shizuku.ShizukuInstaller
import dev.beefers.vendetta.manager.installer.step.Step
import dev.beefers.vendetta.manager.installer.step.StepGroup
import dev.beefers.vendetta.manager.installer.step.StepRunner
-import dev.beefers.vendetta.manager.installer.step.installing.InstallStep
-import dev.beefers.vendetta.manager.installer.util.ManifestPatcher
-import dev.beefers.vendetta.manager.installer.util.Patcher
-import dev.beefers.vendetta.manager.installer.util.Signer
import dev.beefers.vendetta.manager.utils.DiscordVersion
-import dev.beefers.vendetta.manager.utils.copyText
-import dev.beefers.vendetta.manager.utils.isMiui
-import dev.beefers.vendetta.manager.utils.mainThread
import dev.beefers.vendetta.manager.utils.showToast
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.toImmutableMap
@@ -45,10 +23,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-import org.lsposed.patch.util.Logger
import java.io.File
import java.util.concurrent.atomic.AtomicBoolean
-import kotlin.time.measureTimedValue
class InstallerViewModel(
private val context: Context,
@@ -64,6 +40,14 @@ class InstallerViewModel(
}
.toImmutableMap()
+ private val tempLogStorageDir = context.filesDir.resolve("logsTmp").also {
+ it.mkdirs()
+ }
+
+ private val logsString by lazy {
+ runner.logger.logs.joinToString("\n") { it.toString() }
+ }
+
private val installationRunning = AtomicBoolean(false)
private val job = screenModelScope.launch(Dispatchers.Main) {
@@ -90,10 +74,6 @@ class InstallerViewModel(
runner.logger.e(msg)
}
- fun copyDebugInfo() {
- context.copyText(runner.logger.logs.joinToString("\n"))
- }
-
fun clearCache() {
runner.clearCache()
context.showToast(R.string.msg_cleared_cache)
@@ -130,4 +110,36 @@ class InstallerViewModel(
}
}
+ private fun saveToAppStorage(): File {
+ // Delete old logs to prevent junk buildup
+ tempLogStorageDir.deleteRecursively()
+ tempLogStorageDir.mkdirs()
+
+ val tmpFile = tempLogStorageDir.resolve("VD-Manager-${System.currentTimeMillis()}.log")
+ tmpFile.outputStream().use { stream ->
+ stream.write(logsString.toByteArray())
+ }
+
+ return tmpFile
+ }
+
+ fun shareLogs(activityContext: Context) {
+ val saved = saveToAppStorage()
+ val uri = FileProvider.getUriForFile(
+ activityContext,
+ BuildConfig.APPLICATION_ID + ".provider",
+ saved
+ )
+
+ ShareCompat.IntentBuilder(activityContext)
+ .setType("text/plain")
+ .setStream(uri)
+ .apply {
+ intent.apply {
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ }
+ }
+ .startChooser()
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/installer/LogViewerViewModel.kt b/app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/installer/LogViewerViewModel.kt
index 38e47f35..df065ee8 100644
--- a/app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/installer/LogViewerViewModel.kt
+++ b/app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/installer/LogViewerViewModel.kt
@@ -1,18 +1,27 @@
package dev.beefers.vendetta.manager.ui.viewmodel.installer
import android.content.Context
+import android.content.Intent
+import androidx.core.app.ShareCompat
+import androidx.core.content.FileProvider
import cafe.adriel.voyager.core.model.ScreenModel
+import dev.beefers.vendetta.manager.BuildConfig
import dev.beefers.vendetta.manager.R
import dev.beefers.vendetta.manager.installer.util.LogEntry
import dev.beefers.vendetta.manager.utils.copyText
import dev.beefers.vendetta.manager.utils.showToast
+import java.io.File
class LogViewerViewModel(
private val context: Context,
val logs: List
): ScreenModel {
- private val logsString by lazy {
+ private val tempLogStorageDir = context.filesDir.resolve("logsTmp").also {
+ it.mkdirs()
+ }
+
+ val logsString by lazy {
logs.joinToString("\n") { it.toString() }
}
@@ -28,4 +37,35 @@ class LogViewerViewModel(
context.showToast(R.string.msg_copied)
}
+ private fun saveToAppStorage(): File {
+ tempLogStorageDir.deleteRecursively()
+ tempLogStorageDir.mkdirs()
+
+ val tmpFile = tempLogStorageDir.resolve("VD-Manager-${System.currentTimeMillis()}.log")
+ tmpFile.outputStream().use { stream ->
+ stream.write(logsString.toByteArray())
+ }
+
+ return tmpFile
+ }
+
+ fun shareLogs(activityContext: Context) {
+ val saved = saveToAppStorage()
+ val uri = FileProvider.getUriForFile(
+ activityContext,
+ BuildConfig.APPLICATION_ID + ".provider",
+ saved
+ )
+
+ ShareCompat.IntentBuilder(activityContext)
+ .setType("text/plain")
+ .setStream(uri)
+ .apply {
+ intent.apply {
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ }
+ }
+ .startChooser()
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/dev/beefers/vendetta/manager/utils/Utils.kt b/app/src/main/java/dev/beefers/vendetta/manager/utils/Utils.kt
index 0d086c8b..31077ca3 100644
--- a/app/src/main/java/dev/beefers/vendetta/manager/utils/Utils.kt
+++ b/app/src/main/java/dev/beefers/vendetta/manager/utils/Utils.kt
@@ -4,12 +4,18 @@ import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.graphics.Bitmap
+import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.widget.Toast
+import androidx.activity.compose.ManagedActivityResultLauncher
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.content.res.AppCompatResources
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
import androidx.core.graphics.drawable.toBitmap
import dev.beefers.vendetta.manager.BuildConfig
import java.io.BufferedReader
@@ -36,6 +42,21 @@ fun Context.showToast(@StringRes res: Int, vararg params: Any, short: Boolean =
).show()
}
+/**
+ * Remembers an activity result launcher used to save files to a user-specified location
+ */
+@Composable
+fun rememberFileSaveLauncher(content: String, mimeType: String = "text/plain"): ManagedActivityResultLauncher {
+ val context = LocalContext.current
+ return rememberLauncherForActivityResult(contract = ActivityResultContracts.CreateDocument(mimeType)) { uri ->
+ uri?.let {
+ context.contentResolver.openOutputStream(uri).use { stream ->
+ stream?.write(content.toByteArray())
+ }
+ }
+ }
+}
+
private val cachedBitmaps: MutableMap> = mutableMapOf()
context(Context)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d1354f8f..0869a259 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -80,6 +80,9 @@
View logs
Show timestamp
Copy log
+ Save logs to file
+ Share logs
+ More options
Exiting the installer before its finished could corrupt downloaded files, are you sure you want to do that?
diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml
new file mode 100644
index 00000000..a2c0792d
--- /dev/null
+++ b/app/src/main/res/xml/provider_paths.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file