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