From b45c8a7c70cd326996304b06830c8404a39f2f8c Mon Sep 17 00:00:00 2001 From: Ash Davies <1892070+ashdavies@users.noreply.github.com> Date: Wed, 13 Mar 2024 10:14:45 +0100 Subject: [PATCH] Resolve detekt violations with reduction in complexity (#896) --- .../ashdavies/events/EventsRemoteMediator.kt | 55 +++++++++++-------- .../io/ashdavies/check/AppCheckGenerator.kt | 8 ++- app-launcher/android/build.gradle.kts | 2 + .../ashdavies/playground/LauncherActivity.kt | 26 +++++++-- .../ashdavies/playground/ComposeActivity.kt | 47 ---------------- .../io/ashdavies/playground/LauncherScreen.kt | 9 ++- .../io/ashdavies/playground/LauncherMain.kt | 54 +++++++++--------- .../io/ashdavies/cloud/ApplicationTest.kt | 7 ++- detekt-config.yml | 5 ++ .../kotlin/io/ashdavies/http/Result.kt | 10 ++-- .../content/PlatformContextKt.android.kt | 5 ++ .../kotlin/io/ashdavies/content/StrictMode.kt | 13 +++++ 12 files changed, 127 insertions(+), 114 deletions(-) delete mode 100644 app-launcher/common/src/androidMain/kotlin/io/ashdavies/playground/ComposeActivity.kt create mode 100644 platform-scaffold/src/androidMain/kotlin/io/ashdavies/content/StrictMode.kt diff --git a/after-party/src/commonMain/kotlin/io/ashdavies/events/EventsRemoteMediator.kt b/after-party/src/commonMain/kotlin/io/ashdavies/events/EventsRemoteMediator.kt index 99655d536..96a4bec7f 100644 --- a/after-party/src/commonMain/kotlin/io/ashdavies/events/EventsRemoteMediator.kt +++ b/after-party/src/commonMain/kotlin/io/ashdavies/events/EventsRemoteMediator.kt @@ -17,34 +17,30 @@ internal class EventsRemoteMediator( override suspend fun load( loadType: LoadType, state: PagingState, - ): MediatorResult { - val loadKey = when (loadType) { - LoadType.APPEND -> state.lastItemOrNull()?.id ?: return endOfPaginationReached() - LoadType.PREPEND -> return endOfPaginationReached() - LoadType.REFRESH -> null + ): MediatorResult = when (loadType) { + LoadType.PREPEND -> endOfPaginationReached() + else -> when (val lastItem = state.lastItemOrNull()) { + is DatabaseEvent -> load(loadType, lastItem.id) + else -> endOfPaginationReached() } + } - val result: List = try { - eventsCallable(GetEventsRequest(loadKey)) - } catch (exception: SocketTimeoutException) { - return MediatorResult.Error(exception) - } catch (exception: GetEventsError) { - return MediatorResult.Error(exception) - } + private suspend fun load( + loadType: LoadType, + startAt: String?, + ): MediatorResult = when (val result = eventsCallable.result(GetEventsRequest(startAt))) { + is CallableResult.Error<*> -> MediatorResult.Error(result.throwable) + is CallableResult.Success -> { + eventsQueries.transaction { + if (loadType == LoadType.REFRESH) eventsQueries.deleteAll() - eventsQueries.transaction { - if (loadType == LoadType.REFRESH) { - eventsQueries.deleteAll() + result.value.forEach { + eventsQueries.insertOrReplace(it.asDatabaseEvent()) + } } - result.forEach { - eventsQueries.insertOrReplace(it.asDatabaseEvent()) - } + MediatorResult.Success(result.value.isEmpty()) } - - return MediatorResult.Success( - endOfPaginationReached = result.isEmpty(), - ) } private fun endOfPaginationReached(): MediatorResult { @@ -52,6 +48,21 @@ internal class EventsRemoteMediator( } } +private suspend fun GetEventsCallable.result( + request: GetEventsRequest, +): CallableResult> = try { + CallableResult.Success(invoke(request)) +} catch (exception: SocketTimeoutException) { + CallableResult.Error(exception) +} catch (exception: GetEventsError) { + CallableResult.Error(exception) +} + +private sealed interface CallableResult { + data class Error(val throwable: Throwable) : CallableResult + data class Success(val value: T) : CallableResult +} + private fun ApiEvent.asDatabaseEvent(): DatabaseEvent = DatabaseEvent( id = id, name = name, website = website, location = location, status = status, online = online, dateStart = dateStart, diff --git a/app-check/app-check-sdk/src/commonMain/kotlin/io/ashdavies/check/AppCheckGenerator.kt b/app-check/app-check-sdk/src/commonMain/kotlin/io/ashdavies/check/AppCheckGenerator.kt index 40eda2d74..f451e76cb 100644 --- a/app-check/app-check-sdk/src/commonMain/kotlin/io/ashdavies/check/AppCheckGenerator.kt +++ b/app-check/app-check-sdk/src/commonMain/kotlin/io/ashdavies/check/AppCheckGenerator.kt @@ -9,6 +9,7 @@ import io.ktor.http.HttpHeaders import kotlinx.datetime.Clock import kotlinx.serialization.Serializable import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.seconds private const val CUSTOM_EXCHANGE_URL_TEMPLATE = "https://firebaseappcheck.googleapis.com/v1/projects/%s/apps/%s:exchangeCustomToken" @@ -56,12 +57,13 @@ internal fun AppCheckGenerator( setBody(mapOf("customToken" to customToken)) }.body() - val ttlMillis = result.ttl + val ttlSeconds = result.ttl .substring(0, result.ttl.length - 1) - .toLong() * 1000 + .toLong() + .seconds return mapper( - /* ttlMillis */ ttlMillis, + /* ttlMillis */ ttlSeconds.inWholeMilliseconds, /* token */ result.token, ) } diff --git a/app-launcher/android/build.gradle.kts b/app-launcher/android/build.gradle.kts index 36315da03..c080c38b0 100644 --- a/app-launcher/android/build.gradle.kts +++ b/app-launcher/android/build.gradle.kts @@ -38,8 +38,10 @@ kotlin { androidMain.dependencies { implementation(projects.appLauncher.common) implementation(projects.httpClient) + implementation(projects.platformScaffold) implementation(projects.platformSupport) + implementation(libs.androidx.activity.compose) implementation(libs.androidx.core.splashscreen) implementation(libs.google.android.material) implementation(libs.ktor.client.core) diff --git a/app-launcher/android/src/androidMain/kotlin/io/ashdavies/playground/LauncherActivity.kt b/app-launcher/android/src/androidMain/kotlin/io/ashdavies/playground/LauncherActivity.kt index 5808c119f..de521dcd6 100644 --- a/app-launcher/android/src/androidMain/kotlin/io/ashdavies/playground/LauncherActivity.kt +++ b/app-launcher/android/src/androidMain/kotlin/io/ashdavies/playground/LauncherActivity.kt @@ -1,7 +1,13 @@ package io.ashdavies.playground +import android.app.Activity import android.content.pm.PackageManager import android.os.Build +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext @@ -9,13 +15,16 @@ import com.slack.circuit.foundation.CircuitCompositionLocals import com.slack.circuit.foundation.NavigableCircuitContent import com.slack.circuit.foundation.rememberCircuitNavigator import com.slack.circuit.overlay.ContentWithOverlays +import io.ashdavies.content.enableStrictMode +import io.ashdavies.content.isDebuggable import io.ashdavies.http.LocalHttpClient import io.ktor.client.plugins.DefaultRequest import io.ktor.client.request.header import java.security.MessageDigest import java.util.Locale -internal class LauncherActivity : ComposeActivity(content = { +@Composable +private fun Activity.LauncherApp() { CompositionLocalProvider( LocalHttpClient provides LocalHttpClient.current.config { install(DefaultRequest) { @@ -26,9 +35,7 @@ internal class LauncherActivity : ComposeActivity(content = { } }, ) { - val circuit = remember { CircuitConfig(applicationContext) } - - CircuitCompositionLocals(circuit) { + CircuitCompositionLocals(remember { CircuitConfig(applicationContext) }) { ContentWithOverlays { LauncherContent(LocalContext.current) { val backStack = rememberSaveableBackStack(intent.getStringExtra("route")) @@ -41,7 +48,16 @@ internal class LauncherActivity : ComposeActivity(content = { } } } -},) +} + +internal class LauncherActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + enableStrictMode(isDebuggable()) + setContent { LauncherApp() } + } +} @Suppress("DEPRECATION") private fun getSignature(packageManager: PackageManager, packageName: String): String { diff --git a/app-launcher/common/src/androidMain/kotlin/io/ashdavies/playground/ComposeActivity.kt b/app-launcher/common/src/androidMain/kotlin/io/ashdavies/playground/ComposeActivity.kt deleted file mode 100644 index 16c3e8e63..000000000 --- a/app-launcher/common/src/androidMain/kotlin/io/ashdavies/playground/ComposeActivity.kt +++ /dev/null @@ -1,47 +0,0 @@ -package io.ashdavies.playground - -import android.content.Context -import android.content.pm.ApplicationInfo -import android.os.Bundle -import android.os.StrictMode -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.compose.runtime.Composable - -public abstract class ComposeActivity( - private val edgeToEdge: () -> Boolean = { true }, - private val strictMode: Context.() -> Boolean = { isDebuggable() }, - private val content: @Composable ComponentActivity.(Bundle?) -> Unit, -) : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - if (edgeToEdge()) { - enableEdgeToEdge() - } - - if (strictMode()) { - enableStrictMode() - } - - setContent { - content(savedInstanceState) - } - } -} - -private fun Context.isDebuggable(): Boolean { - return applicationInfo.flags != 0 and ApplicationInfo.FLAG_DEBUGGABLE -} - -private fun enableStrictMode(penaltyDeath: Boolean = false) { - val policy = StrictMode.ThreadPolicy.Builder() - .also { if (penaltyDeath) it.penaltyDeath() else it } - .detectAll() - .penaltyLog() - .build() - - StrictMode.setThreadPolicy(policy) -} diff --git a/app-launcher/common/src/commonMain/kotlin/io/ashdavies/playground/LauncherScreen.kt b/app-launcher/common/src/commonMain/kotlin/io/ashdavies/playground/LauncherScreen.kt index 53bb869d3..a7a9a31f4 100644 --- a/app-launcher/common/src/commonMain/kotlin/io/ashdavies/playground/LauncherScreen.kt +++ b/app-launcher/common/src/commonMain/kotlin/io/ashdavies/playground/LauncherScreen.kt @@ -13,7 +13,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowForward +import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -36,6 +36,9 @@ import io.ashdavies.parcelable.Parcelize import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.painterResource +private val LauncherAspectRatio: Float + get() = 1024f / 500f + @Parcelize internal object LauncherScreen : Parcelable, Screen { @@ -88,7 +91,7 @@ private fun LauncherTopAppBar(modifier: Modifier = Modifier) { contentAlignment = Alignment.Center, ) { Icon( - imageVector = Icons.Filled.ArrowForward, + imageVector = Icons.AutoMirrored.Filled.ArrowForward, contentDescription = null, ) } @@ -118,7 +121,7 @@ private fun LauncherItem( painter = imagePainter, contentDescription = item.title, modifier = Modifier - .aspectRatio(1024f / 500f) + .aspectRatio(LauncherAspectRatio) .fillMaxWidth(), contentScale = ContentScale.Crop, ) diff --git a/app-launcher/desktop/src/jvmMain/kotlin/io/ashdavies/playground/LauncherMain.kt b/app-launcher/desktop/src/jvmMain/kotlin/io/ashdavies/playground/LauncherMain.kt index ca5931c00..4c72b9a2e 100644 --- a/app-launcher/desktop/src/jvmMain/kotlin/io/ashdavies/playground/LauncherMain.kt +++ b/app-launcher/desktop/src/jvmMain/kotlin/io/ashdavies/playground/LauncherMain.kt @@ -21,36 +21,34 @@ private class LauncherCommand : CliktCommand() { val route: String? by option(help = "The initial route to navigate to") - override fun run() { - application { - Window( - onCloseRequest = ::exitApplication, - state = rememberWindowState(size = DpSize(450.dp, 975.dp)), - title = commandName, - ) { - val circuit = remember { CircuitConfig(PlatformContext.Default) } + override fun run() = application { + Window( + onCloseRequest = ::exitApplication, + state = rememberWindowState(size = DpSize(450.dp, 975.dp)), + title = commandName, + ) { + val circuit = remember { CircuitConfig(PlatformContext.Default) } - CircuitCompositionLocals(circuit) { - CompositionLocalProvider( - LocalHttpClient provides LocalHttpClient.current.config { - install(DefaultRequest) { - header("User-Agent", System.getProperty("os.name")) - header("X-API-Key", BuildConfig.BROWSER_API_KEY) - } - }, - ) { - LauncherContent(PlatformContext.Default) { - val backStack = rememberSaveableBackStack(route) - - NavigableCircuitContent( - navigator = rememberCircuitNavigator(backStack, ::exitApplication), - backStack = backStack, - decoration = KeyNavigationDecoration( - decoration = circuit.defaultNavDecoration, - onBackInvoked = backStack::pop, - ), - ) + CircuitCompositionLocals(circuit) { + CompositionLocalProvider( + LocalHttpClient provides LocalHttpClient.current.config { + install(DefaultRequest) { + header("User-Agent", System.getProperty("os.name")) + header("X-API-Key", BuildConfig.BROWSER_API_KEY) } + }, + ) { + LauncherContent(PlatformContext.Default) { + val backStack = rememberSaveableBackStack(route) + + NavigableCircuitContent( + navigator = rememberCircuitNavigator(backStack, ::exitApplication), + backStack = backStack, + decoration = KeyNavigationDecoration( + decoration = circuit.defaultNavDecoration, + onBackInvoked = backStack::pop, + ), + ) } } } diff --git a/cloud-run/src/jvmIntegrationTest/kotlin/io/ashdavies/cloud/ApplicationTest.kt b/cloud-run/src/jvmIntegrationTest/kotlin/io/ashdavies/cloud/ApplicationTest.kt index e4a93c9ac..a63d2c1cc 100644 --- a/cloud-run/src/jvmIntegrationTest/kotlin/io/ashdavies/cloud/ApplicationTest.kt +++ b/cloud-run/src/jvmIntegrationTest/kotlin/io/ashdavies/cloud/ApplicationTest.kt @@ -27,6 +27,9 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull +import kotlin.time.Duration.Companion.minutes + +private const val DEFAULT_LIMIT = 50 private val DefaultHttpConfig: HttpClientConfig.() -> Unit = { install(ContentNegotiation, ContentNegotiation.Config::json) @@ -53,7 +56,7 @@ internal class ApplicationTest { contentType(ContentType.Application.Json) }.body() - assertEquals(3_600_000, token.ttlMillis) + assertEquals(60.minutes.inWholeMilliseconds, token.ttlMillis) val verify = client.put("/firebase/token:verify") { header(HttpHeaders.AppCheckToken, token.token) @@ -65,7 +68,7 @@ internal class ApplicationTest { contentType(ContentType.Application.Json) }.body>() - assertEquals(50, events.size) + assertEquals(DEFAULT_LIMIT, events.size) } } diff --git a/detekt-config.yml b/detekt-config.yml index 5f453face..a96f50be2 100644 --- a/detekt-config.yml +++ b/detekt-config.yml @@ -5,7 +5,12 @@ naming: InvalidPackageDeclaration: active: false FunctionNaming: + excludes: ['**/*Test.kt'] functionPattern: '[\w]+' + MatchingDeclarationName: + excludes: + - '**/*.android.kt' + - '**/*.jvm.kt' style: MagicNumber: ignorePropertyDeclaration: true diff --git a/http-client/src/commonMain/kotlin/io/ashdavies/http/Result.kt b/http-client/src/commonMain/kotlin/io/ashdavies/http/Result.kt index 043fe2402..e6343560b 100644 --- a/http-client/src/commonMain/kotlin/io/ashdavies/http/Result.kt +++ b/http-client/src/commonMain/kotlin/io/ashdavies/http/Result.kt @@ -4,13 +4,15 @@ import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract -@PublishedApi -internal class LoadingException(val progress: Float) : Exception() - public val Result<*>.isLoading: Boolean get() = exceptionOrNull() is LoadingException -public fun Result.Companion.loading(progress: Float = 0f): Result = failure(LoadingException(progress)) +@PublishedApi +internal class LoadingException(val progress: Float) : Exception() + +public fun Result.Companion.loading(progress: Float = 0f): Result { + return failure(LoadingException(progress)) +} @OptIn(ExperimentalContracts::class) public inline fun Result.onLoading(action: (progress: Float) -> Unit): Result { diff --git a/platform-scaffold/src/androidMain/kotlin/io/ashdavies/content/PlatformContextKt.android.kt b/platform-scaffold/src/androidMain/kotlin/io/ashdavies/content/PlatformContextKt.android.kt index b8a5c1ff3..1a42fb3e6 100644 --- a/platform-scaffold/src/androidMain/kotlin/io/ashdavies/content/PlatformContextKt.android.kt +++ b/platform-scaffold/src/androidMain/kotlin/io/ashdavies/content/PlatformContextKt.android.kt @@ -2,10 +2,15 @@ package io.ashdavies.content import android.app.Activity import android.content.ContextWrapper +import android.content.pm.ApplicationInfo private val PlatformContext.activity: Activity get() = requireNotNull(findActivity()) { "Could not find activity!" } +public fun PlatformContext.isDebuggable(): Boolean { + return applicationInfo.flags != 0 and ApplicationInfo.FLAG_DEBUGGABLE +} + public actual fun PlatformContext.reportFullyDrawn() { activity.reportFullyDrawn() } diff --git a/platform-scaffold/src/androidMain/kotlin/io/ashdavies/content/StrictMode.kt b/platform-scaffold/src/androidMain/kotlin/io/ashdavies/content/StrictMode.kt new file mode 100644 index 000000000..a47cf67dc --- /dev/null +++ b/platform-scaffold/src/androidMain/kotlin/io/ashdavies/content/StrictMode.kt @@ -0,0 +1,13 @@ +package io.ashdavies.content + +import android.os.StrictMode + +public fun enableStrictMode(penaltyDeath: Boolean = false) { + val policy = StrictMode.ThreadPolicy.Builder() + .also { if (penaltyDeath) it.penaltyDeath() else it } + .detectAll() + .penaltyLog() + .build() + + StrictMode.setThreadPolicy(policy) +}