From d21db0c031aef6378d805d00c7ce109ef0115e04 Mon Sep 17 00:00:00 2001 From: Toby Bridle Date: Wed, 28 Feb 2024 21:32:33 +0000 Subject: [PATCH] --wip-- [skip ci] --- .../java/com/chouten/app/common/Extensions.kt | 71 ++++++++++++------- .../app/data/data_source/module/ModuleDao.kt | 7 ++ .../data/repository/ModuleRepositoryImpl.kt | 14 +++- .../main/java/com/chouten/app/di/AppModule.kt | 3 +- .../app/domain/proto/FilePreferences.kt | 2 + .../module_use_cases/AddModuleUseCase.kt | 56 +++++++++++++-- .../components/preferences/PreferenceItem.kt | 26 +++++++ .../screens/general_screen/GeneralView.kt | 48 ++++++++++++- gradle/libs.versions.toml | 2 +- 9 files changed, 189 insertions(+), 40 deletions(-) create mode 100644 app/src/main/java/com/chouten/app/presentation/ui/components/preferences/PreferenceItem.kt diff --git a/app/src/main/java/com/chouten/app/common/Extensions.kt b/app/src/main/java/com/chouten/app/common/Extensions.kt index b886271..0ad0c2c 100644 --- a/app/src/main/java/com/chouten/app/common/Extensions.kt +++ b/app/src/main/java/com/chouten/app/common/Extensions.kt @@ -1,9 +1,13 @@ package com.chouten.app.common import android.app.Activity +import android.content.ContentResolver import android.content.Context import android.content.ContextWrapper import android.content.res.Configuration +import android.net.Uri +import android.provider.DocumentsContract +import android.util.Log import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.tween @@ -15,7 +19,9 @@ import androidx.compose.ui.graphics.Color import com.chouten.app.domain.model.Version import com.chouten.app.domain.proto.AppearancePreferences import com.chouten.app.domain.proto.appearanceDatastore +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.withContext import java.util.concurrent.TimeUnit @@ -33,12 +39,9 @@ fun ColorScheme.animate(): ColorScheme { } @Composable - fun animateColor(color: Color): Color = - animateColorAsState( - targetValue = color, - animationSpec = animSpec, - label = "Theme Color Transition" - ).value + fun animateColor(color: Color): Color = animateColorAsState( + targetValue = color, animationSpec = animSpec, label = "Theme Color Transition" + ).value return ColorScheme( primary = animateColor(this.primary), @@ -143,17 +146,20 @@ fun Long.formatMinSec(): String { String.format( "%02d:%02d:%02d", TimeUnit.MILLISECONDS.toHours(this), - TimeUnit.MILLISECONDS.toMinutes(this) - - TimeUnit.HOURS.toMinutes(TimeUnit.MILLISECONDS.toHours(this)), - TimeUnit.MILLISECONDS.toSeconds(this) - - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(this)) + TimeUnit.MILLISECONDS.toMinutes(this) - TimeUnit.HOURS.toMinutes( + TimeUnit.MILLISECONDS.toHours(this) + ), + TimeUnit.MILLISECONDS.toSeconds(this) - TimeUnit.MINUTES.toSeconds( + TimeUnit.MILLISECONDS.toMinutes(this) + ) ) } else { String.format( "%02d:%02d", TimeUnit.MILLISECONDS.toMinutes(this), - TimeUnit.MILLISECONDS.toSeconds(this) - - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(this)) + TimeUnit.MILLISECONDS.toSeconds(this) - TimeUnit.MINUTES.toSeconds( + TimeUnit.MILLISECONDS.toMinutes(this) + ) ) } } @@ -180,19 +186,10 @@ internal fun scale(start1: Float, end1: Float, pos: Float, start2: Float, end2: * Scale x.start, x.endInclusive from a1..b1 range to a2..b2 range */ internal fun scale( - start1: Float, - end1: Float, - range: ClosedFloatingPointRange, - start2: Float, - end2: Float -) = - scale(start1, end1, range.start, start2, end2)..scale( - start1, - end1, - range.endInclusive, - start2, - end2 - ) + start1: Float, end1: Float, range: ClosedFloatingPointRange, start2: Float, end2: Float +) = scale(start1, end1, range.start, start2, end2)..scale( + start1, end1, range.endInclusive, start2, end2 +) /** * Calculate fraction for value between a range [end] and [start] coerced into 0f-1f range @@ -208,4 +205,26 @@ fun calculateFraction(start: Float, end: Float, pos: Float) = * @return The parsed [Version] object. * @throws IllegalArgumentException If the version string is not valid. */ -fun String.toVersion(useRegex: Boolean = false) = Version(this, useRegex) \ No newline at end of file +fun String.toVersion(useRegex: Boolean = false) = Version(this, useRegex) + +suspend fun Uri.findDocument( + contentResolver: ContentResolver, document: String +): Uri? = withContext(Dispatchers.IO) { + contentResolver.query( + this@findDocument, arrayOf( + DocumentsContract.Document.COLUMN_DOCUMENT_ID, + DocumentsContract.Document.COLUMN_DISPLAY_NAME + ), null, null, null, null + )?.use { cursor -> + val nameColumn = + cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME) + val idColumn = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID) + while (cursor.moveToNext()) { + Log.d("FindDocument", "Found ${cursor.getString(nameColumn)}") + if (cursor.getString(nameColumn) == document) { + return@use DocumentsContract.buildTreeDocumentUri(this@findDocument.authority, cursor.getString(idColumn)) + } + } + null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/chouten/app/data/data_source/module/ModuleDao.kt b/app/src/main/java/com/chouten/app/data/data_source/module/ModuleDao.kt index e0796ae..2a514e2 100644 --- a/app/src/main/java/com/chouten/app/data/data_source/module/ModuleDao.kt +++ b/app/src/main/java/com/chouten/app/data/data_source/module/ModuleDao.kt @@ -1,6 +1,7 @@ package com.chouten.app.data.data_source.module import androidx.room.Dao +import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query @@ -18,4 +19,10 @@ interface ModuleDao { @Update suspend fun updateModule(moduleModel: ModuleModel) + + @Query("DELETE FROM ModuleModel WHERE id = :moduleId") + suspend fun removeModule(moduleId: String) + + @Delete + suspend fun removeModule(moduleModel: ModuleModel) } \ No newline at end of file diff --git a/app/src/main/java/com/chouten/app/data/repository/ModuleRepositoryImpl.kt b/app/src/main/java/com/chouten/app/data/repository/ModuleRepositoryImpl.kt index 8285217..e564037 100644 --- a/app/src/main/java/com/chouten/app/data/repository/ModuleRepositoryImpl.kt +++ b/app/src/main/java/com/chouten/app/data/repository/ModuleRepositoryImpl.kt @@ -3,7 +3,11 @@ package com.chouten.app.data.repository import android.content.Context import com.chouten.app.data.data_source.module.ModuleDatabase import com.chouten.app.domain.model.ModuleModel +import com.chouten.app.domain.proto.filepathDatastore +import com.chouten.app.domain.proto.moduleDatastore import com.chouten.app.domain.repository.ModuleRepository +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull import javax.inject.Inject class ModuleRepositoryImpl @Inject constructor( @@ -13,22 +17,26 @@ class ModuleRepositoryImpl @Inject constructor( override suspend fun getModules() = moduleDatabase.moduleDao.getModules() /** - * Adds a module to the module folder + * Adds a module to the module database * @param moduleModel: [ModuleModel] - The module (model) */ override suspend fun addModule(moduleModel: ModuleModel) { moduleDatabase.moduleDao.addModule(moduleModel) } + /** + * Update a module entry in the module database + * @param moduleModel: [ModuleModel] - The module (model) + */ override suspend fun updateModule(moduleModel: ModuleModel) { moduleDatabase.moduleDao.updateModule(moduleModel) } /** - * Removes a module from the module folder + * Removes a module from the module database * @param moduleId: [String] - The ID of the module to remove */ override suspend fun removeModule(moduleId: String) { - TODO("Not yet implemented") + moduleDatabase.moduleDao.removeModule(moduleId) } } \ No newline at end of file diff --git a/app/src/main/java/com/chouten/app/di/AppModule.kt b/app/src/main/java/com/chouten/app/di/AppModule.kt index cba9d02..7bb63f0 100644 --- a/app/src/main/java/com/chouten/app/di/AppModule.kt +++ b/app/src/main/java/com/chouten/app/di/AppModule.kt @@ -183,8 +183,7 @@ object AppModule { moduleRepository, httpClient, log, - moduleJsonParser, - moduleDirGetter + moduleJsonParser ), getModuleDir = moduleDirUsecase, removeModule = RemoveModuleUseCase( app.applicationContext, moduleRepository, diff --git a/app/src/main/java/com/chouten/app/domain/proto/FilePreferences.kt b/app/src/main/java/com/chouten/app/domain/proto/FilePreferences.kt index 1b7dbbd..e424564 100644 --- a/app/src/main/java/com/chouten/app/domain/proto/FilePreferences.kt +++ b/app/src/main/java/com/chouten/app/domain/proto/FilePreferences.kt @@ -17,11 +17,13 @@ import kotlinx.serialization.encoding.Encoder * Data class representing the user's file preferences. * @param CHOUTEN_ROOT_DIR The root directory of the Chouten app. (e.g /storage/emulated/0/Documents/Chouten). * @param IS_CHOUTEN_MODULE_DIR_SET Whether the module directory of the Chouten app has been set. + * @param SAVE_MODULE_ARTIFACTS Whether to save .module artifacts during module installation */ @Serializable data class FilePreferences( @Serializable(with = UriSerializer::class) val CHOUTEN_ROOT_DIR: Uri, val IS_CHOUTEN_MODULE_DIR_SET: Boolean = false, + val SAVE_MODULE_ARTIFACTS: Boolean = true ) { companion object { val DEFAULT = FilePreferences( diff --git a/app/src/main/java/com/chouten/app/domain/use_case/module_use_cases/AddModuleUseCase.kt b/app/src/main/java/com/chouten/app/domain/use_case/module_use_cases/AddModuleUseCase.kt index 9832ada..952c379 100644 --- a/app/src/main/java/com/chouten/app/domain/use_case/module_use_cases/AddModuleUseCase.kt +++ b/app/src/main/java/com/chouten/app/domain/use_case/module_use_cases/AddModuleUseCase.kt @@ -4,18 +4,24 @@ import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri +import android.provider.DocumentsContract import android.util.Log import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.toUpperCase import androidx.core.content.FileProvider import com.chouten.app.common.OutOfDateAppException import com.chouten.app.common.OutOfDateModuleException +import com.chouten.app.common.findDocument import com.chouten.app.domain.model.ModuleModel +import com.chouten.app.domain.proto.filepathDatastore import com.chouten.app.domain.repository.ModuleRepository import com.lagradost.nicehttp.Requests import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.withContext +import okio.buffer +import okio.sink +import okio.source import java.io.ByteArrayOutputStream import java.io.File import java.io.FileNotFoundException @@ -149,9 +155,6 @@ class AddModuleUseCase @Inject constructor( } } - // We have unzipped and can delete the cached .module file - cachedModule?.delete() - // Get the metadata file val metadataInputStream = destinationDir.resolve("metadata.json").also { if (!it.exists()) safeException( @@ -309,12 +312,55 @@ class AddModuleUseCase @Inject constructor( } val os = ByteArrayOutputStream() - BitmapFactory.decodeStream(inputStream).apply { + BitmapFactory.decodeFile(this@apply.absolutePath)?.apply { compress(Bitmap.CompressFormat.PNG, 80, os) - } + } ?: log("Could not parse icon.png") module = module.copy(metadata = module.metadata.copy(icon = os.toByteArray())) } + val preferences = mContext.filepathDatastore.data.firstOrNull() + preferences?.CHOUTEN_ROOT_DIR?.let { + if (it == Uri.EMPTY) { + log("CHOUTEN_ROOT_DIR is empty. Cannot copy module artifact") + return@let + } else if (!preferences.SAVE_MODULE_ARTIFACTS) { + log("SAVE_MODULE_ARTIFACTS is false. Not saving module artifact") + return@let + } + + val childDocumentsUri = DocumentsContract.buildChildDocumentsUriUsingTree( + it, DocumentsContract.getTreeDocumentId( + DocumentsContract.buildChildDocumentsUriUsingTree( + it, DocumentsContract.getTreeDocumentId(it) + ).findDocument(contentResolver, "Modules") ?: safeException( + IOException( + "Could not find Module Dir at $it" + ), destinationDir + ) + ) + ) + + val displayName = "${module.name}_v${module.version}_${module.id}.module" + log("Adding artifact $displayName to $childDocumentsUri") + childDocumentsUri.findDocument(contentResolver, displayName)?.let { _ -> + log("Existing artifact exists within $childDocumentsUri") + } ?: run { + val moduleUri = DocumentsContract.createDocument( + contentResolver, childDocumentsUri, "application/octet-stream", displayName + ) ?: throw IOException("Could not save module artifact") + val stream = if (isRemote) { + cachedModule?.inputStream() + } else contentResolver.openInputStream(uri) + stream?.apply { + contentResolver.openOutputStream(moduleUri)?.sink()?.buffer() + ?.use { buffer -> + buffer.writeAll(source()) + } + close() + } + } + } + log("Adding Module $module") moduleRepository.addModule(module) destinationDir.delete() diff --git a/app/src/main/java/com/chouten/app/presentation/ui/components/preferences/PreferenceItem.kt b/app/src/main/java/com/chouten/app/presentation/ui/components/preferences/PreferenceItem.kt new file mode 100644 index 0000000..2bca43b --- /dev/null +++ b/app/src/main/java/com/chouten/app/presentation/ui/components/preferences/PreferenceItem.kt @@ -0,0 +1,26 @@ +package com.chouten.app.presentation.ui.components.preferences + +import androidx.compose.foundation.clickable +import androidx.compose.material3.ListItem +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun PreferenceItem( + modifier: Modifier = Modifier, + headlineContent: @Composable () -> Unit, + supportingContent: @Composable (() -> Unit) = {}, + icon: @Composable (() -> Unit) = {}, + callback: () -> Unit +) { + ListItem( + modifier = Modifier + .clickable { + callback() + } + .then(modifier), + headlineContent = headlineContent, + supportingContent = supportingContent, + leadingContent = icon + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/chouten/app/presentation/ui/screens/more/screens/general_screen/GeneralView.kt b/app/src/main/java/com/chouten/app/presentation/ui/screens/more/screens/general_screen/GeneralView.kt index 3a67a87..b5c5f19 100644 --- a/app/src/main/java/com/chouten/app/presentation/ui/screens/more/screens/general_screen/GeneralView.kt +++ b/app/src/main/java/com/chouten/app/presentation/ui/screens/more/screens/general_screen/GeneralView.kt @@ -21,9 +21,16 @@ import androidx.compose.ui.unit.dp import com.chouten.app.R import com.chouten.app.common.MoreNavGraph import com.chouten.app.common.UiText +import com.chouten.app.domain.proto.CrashReportStore +import com.chouten.app.domain.proto.FilePreferences import com.chouten.app.domain.proto.crashReportStore +import com.chouten.app.domain.proto.filepathDatastore import com.chouten.app.presentation.ui.ChoutenAppViewModel +import com.chouten.app.presentation.ui.components.common.AppState +import com.chouten.app.presentation.ui.components.common.rememberAppState +import com.chouten.app.presentation.ui.components.preferences.PreferenceItem import com.chouten.app.presentation.ui.components.preferences.PreferenceToggle +import com.chouten.app.presentation.ui.safPicker import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator @@ -32,10 +39,15 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator @Destination() @Composable fun GeneralView( - appViewModel: ChoutenAppViewModel, navigator: DestinationsNavigator + appState: AppState = rememberAppState(), + appViewModel: ChoutenAppViewModel, + navigator: DestinationsNavigator ) { val context = LocalContext.current - val crashReportStore by context.crashReportStore.data.collectAsState(null) + val crashReportStore by context.crashReportStore.data.collectAsState(CrashReportStore.DEFAULT) + val filepathStore by context.filepathDatastore.data.collectAsState(FilePreferences.DEFAULT) + + val picker = safPicker(appState = appState) Scaffold(topBar = { TopAppBar(title = { Text(UiText.StringRes(R.string.general).string()) }, navigationIcon = { @@ -72,7 +84,37 @@ fun GeneralView( } } }, - initial = crashReportStore?.enabled ?: true, + initial = crashReportStore.enabled, + ) + PreferenceItem(headlineContent = { + Text(UiText.Literal("Change Data Directory").string()) + }, supportingContent = { + Text( + UiText.Literal("Change the custom storage directory for Chouten data").string() + ) + }, callback = { + picker.launch(filepathStore.CHOUTEN_ROOT_DIR) + }) + PreferenceToggle( + headlineContent = { + Text( + UiText.Literal("Save Module Artifacts").string() + ) + }, + supportingContent = { + Text( + UiText.Literal("Keep module artifacts (.module) during module installation") + .string() + ) + }, + onToggle = { isEnabled -> + appViewModel.runAsync { + context.filepathDatastore.updateData { preferences -> + preferences.copy(SAVE_MODULE_ARTIFACTS = isEnabled) + } + } + }, + initial = filepathStore.SAVE_MODULE_ARTIFACTS, ) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 893b40e..e0457ae 100755 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.4.0-alpha07" +agp = "8.4.0-alpha09" acraHttp = "5.11.3" autoService = "1.1.1" coilCompose = "2.4.0"