diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml index c5ad6558..f517791e 100644 --- a/app/detekt-baseline.xml +++ b/app/detekt-baseline.xml @@ -4,10 +4,8 @@ ForbiddenImport:AcsMissingScreen.kt$import androidx.compose.material3.MaterialTheme ForbiddenImport:CrashReportScreen.kt$import androidx.compose.material3.MaterialTheme - LambdaParameterInRestartableEffect:AslNavHost.kt$onOpenProjectConsumed LongMethod:AslNavHost.kt$@Composable fun AslNavHost( startDestination: String, openProjectId: String? = null, onOpenProjectConsumed: () -> Unit = {}, navController: NavHostController = rememberNavController(), ) MatchingDeclarationName:BlockingErrorScreen.kt$BlockingErrorType ParameterNaming:AslNavHost.kt$onOpenProjectConsumed - WildcardImport:ExampleUnitTest.kt$import org.junit.Assert.* diff --git a/app/src/main/java/com/ahmadkharfan/androidstudiolite/navigation/AslNavHost.kt b/app/src/main/java/com/ahmadkharfan/androidstudiolite/navigation/AslNavHost.kt index cd4162df..72c9e5f6 100644 --- a/app/src/main/java/com/ahmadkharfan/androidstudiolite/navigation/AslNavHost.kt +++ b/app/src/main/java/com/ahmadkharfan/androidstudiolite/navigation/AslNavHost.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -44,6 +45,7 @@ fun AslNavHost( onOpenProjectConsumed: () -> Unit = {}, navController: NavHostController = rememberNavController(), ) { + val currentOnOpenProjectConsumed by rememberUpdatedState(onOpenProjectConsumed) LaunchedEffect(openProjectId) { val id = openProjectId ?: return@LaunchedEffect @@ -52,7 +54,7 @@ fun AslNavHost( launchSingleTop = true } } - onOpenProjectConsumed() + currentOnOpenProjectConsumed() } NavHost( navController = navController, @@ -180,7 +182,13 @@ fun AslNavHost( val target = runCatching { GitDiffTarget.valueOf(backStackEntry.arguments?.getString("target").orEmpty()) }.getOrDefault(GitDiffTarget.INDEX_TO_WORKTREE) - GitDiffRoute(projectId, path, target, commitId, onBack = { navController.popBackStack() }) + GitDiffRoute( + projectId = projectId, + path = path, + target = target, + onBack = { navController.popBackStack() }, + commitId = commitId, + ) } composable( diff --git a/app/src/test/java/com/ahmadkharfan/androidstudiolite/ExampleUnitTest.kt b/app/src/test/java/com/ahmadkharfan/androidstudiolite/ExampleUnitTest.kt index f969470c..cb5ef526 100644 --- a/app/src/test/java/com/ahmadkharfan/androidstudiolite/ExampleUnitTest.kt +++ b/app/src/test/java/com/ahmadkharfan/androidstudiolite/ExampleUnitTest.kt @@ -2,7 +2,7 @@ package com.ahmadkharfan.androidstudiolite import org.junit.Test -import org.junit.Assert.* +import org.junit.Assert.assertEquals class ExampleUnitTest { @Test diff --git a/data/ai/detekt-baseline.xml b/data/ai/detekt-baseline.xml index 91239d43..0d4e72a5 100644 --- a/data/ai/detekt-baseline.xml +++ b/data/ai/detekt-baseline.xml @@ -9,13 +9,6 @@ CyclomaticComplexMethod:AgentReplyParser.kt$AgentReplyParser$fun toActionOrNull(element: JsonElement): AgentAction? InstanceOfCheckForException:AiLlmGateway.kt$AiLlmGateway$e is AiLlmException LoopWithTooManyJumpStatements:AgentReplySalvage.kt$AgentReplySalvage$while - NewLineAtEndOfFile:AnthropicProvider.kt$com.ahmadkharfan.androidstudiolite.data.ai.llm.AnthropicProvider.kt - NewLineAtEndOfFile:GeminiProvider.kt$com.ahmadkharfan.androidstudiolite.data.ai.llm.GeminiProvider.kt - NewLineAtEndOfFile:LlmHttp.kt$com.ahmadkharfan.androidstudiolite.data.ai.llm.LlmHttp.kt - NewLineAtEndOfFile:LlmProvider.kt$com.ahmadkharfan.androidstudiolite.data.ai.llm.LlmProvider.kt - NewLineAtEndOfFile:LlmProviderRegistry.kt$com.ahmadkharfan.androidstudiolite.data.ai.llm.LlmProviderRegistry.kt - NewLineAtEndOfFile:LlmProviderRegistryTest.kt$com.ahmadkharfan.androidstudiolite.data.ai.llm.LlmProviderRegistryTest.kt - NewLineAtEndOfFile:OpenAiCompatProvider.kt$com.ahmadkharfan.androidstudiolite.data.ai.llm.OpenAiCompatProvider.kt TooGenericExceptionCaught:AiLlmGateway.kt$AiLlmGateway$e: Exception diff --git a/data/ai/src/main/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/AnthropicProvider.kt b/data/ai/src/main/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/AnthropicProvider.kt index 8b0d17ba..1e55c04b 100644 --- a/data/ai/src/main/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/AnthropicProvider.kt +++ b/data/ai/src/main/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/AnthropicProvider.kt @@ -125,4 +125,4 @@ private data class AnthropicMessage(val role: String, val content: String) private data class AnthropicResponse(val content: List) @Serializable -private data class AnthropicContentBlock(val type: String, val text: String = "") \ No newline at end of file +private data class AnthropicContentBlock(val type: String, val text: String = "") diff --git a/data/ai/src/main/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/GeminiProvider.kt b/data/ai/src/main/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/GeminiProvider.kt index beed81b2..7eea2791 100644 --- a/data/ai/src/main/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/GeminiProvider.kt +++ b/data/ai/src/main/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/GeminiProvider.kt @@ -116,4 +116,4 @@ private data class GeminiCandidate(val content: GeminiContent? = null) private data class GeminiModelsResponse(val models: List = emptyList()) @Serializable -private data class GeminiModelInfo(val name: String) \ No newline at end of file +private data class GeminiModelInfo(val name: String) diff --git a/data/ai/src/main/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/LlmHttp.kt b/data/ai/src/main/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/LlmHttp.kt index 1b9f779c..fc935939 100644 --- a/data/ai/src/main/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/LlmHttp.kt +++ b/data/ai/src/main/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/LlmHttp.kt @@ -85,4 +85,4 @@ internal fun llmErrorMessage(body: String, code: Int): String { ?: root["message"]?.jsonPrimitive?.contentOrNull ?: body.take(200) }.getOrDefault(body.take(200)).let { "HTTP $code: $it" } -} \ No newline at end of file +} diff --git a/data/ai/src/main/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/LlmProvider.kt b/data/ai/src/main/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/LlmProvider.kt index 35b28672..190f080b 100644 --- a/data/ai/src/main/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/LlmProvider.kt +++ b/data/ai/src/main/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/LlmProvider.kt @@ -19,4 +19,4 @@ internal interface LlmProvider { fun stream(request: LlmChatRequest, onDelta: (String) -> Unit) fun listModels(apiKey: String, baseUrl: String?): List -} \ No newline at end of file +} diff --git a/data/ai/src/main/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/LlmProviderRegistry.kt b/data/ai/src/main/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/LlmProviderRegistry.kt index 775e5116..35164ff3 100644 --- a/data/ai/src/main/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/LlmProviderRegistry.kt +++ b/data/ai/src/main/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/LlmProviderRegistry.kt @@ -17,4 +17,4 @@ internal class LlmProviderRegistry(http: LlmHttpClient) { providers[providerId] ?: throw AiLlmException("Unknown provider: $providerId") fun find(providerId: String): LlmProvider? = providers[providerId] -} \ No newline at end of file +} diff --git a/data/ai/src/main/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/OpenAiCompatProvider.kt b/data/ai/src/main/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/OpenAiCompatProvider.kt index 11e02e3f..1ecff3ba 100644 --- a/data/ai/src/main/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/OpenAiCompatProvider.kt +++ b/data/ai/src/main/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/OpenAiCompatProvider.kt @@ -120,4 +120,4 @@ private data class OpenAiMessage(val role: String, val content: String) private data class OpenAiChatResponse(val choices: List = emptyList()) @Serializable -private data class OpenAiChoice(val message: OpenAiMessage? = null) \ No newline at end of file +private data class OpenAiChoice(val message: OpenAiMessage? = null) diff --git a/data/ai/src/test/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/LlmProviderRegistryTest.kt b/data/ai/src/test/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/LlmProviderRegistryTest.kt index c7e54cba..b479c72c 100644 --- a/data/ai/src/test/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/LlmProviderRegistryTest.kt +++ b/data/ai/src/test/java/com/ahmadkharfan/androidstudiolite/data/ai/llm/LlmProviderRegistryTest.kt @@ -160,4 +160,4 @@ class LlmErrorMessageTest { assertEquals("HTTP 500: not json", llmErrorMessage("not json", 500)) assertEquals("HTTP 503", llmErrorMessage("", 503)) } -} \ No newline at end of file +} diff --git a/data/build/detekt-baseline.xml b/data/build/detekt-baseline.xml index 5b7af5a1..16ba5907 100644 --- a/data/build/detekt-baseline.xml +++ b/data/build/detekt-baseline.xml @@ -5,12 +5,6 @@ ComplexCondition:GradleScriptScanner.kt$GToken$s.length >= 2 && (s[0] == '"' || s[0] == '\'') && s.last() == s[0] ComplexCondition:GradleScriptScanner.kt$GradleScriptScanner.GradleTokenizer$end < text.length && (text[end].isLetterOrDigit() || text[end] == '.' || text[end] == '_') ComplexCondition:VersionCatalogParser.kt$VersionCatalogParser$t.length >= 2 && (t.first() == '"' || t.first() == '\'') && t.last() == t.first() - ConstructorParameterNaming:GitHubDeviceFlowAuthenticator.kt$GitHubDeviceFlowAuthenticator.AccessTokenResponse$val access_token: String? = null - ConstructorParameterNaming:GitHubDeviceFlowAuthenticator.kt$GitHubDeviceFlowAuthenticator.AccessTokenResponse$val error_description: String? = null - ConstructorParameterNaming:GitHubDeviceFlowAuthenticator.kt$GitHubDeviceFlowAuthenticator.DeviceCodeResponse$val device_code: String? = null - ConstructorParameterNaming:GitHubDeviceFlowAuthenticator.kt$GitHubDeviceFlowAuthenticator.DeviceCodeResponse$val expires_in: Int? = null - ConstructorParameterNaming:GitHubDeviceFlowAuthenticator.kt$GitHubDeviceFlowAuthenticator.DeviceCodeResponse$val user_code: String? = null - ConstructorParameterNaming:GitHubDeviceFlowAuthenticator.kt$GitHubDeviceFlowAuthenticator.DeviceCodeResponse$val verification_uri: String? = null CyclomaticComplexMethod:BuildGradleParser.kt$BuildGradleParser$private fun parseAndroid(tokens: List<GToken>): ParsedAndroidBlock? CyclomaticComplexMethod:GradleProjectReader.kt$GradleProjectReader$private fun moduleType(script: ParsedBuildScript, catalog: VersionCatalog?): ModuleType CyclomaticComplexMethod:MiniJson.kt$MiniJson.Parser$private fun parseString(): String @@ -23,8 +17,6 @@ LoopWithTooManyJumpStatements:MiniJson.kt$MiniJson.Parser$while LoopWithTooManyJumpStatements:SelfSignedCertGenerator.kt$SelfSignedCertGenerator$while LoopWithTooManyJumpStatements:VersionCatalogParser.kt$VersionCatalogParser$for - SwallowedException:KeystoreFiles.kt$KeystoreFiles$e: UnrecoverableKeyException - SwallowedException:KeystoreFiles.kt$KeystoreFiles$e: java.io.IOException ThrowsCount:ArtifactDownloader.kt$ArtifactDownloader$private fun validateArtifact(file: File, expectation: ArtifactExpectation) ThrowsCount:KeystoreFiles.kt$KeystoreFiles$fun import(storeFile: File, storePassword: String, keyAlias: String, keyPassword: String): SigningConfig ThrowsCount:RemoteClient.kt$RemoteClient$private suspend fun executeWithRetry(request: Request, allowUnauthorizedThrow: Boolean): ResponseSnapshot @@ -33,11 +25,5 @@ TooGenericExceptionCaught:PlayIntegrityTokenProvider.kt$PlayIntegrityTokenProvider$e: Throwable TooGenericExceptionCaught:RemoteBuildSystem.kt$RemoteBuildSystem$t: Throwable TooGenericExceptionCaught:RemoteClient.kt$RemoteClient$t: Throwable - UnusedParameter:DependenciesBlockEditor.kt$DependenciesBlockEditor$text: String - UseCheckOrError:RemoteBuildSystem.kt$RemoteBuildSystem$throw IllegalStateException("Configure a release keystore in Settings before building a release artifact.") - UseCheckOrError:RemoteBuildSystem.kt$RemoteBuildSystem$throw IllegalStateException("The configured release keystore is missing or unreadable.") - UseRequire:MiniJson.kt$MiniJson.Parser$throw IllegalArgumentException("Expected '$c' at $i") - UseRequire:MiniJson.kt$MiniJson.Parser$throw IllegalArgumentException("Invalid literal at $i") - UseRequire:MiniJson.kt$MiniJson.Parser$throw IllegalArgumentException("Unexpected end of JSON") diff --git a/data/build/src/main/java/com/ahmadkharfan/androidstudiolite/data/buildsystem/signing/KeystoreFiles.kt b/data/build/src/main/java/com/ahmadkharfan/androidstudiolite/data/buildsystem/signing/KeystoreFiles.kt index 35ea3692..50b7bc9f 100644 --- a/data/build/src/main/java/com/ahmadkharfan/androidstudiolite/data/buildsystem/signing/KeystoreFiles.kt +++ b/data/build/src/main/java/com/ahmadkharfan/androidstudiolite/data/buildsystem/signing/KeystoreFiles.kt @@ -38,7 +38,7 @@ internal object KeystoreFiles { try { params.storeFile.outputStream().use { keyStore.store(it, params.storePassword.toCharArray()) } } catch (e: java.io.IOException) { - throw KeystoreException(KeystoreError.Io(e.message ?: "Could not write keystore")) + throw KeystoreException(KeystoreError.Io(e.message ?: "Could not write keystore"), e) } return SigningConfig( storeFile = params.storeFile, @@ -71,7 +71,7 @@ internal object KeystoreFiles { throw KeystoreException(KeystoreError.InvalidParams("Alias '$keyAlias' is not a private-key entry")) } } catch (e: UnrecoverableKeyException) { - throw KeystoreException(KeystoreError.WrongKeyPassword) + throw KeystoreException(KeystoreError.WrongKeyPassword, e) } return SigningConfig(storeFile, storePassword, keyAlias, keyPassword, isDebug = false) } diff --git a/data/build/src/main/java/com/ahmadkharfan/androidstudiolite/data/gradle/deps/DependenciesBlockEditor.kt b/data/build/src/main/java/com/ahmadkharfan/androidstudiolite/data/gradle/deps/DependenciesBlockEditor.kt index e1369cd1..f331bd5b 100644 --- a/data/build/src/main/java/com/ahmadkharfan/androidstudiolite/data/gradle/deps/DependenciesBlockEditor.kt +++ b/data/build/src/main/java/com/ahmadkharfan/androidstudiolite/data/gradle/deps/DependenciesBlockEditor.kt @@ -38,7 +38,7 @@ object DependenciesBlockEditor { sb.append("\n\ndependencies {\n ").append(line).append("\n}\n") Result.Changed(sb.toString()) } else { - val insertOffset = insertionOffset(tokens, body, text) + val insertOffset = insertionOffset(tokens, body) val indent = detectIndent(text, tokens, body) val edited = StringBuilder(text) .insert(insertOffset, "\n$indent$line") @@ -74,7 +74,6 @@ object DependenciesBlockEditor { private fun insertionOffset( tokens: List, body: IntRange, - text: String, ): Int { for (i in body.last downTo body.first) { diff --git a/data/build/src/main/java/com/ahmadkharfan/androidstudiolite/data/gradle/util/MiniJson.kt b/data/build/src/main/java/com/ahmadkharfan/androidstudiolite/data/gradle/util/MiniJson.kt index 4e3c55b6..0ee9391a 100644 --- a/data/build/src/main/java/com/ahmadkharfan/androidstudiolite/data/gradle/util/MiniJson.kt +++ b/data/build/src/main/java/com/ahmadkharfan/androidstudiolite/data/gradle/util/MiniJson.kt @@ -9,7 +9,7 @@ object MiniJson { fun parseValue(): Any? { skipWs() - if (i >= s.length) throw IllegalArgumentException("Unexpected end of JSON") + require(i < s.length) { "Unexpected end of JSON" } return when (s[i]) { '{' -> parseObject() '[' -> parseArray() @@ -85,19 +85,26 @@ object MiniJson { return s.substring(start, i).toDouble() } - private fun parseBoolean(): Boolean = - if (s.startsWith("true", i)) { i += 4; true } - else if (s.startsWith("false", i)) { i += 5; false } - else throw IllegalArgumentException("Invalid literal at $i") + private fun parseBoolean(): Boolean { + if (s.startsWith("true", i)) { + i += 4 + return true + } + require(s.startsWith("false", i)) { "Invalid literal at $i" } + i += 5 + return false + } - private fun parseNull(): Any? = - if (s.startsWith("null", i)) { i += 4; null } - else throw IllegalArgumentException("Invalid literal at $i") + private fun parseNull(): Any? { + require(s.startsWith("null", i)) { "Invalid literal at $i" } + i += 4 + return null + } private fun skipWs() { while (i < s.length && s[i].isWhitespace()) i++ } private fun peek(): Char = if (i < s.length) s[i] else '\u0000' private fun expect(c: Char) { - if (peek() != c) throw IllegalArgumentException("Expected '$c' at $i") + require(peek() == c) { "Expected '$c' at $i" } i++ } } diff --git a/data/build/src/main/java/com/ahmadkharfan/androidstudiolite/data/remote/RemoteBuildSystem.kt b/data/build/src/main/java/com/ahmadkharfan/androidstudiolite/data/remote/RemoteBuildSystem.kt index 136167fd..1c52e719 100644 --- a/data/build/src/main/java/com/ahmadkharfan/androidstudiolite/data/remote/RemoteBuildSystem.kt +++ b/data/build/src/main/java/com/ahmadkharfan/androidstudiolite/data/remote/RemoteBuildSystem.kt @@ -239,9 +239,9 @@ class RemoteBuildSystem internal constructor( if (request.buildType.equals("release", ignoreCase = true) || (request.buildType == null && RemoteBuildRequestFactory.isReleaseVariant(request.variantName))) { val config = releaseSigningResolver() - ?: throw IllegalStateException("Configure a release keystore in Settings before building a release artifact.") + ?: error("Configure a release keystore in Settings before building a release artifact.") RemoteBuildRequestFactory.releaseSigningMaterial(config, encodeBase64 = encodeBase64) - ?: throw IllegalStateException("The configured release keystore is missing or unreadable.") + ?: error("The configured release keystore is missing or unreadable.") } else { null } diff --git a/data/build/src/main/java/com/ahmadkharfan/androidstudiolite/data/remote/github/GitHubDeviceFlowAuthenticator.kt b/data/build/src/main/java/com/ahmadkharfan/androidstudiolite/data/remote/github/GitHubDeviceFlowAuthenticator.kt index 6245ecc4..46de2612 100644 --- a/data/build/src/main/java/com/ahmadkharfan/androidstudiolite/data/remote/github/GitHubDeviceFlowAuthenticator.kt +++ b/data/build/src/main/java/com/ahmadkharfan/androidstudiolite/data/remote/github/GitHubDeviceFlowAuthenticator.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import okhttp3.FormBody @@ -184,27 +185,19 @@ class GitHubDeviceFlowAuthenticator( @Serializable private data class DeviceCodeResponse( - val device_code: String? = null, - val user_code: String? = null, - val verification_uri: String? = null, - val expires_in: Int? = null, + @SerialName("device_code") val deviceCode: String? = null, + @SerialName("user_code") val userCode: String? = null, + @SerialName("verification_uri") val verificationUri: String? = null, + @SerialName("expires_in") val expiresIn: Int? = null, val interval: Int? = null, - ) { - val deviceCode get() = device_code - val userCode get() = user_code - val verificationUri get() = verification_uri - val expiresIn get() = expires_in - } + ) @Serializable private data class AccessTokenResponse( - val access_token: String? = null, + @SerialName("access_token") val accessToken: String? = null, val error: String? = null, - val error_description: String? = null, - ) { - val accessToken get() = access_token - val errorDescription get() = error_description - } + @SerialName("error_description") val errorDescription: String? = null, + ) @Serializable private data class GitHubUser(val login: String? = null) diff --git a/designsystem/detekt-baseline.xml b/designsystem/detekt-baseline.xml index 9d56895a..ca6370af 100644 --- a/designsystem/detekt-baseline.xml +++ b/designsystem/detekt-baseline.xml @@ -2,8 +2,6 @@ - ComposableParamOrder:Appear.kt$AslStaggeredAppear - ComposableParamOrder:StatusChip.kt$AslStatusChip CyclomaticComplexMethod:ApiKeyCard.kt$@Composable fun AslApiKeyCard( provider: String, value: String, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, providerIcon: String = "sparkles", description: String? = null, placeholder: String = "sk-…", status: AslApiKeyStatus = AslApiKeyStatus.None, errorMessage: String? = null, onTest: (String) -> Unit = {}, testing: Boolean = false, onCollapse: (() -> Unit)? = null, ) CyclomaticComplexMethod:AslMarkdownText.kt$@Composable fun AslMarkdownText( markdown: String, modifier: Modifier = Modifier, onCopyCode: (String) -> Unit = {}, ) CyclomaticComplexMethod:BottomToolPanel.kt$@Composable fun AslBottomToolPanel( tabs: List<AslBottomPanelTab>, activeId: String?, modifier: Modifier = Modifier, contentHeight: Dp = 0.dp, defaultContentHeight: Dp = 260.dp, onContentHeightChange: (Dp) -> Unit = {}, onSelect: (String) -> Unit = {}, onToggle: () -> Unit = {}, content: @Composable () -> Unit = {}, ) diff --git a/designsystem/src/main/java/com/ahmadkharfan/androidstudiolite/designsystem/animation/Appear.kt b/designsystem/src/main/java/com/ahmadkharfan/androidstudiolite/designsystem/animation/Appear.kt index 7a605e79..c6848ea6 100644 --- a/designsystem/src/main/java/com/ahmadkharfan/androidstudiolite/designsystem/animation/Appear.kt +++ b/designsystem/src/main/java/com/ahmadkharfan/androidstudiolite/designsystem/animation/Appear.kt @@ -12,8 +12,8 @@ import com.ahmadkharfan.androidstudiolite.designsystem.theme.AslMotion @Composable fun AslStaggeredAppear( - index: Int = 0, modifier: Modifier = Modifier, + index: Int = 0, staggerMillis: Int = 45, content: @Composable () -> Unit, ) { diff --git a/designsystem/src/main/java/com/ahmadkharfan/androidstudiolite/designsystem/component/feedback/StatusChip.kt b/designsystem/src/main/java/com/ahmadkharfan/androidstudiolite/designsystem/component/feedback/StatusChip.kt index c014bd7b..682a7d51 100644 --- a/designsystem/src/main/java/com/ahmadkharfan/androidstudiolite/designsystem/component/feedback/StatusChip.kt +++ b/designsystem/src/main/java/com/ahmadkharfan/androidstudiolite/designsystem/component/feedback/StatusChip.kt @@ -37,8 +37,8 @@ private fun spec(status: AslStatus): StatusSpec = when (status) { @Composable fun AslStatusChip( - status: AslStatus = AslStatus.Success, modifier: Modifier = Modifier, + status: AslStatus = AslStatus.Success, label: String? = null, ) { val colors = AslTheme.colors diff --git a/domain/src/main/java/com/ahmadkharfan/androidstudiolite/domain/signing/SigningModels.kt b/domain/src/main/java/com/ahmadkharfan/androidstudiolite/domain/signing/SigningModels.kt index f8f5d5c0..f71951d3 100644 --- a/domain/src/main/java/com/ahmadkharfan/androidstudiolite/domain/signing/SigningModels.kt +++ b/domain/src/main/java/com/ahmadkharfan/androidstudiolite/domain/signing/SigningModels.kt @@ -45,4 +45,7 @@ sealed interface KeystoreError { data class Io(val message: String) : KeystoreError } -class KeystoreException(val error: KeystoreError) : Exception(error.toString()) +class KeystoreException( + val error: KeystoreError, + cause: Throwable? = null, +) : Exception(error.toString(), cause) diff --git a/feature/editor/detekt-baseline.xml b/feature/editor/detekt-baseline.xml index 8d214ecf..31453613 100644 --- a/feature/editor/detekt-baseline.xml +++ b/feature/editor/detekt-baseline.xml @@ -25,8 +25,6 @@ ForbiddenImport:AssetsScreen.kt$import androidx.compose.material3.MaterialTheme ForbiddenImport:FileTreeSearchPanel.kt$import androidx.compose.material3.MaterialTheme ForbiddenImport:VariantsScreen.kt$import androidx.compose.material3.MaterialTheme - LambdaParameterInRestartableEffect:AslEditableCodeEditor.kt$onVolumeKey - LambdaParameterInRestartableEffect:EditorScreen.kt$onConflictPathOpened LongMethod:AiChatScreen.kt$@Composable private fun AiChatScreen( uiState: AiChatUiState, interactionListener: AiChatInteractionListener, onClose: () -> Unit, onOpenAiAgentSettings: () -> Unit, ) LongMethod:AiChatScreen.kt$@Composable private fun ChatControlsSheet( uiState: AiChatUiState, interactionListener: AiChatInteractionListener, ) LongMethod:AslEditableCodeEditor.kt$@Composable fun AslEditableCodeEditor( session: EditorSession, fontSizeSp: Int, tabSize: Int, onEdited: () -> Unit, onCaretMoved: (line: Int, column: Int) -> Unit, modifier: Modifier = Modifier, colorSchemeId: String = "darcula", fontFamilyId: String = "jetbrains", gitLineStatus: Map<Int, AslLineGit> = emptyMap(), breakpoints: Set<Int> = emptySet(), findQuery: String = "", findCurrentMatch: Int = 0, revealNonce: Int = 0, revealOffset: Int = 0, enableVolumeKeys: Boolean = true, projectIndex: ProjectSymbolIndex = ProjectSymbolIndex.EMPTY, ) diff --git a/feature/editor/src/main/java/com/ahmadkharfan/androidstudiolite/feature/editor/EditorScreen.kt b/feature/editor/src/main/java/com/ahmadkharfan/androidstudiolite/feature/editor/EditorScreen.kt index 3c39f14e..dbeab724 100644 --- a/feature/editor/src/main/java/com/ahmadkharfan/androidstudiolite/feature/editor/EditorScreen.kt +++ b/feature/editor/src/main/java/com/ahmadkharfan/androidstudiolite/feature/editor/EditorScreen.kt @@ -23,15 +23,14 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.zIndex import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.LifecycleEventEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.flow.collectLatest import org.koin.androidx.compose.koinViewModel @@ -95,6 +94,7 @@ fun EditorRoute( viewModel: EditorViewModel = koinViewModel { parametersOf(projectId) }, ) { val uiState by viewModel.state.collectAsStateWithLifecycle() + val currentOnConflictPathOpened by rememberUpdatedState(onConflictPathOpened) val notificationPermissionLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestPermission(), @@ -103,7 +103,7 @@ fun EditorRoute( LaunchedEffect(openConflictPath) { openConflictPath?.let { viewModel.onOpenFile(it, File(it).name) - onConflictPathOpened() + currentOnConflictPathOpened() } } @@ -123,18 +123,8 @@ fun EditorRoute( } - val lifecycleOwner = LocalLifecycleOwner.current - DisposableEffect(lifecycleOwner) { - val observer = LifecycleEventObserver { _, event -> - when (event) { - Lifecycle.Event.ON_RESUME -> viewModel.onAppForegrounded() - Lifecycle.Event.ON_STOP -> viewModel.flushPendingSaves() - else -> Unit - } - } - lifecycleOwner.lifecycle.addObserver(observer) - onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } - } + LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { viewModel.onAppForegrounded() } + LifecycleEventEffect(Lifecycle.Event.ON_STOP) { viewModel.flushPendingSaves() } EditorScreen( uiState = uiState, diff --git a/feature/editor/src/main/java/com/ahmadkharfan/androidstudiolite/feature/editor/view/AslEditableCodeEditor.kt b/feature/editor/src/main/java/com/ahmadkharfan/androidstudiolite/feature/editor/view/AslEditableCodeEditor.kt index b9686896..d72e1094 100644 --- a/feature/editor/src/main/java/com/ahmadkharfan/androidstudiolite/feature/editor/view/AslEditableCodeEditor.kt +++ b/feature/editor/src/main/java/com/ahmadkharfan/androidstudiolite/feature/editor/view/AslEditableCodeEditor.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds @@ -168,16 +169,17 @@ fun EditorVolumeScrollEffect( enabled: Boolean, onVolumeKey: (volumeUp: Boolean) -> Unit, ) { + val currentOnVolumeKey by rememberUpdatedState(onVolumeKey) DisposableEffect(enabled) { if (!enabled) return@DisposableEffect onDispose {} EditorVolumeKeyDispatcher.handler = { event -> when (event.keyCode) { KeyEvent.KEYCODE_VOLUME_DOWN -> { - onVolumeKey(false) + currentOnVolumeKey(false) true } KeyEvent.KEYCODE_VOLUME_UP -> { - onVolumeKey(true) + currentOnVolumeKey(true) true } else -> false diff --git a/feature/git/detekt-baseline.xml b/feature/git/detekt-baseline.xml index 69c0e8e1..3963bc55 100644 --- a/feature/git/detekt-baseline.xml +++ b/feature/git/detekt-baseline.xml @@ -2,14 +2,12 @@ - ComposableParamOrder:GitDiffScreen.kt$GitDiffRoute CyclomaticComplexMethod:GitAuthPrompt.kt$@Composable fun GitHubAuthDialog(state: GitAuthPromptState, actions: GitAuthPromptActions) CyclomaticComplexMethod:GitErrorMessageMapper.kt$fun gitErrorMessage(error: Throwable): String CyclomaticComplexMethod:GitHistoryScreen.kt$@Composable private fun GitHistoryScreen( uiState: GitHistoryUiState, interactionListener: GitHistoryInteractionListener, onBack: () -> Unit, onOpenDiff: (String, String) -> Unit, ) CyclomaticComplexMethod:GitPanelScreen.kt$@Composable private fun GitPanelScreen( uiState: GitPanelUiState, interactionListener: GitPanelInteractionListener, onClose: () -> Unit, onOpenDiff: (String, GitDiffTarget) -> Unit, onOpenHistory: () -> Unit, onOpenBranches: () -> Unit, onOpenTags: () -> Unit, onOpenStashes: () -> Unit, onOpenConflicts: () -> Unit, ) ForbiddenImport:GitDiffScreen.kt$import androidx.compose.material3.MaterialTheme ForbiddenImport:GitHistoryScreen.kt$import androidx.compose.material3.MaterialTheme - LambdaParameterInRestartableEffect:GitHistoryScreen.kt$onLoadNext LongMethod:GitAuthPrompt.kt$@Composable fun GitHubAuthDialog(state: GitAuthPromptState, actions: GitAuthPromptActions) LongMethod:GitDiffScreen.kt$@Composable private fun GitDiffScreen( uiState: GitDiffUiState, interactionListener: GitDiffInteractionListener, onBack: () -> Unit, ) LongMethod:GitHistoryScreen.kt$@Composable private fun GitHistoryScreen( uiState: GitHistoryUiState, interactionListener: GitHistoryInteractionListener, onBack: () -> Unit, onOpenDiff: (String, String) -> Unit, ) diff --git a/feature/git/src/main/java/com/ahmadkharfan/androidstudiolite/feature/editor/git/GitPanelScreen.kt b/feature/git/src/main/java/com/ahmadkharfan/androidstudiolite/feature/editor/git/GitPanelScreen.kt index 0955f125..10397fb5 100644 --- a/feature/git/src/main/java/com/ahmadkharfan/androidstudiolite/feature/editor/git/GitPanelScreen.kt +++ b/feature/git/src/main/java/com/ahmadkharfan/androidstudiolite/feature/editor/git/GitPanelScreen.kt @@ -18,7 +18,6 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -27,10 +26,9 @@ import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.LifecycleEventEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import androidx.lifecycle.compose.LocalLifecycleOwner import kotlinx.coroutines.delay import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf @@ -81,14 +79,7 @@ fun GitPanelRoute( viewModel: GitPanelViewModel = koinViewModel { parametersOf(projectId) }, ) { val uiState by viewModel.state.collectAsStateWithLifecycle() - val lifecycleOwner = LocalLifecycleOwner.current - DisposableEffect(lifecycleOwner) { - val observer = LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_RESUME) viewModel.onAppForegrounded() - } - lifecycleOwner.lifecycle.addObserver(observer) - onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } - } + LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { viewModel.onAppForegrounded() } GitPanelScreen( uiState = uiState, interactionListener = viewModel, diff --git a/feature/git/src/main/java/com/ahmadkharfan/androidstudiolite/feature/editor/git/diff/GitDiffScreen.kt b/feature/git/src/main/java/com/ahmadkharfan/androidstudiolite/feature/editor/git/diff/GitDiffScreen.kt index 0bdfe1bb..fe105600 100644 --- a/feature/git/src/main/java/com/ahmadkharfan/androidstudiolite/feature/editor/git/diff/GitDiffScreen.kt +++ b/feature/git/src/main/java/com/ahmadkharfan/androidstudiolite/feature/editor/git/diff/GitDiffScreen.kt @@ -74,8 +74,8 @@ fun GitDiffRoute( projectId: String, path: String, target: GitDiffTarget, - commitId: String? = null, onBack: () -> Unit, + commitId: String? = null, viewModel: GitDiffViewModel = koinViewModel { parametersOf(projectId, path, target, commitId.orEmpty()) }, ) { val uiState by viewModel.state.collectAsStateWithLifecycle() diff --git a/feature/git/src/main/java/com/ahmadkharfan/androidstudiolite/feature/editor/git/history/GitHistoryScreen.kt b/feature/git/src/main/java/com/ahmadkharfan/androidstudiolite/feature/editor/git/history/GitHistoryScreen.kt index ac89aaaf..88868347 100644 --- a/feature/git/src/main/java/com/ahmadkharfan/androidstudiolite/feature/editor/git/history/GitHistoryScreen.kt +++ b/feature/git/src/main/java/com/ahmadkharfan/androidstudiolite/feature/editor/git/history/GitHistoryScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier @@ -195,10 +196,13 @@ private fun HistoryList( onReset: (String) -> Unit, ) { val listState = rememberLazyListState() + val currentOnLoadNext by rememberUpdatedState(onLoadNext) LaunchedEffect(listState, state.nextCursor) { snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 } .distinctUntilChanged() - .collect { index -> if (state.nextCursor != null && index >= state.commits.lastIndex - 4) onLoadNext() } + .collect { index -> + if (state.nextCursor != null && index >= state.commits.lastIndex - 4) currentOnLoadNext() + } } LazyColumn(state = listState, modifier = Modifier.fillMaxSize()) { items(state.commits, key = { it.id }) { commit -> diff --git a/feature/onboarding/detekt-baseline.xml b/feature/onboarding/detekt-baseline.xml index 6ba975d1..8f37cc4a 100644 --- a/feature/onboarding/detekt-baseline.xml +++ b/feature/onboarding/detekt-baseline.xml @@ -2,13 +2,11 @@ - ExplicitItLambdaParameter:PermissionsScreen.kt${ _, it -> it.id } ForbiddenImport:CompleteScreen.kt$import androidx.compose.material3.MaterialTheme ForbiddenImport:HowItWorksScreen.kt$import androidx.compose.material3.MaterialTheme ForbiddenImport:PermissionsScreen.kt$import androidx.compose.material3.MaterialTheme ForbiddenImport:WelcomeScreen.kt$import androidx.compose.material3.MaterialTheme LongMethod:HowItWorksScreen.kt$@Composable private fun HowItWorksScreen(onContinue: () -> Unit) ParameterNaming:WelcomeScreen.kt$onGetStarted - UnusedPrivateMember:WelcomeScreen.kt$@Preview @Composable private fun WelcomeBulletPreview() diff --git a/feature/onboarding/src/main/java/com/ahmadkharfan/androidstudiolite/feature/onboarding/permissions/PermissionsScreen.kt b/feature/onboarding/src/main/java/com/ahmadkharfan/androidstudiolite/feature/onboarding/permissions/PermissionsScreen.kt index d7e49acb..2b77de79 100644 --- a/feature/onboarding/src/main/java/com/ahmadkharfan/androidstudiolite/feature/onboarding/permissions/PermissionsScreen.kt +++ b/feature/onboarding/src/main/java/com/ahmadkharfan/androidstudiolite/feature/onboarding/permissions/PermissionsScreen.kt @@ -140,7 +140,7 @@ private fun PermissionsList( modifier = modifier, verticalArrangement = Arrangement.spacedBy(12.dp), ) { - itemsIndexed(uiState.permissions, key = { _, it -> it.id }) { index, permission -> + itemsIndexed(uiState.permissions, key = { _, permission -> permission.id }) { index, permission -> AslStaggeredAppear(index = index) { AslPermissionCard( title = permission.title, diff --git a/feature/projects/detekt-baseline.xml b/feature/projects/detekt-baseline.xml index 4eb3d9a1..01518266 100644 --- a/feature/projects/detekt-baseline.xml +++ b/feature/projects/detekt-baseline.xml @@ -5,15 +5,6 @@ ForbiddenImport:CloneRepoScreen.kt$import androidx.compose.material3.MaterialTheme ForbiddenImport:ConfigureStep.kt$import androidx.compose.material3.MaterialTheme ForbiddenImport:HubScreen.kt$import androidx.compose.material3.MaterialTheme - ForbiddenImport:HubSectionHeader.kt$import androidx.compose.material3.MaterialTheme - LambdaParameterInRestartableEffect:CloneRepoScreen.kt$onCloned - LambdaParameterInRestartableEffect:CreateProjectScreen.kt$onCreated - LambdaParameterInRestartableEffect:CreateProjectScreen.kt$onPickedFolderConsumed - LambdaParameterInRestartableEffect:HubScreen.kt$onCreateProject - LambdaParameterInRestartableEffect:HubScreen.kt$onOpenPreferences - LambdaParameterInRestartableEffect:HubScreen.kt$onOpenProject - LambdaParameterInRestartableEffect:HubScreen.kt$onPickedFolderConsumed - LambdaParameterInRestartableEffect:OpenProjectScreen.kt$onProjectSelected LongMethod:FolderPickerScreen.kt$@Composable private fun FolderPickerScreen( uiState: FolderPickerUiState, interactionListener: FolderPickerInteractionListener, onCancel: () -> Unit, onSelect: () -> Unit, ) LongParameterList:ConfigureStep.kt$( projectName: String, packageName: String, location: String, minSdk: String, language: String, supportsJava: Boolean, nameError: String?, packageError: String?, onNameChanged: (String) -> Unit, onPackageChanged: (String) -> Unit, onLocationChanged: (String) -> Unit, onMinSdkChanged: (String) -> Unit, onLanguageChanged: (String) -> Unit, onBrowseLocation: () -> Unit, modifier: Modifier = Modifier, ) ModifierMissing:HubScreen.kt$HubRoute @@ -30,6 +21,5 @@ ParameterNaming:FolderPickerScreen.kt$onFolderSelected ParameterNaming:HubScreen.kt$onPickedFolderConsumed ParameterNaming:OpenProjectScreen.kt$onProjectSelected - UseCheckOrError:CreateProjectViewModel.kt$CreateProjectViewModel$throw IllegalStateException("No template selected") diff --git a/feature/projects/src/main/java/com/ahmadkharfan/androidstudiolite/feature/clonerepo/CloneRepoScreen.kt b/feature/projects/src/main/java/com/ahmadkharfan/androidstudiolite/feature/clonerepo/CloneRepoScreen.kt index 38e4c767..ba896aa3 100644 --- a/feature/projects/src/main/java/com/ahmadkharfan/androidstudiolite/feature/clonerepo/CloneRepoScreen.kt +++ b/feature/projects/src/main/java/com/ahmadkharfan/androidstudiolite/feature/clonerepo/CloneRepoScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -35,10 +36,11 @@ fun CloneRepoRoute( viewModel: CloneRepoViewModel = koinViewModel(), ) { val uiState by viewModel.state.collectAsStateWithLifecycle() + val currentOnCloned by rememberUpdatedState(onCloned) LaunchedEffect(uiState.clonedProjectId) { val clonedProjectId = uiState.clonedProjectId ?: return@LaunchedEffect - onCloned(clonedProjectId) + currentOnCloned(clonedProjectId) viewModel.onClonedProjectOpened() } diff --git a/feature/projects/src/main/java/com/ahmadkharfan/androidstudiolite/feature/createproject/CreateProjectScreen.kt b/feature/projects/src/main/java/com/ahmadkharfan/androidstudiolite/feature/createproject/CreateProjectScreen.kt index e45c505e..43e937ac 100644 --- a/feature/projects/src/main/java/com/ahmadkharfan/androidstudiolite/feature/createproject/CreateProjectScreen.kt +++ b/feature/projects/src/main/java/com/ahmadkharfan/androidstudiolite/feature/createproject/CreateProjectScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -34,14 +35,16 @@ fun CreateProjectRoute( viewModel: CreateProjectViewModel = koinViewModel(), ) { val uiState by viewModel.state.collectAsStateWithLifecycle() + val currentOnCreated by rememberUpdatedState(onCreated) + val currentOnPickedFolderConsumed by rememberUpdatedState(onPickedFolderConsumed) LaunchedEffect(uiState.createdProjectId) { - uiState.createdProjectId?.let(onCreated) + uiState.createdProjectId?.let(currentOnCreated) } LaunchedEffect(pickedFolder) { pickedFolder?.let { viewModel.onLocationChanged(it) - onPickedFolderConsumed() + currentOnPickedFolderConsumed() } } diff --git a/feature/projects/src/main/java/com/ahmadkharfan/androidstudiolite/feature/createproject/CreateProjectViewModel.kt b/feature/projects/src/main/java/com/ahmadkharfan/androidstudiolite/feature/createproject/CreateProjectViewModel.kt index 7448b88a..72d0a545 100644 --- a/feature/projects/src/main/java/com/ahmadkharfan/androidstudiolite/feature/createproject/CreateProjectViewModel.kt +++ b/feature/projects/src/main/java/com/ahmadkharfan/androidstudiolite/feature/createproject/CreateProjectViewModel.kt @@ -148,7 +148,7 @@ class CreateProjectViewModel( onStart = { updateState { copy(creating = true) } }, block = { val templateId = state.value.selectedTemplateId - ?: throw IllegalStateException("No template selected") + ?: error("No template selected") projectRepository.createProject( NewProjectSpec( name = s.projectName, diff --git a/feature/projects/src/main/java/com/ahmadkharfan/androidstudiolite/feature/hub/HubScreen.kt b/feature/projects/src/main/java/com/ahmadkharfan/androidstudiolite/feature/hub/HubScreen.kt index e9c5b5cb..c1d45f24 100644 --- a/feature/projects/src/main/java/com/ahmadkharfan/androidstudiolite/feature/hub/HubScreen.kt +++ b/feature/projects/src/main/java/com/ahmadkharfan/androidstudiolite/feature/hub/HubScreen.kt @@ -29,6 +29,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -77,20 +78,24 @@ fun HubRoute( viewModel: HubViewModel = koinViewModel(), ) { val uiState by viewModel.state.collectAsStateWithLifecycle() + val currentOnOpenProject by rememberUpdatedState(onOpenProject) + val currentOnCreateProject by rememberUpdatedState(onCreateProject) + val currentOnOpenPreferences by rememberUpdatedState(onOpenPreferences) + val currentOnPickedFolderConsumed by rememberUpdatedState(onPickedFolderConsumed) LaunchedEffect(pickedFolder) { pickedFolder?.let { path -> viewModel.onFolderPicked(path) - onPickedFolderConsumed() + currentOnPickedFolderConsumed() } } LaunchedEffect(Unit) { viewModel.effect.collectLatest { effect -> when (effect) { - is HubEffect.NavigateToProject -> onOpenProject(effect.id) - HubEffect.NavigateToCreateProject -> onCreateProject() - HubEffect.NavigateToPreferences -> onOpenPreferences() + is HubEffect.NavigateToProject -> currentOnOpenProject(effect.id) + HubEffect.NavigateToCreateProject -> currentOnCreateProject() + HubEffect.NavigateToPreferences -> currentOnOpenPreferences() } } } diff --git a/feature/projects/src/main/java/com/ahmadkharfan/androidstudiolite/feature/openproject/OpenProjectScreen.kt b/feature/projects/src/main/java/com/ahmadkharfan/androidstudiolite/feature/openproject/OpenProjectScreen.kt index ad1a7d48..c10e9dd3 100644 --- a/feature/projects/src/main/java/com/ahmadkharfan/androidstudiolite/feature/openproject/OpenProjectScreen.kt +++ b/feature/projects/src/main/java/com/ahmadkharfan/androidstudiolite/feature/openproject/OpenProjectScreen.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -29,11 +30,12 @@ fun OpenProjectRoute( viewModel: OpenProjectViewModel = koinViewModel(), ) { val uiState by viewModel.state.collectAsStateWithLifecycle() + val currentOnProjectSelected by rememberUpdatedState(onProjectSelected) LaunchedEffect(Unit) { viewModel.effect.collectLatest { effect -> when (effect) { - is OpenProjectEffect.NavigateToProject -> onProjectSelected(effect.id) + is OpenProjectEffect.NavigateToProject -> currentOnProjectSelected(effect.id) } } } diff --git a/feature/terminal/detekt-baseline.xml b/feature/terminal/detekt-baseline.xml index 837b901b..66bbb289 100644 --- a/feature/terminal/detekt-baseline.xml +++ b/feature/terminal/detekt-baseline.xml @@ -6,24 +6,15 @@ CyclomaticComplexMethod:TerminalEmulator.kt$TerminalEmulator$private fun applySgr(params: List<Int?>) CyclomaticComplexMethod:TerminalEmulator.kt$TerminalEmulator$private fun dispatchCsi(final: Char) CyclomaticComplexMethod:TerminalEmulatorView.kt$@Composable fun TerminalEmulatorView( screen: TerminalScreen, background: Color, foreground: Color, cursorColor: Color, onKey: (String) -> Unit, onSpecialKey: (TerminalKey) -> Unit, onResize: (rows: Int, cols: Int) -> Unit, modifier: Modifier = Modifier, enableVolumeKeys: Boolean = true, requestKeyboardOnAttach: Boolean = true, ) - EmptyElseBlock:TerminalEmulator.kt$TerminalEmulator$if (cursorCol > 0) cursorCol-- EmptyFunctionBlock:TerminalSessionManagerTest.kt$TerminalSessionManagerTest.FakeTerminalRepository${} ForbiddenImport:TerminalScreen.kt$import androidx.compose.material3.MaterialTheme ForbiddenImport:TerminalSettingsSheet.kt$import androidx.compose.material3.MaterialTheme - LambdaParameterInRestartableEffect:TerminalEmulatorView.kt$onResize - LambdaParameterInRestartableEffect:TerminalVolumeScrollEffect.kt$onVolumeKey LongMethod:TerminalEmulatorView.kt$@Composable fun TerminalEmulatorView( screen: TerminalScreen, background: Color, foreground: Color, cursorColor: Color, onKey: (String) -> Unit, onSpecialKey: (TerminalKey) -> Unit, onResize: (rows: Int, cols: Int) -> Unit, modifier: Modifier = Modifier, enableVolumeKeys: Boolean = true, requestKeyboardOnAttach: Boolean = true, ) LongMethod:TerminalSettingsSheet.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun TerminalSettingsSheet( linux: LinuxStatus, onDismiss: () -> Unit, onInstallLinux: () -> Unit, onReinstallLinux: () -> Unit, ) LongParameterList:TerminalEmulatorView.kt$( native: android.graphics.Canvas, paint: Paint, builder: StringBuilder, cells: List<TerminalCell>, top: Float, baseline: Float, charWidthPx: Float, lineHeightPx: Float, foreground: Color, background: Color, cursorColor: Color, cursorCol: Int, lightBackground: Boolean, ) LongParameterList:TerminalEmulatorView.kt$( screen: TerminalScreen, background: Color, foreground: Color, cursorColor: Color, onKey: (String) -> Unit, onSpecialKey: (TerminalKey) -> Unit, onResize: (rows: Int, cols: Int) -> Unit, modifier: Modifier = Modifier, enableVolumeKeys: Boolean = true, requestKeyboardOnAttach: Boolean = true, ) LoopWithTooManyJumpStatements:TerminalEmulatorView.kt$for LoopWithTooManyJumpStatements:TerminalEmulatorView.kt$while - ThrowsCount:LinuxBootstrapInstaller.kt$LinuxBootstrapInstaller$private fun downloadWithChecksum(url: String, expectedSha256: String, dest: File) TooGenericExceptionCaught:LinuxBootstrapInstaller.kt$LinuxBootstrapInstaller$t: Throwable - UseCheckOrError:LinuxBootstrapInstaller.kt$LinuxBootstrapInstaller$throw IllegalStateException("checksum mismatch") - UseCheckOrError:LinuxBootstrapInstaller.kt$LinuxBootstrapInstaller$throw IllegalStateException("could not move rootfs into place") - UseCheckOrError:LinuxBootstrapInstaller.kt$LinuxBootstrapInstaller$throw IllegalStateException("download failed: HTTP ${response.code}") - UseCheckOrError:LinuxBootstrapInstaller.kt$LinuxBootstrapInstaller$throw IllegalStateException("empty response body") - UseCheckOrError:LinuxBootstrapInstaller.kt$LinuxBootstrapInstaller$throw IllegalStateException("unsafe path in archive: ${entry.name}") diff --git a/feature/terminal/src/main/java/com/ahmadkharfan/androidstudiolite/core/linux/LinuxBootstrapInstaller.kt b/feature/terminal/src/main/java/com/ahmadkharfan/androidstudiolite/core/linux/LinuxBootstrapInstaller.kt index 4fa8115d..4112e020 100644 --- a/feature/terminal/src/main/java/com/ahmadkharfan/androidstudiolite/core/linux/LinuxBootstrapInstaller.kt +++ b/feature/terminal/src/main/java/com/ahmadkharfan/androidstudiolite/core/linux/LinuxBootstrapInstaller.kt @@ -96,9 +96,7 @@ class LinuxBootstrapInstaller( extract(archive, staging) proot.rootfsDir.deleteRecursively() - if (!staging.renameTo(proot.rootfsDir)) { - throw IllegalStateException("could not move rootfs into place") - } + check(staging.renameTo(proot.rootfsDir)) { "could not move rootfs into place" } proot.prepareRuntime() File(proot.ensureRootfsParent(), ".linux-installed").writeText("ok") bootstrapPackages() @@ -156,8 +154,8 @@ class LinuxBootstrapInstaller( private fun downloadWithChecksum(url: String, expectedSha256: String, dest: File) { val request = Request.Builder().url(url).build() client.newCall(request).execute().use { response -> - if (!response.isSuccessful) throw IllegalStateException("download failed: HTTP ${response.code}") - val body = response.body ?: throw IllegalStateException("empty response body") + check(response.isSuccessful) { "download failed: HTTP ${response.code}" } + val body = response.body ?: error("empty response body") val total = body.contentLength() val digest = MessageDigest.getInstance("SHA-256") body.byteStream().use { input -> @@ -176,9 +174,7 @@ class LinuxBootstrapInstaller( } } val actual = digest.digest().joinToString("") { "%02x".format(it) } - if (!actual.equals(expectedSha256, ignoreCase = true)) { - throw IllegalStateException("checksum mismatch") - } + check(actual.equals(expectedSha256, ignoreCase = true)) { "checksum mismatch" } } } @@ -194,9 +190,10 @@ class LinuxBootstrapInstaller( } val out = File(into, relative) - if (!out.canonicalPath.startsWith(canonicalInto + File.separator) && out.canonicalPath != canonicalInto) { - throw IllegalStateException("unsafe path in archive: ${entry.name}") - } + check( + out.canonicalPath.startsWith(canonicalInto + File.separator) || + out.canonicalPath == canonicalInto, + ) { "unsafe path in archive: ${entry.name}" } when { entry.isDirectory -> out.mkdirs() entry.isSymbolicLink -> { diff --git a/feature/terminal/src/main/java/com/ahmadkharfan/androidstudiolite/feature/terminal/TerminalEmulatorView.kt b/feature/terminal/src/main/java/com/ahmadkharfan/androidstudiolite/feature/terminal/TerminalEmulatorView.kt index d9423b93..ae1b5b0b 100644 --- a/feature/terminal/src/main/java/com/ahmadkharfan/androidstudiolite/feature/terminal/TerminalEmulatorView.kt +++ b/feature/terminal/src/main/java/com/ahmadkharfan/androidstudiolite/feature/terminal/TerminalEmulatorView.kt @@ -107,6 +107,7 @@ fun TerminalEmulatorView( val context = LocalContext.current val density = LocalDensity.current val colors = AslTheme.colors + val currentOnResize by rememberUpdatedState(onResize) val lightTerminal = remember(background) { val r = background.red val g = background.green @@ -382,7 +383,7 @@ fun TerminalEmulatorView( if (rows != lastEmittedRows || cols != lastEmittedCols) { lastEmittedRows = rows lastEmittedCols = cols - onResize(rows, cols) + currentOnResize(rows, cols) } } diff --git a/feature/terminal/src/main/java/com/ahmadkharfan/androidstudiolite/feature/terminal/TerminalVolumeScrollEffect.kt b/feature/terminal/src/main/java/com/ahmadkharfan/androidstudiolite/feature/terminal/TerminalVolumeScrollEffect.kt index 3c558064..6d5d0ec2 100644 --- a/feature/terminal/src/main/java/com/ahmadkharfan/androidstudiolite/feature/terminal/TerminalVolumeScrollEffect.kt +++ b/feature/terminal/src/main/java/com/ahmadkharfan/androidstudiolite/feature/terminal/TerminalVolumeScrollEffect.kt @@ -3,22 +3,25 @@ package com.ahmadkharfan.androidstudiolite.feature.terminal import android.view.KeyEvent import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState @Composable fun TerminalVolumeScrollEffect( enabled: Boolean, onVolumeKey: (volumeUp: Boolean) -> Unit, ) { + val currentOnVolumeKey by rememberUpdatedState(onVolumeKey) DisposableEffect(enabled) { if (!enabled) return@DisposableEffect onDispose {} TerminalVolumeKeyDispatcher.handler = { event -> when (event.keyCode) { KeyEvent.KEYCODE_VOLUME_DOWN -> { - onVolumeKey(false) + currentOnVolumeKey(false) true } KeyEvent.KEYCODE_VOLUME_UP -> { - onVolumeKey(true) + currentOnVolumeKey(true) true } else -> false diff --git a/feature/terminal/src/main/java/com/ahmadkharfan/androidstudiolite/feature/terminal/emulator/TerminalEmulator.kt b/feature/terminal/src/main/java/com/ahmadkharfan/androidstudiolite/feature/terminal/emulator/TerminalEmulator.kt index 0cc3aebf..52475def 100644 --- a/feature/terminal/src/main/java/com/ahmadkharfan/androidstudiolite/feature/terminal/emulator/TerminalEmulator.kt +++ b/feature/terminal/src/main/java/com/ahmadkharfan/androidstudiolite/feature/terminal/emulator/TerminalEmulator.kt @@ -128,7 +128,10 @@ class TerminalEmulator(rows: Int, cols: Int) { '\u001B' -> { state = State.ESC; paramBuf.setLength(0) } '\n', '\u000B', '\u000C' -> lineFeed() '\r' -> { cursorCol = 0; wrapPending = false } - '\b' -> { if (cursorCol > 0) cursorCol--; wrapPending = false } + '\b' -> { + if (cursorCol > 0) cursorCol-- + wrapPending = false + } '\t' -> tab() '\u0007' -> Unit else -> if (c >= ' ') putChar(c)