diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index e3014cce..a592d94f 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -5,14 +5,10 @@ on: branches: [ main ] jobs: - # This workflow contains a single job called "build" build: - # The type of runner that the job will run on runs-on: ubuntu-latest - # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - name: Check out code uses: actions/checkout@v4 with: @@ -30,7 +26,19 @@ jobs: - name: Setup Gradle uses: gradle/gradle-build-action@v2 - - name: Run app Checks + - name: Run test without shared tests + run: ./gradlew test -Pshared-tests-are-android-tests=false + +# - name: Run test with shared tests +# run: ./gradlew test -Pshared-tests-are-android-tests=true + + - name: Run test without shared tests + run: ./gradlew assembleAndroidTest -Pshared-tests-are-android-tests=false + +# - name: Run test with shared tests +# run: ./gradlew assembleAndroidTest -Pshared-tests-are-android-tests=true + + - name: Run check run: ./gradlew check - name: Run core Checks @@ -55,3 +63,85 @@ jobs: name: Reports path: '**/build/reports/*' retention-days: 2 + + check-libraries: + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v4 + with: + fetch-depth: '0' + + - name: Check out java + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: 17 + + - name: Copy CI gradle.properties + run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + + - name: Run core Checks + run: ./gradlew :toggles-core:check + + - name: Run flow Checks + run: ./gradlew :toggles-flow:check + + - name: Run flow noop Checks + run: ./gradlew :toggles-flow-noop:check + + - name: Run prefs Checks + run: ./gradlew :toggles-prefs:check + + - name: Run prefs noop Checks + run: ./gradlew :toggles-prefs-noop:check + + - name: Upload reports + uses: actions/upload-artifact@v4 + if: failure() + with: + name: Reports + path: '**/build/reports/*' + retention-days: 2 + + emulator-tests: + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v4 + with: + fetch-depth: '0' + + - name: Check out java + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: 17 + + - name: Copy CI gradle.properties + run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + + - name: Enable KVM group perms + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: run tests + run: ./gradlew :toggles-app:pixel2api30DebugAndroidTest + + - name: "Upload reports" + uses: actions/upload-artifact@v4 + if: failure() + with: + name: "Android test results" + path: '**/build/reports/*' + retention-days: 2 diff --git a/.gitignore b/.gitignore index d276bb19..17eb19cd 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,6 @@ captures/ **/release/*.* -build-cache \ No newline at end of file +build-cache + +.kotlin diff --git a/build-logic/conventions/build.gradle.kts b/build-logic/conventions/build.gradle.kts index ff3a239f..952804da 100644 --- a/build-logic/conventions/build.gradle.kts +++ b/build-logic/conventions/build.gradle.kts @@ -10,11 +10,13 @@ java { dependencies { // implementation("gradle.plugin.com.github.spotbugs.snom:spotbugs-gradle-plugin:4.7.2") - implementation("se.premex:ownership-gradle-plugin:0.0.11") + implementation(libs.se.premex.ownership.gradle.plugin) implementation(libs.io.gitlab.arturbosch.detekt.detekt.gradle.plugin) implementation(libs.com.android.tools.build.gradle) implementation(libs.org.jetbrains.kotlin.kotlin.gradle.plugin) - implementation("com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:1.9.10-1.0.13") + implementation(libs.com.google.devtools.ksp.com.google.devtools.ksp.gradle.plugin) + implementation(libs.com.squareup.kotlinpoet) + implementation(libs.org.jetbrains.kotlin.compose.compiler.gradle.plugin) // https://github.com/gradle/gradle/issues/15383 implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) diff --git a/build-logic/conventions/settings.gradle.kts b/build-logic/conventions/settings.gradle.kts index 427ea61f..7afd983f 100644 --- a/build-logic/conventions/settings.gradle.kts +++ b/build-logic/conventions/settings.gradle.kts @@ -1,4 +1,4 @@ -rootProject.name="conventions" +rootProject.name = "conventions" pluginManagement { repositories { @@ -23,6 +23,13 @@ dependencyResolutionManagement { } plugins { - id("com.gradle.enterprise") version "3.16.1" - id("org.gradle.toolchains.foojay-resolver-convention") version("0.7.0") + id("com.gradle.develocity") version "3.18" + id("org.gradle.toolchains.foojay-resolver-convention") version ("0.8.0") +} + +develocity { + buildScan { + termsOfUseUrl.set("https://gradle.com/help/legal-terms-of-use") + termsOfUseAgree.set("yes") + } } diff --git a/build-logic/conventions/src/main/kotlin/toggles.android.application-conventions.gradle.kts b/build-logic/conventions/src/main/kotlin/toggles.android.application-conventions.gradle.kts index aabf6ca6..ac268315 100644 --- a/build-logic/conventions/src/main/kotlin/toggles.android.application-conventions.gradle.kts +++ b/build-logic/conventions/src/main/kotlin/toggles.android.application-conventions.gradle.kts @@ -9,31 +9,19 @@ plugins { kotlin("android") id("kotlin-android") id("toggles.detekt-conventions") + id("org.jetbrains.kotlin.plugin.compose") } android { - compileSdk = 34 + compileSdk = 35 defaultConfig { minSdk = 21 - targetSdk = 34 + targetSdk = 35 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } - buildFeatures { - compose = true - } - - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() - } - - @Suppress("UnstableApiUsage") - testOptions { - unitTests.isIncludeAndroidResources = true - } - lint { baseline = file("lint-baseline.xml") checkReleaseBuilds = true @@ -41,6 +29,50 @@ android { warningsAsErrors = true lintConfig = File("../lint.xml") } + + testOptions { + animationsDisabled = true + execution = "ANDROIDX_TEST_ORCHESTRATOR" + unitTests { + isReturnDefaultValues = true + isIncludeAndroidResources = true + } + managedDevices { + devices { + maybeCreate("pixel2api30").apply { + // Use device profiles you typically see in Android Studio. + device = "Pixel 2" + // Use only API levels 30 and higher. + apiLevel = 30 + // To include Google services, use "google". + systemImageSource = "aosp" + } + } + } + } + + sourceSets { +// val sharedTestDir = "src/sharedTest/" +// if(project.file(sharedTestDir).exists()) { +// val sharedTestSourceDir = sharedTestDir + "java" +// val sharedTestResourceDir = sharedTestDir + "resources" +// val sharedIsAndroid = +// providers.gradleProperty("shared-tests-are-android-tests").get().toBoolean() +// if (sharedIsAndroid) { +// logger.lifecycle("Shared tests are androidTests") +// named("androidTest") { +// java.srcDir(sharedTestSourceDir) +// resources.srcDir(sharedTestResourceDir) +// } +// } else { +// logger.lifecycle("Shared tests are unitTests") +// named("test") { +// java.srcDir(sharedTestSourceDir) +// resources.srcDir(sharedTestResourceDir) +// } +// } +// } + } } kotlin { @@ -51,4 +83,13 @@ kotlin { dependencies { lintChecks(libs.com.slack.lint.compose.compose.lint.checks) + + testImplementation(libs.androidx.test.ext.junit) + testImplementation(libs.org.robolectric) + testImplementation(libs.androidx.test.runner) + + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.androidx.test.runner) + + androidTestUtil(libs.androidx.test.orchestrator) } diff --git a/build-logic/conventions/src/main/kotlin/toggles.android.library-conventions.gradle.kts b/build-logic/conventions/src/main/kotlin/toggles.android.library-conventions.gradle.kts index 9a2b6651..050ac20b 100644 --- a/build-logic/conventions/src/main/kotlin/toggles.android.library-conventions.gradle.kts +++ b/build-logic/conventions/src/main/kotlin/toggles.android.library-conventions.gradle.kts @@ -1,10 +1,8 @@ - plugins { id("com.android.library") kotlin("android") id("io.gitlab.arturbosch.detekt") id("toggles.detekt-library-conventions") - } android { diff --git a/build-logic/conventions/src/main/kotlin/toggles.android.module-conventions.gradle.kts b/build-logic/conventions/src/main/kotlin/toggles.android.module-conventions.gradle.kts index 7287ad55..f289258a 100644 --- a/build-logic/conventions/src/main/kotlin/toggles.android.module-conventions.gradle.kts +++ b/build-logic/conventions/src/main/kotlin/toggles.android.module-conventions.gradle.kts @@ -25,6 +25,50 @@ android { warningsAsErrors = true lintConfig = File("../../lint.xml") } + + testOptions { + animationsDisabled = true + execution = "ANDROIDX_TEST_ORCHESTRATOR" + unitTests { + isReturnDefaultValues = true + isIncludeAndroidResources = true + } + managedDevices { + devices { + maybeCreate("pixel2api30").apply { + // Use device profiles you typically see in Android Studio. + device = "Pixel 2" + // Use only API levels 30 and higher. + apiLevel = 30 + // To include Google services, use "google". + systemImageSource = "aosp" + } + } + } + } + + sourceSets { +// val sharedTestDir = "src/sharedTest/" +// if(project.file(sharedTestDir).exists()) { +// val sharedTestSourceDir = sharedTestDir + "java" +// val sharedTestResourceDir = sharedTestDir + "resources" +// val sharedIsAndroid = +// providers.gradleProperty("shared-tests-are-android-tests").get().toBoolean() +// if (sharedIsAndroid) { +// logger.lifecycle("Shared tests are androidTests") +// named("androidTest") { +// java.srcDir(sharedTestSourceDir) +// resources.srcDir(sharedTestResourceDir) +// } +// } else { +// logger.lifecycle("Shared tests are unitTests") +// named("test") { +// java.srcDir(sharedTestSourceDir) +// resources.srcDir(sharedTestResourceDir) +// } +// } +// } + } } kotlin { @@ -35,4 +79,13 @@ kotlin { dependencies { lintChecks(libs.com.slack.lint.compose.compose.lint.checks) + + testImplementation(libs.androidx.test.ext.junit) + testImplementation(libs.org.robolectric) + testImplementation(libs.androidx.test.runner) + + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.androidx.test.runner) + + androidTestUtil(libs.androidx.test.orchestrator) } \ No newline at end of file diff --git a/build-logic/conventions/src/main/kotlin/toggles.detekt-conventions.gradle.kts b/build-logic/conventions/src/main/kotlin/toggles.detekt-conventions.gradle.kts index 5671e3d9..234d4095 100644 --- a/build-logic/conventions/src/main/kotlin/toggles.detekt-conventions.gradle.kts +++ b/build-logic/conventions/src/main/kotlin/toggles.detekt-conventions.gradle.kts @@ -6,7 +6,7 @@ plugins { } dependencies { - detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.22.0") + detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.6") } detekt { diff --git a/build.gradle.kts b/build.gradle.kts index 84720ee4..6d7ace91 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,6 +18,14 @@ buildscript { } } +versionCatalogUpdate { + keep { + keepUnusedVersions = true + keepUnusedLibraries = true + keepUnusedPlugins = true + } +} + plugins { alias(libs.plugins.com.github.ben.manes.versions) alias(libs.plugins.nl.littlerobots.version.catalog.update) @@ -28,6 +36,13 @@ plugins { alias(libs.plugins.com.autonomousapps.dependency.analysis) } +develocity { + buildScan { + termsOfUseUrl.set("https://gradle.com/help/legal-terms-of-use") + termsOfUseAgree.set("yes") + } +} + fun isNonStable(version: String): Boolean { val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.uppercase(Locale.getDefault()).contains(it) @@ -44,5 +59,5 @@ tasks.named("dependencyUpdates", DependencyUpdatesTask::class.java).configure { } task("clean") { - delete(rootProject.buildDir) + delete(rootProject.layout.buildDirectory) } diff --git a/gradle.properties b/gradle.properties index 0227d7ee..9594a0f4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2g -Xms500m -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx12120m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit @@ -42,3 +42,7 @@ android.enableJetifier=false org.gradle.caching=true org.gradle.configuration-cache=true org.gradle.configuration-cache.max-problems=5 + +shared-tests-are-android-tests=false + +se-eelde-toggles-use-local-libraries=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 847daf37..6cbe9870 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,106 +1,71 @@ [versions] -agp = "8.2.1" -androidx-activity = "1.7.1" -androidx-appcompat = "1.6.1" +agp = "8.6.1" +androidx-activity = "1.9.2" +androidx-appcompat = "1.7.0" androidx-arch-core = "2.2.0" -androidx-collection = "1.1.0" -androidx-compose-bom = "2023.10.01" -androidx-compose-compiler = "1.5.8" -androidx-core = "1.10.0" -androidx-hilt = "1.0.0" -androidx-lifecycle = "2.6.1" -androidx-navigation = "2.7.0" -androidx-room = "2.6.0-alpha03" -androidx-savedstate = "1.2.1" -androidx-test = "1.5.0" -androidx-vectordrawable = "1.1.0" -com-google-dagger = "2.48" -com-squareup-leakcanary = "2.8.1" -com-squareup-moshi = "1.15.0" -dokka = "1.9.0" -espresso-core = "3.5.1" +androidx-compose-bom = "2024.09.02" +androidx-hilt = "1.2.0" +androidx-lifecycle = "2.8.6" +androidx-navigation = "2.8.1" +androidx-room = "2.6.1" +androidx-test = "1.6.1" +com-google-dagger = "2.52" +com-squareup-leakcanary = "2.14" +com-squareup-moshi = "1.15.1" +dokka = "1.9.20" +espresso-core = "3.6.1" +io-gitlab-arturbosch-detekt = "1.23.6" junit = "4.13.2" -kotlin = "1.9.22" -org-jetbrains-kotlinx = "1.6.4" +kotlin = "2.0.20" +ksp = "2.0.20-1.0.25" +orchestrator = "1.5.0" +org-jetbrains-kotlinx = "1.8.1" +se-eelde-toggles = "0.0.1" [libraries] -androidx-activity = { module = "androidx.activity:activity", version.ref = "androidx-activity" } androidx-activity-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } -androidx-activity-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" } androidx-annotation = "androidx.annotation:annotation:1.6.0" androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } -androidx-arch-core-core-common = { module = "androidx.arch.core:core-common", version.ref = "androidx-arch-core" } -androidx-arch-core-core-runtime = { module = "androidx.arch.core:core-runtime", version.ref = "androidx-arch-core" } androidx-arch-core-core-testing = { module = "androidx.arch.core:core-testing", version.ref = "androidx-arch-core" } -androidx-collection = { module = "androidx.collection:collection", version.ref = "androidx-collection" } -androidx-collection-collection-ktx = { module = "androidx.collection:collection-ktx", version.ref = "androidx-collection" } androidx-compose-animation = { module = "androidx.compose.animation:animation" } -androidx-compose-animation-animation-core = { module = "androidx.compose.animation:animation-core" } androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" } androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" } androidx-compose-foundation-foundation-layout = { module = "androidx.compose.foundation:foundation-layout" } -androidx-compose-material-material-icons-core = { module = "androidx.compose.material:material-icons-core" } androidx-compose-material-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } -androidx-compose-material-material-ripple = { module = "androidx.compose.material:material-ripple" } androidx-compose-material3 = { module = "androidx.compose.material3:material3" } androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" } -androidx-compose-runtime-runtime-saveable = { module = "androidx.compose.runtime:runtime-saveable" } androidx-compose-ui = { module = "androidx.compose.ui:ui" } -androidx-compose-ui-ui-geometry = { module = "androidx.compose.ui:ui-geometry" } -androidx-compose-ui-ui-graphics = { module = "androidx.compose.ui:ui-graphics" } androidx-compose-ui-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } androidx-compose-ui-ui-text = { module = "androidx.compose.ui:ui-text" } androidx-compose-ui-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } -androidx-compose-ui-ui-tooling-data = { module = "androidx.compose.ui:ui-tooling-data" } androidx-compose-ui-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } -androidx-compose-ui-ui-unit = { module = "androidx.compose.ui:ui-unit" } -androidx-compose-ui-ui-util = { module = "androidx.compose.ui:ui-util" } -androidx-concurrent-concurrent-futures = "androidx.concurrent:concurrent-futures:1.1.0" -androidx-core = { module = "androidx.core:core", version.ref = "androidx-core" } -androidx-core-core-ktx = "androidx.core:core-ktx:1.9.0" -androidx-cursoradapter = "androidx.cursoradapter:cursoradapter:1.0.0" -androidx-customview-customview-poolingcontainer = "androidx.customview:customview-poolingcontainer:1.0.0" -androidx-fragment = "androidx.fragment:fragment:1.5.1" +androidx-core-core-ktx = "androidx.core:core-ktx:1.13.1" +androidx-core-core-splashscreen = "androidx.core:core-splashscreen:1.0.1" androidx-hilt-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "androidx-hilt" } -androidx-hilt-hilt-navigation = { module = "androidx.hilt:hilt-navigation", version.ref = "androidx-hilt" } androidx-hilt-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidx-hilt" } -androidx-interpolator = "androidx.interpolator:interpolator:1.0.0" -androidx-lifecycle-lifecycle-common = { module = "androidx.lifecycle:lifecycle-common", version.ref = "androidx-lifecycle" } androidx-lifecycle-lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "androidx-lifecycle" } -androidx-lifecycle-lifecycle-runtime-compose = "androidx.lifecycle:lifecycle-runtime-compose:2.6.1" -androidx-lifecycle-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "androidx-lifecycle" } +androidx-lifecycle-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } androidx-lifecycle-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" } -androidx-lifecycle-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidx-lifecycle" } androidx-lifecycle-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } androidx-lifecycle-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" } -androidx-lifecycle-lifecycle-viewmodel-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "androidx-lifecycle" } -androidx-loader = "androidx.loader:loader:1.0.0" -androidx-navigation-navigation-common = { module = "androidx.navigation:navigation-common", version.ref = "androidx-navigation" } -androidx-navigation-navigation-common-ktx = { module = "androidx.navigation:navigation-common-ktx", version.ref = "androidx-navigation" } androidx-navigation-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } -androidx-navigation-navigation-runtime = { module = "androidx.navigation:navigation-runtime", version.ref = "androidx-navigation" } androidx-navigation-navigation-runtime-ktx = { module = "androidx.navigation:navigation-runtime-ktx", version.ref = "androidx-navigation" } androidx-room-room-compiler = { module = "androidx.room:room-compiler", version.ref = "androidx-room" } androidx-room-room-ktx = { module = "androidx.room:room-ktx", version.ref = "androidx-room" } androidx-room-room-runtime = { module = "androidx.room:room-runtime", version.ref = "androidx-room" } androidx-room-room-testing = { module = "androidx.room:room-testing", version.ref = "androidx-room" } -androidx-savedstate = { module = "androidx.savedstate:savedstate", version.ref = "androidx-savedstate" } -androidx-startup-startup-runtime = "androidx.startup:startup-runtime:1.1.1" -androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test" } +androidx-startup-startup-runtime = "androidx.startup:startup-runtime:1.2.0" androidx-test-core-ktx = { module = "androidx.test:core-ktx", version.ref = "androidx-test" } androidx-test-espresso-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso-core" } -androidx-test-ext-junit = "androidx.test.ext:junit:1.1.5" -androidx-test-ext-truth = "androidx.test.ext:truth:1.5.0" +androidx-test-ext-junit = "androidx.test.ext:junit:1.2.1" +androidx-test-ext-truth = "androidx.test.ext:truth:1.6.0" +androidx-test-orchestrator = { module = "androidx.test:orchestrator", version.ref = "orchestrator" } androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test" } -androidx-test-runner = "androidx.test:runner:1.5.2" -androidx-tracing = "androidx.tracing:tracing:1.0.0" -androidx-vectordrawable = { module = "androidx.vectordrawable:vectordrawable", version.ref = "androidx-vectordrawable" } -androidx-vectordrawable-vectordrawable-animated = { module = "androidx.vectordrawable:vectordrawable-animated", version.ref = "androidx-vectordrawable" } -androidx-versionedparcelable = "androidx.versionedparcelable:versionedparcelable:1.1.1" -androidx-viewpager = "androidx.viewpager:viewpager:1.0.0" -app-cash-licensee-licensee-gradle-plugin = "app.cash.licensee:licensee-gradle-plugin:1.8.0" -com-android-tools-build-gradle = "com.android.tools.build:gradle:8.2.1" -com-google-code-findbugs-jsr305 = "com.google.code.findbugs:jsr305:3.0.2" +androidx-test-runner = "androidx.test:runner:1.6.2" +app-cash-licensee-licensee-gradle-plugin = "app.cash.licensee:licensee-gradle-plugin:1.11.0" +app-cash-turbine = "app.cash.turbine:turbine:1.1.0" +co-touchlab-kermit = "co.touchlab:kermit:2.0.4" +com-android-tools-build-gradle = "com.android.tools.build:gradle:8.6.1" com-google-dagger = { module = "com.google.dagger:dagger", version.ref = "com-google-dagger" } com-google-dagger-dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "com-google-dagger" } com-google-dagger-hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "com-google-dagger" } @@ -108,43 +73,46 @@ com-google-dagger-hilt-android-compiler = { module = "com.google.dagger:hilt-and com-google-dagger-hilt-android-gradle-plugin = { module = "com.google.dagger:hilt-android-gradle-plugin", version.ref = "com-google-dagger" } com-google-dagger-hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "com-google-dagger" } com-google-dagger-hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "com-google-dagger" } -com-google-firebase-firebase-crashlytics-gradle = "com.google.firebase:firebase-crashlytics-gradle:2.9.8" -com-google-gms-google-services = "com.google.gms:google-services:4.3.15" -com-slack-lint-compose-compose-lint-checks = "com.slack.lint.compose:compose-lint-checks:1.2.0" +com-google-devtools-ksp-com-google-devtools-ksp-gradle-plugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" } +com-google-gms-google-services = "com.google.gms:google-services:4.4.2" +com-slack-lint-compose-compose-lint-checks = "com.slack.lint.compose:compose-lint-checks:1.3.1" +com-squareup-kotlinpoet = "com.squareup:kotlinpoet:1.16.0" com-squareup-leakcanary-leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "com-squareup-leakcanary" } com-squareup-moshi = { module = "com.squareup.moshi:moshi", version.ref = "com-squareup-moshi" } com-squareup-moshi-moshi-kotlin-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "com-squareup-moshi" } -com-squareup-okio = "com.squareup.okio:okio:3.5.0" -espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso-core" } -io-gitlab-arturbosch-detekt-detekt-formatting = "io.gitlab.arturbosch.detekt:detekt-formatting:1.23.1" -io-gitlab-arturbosch-detekt-detekt-gradle-plugin = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.1" -io-gitlab-arturbosch-detekt-detekt-rules-libraries = "io.gitlab.arturbosch.detekt:detekt-rules-libraries:1.23.1" +com-squareup-okio = "com.squareup.okio:okio:3.9.1" +io-gitlab-arturbosch-detekt-detekt-formatting = "io.gitlab.arturbosch.detekt:detekt-formatting:1.23.7" +io-gitlab-arturbosch-detekt-detekt-gradle-plugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "io-gitlab-arturbosch-detekt" } +io-gitlab-arturbosch-detekt-detekt-rules-libraries = { module = "io.gitlab.arturbosch.detekt:detekt-rules-libraries", version.ref = "io-gitlab-arturbosch-detekt" } junit = { module = "junit:junit", version.ref = "junit" } org-jetbrains-dokka-dokka-gradle-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } +org-jetbrains-kotlin-compose-compiler-gradle-plugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" } org-jetbrains-kotlin-kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } -org-jetbrains-kotlin-kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } -org-jetbrains-kotlin-kotlin-stdlib-jdk8 = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0" -org-jetbrains-kotlinx-kotlinx-collections-immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5" +org-jetbrains-kotlinx-kotlinx-collections-immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.8" org-jetbrains-kotlinx-kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" } org-jetbrains-kotlinx-kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "org-jetbrains-kotlinx" } -org-robolectric = "org.robolectric:robolectric:4.10.3" -se-eelde-toggles-toggles-core = { module = "se.eelde.toggles:toggles-core" } -se-eelde-toggles-toggles-flow = { module = "se.eelde.toggles:toggles-flow" } -se-eelde-toggles-toggles-prefs = { module = "se.eelde.toggles:toggles-prefs" } -#se-eelde-toggles-toggles-core = "se.eelde.toggles:toggles-core:0.0.2" -#se-eelde-toggles-toggles-flow = "se.eelde.toggles:toggles-flow:0.0.1" -#se-eelde-toggles-toggles-prefs = "se.eelde.toggles:toggles-prefs:0.0.1" +org-jetbrains-kotlinx-kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test" } +org-jetbrains-kotlinx-kotlinx-serialization-json = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3" +org-robolectric = "org.robolectric:robolectric:4.13" +#se-eelde-toggles-toggles-core = { module = "se.eelde.toggles:toggles-core" } +#se-eelde-toggles-toggles-flow = { module = "se.eelde.toggles:toggles-flow" } +#se-eelde-toggles-toggles-prefs = { module = "se.eelde.toggles:toggles-prefs" } +se-eelde-toggles-toggles-core = "se.eelde.toggles:toggles-core:0.0.2" +se-eelde-toggles-toggles-flow = { module = "se.eelde.toggles:toggles-flow", version.ref = "se-eelde-toggles" } +se-eelde-toggles-toggles-prefs = { module = "se.eelde.toggles:toggles-prefs", version.ref = "se-eelde-toggles" } +se-premex-ownership-gradle-plugin = "se.premex:ownership-gradle-plugin:0.0.11" + [plugins] com-android-library = { id = "com.android.library", version.ref = "agp" } -com-autonomousapps-dependency-analysis = "com.autonomousapps.dependency-analysis:1.28.0" -com-github-ben-manes-versions = "com.github.ben-manes.versions:0.50.0" -com-github-triplet-play = "com.github.triplet.play:3.8.4" +com-autonomousapps-dependency-analysis = "com.autonomousapps.dependency-analysis:2.0.2" +com-github-ben-manes-versions = "com.github.ben-manes.versions:0.51.0" +com-github-triplet-play = "com.github.triplet.play:3.11.0" com-google-dagger-hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "com-google-dagger" } -com-google-devtools-ksp = "com.google.devtools.ksp:1.9.22-1.0.16" -com-google-firebase-crashlytics = "com.google.firebase.crashlytics:2.9.8" -com-google-gms-google-services = "com.google.gms.google-services:4.3.15" +com-google-devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +com-google-gms-google-services = "com.google.gms.google-services:4.4.2" com-vanniktech-maven-publish = "com.vanniktech.maven.publish:0.27.0" -nl-littlerobots-version-catalog-update = "nl.littlerobots.version-catalog-update:0.8.3" +nl-littlerobots-version-catalog-update = "nl.littlerobots.version-catalog-update:0.8.4" org-jetbrains-dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } -org-jetbrains-kotlinx-binary-compatibility-validator = "org.jetbrains.kotlinx.binary-compatibility-validator:0.13.2" -se-premex-gross = "se.premex.gross:0.2.0" +org-jetbrains-kotlin-plugin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +org-jetbrains-kotlinx-binary-compatibility-validator = "org.jetbrains.kotlinx.binary-compatibility-validator:0.16.3" +se-premex-gross = "se.premex.gross:0.4.2" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1af9e093..9355b415 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/modules/applications/build.gradle.kts b/modules/applications/build.gradle.kts index 583950ac..f53e6f09 100644 --- a/modules/applications/build.gradle.kts +++ b/modules/applications/build.gradle.kts @@ -2,16 +2,11 @@ plugins { id("toggles.android.module-conventions") id("toggles.ownership-conventions") alias(libs.plugins.com.google.devtools.ksp) + id("org.jetbrains.kotlin.plugin.compose") } android { namespace = "se.eelde.toggles.applications" - buildFeatures { - compose = true - } - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() - } } dependencies { @@ -20,6 +15,7 @@ dependencies { implementation(projects.modules.database) implementation(libs.androidx.core.core.ktx) implementation(libs.androidx.appcompat) + implementation(projects.modules.routes) implementation(libs.androidx.navigation.navigation.compose) implementation(libs.androidx.hilt.hilt.navigation.compose) implementation(libs.androidx.compose.runtime) diff --git a/modules/applications/src/main/java/se/eelde/toggles/applications/ApplicationEntry.kt b/modules/applications/src/main/java/se/eelde/toggles/applications/ApplicationEntry.kt index 3b705497..7641a835 100644 --- a/modules/applications/src/main/java/se/eelde/toggles/applications/ApplicationEntry.kt +++ b/modules/applications/src/main/java/se/eelde/toggles/applications/ApplicationEntry.kt @@ -37,6 +37,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import kotlinx.coroutines.launch +import se.eelde.toggles.routes.Applications @OptIn(ExperimentalMaterial3Api::class) fun NavGraphBuilder.applicationNavigations( @@ -45,7 +46,7 @@ fun NavGraphBuilder.applicationNavigations( navigateToOss: () -> Unit, navigateToHelp: () -> Unit, ) { - composable("applications") { + composable { val viewModel = hiltViewModel() val viewState = viewModel.state.collectAsState() diff --git a/modules/applications/src/main/java/se/eelde/toggles/applications/ApplicationViewModel.kt b/modules/applications/src/main/java/se/eelde/toggles/applications/ApplicationViewModel.kt index 8d0df56b..86d1d606 100644 --- a/modules/applications/src/main/java/se/eelde/toggles/applications/ApplicationViewModel.kt +++ b/modules/applications/src/main/java/se/eelde/toggles/applications/ApplicationViewModel.kt @@ -5,10 +5,9 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import se.eelde.toggles.database.WrenchApplication -import se.eelde.toggles.database.WrenchApplicationDao +import se.eelde.toggles.database.dao.application.TogglesApplicationDao import javax.inject.Inject internal data class ViewState( @@ -22,7 +21,7 @@ internal sealed class PartialViewState { } @HiltViewModel -internal class ApplicationViewModel @Inject constructor(applicationDao: WrenchApplicationDao) : +internal class ApplicationViewModel @Inject constructor(applicationDao: TogglesApplicationDao) : ViewModel() { private val _state = MutableStateFlow(reduce(ViewState(), PartialViewState.Empty)) diff --git a/modules/applications/src/sharedTest/java/se/eelde/toggles/applications/ExampleSharedTest.kt b/modules/applications/src/sharedTest/java/se/eelde/toggles/applications/ExampleSharedTest.kt new file mode 100644 index 00000000..027c9e74 --- /dev/null +++ b/modules/applications/src/sharedTest/java/se/eelde/toggles/applications/ExampleSharedTest.kt @@ -0,0 +1,26 @@ +package se.eelde.toggles.applications + +import android.app.Application +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * ./gradlew :modules:applications:pixel2api30DebugAndroidTest -Pshared-tests-are-android-tests=false + * ./gradlew :modules:applications:pixel2api30DebugAndroidTest -Pshared-tests-are-android-tests=true + */ +@RunWith(AndroidJUnit4::class) +class ExampleSharedTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } + + @Test + fun useAppContext() { + val context = ApplicationProvider.getApplicationContext() + assertEquals("se.eelde.toggles.applications.test", context.packageName) + } +} diff --git a/modules/applications/src/sharedTest/resources/robolectric.properties b/modules/applications/src/sharedTest/resources/robolectric.properties new file mode 100644 index 00000000..37420983 --- /dev/null +++ b/modules/applications/src/sharedTest/resources/robolectric.properties @@ -0,0 +1 @@ +sdk=33 \ No newline at end of file diff --git a/modules/booleanconfiguration/build.gradle.kts b/modules/booleanconfiguration/build.gradle.kts index 72d059a6..fac5cf1d 100644 --- a/modules/booleanconfiguration/build.gradle.kts +++ b/modules/booleanconfiguration/build.gradle.kts @@ -2,16 +2,11 @@ plugins { id("toggles.android.module-conventions") id("toggles.ownership-conventions") alias(libs.plugins.com.google.devtools.ksp) + id("org.jetbrains.kotlin.plugin.compose") } android { namespace = "se.eelde.toggles.booleanconfiguration" - buildFeatures { - compose = true - } - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() - } } dependencies { @@ -19,6 +14,7 @@ dependencies { implementation(projects.modules.composeTheme) implementation(projects.modules.database) implementation(projects.modules.provider) + implementation(projects.modules.routes) implementation(libs.androidx.core.core.ktx) implementation(libs.androidx.appcompat) implementation(libs.androidx.navigation.navigation.compose) diff --git a/modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration/BooleanValueView.kt b/modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration/BooleanValueView.kt index a94a76e9..9b4972e0 100644 --- a/modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration/BooleanValueView.kt +++ b/modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration/BooleanValueView.kt @@ -1,5 +1,6 @@ package se.eelde.toggles.booleanconfiguration +import android.annotation.SuppressLint import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -26,22 +27,52 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.launch +import se.eelde.toggles.routes.BooleanConfiguration -@OptIn(ExperimentalMaterial3Api::class) +@SuppressLint("ComposeViewModelInjection") @Composable fun BooleanValueView( - modifier: Modifier = Modifier, - viewModel: FragmentBooleanValueViewModel = hiltViewModel(), + booleanConfiguration: BooleanConfiguration, back: () -> Unit, ) { + val viewModel: BooleanValueViewModel = + hiltViewModel( + creationCallback = { factory -> + factory.create(booleanConfiguration) + } + ) + val viewState by viewModel.state.collectAsStateWithLifecycle() + + val scope = rememberCoroutineScope() + + BooleanValueView( + viewState = viewState, + save = { scope.launch { viewModel.saveClick() } }, + revert = { scope.launch { viewModel.revertClick() } }, + checkedChanged = { viewModel.checkedChanged(it) }, + popBackStack = back, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +@Suppress("LongParameterList", "LongMethod") +fun BooleanValueView( + viewState: ViewState, + save: () -> Unit, + revert: () -> Unit, + checkedChanged: (Boolean) -> Unit, + popBackStack: () -> Unit, + modifier: Modifier = Modifier, +) { Scaffold( topBar = { TopAppBar( title = { Text("Boolean configuration") }, navigationIcon = { - IconButton(onClick = { back() }) { + IconButton(onClick = { popBackStack() }) { Icon( imageVector = Icons.Filled.ArrowBack, contentDescription = null @@ -51,61 +82,46 @@ fun BooleanValueView( ) }, ) { paddingValues -> - BooleanValueView( - uiState = viewState, - popBackStack = { back() }, - revert = { viewModel.revertClick() }, - save = { viewModel.saveClick() }, - setBooleanValue = { viewModel.checkedChanged(it) }, - modifier = modifier.padding(paddingValues), - ) - } -} + val scope = rememberCoroutineScope() + Surface( + modifier = modifier + .padding(paddingValues) + .padding(16.dp) + ) { + Column { + Text( + modifier = Modifier.padding(8.dp), + style = MaterialTheme.typography.headlineMedium, + text = viewState.title ?: "" + ) -@Composable -@Suppress("LongParameterList") -internal fun BooleanValueView( - uiState: ViewState, - popBackStack: () -> Unit, - setBooleanValue: (Boolean) -> Unit, - revert: suspend () -> Unit, - save: suspend () -> Unit, - modifier: Modifier = Modifier, -) { - val scope = rememberCoroutineScope() - - Surface(modifier = modifier.padding(16.dp)) { - Column { - Text( - modifier = Modifier.padding(8.dp), - style = MaterialTheme.typography.headlineMedium, - text = uiState.title ?: "" - ) - - Switch( - modifier = Modifier - .padding(8.dp) - .align(End), - checked = uiState.checked ?: false, - onCheckedChange = { setBooleanValue(it) } - ) - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { - Button(modifier = Modifier.padding(8.dp), onClick = { - scope.launch { - revert() - popBackStack() + Switch( + modifier = Modifier + .padding(8.dp) + .align(End), + checked = viewState.checked ?: false, + onCheckedChange = { + checkedChanged(it) + } + ) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + Button(modifier = Modifier.padding(8.dp), onClick = { + scope.launch { + revert() + popBackStack() + } + }) { + Text("Revert") } - }) { - Text("Revert") - } - Button(modifier = Modifier.padding(8.dp), onClick = { - scope.launch { - save() - popBackStack() + Button(modifier = Modifier.padding(8.dp), onClick = { + scope.launch { + save() + popBackStack() + } + }) { + Text("Save") } - }) { - Text("Save") } } } diff --git a/modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration/BooleanValueViewModel.kt b/modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration/BooleanValueViewModel.kt index b3ee2a74..a3e2a9e3 100644 --- a/modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration/BooleanValueViewModel.kt +++ b/modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration/BooleanValueViewModel.kt @@ -1,9 +1,11 @@ package se.eelde.toggles.booleanconfiguration import android.app.Application -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -12,12 +14,12 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import se.eelde.toggles.core.TogglesProviderContract -import se.eelde.toggles.database.WrenchConfigurationDao import se.eelde.toggles.database.WrenchConfigurationValue -import se.eelde.toggles.database.WrenchConfigurationValueDao +import se.eelde.toggles.database.dao.application.TogglesConfigurationDao +import se.eelde.toggles.database.dao.application.TogglesConfigurationValueDao import se.eelde.toggles.provider.notifyUpdate +import se.eelde.toggles.routes.BooleanConfiguration import java.util.Date -import javax.inject.Inject data class ViewState( val title: String? = null, @@ -34,22 +36,29 @@ private sealed class PartialViewState { object Reverting : PartialViewState() } -@HiltViewModel -class FragmentBooleanValueViewModel @Inject internal constructor( - savedStateHandle: SavedStateHandle, +@HiltViewModel(assistedFactory = BooleanValueViewModel.Factory::class) +class BooleanValueViewModel @AssistedInject internal constructor( private val application: Application, - private val configurationDao: WrenchConfigurationDao, - private val configurationValueDao: WrenchConfigurationValueDao + private val configurationDao: TogglesConfigurationDao, + private val configurationValueDao: TogglesConfigurationValueDao, + @Assisted booleanConfiguration: BooleanConfiguration ) : ViewModel() { + @AssistedFactory + interface Factory { + fun create( + booleanConfiguration: BooleanConfiguration + ): BooleanValueViewModel + } + + private val configurationId = booleanConfiguration.configurationId + private val scopeId = booleanConfiguration.scopeId + private val _state = MutableStateFlow(reduce(ViewState(), PartialViewState.Empty)) val state: StateFlow get() = _state - private val configurationId: Long = savedStateHandle.get("configurationId")!! - private val scopeId: Long = savedStateHandle.get("scopeId")!! - private var selectedConfigurationValue: WrenchConfigurationValue? = null init { diff --git a/modules/compose-theme/build.gradle.kts b/modules/compose-theme/build.gradle.kts index 60e589b2..ea537764 100644 --- a/modules/compose-theme/build.gradle.kts +++ b/modules/compose-theme/build.gradle.kts @@ -1,16 +1,11 @@ plugins { id("toggles.android.module-conventions") id("toggles.ownership-conventions") + id("org.jetbrains.kotlin.plugin.compose") } android { namespace = "se.eelde.toggles.composetheme" - buildFeatures { - compose = true - } - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() - } } dependencies { implementation(platform(libs.androidx.compose.bom)) diff --git a/modules/compose-theme/src/main/java/se/eelde/toggles/composetheme/Color.kt b/modules/compose-theme/src/main/java/se/eelde/toggles/composetheme/Color.kt new file mode 100644 index 00000000..960b4674 --- /dev/null +++ b/modules/compose-theme/src/main/java/se/eelde/toggles/composetheme/Color.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:Suppress("MagicNumber") + +package se.eelde.toggles.composetheme + +import androidx.compose.ui.graphics.Color + +/** + * Now in Android colors. + */ +internal val Blue10 = Color(0xFF001F28) +internal val Blue20 = Color(0xFF003544) +internal val Blue30 = Color(0xFF004D61) +internal val Blue40 = Color(0xFF006780) +internal val Blue80 = Color(0xFF5DD5FC) +internal val Blue90 = Color(0xFFB8EAFF) +internal val DarkGreen10 = Color(0xFF0D1F12) +internal val DarkGreen20 = Color(0xFF223526) +internal val DarkGreen30 = Color(0xFF394B3C) +internal val DarkGreen40 = Color(0xFF4F6352) +internal val DarkGreen80 = Color(0xFFB7CCB8) +internal val DarkGreen90 = Color(0xFFD3E8D3) +internal val DarkGreenGray10 = Color(0xFF1A1C1A) +internal val DarkGreenGray20 = Color(0xFF2F312E) +internal val DarkGreenGray90 = Color(0xFFE2E3DE) +internal val DarkGreenGray95 = Color(0xFFF0F1EC) +internal val DarkGreenGray99 = Color(0xFFFBFDF7) +internal val DarkPurpleGray10 = Color(0xFF201A1B) +internal val DarkPurpleGray20 = Color(0xFF362F30) +internal val DarkPurpleGray90 = Color(0xFFECDFE0) +internal val DarkPurpleGray95 = Color(0xFFFAEEEF) +internal val DarkPurpleGray99 = Color(0xFFFCFCFC) +internal val Green10 = Color(0xFF00210B) +internal val Green20 = Color(0xFF003919) +internal val Green30 = Color(0xFF005227) +internal val Green40 = Color(0xFF006D36) +internal val Green80 = Color(0xFF0EE37C) +internal val Green90 = Color(0xFF5AFF9D) +internal val GreenGray30 = Color(0xFF414941) +internal val GreenGray50 = Color(0xFF727971) +internal val GreenGray60 = Color(0xFF8B938A) +internal val GreenGray80 = Color(0xFFC1C9BF) +internal val GreenGray90 = Color(0xFFDDE5DB) +internal val Orange10 = Color(0xFF380D00) +internal val Orange20 = Color(0xFF5B1A00) +internal val Orange30 = Color(0xFF812800) +internal val Orange40 = Color(0xFFA23F16) +internal val Orange80 = Color(0xFFFFB59B) +internal val Orange90 = Color(0xFFFFDBCF) +internal val Purple10 = Color(0xFF36003C) +internal val Purple20 = Color(0xFF560A5D) +internal val Purple30 = Color(0xFF702776) +internal val Purple40 = Color(0xFF8B418F) +internal val Purple80 = Color(0xFFFFA9FE) +internal val Purple90 = Color(0xFFFFD6FA) +internal val PurpleGray30 = Color(0xFF4D444C) +internal val PurpleGray50 = Color(0xFF7F747C) +internal val PurpleGray60 = Color(0xFF998D96) +internal val PurpleGray80 = Color(0xFFD0C3CC) +internal val PurpleGray90 = Color(0xFFEDDEE8) +internal val Red10 = Color(0xFF410002) +internal val Red20 = Color(0xFF690005) +internal val Red30 = Color(0xFF93000A) +internal val Red40 = Color(0xFFBA1A1A) +internal val Red80 = Color(0xFFFFB4AB) +internal val Red90 = Color(0xFFFFDAD6) +internal val Teal10 = Color(0xFF001F26) +internal val Teal20 = Color(0xFF02363F) +internal val Teal30 = Color(0xFF214D56) +internal val Teal40 = Color(0xFF3A656F) +internal val Teal80 = Color(0xFFA2CED9) +internal val Teal90 = Color(0xFFBEEAF6) diff --git a/modules/compose-theme/src/main/java/se/eelde/toggles/composetheme/Theme.kt b/modules/compose-theme/src/main/java/se/eelde/toggles/composetheme/Theme.kt index 207ab0f1..1778c490 100644 --- a/modules/compose-theme/src/main/java/se/eelde/toggles/composetheme/Theme.kt +++ b/modules/compose-theme/src/main/java/se/eelde/toggles/composetheme/Theme.kt @@ -1,69 +1,162 @@ package se.eelde.toggles.composetheme -import android.app.Activity import android.os.Build +import androidx.annotation.ChecksSdkIntAtLeast +import androidx.annotation.VisibleForTesting import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Typography import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp -import androidx.core.view.ViewCompat -private val DarkColorScheme = darkColorScheme( - primary = Color(color = 0xFF7673C5), - secondary = Color(color = 0xFF07BD46), - tertiary = Color(color = 0xFF07BD46) +/** + * Light default theme color scheme + */ +@VisibleForTesting +val LightDefaultColorScheme = lightColorScheme( + primary = Purple40, + onPrimary = Color.White, + primaryContainer = Purple90, + onPrimaryContainer = Purple10, + secondary = Orange40, + onSecondary = Color.White, + secondaryContainer = Orange90, + onSecondaryContainer = Orange10, + tertiary = Blue40, + onTertiary = Color.White, + tertiaryContainer = Blue90, + onTertiaryContainer = Blue10, + error = Red40, + onError = Color.White, + errorContainer = Red90, + onErrorContainer = Red10, + background = DarkPurpleGray99, + onBackground = DarkPurpleGray10, + surface = DarkPurpleGray99, + onSurface = DarkPurpleGray10, + surfaceVariant = PurpleGray90, + onSurfaceVariant = PurpleGray30, + inverseSurface = DarkPurpleGray20, + inverseOnSurface = DarkPurpleGray95, + outline = PurpleGray50, ) -private val LightColorScheme = lightColorScheme( - primary = Color(color = 0xFF1E1A87), - secondary = Color(color = 0xff287F46), - tertiary = Color(color = 0xff287F46) +/** + * Dark default theme color scheme + */ +@VisibleForTesting +val DarkDefaultColorScheme = darkColorScheme( + primary = Purple80, + onPrimary = Purple20, + primaryContainer = Purple30, + onPrimaryContainer = Purple90, + secondary = Orange80, + onSecondary = Orange20, + secondaryContainer = Orange30, + onSecondaryContainer = Orange90, + tertiary = Blue80, + onTertiary = Blue20, + tertiaryContainer = Blue30, + onTertiaryContainer = Blue90, + error = Red80, + onError = Red20, + errorContainer = Red30, + onErrorContainer = Red90, + background = DarkPurpleGray10, + onBackground = DarkPurpleGray90, + surface = DarkPurpleGray10, + onSurface = DarkPurpleGray90, + surfaceVariant = PurpleGray30, + onSurfaceVariant = PurpleGray80, + inverseSurface = DarkPurpleGray90, + inverseOnSurface = DarkPurpleGray10, + outline = PurpleGray60, ) -val Typography = Typography( - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ) +/** + * Light Android theme color scheme + */ +@VisibleForTesting +val LightAndroidColorScheme = lightColorScheme( + primary = Green40, + onPrimary = Color.White, + primaryContainer = Green90, + onPrimaryContainer = Green10, + secondary = DarkGreen40, + onSecondary = Color.White, + secondaryContainer = DarkGreen90, + onSecondaryContainer = DarkGreen10, + tertiary = Teal40, + onTertiary = Color.White, + tertiaryContainer = Teal90, + onTertiaryContainer = Teal10, + error = Red40, + onError = Color.White, + errorContainer = Red90, + onErrorContainer = Red10, + background = DarkGreenGray99, + onBackground = DarkGreenGray10, + surface = DarkGreenGray99, + onSurface = DarkGreenGray10, + surfaceVariant = GreenGray90, + onSurfaceVariant = GreenGray30, + inverseSurface = DarkGreenGray20, + inverseOnSurface = DarkGreenGray95, + outline = GreenGray50, +) + +/** + * Dark Android theme color scheme + */ +@VisibleForTesting +val DarkAndroidColorScheme = darkColorScheme( + primary = Green80, + onPrimary = Green20, + primaryContainer = Green30, + onPrimaryContainer = Green90, + secondary = DarkGreen80, + onSecondary = DarkGreen20, + secondaryContainer = DarkGreen30, + onSecondaryContainer = DarkGreen90, + tertiary = Teal80, + onTertiary = Teal20, + tertiaryContainer = Teal30, + onTertiaryContainer = Teal90, + error = Red80, + onError = Red20, + errorContainer = Red30, + onErrorContainer = Red90, + background = DarkGreenGray10, + onBackground = DarkGreenGray90, + surface = DarkGreenGray10, + onSurface = DarkGreenGray90, + surfaceVariant = GreenGray30, + onSurfaceVariant = GreenGray80, + inverseSurface = DarkGreenGray90, + inverseOnSurface = DarkGreenGray10, + outline = GreenGray60, ) @Composable fun TogglesTheme( darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, - content: @Composable () -> Unit + androidTheme: Boolean = false, + disableDynamicTheming: Boolean = true, + content: @Composable () -> Unit, ) { + // Color scheme val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + androidTheme -> if (darkTheme) DarkAndroidColorScheme else LightAndroidColorScheme + !disableDynamicTheming && supportsDynamicTheming() -> { val context = LocalContext.current if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) } - darkTheme -> DarkColorScheme - else -> LightColorScheme - } - val view = LocalView.current - if (!view.isInEditMode) { - SideEffect { - (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb() - ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme - } + + else -> if (darkTheme) DarkDefaultColorScheme else LightDefaultColorScheme } MaterialTheme( @@ -72,3 +165,6 @@ fun TogglesTheme( content = content ) } + +@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S) +fun supportsDynamicTheming() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S diff --git a/modules/compose-theme/src/main/java/se/eelde/toggles/composetheme/Type.kt b/modules/compose-theme/src/main/java/se/eelde/toggles/composetheme/Type.kt new file mode 100644 index 00000000..69e8756e --- /dev/null +++ b/modules/compose-theme/src/main/java/se/eelde/toggles/composetheme/Type.kt @@ -0,0 +1,146 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package se.eelde.toggles.composetheme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.LineHeightStyle +import androidx.compose.ui.text.style.LineHeightStyle.Alignment +import androidx.compose.ui.text.style.LineHeightStyle.Trim +import androidx.compose.ui.unit.sp + +internal val Typography = Typography( + displayLarge = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 57.sp, + lineHeight = 64.sp, + letterSpacing = (-0.25).sp, + ), + displayMedium = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 45.sp, + lineHeight = 52.sp, + letterSpacing = 0.sp, + ), + displaySmall = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 36.sp, + lineHeight = 44.sp, + letterSpacing = 0.sp, + ), + headlineLarge = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 32.sp, + lineHeight = 40.sp, + letterSpacing = 0.sp, + ), + headlineMedium = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 28.sp, + lineHeight = 36.sp, + letterSpacing = 0.sp, + ), + headlineSmall = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 24.sp, + lineHeight = 32.sp, + letterSpacing = 0.sp, + lineHeightStyle = LineHeightStyle( + alignment = Alignment.Bottom, + trim = Trim.None, + ), + ), + titleLarge = TextStyle( + fontWeight = FontWeight.Bold, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp, + lineHeightStyle = LineHeightStyle( + alignment = Alignment.Bottom, + trim = Trim.LastLineBottom, + ), + ), + titleMedium = TextStyle( + fontWeight = FontWeight.Bold, + fontSize = 18.sp, + lineHeight = 24.sp, + letterSpacing = 0.1.sp, + ), + titleSmall = TextStyle( + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + ), + // Default text style + bodyLarge = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp, + lineHeightStyle = LineHeightStyle( + alignment = Alignment.Center, + trim = Trim.None, + ), + ), + bodyMedium = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.25.sp, + ), + bodySmall = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.4.sp, + ), + // Used for Button + labelLarge = TextStyle( + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + lineHeightStyle = LineHeightStyle( + alignment = Alignment.Center, + trim = Trim.LastLineBottom, + ), + ), + // Used for Navigation items + labelMedium = TextStyle( + fontWeight = FontWeight.Medium, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp, + lineHeightStyle = LineHeightStyle( + alignment = Alignment.Center, + trim = Trim.LastLineBottom, + ), + ), + // Used for Tag + labelSmall = TextStyle( + fontWeight = FontWeight.Medium, + fontSize = 10.sp, + lineHeight = 14.sp, + letterSpacing = 0.sp, + lineHeightStyle = LineHeightStyle( + alignment = Alignment.Center, + trim = Trim.LastLineBottom, + ), + ), +) diff --git a/modules/configurations/build.gradle.kts b/modules/configurations/build.gradle.kts index a9541d7f..d78c6596 100644 --- a/modules/configurations/build.gradle.kts +++ b/modules/configurations/build.gradle.kts @@ -2,16 +2,11 @@ plugins { id("toggles.android.module-conventions") id("toggles.ownership-conventions") alias(libs.plugins.com.google.devtools.ksp) + id("org.jetbrains.kotlin.plugin.compose") } android { namespace = "se.eelde.toggles.configurations" - buildFeatures { - compose = true - } - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() - } } dependencies { @@ -22,6 +17,7 @@ dependencies { implementation(libs.androidx.core.core.ktx) implementation(libs.androidx.lifecycle.lifecycle.runtime.compose) implementation(libs.androidx.appcompat) + implementation(projects.modules.routes) implementation(libs.androidx.navigation.navigation.compose) implementation(libs.androidx.hilt.hilt.navigation.compose) implementation(libs.androidx.compose.runtime) diff --git a/modules/configurations/src/main/java/se/eelde/toggles/configurations/ConfigurationViewModel.kt b/modules/configurations/src/main/java/se/eelde/toggles/configurations/ConfigurationViewModel.kt index 97ca2f51..4308ff13 100644 --- a/modules/configurations/src/main/java/se/eelde/toggles/configurations/ConfigurationViewModel.kt +++ b/modules/configurations/src/main/java/se/eelde/toggles/configurations/ConfigurationViewModel.kt @@ -7,6 +7,9 @@ import android.text.TextUtils import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -18,12 +21,11 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import se.eelde.toggles.database.WrenchApplication -import se.eelde.toggles.database.WrenchApplicationDao -import se.eelde.toggles.database.WrenchConfigurationDao import se.eelde.toggles.database.WrenchConfigurationWithValues import se.eelde.toggles.database.WrenchScope -import se.eelde.toggles.database.WrenchScopeDao -import javax.inject.Inject +import se.eelde.toggles.database.dao.application.TogglesApplicationDao +import se.eelde.toggles.database.dao.application.TogglesConfigurationDao +import se.eelde.toggles.database.dao.application.TogglesScopeDao internal data class ViewState( val application: WrenchApplication? = null, @@ -42,16 +44,20 @@ internal sealed class PartialViewState { } @OptIn(ExperimentalCoroutinesApi::class) -@HiltViewModel +@HiltViewModel(assistedFactory = ConfigurationViewModel.Factory::class) @Suppress("StaticFieldLeak") -class ConfigurationViewModel @Inject internal constructor( +class ConfigurationViewModel @AssistedInject internal constructor( @ApplicationContext private val context: Context, - private val applicationDao: WrenchApplicationDao, - configurationDao: WrenchConfigurationDao, - scopeDao: WrenchScopeDao, + private val applicationDao: TogglesApplicationDao, + configurationDao: TogglesConfigurationDao, + scopeDao: TogglesScopeDao, val savedStateHandle: SavedStateHandle, + @Assisted val applicationId: Long, ) : ViewModel() { - private val applicationId: Long = savedStateHandle.get("applicationId")!! + @AssistedFactory + interface Factory { + fun create(applicationId: Long): ConfigurationViewModel + } private val _state = MutableStateFlow(reduce(ViewState(), PartialViewState.Empty)) @@ -107,6 +113,7 @@ class ConfigurationViewModel @Inject internal constructor( is PartialViewState.Configurations -> viewState.copy( configurations = partialViewState.configurations ) + PartialViewState.Empty -> viewState is PartialViewState.DefaultScope -> viewState.copy(defaultScope = partialViewState.scope) is PartialViewState.SelectedScope -> viewState.copy(selectedScope = partialViewState.scope) diff --git a/modules/configurations/src/main/java/se/eelde/toggles/configurations/ConfigurationsEntry.kt b/modules/configurations/src/main/java/se/eelde/toggles/configurations/ConfigurationsEntry.kt index 7665b9a7..7f86da13 100644 --- a/modules/configurations/src/main/java/se/eelde/toggles/configurations/ConfigurationsEntry.kt +++ b/modules/configurations/src/main/java/se/eelde/toggles/configurations/ConfigurationsEntry.kt @@ -7,13 +7,13 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.outlined.List import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.outlined.Cyclone import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Info -import androidx.compose.material.icons.outlined.List import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api @@ -32,9 +32,9 @@ import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavType import androidx.navigation.compose.composable -import androidx.navigation.navArgument +import androidx.navigation.toRoute +import se.eelde.toggles.routes.Configurations @Suppress("LongMethod", "LongParameterList") @OptIn(ExperimentalMaterial3Api::class) @@ -46,11 +46,14 @@ fun NavGraphBuilder.configurationsNavigations( navigateToScopeView: (Long) -> Unit, back: () -> Unit, ) { - composable( - "configurations/{applicationId}", - arguments = listOf(navArgument("applicationId") { type = NavType.LongType }) - ) { - val viewModel: ConfigurationViewModel = hiltViewModel() + composable { backStackEntry -> + val configurations: Configurations = backStackEntry.toRoute() + + val viewModel: ConfigurationViewModel = + hiltViewModel( + creationCallback = { factory -> factory.create(applicationId = configurations.applicationId) } + ) + val uiState = viewModel.state.collectAsStateWithLifecycle() val launcher = @@ -91,7 +94,10 @@ fun NavGraphBuilder.configurationsNavigations( navigationIcon = { IconButton(onClick = { back() }) { - Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = null) + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null + ) } }, actions = { @@ -138,7 +144,7 @@ fun NavGraphBuilder.configurationsNavigations( onClick = { navigateToScopeView(uiState.value.application!!.id) }, leadingIcon = { Icon( - imageVector = Icons.Outlined.List, + imageVector = Icons.AutoMirrored.Outlined.List, contentDescription = null ) } diff --git a/modules/database/src/main/java/se/eelde/toggles/database/WrenchDatabase.kt b/modules/database/src/main/java/se/eelde/toggles/database/WrenchDatabase.kt index 450644ea..86eddf56 100644 --- a/modules/database/src/main/java/se/eelde/toggles/database/WrenchDatabase.kt +++ b/modules/database/src/main/java/se/eelde/toggles/database/WrenchDatabase.kt @@ -3,6 +3,16 @@ package se.eelde.toggles.database import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters +import se.eelde.toggles.database.dao.application.TogglesApplicationDao +import se.eelde.toggles.database.dao.application.TogglesConfigurationDao +import se.eelde.toggles.database.dao.application.TogglesConfigurationValueDao +import se.eelde.toggles.database.dao.application.TogglesPredefinedConfigurationValueDao +import se.eelde.toggles.database.dao.application.TogglesScopeDao +import se.eelde.toggles.database.dao.provider.ProviderApplicationDao +import se.eelde.toggles.database.dao.provider.ProviderConfigurationDao +import se.eelde.toggles.database.dao.provider.ProviderConfigurationValueDao +import se.eelde.toggles.database.dao.provider.ProviderPredefinedConfigurationValueDao +import se.eelde.toggles.database.dao.provider.ProviderScopeDao @Database( entities = [ @@ -17,13 +27,18 @@ import androidx.room.TypeConverters @TypeConverters(RoomDateConverter::class) abstract class WrenchDatabase : RoomDatabase() { - abstract fun applicationDao(): WrenchApplicationDao + abstract fun togglesApplicationDao(): TogglesApplicationDao + abstract fun providerApplicationDao(): ProviderApplicationDao - abstract fun configurationDao(): WrenchConfigurationDao + abstract fun togglesConfigurationDao(): TogglesConfigurationDao + abstract fun providerConfigurationDao(): ProviderConfigurationDao - abstract fun configurationValueDao(): WrenchConfigurationValueDao + abstract fun togglesConfigurationValueDao(): TogglesConfigurationValueDao + abstract fun providerConfigurationValueDao(): ProviderConfigurationValueDao - abstract fun predefinedConfigurationValueDao(): WrenchPredefinedConfigurationValueDao + abstract fun togglesPredefinedConfigurationValueDao(): TogglesPredefinedConfigurationValueDao + abstract fun providerPredefinedConfigurationValueDao(): ProviderPredefinedConfigurationValueDao - abstract fun scopeDao(): WrenchScopeDao + abstract fun togglesScopeDao(): TogglesScopeDao + abstract fun providerScopeDao(): ProviderScopeDao } diff --git a/modules/database/src/main/java/se/eelde/toggles/database/WrenchScope.kt b/modules/database/src/main/java/se/eelde/toggles/database/WrenchScope.kt index 5511cc25..47d03c9a 100644 --- a/modules/database/src/main/java/se/eelde/toggles/database/WrenchScope.kt +++ b/modules/database/src/main/java/se/eelde/toggles/database/WrenchScope.kt @@ -5,6 +5,7 @@ import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.Index import androidx.room.PrimaryKey +import se.eelde.toggles.core.ColumnNames import se.eelde.toggles.database.tables.ApplicationTable import se.eelde.toggles.database.tables.ScopeTable import java.util.Date @@ -40,7 +41,7 @@ data class WrenchScope constructor( companion object { - const val SCOPE_DEFAULT = "wrench_default" + const val SCOPE_DEFAULT = ColumnNames.ToggleScope.DEFAULT_SCOPE const val SCOPE_USER = "Development scope" fun newWrenchScope() = WrenchScope(0, 0, SCOPE_DEFAULT, Date()) diff --git a/modules/database/src/main/java/se/eelde/toggles/database/WrenchApplicationDao.kt b/modules/database/src/main/java/se/eelde/toggles/database/dao/application/TogglesApplicationDao.kt similarity index 60% rename from modules/database/src/main/java/se/eelde/toggles/database/WrenchApplicationDao.kt rename to modules/database/src/main/java/se/eelde/toggles/database/dao/application/TogglesApplicationDao.kt index 4a023250..fc25e032 100644 --- a/modules/database/src/main/java/se/eelde/toggles/database/WrenchApplicationDao.kt +++ b/modules/database/src/main/java/se/eelde/toggles/database/dao/application/TogglesApplicationDao.kt @@ -1,14 +1,14 @@ -package se.eelde.toggles.database +package se.eelde.toggles.database.dao.application import androidx.room.Dao import androidx.room.Delete -import androidx.room.Insert import androidx.room.Query import kotlinx.coroutines.flow.Flow +import se.eelde.toggles.database.WrenchApplication import se.eelde.toggles.database.tables.ApplicationTable @Dao -interface WrenchApplicationDao { +interface TogglesApplicationDao { @Query("SELECT * FROM " + ApplicationTable.TABLE_NAME) fun getApplications(): Flow> @@ -16,12 +16,6 @@ interface WrenchApplicationDao { @Query("SELECT * FROM " + ApplicationTable.TABLE_NAME + " WHERE id = (:id)") suspend fun getApplication(id: Long): WrenchApplication? - @Query("SELECT * FROM " + ApplicationTable.TABLE_NAME + " WHERE packageName IN (:packageName)") - fun loadByPackageName(packageName: String): WrenchApplication? - - @Insert - fun insert(application: WrenchApplication): Long - @Delete suspend fun delete(application: WrenchApplication) } diff --git a/modules/database/src/main/java/se/eelde/toggles/database/WrenchConfigurationDao.kt b/modules/database/src/main/java/se/eelde/toggles/database/dao/application/TogglesConfigurationDao.kt similarity index 68% rename from modules/database/src/main/java/se/eelde/toggles/database/WrenchConfigurationDao.kt rename to modules/database/src/main/java/se/eelde/toggles/database/dao/application/TogglesConfigurationDao.kt index f2a216e5..809b6cd6 100644 --- a/modules/database/src/main/java/se/eelde/toggles/database/WrenchConfigurationDao.kt +++ b/modules/database/src/main/java/se/eelde/toggles/database/dao/application/TogglesConfigurationDao.kt @@ -1,6 +1,6 @@ @file:Suppress("MaxLineLength") -package se.eelde.toggles.database +package se.eelde.toggles.database.dao.application import android.database.Cursor import androidx.room.Dao @@ -8,6 +8,8 @@ import androidx.room.Insert import androidx.room.Query import androidx.room.Transaction import kotlinx.coroutines.flow.Flow +import se.eelde.toggles.database.WrenchConfiguration +import se.eelde.toggles.database.WrenchConfigurationWithValues import se.eelde.toggles.database.tables.ConfigurationTable import se.eelde.toggles.database.tables.ConfigurationValueTable import se.eelde.toggles.database.tables.ScopeTable @@ -15,7 +17,7 @@ import java.util.Date @Suppress("TooManyFunctions") @Dao -interface WrenchConfigurationDao { +interface TogglesConfigurationDao { @Query( "SELECT configuration.id, " + @@ -52,6 +54,19 @@ interface WrenchConfigurationDao { ) fun getToggles(configurationKey: String, scopeId: List): Cursor + @Query( + "SELECT configuration.id, " + + " configuration.configurationKey, " + + " configuration.configurationType," + + " configurationValue.value," + + " scope.name as scope" + + " FROM " + ConfigurationTable.TABLE_NAME + + " INNER JOIN " + ConfigurationValueTable.TABLE_NAME + " ON configuration.id = configurationValue.configurationId " + + " INNER JOIN " + ScopeTable.TABLE_NAME + " ON configurationValue.scope = scope.id " + + " WHERE configuration.configurationKey = (:configurationKey)" + ) + fun getToggles(configurationKey: String): Cursor + @Query( "SELECT * " + " FROM " + ConfigurationTable.TABLE_NAME + @@ -94,9 +109,30 @@ interface WrenchConfigurationDao { query: String ): Flow> + @Query("DELETE FROM configuration WHERE applicationId = :callingApplication AND id = :id") + fun deleteConfiguration(callingApplication: Long, id: Long): Int + + @Query( + "DELETE FROM configuration WHERE applicationId = :callingApplication AND configurationKey = :configurationKey" + ) + fun deleteConfiguration(callingApplication: Long, configurationKey: String): Int + @Insert fun insert(wrenchConfiguration: WrenchConfiguration): Long @Query("UPDATE configuration set lastUse=:date WHERE id= :configurationId") suspend fun touch(configurationId: Long, date: Date) + + @Query( + "UPDATE configuration SET configurationKey = :key, configurationType = :type WHERE applicationId = :callingApplication AND id= :id" + ) + fun updateConfiguration(callingApplication: Long, id: Long, key: String, type: String): Int + + @Query("SELECT * FROM configuration WHERE id = :configurationId and applicationId = :callingApplication") + fun getConfigurationCursor(callingApplication: Long, configurationId: Long): Cursor + + @Query( + "SELECT * FROM configuration WHERE configurationKey = :configurationKey and applicationId = :callingApplication" + ) + fun getConfigurationCursor(callingApplication: Long, configurationKey: String): Cursor } diff --git a/modules/database/src/main/java/se/eelde/toggles/database/WrenchConfigurationValueDao.kt b/modules/database/src/main/java/se/eelde/toggles/database/dao/application/TogglesConfigurationValueDao.kt similarity index 92% rename from modules/database/src/main/java/se/eelde/toggles/database/WrenchConfigurationValueDao.kt rename to modules/database/src/main/java/se/eelde/toggles/database/dao/application/TogglesConfigurationValueDao.kt index f0bfc637..802f00f2 100644 --- a/modules/database/src/main/java/se/eelde/toggles/database/WrenchConfigurationValueDao.kt +++ b/modules/database/src/main/java/se/eelde/toggles/database/dao/application/TogglesConfigurationValueDao.kt @@ -1,6 +1,6 @@ @file:Suppress("MaxLineLength") -package se.eelde.toggles.database +package se.eelde.toggles.database.dao.application import androidx.room.Dao import androidx.room.Delete @@ -8,11 +8,11 @@ import androidx.room.Insert import androidx.room.Query import androidx.room.Update import kotlinx.coroutines.flow.Flow +import se.eelde.toggles.database.WrenchConfigurationValue import se.eelde.toggles.database.tables.ConfigurationValueTable @Dao -interface WrenchConfigurationValueDao { - +interface TogglesConfigurationValueDao { @Query( "SELECT * FROM " + ConfigurationValueTable.TABLE_NAME + " WHERE " + ConfigurationValueTable.COL_CONFIG_ID + " = (:configurationId) AND " + ConfigurationValueTable.COL_SCOPE + " = (:scopeId)" diff --git a/modules/database/src/main/java/se/eelde/toggles/database/WrenchPredefinedConfigurationValueDao.kt b/modules/database/src/main/java/se/eelde/toggles/database/dao/application/TogglesPredefinedConfigurationValueDao.kt similarity index 84% rename from modules/database/src/main/java/se/eelde/toggles/database/WrenchPredefinedConfigurationValueDao.kt rename to modules/database/src/main/java/se/eelde/toggles/database/dao/application/TogglesPredefinedConfigurationValueDao.kt index a476f1c9..545874e5 100644 --- a/modules/database/src/main/java/se/eelde/toggles/database/WrenchPredefinedConfigurationValueDao.kt +++ b/modules/database/src/main/java/se/eelde/toggles/database/dao/application/TogglesPredefinedConfigurationValueDao.kt @@ -1,15 +1,16 @@ @file:Suppress("MaxLineLength") -package se.eelde.toggles.database +package se.eelde.toggles.database.dao.application import androidx.room.Dao import androidx.room.Insert import androidx.room.Query import kotlinx.coroutines.flow.Flow +import se.eelde.toggles.database.WrenchPredefinedConfigurationValue import se.eelde.toggles.database.tables.PredefinedConfigurationValueTable @Dao -interface WrenchPredefinedConfigurationValueDao { +interface TogglesPredefinedConfigurationValueDao { @Query( """SELECT * FROM ${PredefinedConfigurationValueTable.TABLE_NAME} WHERE ${PredefinedConfigurationValueTable.COL_CONFIG_ID} = (:configurationId)""" diff --git a/modules/database/src/main/java/se/eelde/toggles/database/WrenchScopeDao.kt b/modules/database/src/main/java/se/eelde/toggles/database/dao/application/TogglesScopeDao.kt similarity index 93% rename from modules/database/src/main/java/se/eelde/toggles/database/WrenchScopeDao.kt rename to modules/database/src/main/java/se/eelde/toggles/database/dao/application/TogglesScopeDao.kt index a8a9a735..6a6fcd05 100644 --- a/modules/database/src/main/java/se/eelde/toggles/database/WrenchScopeDao.kt +++ b/modules/database/src/main/java/se/eelde/toggles/database/dao/application/TogglesScopeDao.kt @@ -1,6 +1,6 @@ @file:Suppress("MaxLineLength") -package se.eelde.toggles.database +package se.eelde.toggles.database.dao.application import androidx.room.Dao import androidx.room.Delete @@ -8,10 +8,11 @@ import androidx.room.Insert import androidx.room.Query import androidx.room.Update import kotlinx.coroutines.flow.Flow +import se.eelde.toggles.database.WrenchScope import se.eelde.toggles.database.tables.ScopeTable @Dao -interface WrenchScopeDao { +interface TogglesScopeDao { @Query( "SELECT * FROM " + ScopeTable.TABLE_NAME + " WHERE " + ScopeTable.COL_APP_ID + " = (:applicationId) AND " + ScopeTable.COL_NAME + " != '" + WrenchScope.SCOPE_DEFAULT + "'" diff --git a/modules/database/src/main/java/se/eelde/toggles/database/dao/provider/ProviderApplicationDao.kt b/modules/database/src/main/java/se/eelde/toggles/database/dao/provider/ProviderApplicationDao.kt new file mode 100644 index 00000000..6b06455c --- /dev/null +++ b/modules/database/src/main/java/se/eelde/toggles/database/dao/provider/ProviderApplicationDao.kt @@ -0,0 +1,17 @@ +package se.eelde.toggles.database.dao.provider + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import se.eelde.toggles.database.WrenchApplication +import se.eelde.toggles.database.tables.ApplicationTable + +@Dao +interface ProviderApplicationDao { + + @Query("SELECT * FROM " + ApplicationTable.TABLE_NAME + " WHERE packageName IN (:packageName)") + fun loadByPackageName(packageName: String): WrenchApplication? + + @Insert + fun insert(application: WrenchApplication): Long +} diff --git a/modules/database/src/main/java/se/eelde/toggles/database/dao/provider/ProviderConfigurationDao.kt b/modules/database/src/main/java/se/eelde/toggles/database/dao/provider/ProviderConfigurationDao.kt new file mode 100644 index 00000000..04063cc4 --- /dev/null +++ b/modules/database/src/main/java/se/eelde/toggles/database/dao/provider/ProviderConfigurationDao.kt @@ -0,0 +1,121 @@ +@file:Suppress("MaxLineLength") + +package se.eelde.toggles.database.dao.provider + +import android.database.Cursor +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import se.eelde.toggles.database.WrenchConfiguration +import se.eelde.toggles.database.tables.ConfigurationTable +import se.eelde.toggles.database.tables.ConfigurationValueTable +import se.eelde.toggles.database.tables.ScopeTable +import java.util.Date + +@Suppress("TooManyFunctions") +@Dao +interface ProviderConfigurationDao { + + @Query( + "SELECT configuration.id, " + + " configuration.configurationKey, " + + " configuration.configurationType," + + " configurationValue.value" + + " FROM " + ConfigurationTable.TABLE_NAME + + " INNER JOIN " + ConfigurationValueTable.TABLE_NAME + " ON configuration.id = configurationValue.configurationId " + + " WHERE configuration.id = (:configurationId) AND configurationValue.scope = (:scopeId)" + ) + fun getToggle(configurationId: Long, scopeId: Long): Cursor + + @Query( + "SELECT configuration.id, " + + " configuration.configurationKey, " + + " configuration.configurationType," + + " configurationValue.value" + + " FROM " + ConfigurationTable.TABLE_NAME + + " INNER JOIN " + ConfigurationValueTable.TABLE_NAME + " ON configuration.id = configurationValue.configurationId " + + " WHERE configuration.configurationKey = (:configurationKey) AND configurationValue.scope = (:scopeId)" + ) + fun getToggle(configurationKey: String, scopeId: Long): Cursor + + @Query( + "SELECT configuration.id, " + + " configuration.configurationKey, " + + " configuration.configurationType," + + " configurationValue.value," + + " scope.name as scope" + + " FROM " + ConfigurationTable.TABLE_NAME + + " INNER JOIN " + ConfigurationValueTable.TABLE_NAME + " ON configuration.id = configurationValue.configurationId " + + " INNER JOIN " + ScopeTable.TABLE_NAME + " ON configurationValue.scope = scope.id " + + " WHERE configuration.configurationKey = (:configurationKey) AND configurationValue.scope IN (:scopeId)" + ) + fun getToggles(configurationKey: String, scopeId: List): Cursor + + @Query( + "SELECT configuration.id, " + + " configuration.configurationKey, " + + " configuration.configurationType," + + " configurationValue.value," + + " scope.name as scope" + + " FROM " + ConfigurationTable.TABLE_NAME + + " INNER JOIN " + ConfigurationValueTable.TABLE_NAME + " ON configuration.id = configurationValue.configurationId " + + " INNER JOIN " + ScopeTable.TABLE_NAME + " ON configurationValue.scope = scope.id " + + " WHERE configuration.configurationKey = (:configurationKey)" + ) + fun getToggles(configurationKey: String): Cursor + + @Query( + "SELECT * " + + " FROM " + ConfigurationTable.TABLE_NAME + + " WHERE configuration.applicationId = (:applicationId) AND configuration.configurationKey = (:configurationKey)" + ) + fun getWrenchConfiguration(applicationId: Long, configurationKey: String): WrenchConfiguration? + + @Query("DELETE FROM configuration WHERE applicationId = :callingApplication AND id = :id") + fun deleteConfiguration(callingApplication: Long, id: Long): Int + + @Query( + "DELETE FROM configuration WHERE applicationId = :callingApplication AND configurationKey = :configurationKey" + ) + fun deleteConfiguration(callingApplication: Long, configurationKey: String): Int + + @Insert + fun insert(wrenchConfiguration: WrenchConfiguration): Long + + @Query("UPDATE configuration set lastUse=:date WHERE id= :configurationId") + suspend fun touch(configurationId: Long, date: Date) + + @Query( + "UPDATE configuration SET configurationKey = :key, configurationType = :type WHERE applicationId = :callingApplication AND id= :id" + ) + fun updateConfiguration(callingApplication: Long, id: Long, key: String, type: String): Int + + @Query( + "SELECT * FROM configuration WHERE applicationId = :callingApplication" + ) + fun getConfigurationCursor(callingApplication: Long): Cursor + + @Query("SELECT * FROM configuration WHERE id = :configurationId and applicationId = :callingApplication") + fun getConfigurationCursor(callingApplication: Long, configurationId: Long): Cursor + + @Query( + "SELECT * FROM configuration WHERE configurationKey = :configurationKey and applicationId = :callingApplication" + ) + fun getConfigurationCursor(callingApplication: Long, configurationKey: String): Cursor + + @Query( + """SELECT configurationValue.* FROM configuration +INNER JOIN configurationValue ON configuration.id = configurationValue.configurationId +WHERE configuration.applicationId = :callingApplication AND configurationId = :configurationId +""" + ) + fun getConfigurationValueCursor(callingApplication: Long, configurationId: Long): Cursor + + @Query( + """SELECT configurationValue.* FROM configuration +INNER JOIN configurationValue ON configuration.id = configurationValue.configurationId +WHERE configuration.applicationId = :callingApplication AND configurationKey = :configurationKey +""" + ) + fun getConfigurationValueCursor(callingApplication: Long, configurationKey: String): Cursor +} diff --git a/modules/database/src/main/java/se/eelde/toggles/database/dao/provider/ProviderConfigurationValueDao.kt b/modules/database/src/main/java/se/eelde/toggles/database/dao/provider/ProviderConfigurationValueDao.kt new file mode 100644 index 00000000..80c5c4a3 --- /dev/null +++ b/modules/database/src/main/java/se/eelde/toggles/database/dao/provider/ProviderConfigurationValueDao.kt @@ -0,0 +1,33 @@ +@file:Suppress("MaxLineLength") + +package se.eelde.toggles.database.dao.provider + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import se.eelde.toggles.database.WrenchConfigurationValue +import se.eelde.toggles.database.tables.ConfigurationValueTable + +@Dao +interface ProviderConfigurationValueDao { + @Query( + "UPDATE " + ConfigurationValueTable.TABLE_NAME + + " SET " + ConfigurationValueTable.COL_VALUE + " = (:value)" + + " WHERE " + ConfigurationValueTable.COL_CONFIG_ID + " = (:configurationId) AND " + ConfigurationValueTable.COL_SCOPE + " = (:scopeId) " + ) + fun updateConfigurationValueSync(configurationId: Long, scopeId: Long, value: String): Int + + @Insert + fun insertSync(wrenchConfigurationValue: WrenchConfigurationValue): Long + + @Insert + suspend fun insert(wrenchConfigurationValue: WrenchConfigurationValue): Long + + @Update + fun update(wrenchConfigurationValue: WrenchConfigurationValue): Int + + @Delete + suspend fun delete(selectedConfigurationValue: WrenchConfigurationValue) +} diff --git a/modules/database/src/main/java/se/eelde/toggles/database/dao/provider/ProviderPredefinedConfigurationValueDao.kt b/modules/database/src/main/java/se/eelde/toggles/database/dao/provider/ProviderPredefinedConfigurationValueDao.kt new file mode 100644 index 00000000..5ea4bedf --- /dev/null +++ b/modules/database/src/main/java/se/eelde/toggles/database/dao/provider/ProviderPredefinedConfigurationValueDao.kt @@ -0,0 +1,24 @@ +@file:Suppress("MaxLineLength") + +package se.eelde.toggles.database.dao.provider + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import se.eelde.toggles.database.WrenchPredefinedConfigurationValue +import se.eelde.toggles.database.tables.PredefinedConfigurationValueTable + +@Dao +interface ProviderPredefinedConfigurationValueDao { + + @Query( + """SELECT * FROM ${PredefinedConfigurationValueTable.TABLE_NAME} WHERE ${PredefinedConfigurationValueTable.COL_CONFIG_ID} = (:configurationId) AND ${PredefinedConfigurationValueTable.COL_VALUE} = (:value) """ + ) + fun getByConfigurationAndValueId( + configurationId: Long, + value: String + ): WrenchPredefinedConfigurationValue + + @Insert + fun insert(fullConfig: WrenchPredefinedConfigurationValue): Long +} diff --git a/modules/database/src/main/java/se/eelde/toggles/database/dao/provider/ProviderScopeDao.kt b/modules/database/src/main/java/se/eelde/toggles/database/dao/provider/ProviderScopeDao.kt new file mode 100644 index 00000000..a8e935a7 --- /dev/null +++ b/modules/database/src/main/java/se/eelde/toggles/database/dao/provider/ProviderScopeDao.kt @@ -0,0 +1,34 @@ +@file:Suppress("MaxLineLength") + +package se.eelde.toggles.database.dao.provider + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import se.eelde.toggles.database.WrenchScope +import se.eelde.toggles.database.tables.ScopeTable + +@Dao +interface ProviderScopeDao { + + @Insert + fun insert(scope: WrenchScope): Long + + @Delete + fun delete(scope: WrenchScope) + + @Query( + "SELECT * FROM " + ScopeTable.TABLE_NAME + " WHERE " + ScopeTable.COL_APP_ID + " = (:applicationId) ORDER BY " + ScopeTable.COL_SELECTED_TIMESTAMP + " DESC LIMIT 1" + ) + fun getSelectedScope(applicationId: Long): WrenchScope? + + @Query( + "SELECT * FROM " + ScopeTable.TABLE_NAME + " WHERE " + ScopeTable.COL_APP_ID + " = (:applicationId) AND " + ScopeTable.COL_NAME + " = '" + WrenchScope.SCOPE_DEFAULT + "'" + ) + fun getDefaultScope(applicationId: Long): WrenchScope? + + @Update + suspend fun update(scope: WrenchScope) +} diff --git a/modules/database/src/main/java/se/eelde/toggles/database/tables/ConfigurationTable.kt b/modules/database/src/main/java/se/eelde/toggles/database/tables/ConfigurationTable.kt index 8d2e7aab..e7783d83 100644 --- a/modules/database/src/main/java/se/eelde/toggles/database/tables/ConfigurationTable.kt +++ b/modules/database/src/main/java/se/eelde/toggles/database/tables/ConfigurationTable.kt @@ -7,7 +7,7 @@ interface ConfigurationTable { const val TABLE_NAME = "configuration" const val COL_ID = ColumnNames.Toggle.COL_ID const val COL_APP_ID = "applicationId" - const val COL_KEY = ColumnNames.Toggle.COL_KEY - const val COL_TYPE = ColumnNames.Toggle.COL_TYPE + const val COL_KEY = ColumnNames.Configuration.COL_KEY + const val COL_TYPE = ColumnNames.Configuration.COL_TYPE } } diff --git a/modules/database/src/main/java/se/eelde/toggles/database/tables/ConfigurationValueTable.kt b/modules/database/src/main/java/se/eelde/toggles/database/tables/ConfigurationValueTable.kt index 08887832..d13a1363 100644 --- a/modules/database/src/main/java/se/eelde/toggles/database/tables/ConfigurationValueTable.kt +++ b/modules/database/src/main/java/se/eelde/toggles/database/tables/ConfigurationValueTable.kt @@ -5,9 +5,9 @@ import se.eelde.toggles.core.ColumnNames interface ConfigurationValueTable { companion object { const val TABLE_NAME = "configurationValue" - const val COL_ID = ColumnNames.Toggle.COL_ID - const val COL_CONFIG_ID = "configurationId" - const val COL_VALUE = ColumnNames.Toggle.COL_VALUE - const val COL_SCOPE = "scope" + const val COL_ID = ColumnNames.ConfigurationValue.COL_ID + const val COL_CONFIG_ID = ColumnNames.ConfigurationValue.COL_CONFIG_ID + const val COL_VALUE = ColumnNames.ConfigurationValue.COL_VALUE + const val COL_SCOPE = ColumnNames.ConfigurationValue.COL_SCOPE } } diff --git a/modules/database/src/main/java/se/eelde/toggles/database/tables/ScopeTable.kt b/modules/database/src/main/java/se/eelde/toggles/database/tables/ScopeTable.kt index 729618ae..f9166e88 100644 --- a/modules/database/src/main/java/se/eelde/toggles/database/tables/ScopeTable.kt +++ b/modules/database/src/main/java/se/eelde/toggles/database/tables/ScopeTable.kt @@ -1,11 +1,13 @@ package se.eelde.toggles.database.tables +import se.eelde.toggles.core.ColumnNames + interface ScopeTable { companion object { const val TABLE_NAME = "scope" - const val COL_ID = "id" - const val COL_APP_ID = "applicationId" - const val COL_NAME = "name" - const val COL_SELECTED_TIMESTAMP = "selectedTimestamp" + const val COL_ID = ColumnNames.ToggleScope.COL_ID + const val COL_APP_ID = ColumnNames.ToggleScope.COL_APP_ID + const val COL_NAME = ColumnNames.ToggleScope.COL_NAME + const val COL_SELECTED_TIMESTAMP = ColumnNames.ToggleScope.COL_SELECTED_TIMESTAMP } } diff --git a/modules/enumconfiguration/build.gradle.kts b/modules/enumconfiguration/build.gradle.kts index 531d1e92..91f2cc1f 100644 --- a/modules/enumconfiguration/build.gradle.kts +++ b/modules/enumconfiguration/build.gradle.kts @@ -2,16 +2,11 @@ plugins { id("toggles.android.module-conventions") id("toggles.ownership-conventions") alias(libs.plugins.com.google.devtools.ksp) + id("org.jetbrains.kotlin.plugin.compose") } android { namespace = "se.eelde.toggles.enumconfiguration" - buildFeatures { - compose = true - } - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() - } } dependencies { @@ -19,6 +14,7 @@ dependencies { implementation(projects.modules.composeTheme) implementation(projects.modules.database) implementation(projects.modules.provider) + implementation(projects.modules.routes) implementation(libs.androidx.core.core.ktx) implementation(libs.androidx.appcompat) implementation(libs.androidx.navigation.navigation.compose) diff --git a/modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration/EnumValueView.kt b/modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration/EnumValueView.kt index 04df1c05..223b3496 100644 --- a/modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration/EnumValueView.kt +++ b/modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration/EnumValueView.kt @@ -33,7 +33,7 @@ import kotlinx.coroutines.launch @Composable fun EnumValueView( modifier: Modifier = Modifier, - viewModel: FragmentEnumValueViewModel = hiltViewModel(), + viewModel: EnumValueViewModel = hiltViewModel(), back: () -> Unit, ) { val viewState by viewModel.state.collectAsStateWithLifecycle() diff --git a/modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration/EnumValueViewModel.kt b/modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration/EnumValueViewModel.kt index 186403a9..bd086693 100644 --- a/modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration/EnumValueViewModel.kt +++ b/modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration/EnumValueViewModel.kt @@ -1,9 +1,11 @@ package se.eelde.toggles.enumconfiguration import android.app.Application -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -12,15 +14,15 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import se.eelde.toggles.core.TogglesProviderContract -import se.eelde.toggles.database.WrenchConfigurationDao import se.eelde.toggles.database.WrenchConfigurationValue -import se.eelde.toggles.database.WrenchConfigurationValueDao import se.eelde.toggles.database.WrenchPredefinedConfigurationValue -import se.eelde.toggles.database.WrenchPredefinedConfigurationValueDao +import se.eelde.toggles.database.dao.application.TogglesConfigurationDao +import se.eelde.toggles.database.dao.application.TogglesConfigurationValueDao +import se.eelde.toggles.database.dao.application.TogglesPredefinedConfigurationValueDao import se.eelde.toggles.provider.notifyInsert import se.eelde.toggles.provider.notifyUpdate +import se.eelde.toggles.routes.EnumConfiguration import java.util.Date -import javax.inject.Inject data class ViewState( val title: String? = null, @@ -43,22 +45,29 @@ internal sealed class PartialViewState { data object Reverting : PartialViewState() } -@HiltViewModel -class FragmentEnumValueViewModel @Inject internal constructor( - private val savedStateHandle: SavedStateHandle, +@HiltViewModel(assistedFactory = EnumValueViewModel.Factory::class) +class EnumValueViewModel @AssistedInject internal constructor( private val application: Application, - private val configurationDao: WrenchConfigurationDao, - private val configurationValueDao: WrenchConfigurationValueDao, - private val predefinedConfigurationValueDao: WrenchPredefinedConfigurationValueDao + private val configurationDao: TogglesConfigurationDao, + private val configurationValueDao: TogglesConfigurationValueDao, + private val predefinedConfigurationValueDao: TogglesPredefinedConfigurationValueDao, + @Assisted enumConfiguration: EnumConfiguration, ) : ViewModel() { + @AssistedFactory + interface Factory { + fun create( + enumConfiguration: EnumConfiguration + ): EnumValueViewModel + } + private val _state = MutableStateFlow(reduce(ViewState(), PartialViewState.Empty)) val state: StateFlow get() = _state - private val configurationId: Long = savedStateHandle.get("configurationId")!! - private val scopeId: Long = savedStateHandle.get("scopeId")!! + private val configurationId: Long = enumConfiguration.configurationId + private val scopeId: Long = enumConfiguration.scopeId private var selectedConfigurationValue: WrenchConfigurationValue? = null diff --git a/modules/help/build.gradle.kts b/modules/help/build.gradle.kts index 62ab0678..59e09102 100644 --- a/modules/help/build.gradle.kts +++ b/modules/help/build.gradle.kts @@ -2,16 +2,11 @@ plugins { id("toggles.android.module-conventions") id("toggles.ownership-conventions") alias(libs.plugins.com.google.devtools.ksp) + id("org.jetbrains.kotlin.plugin.compose") } android { namespace = "se.eelde.toggles.help" - buildFeatures { - compose = true - } - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() - } } dependencies { diff --git a/modules/integerconfiguration/build.gradle.kts b/modules/integerconfiguration/build.gradle.kts index 1adf999a..0e491618 100644 --- a/modules/integerconfiguration/build.gradle.kts +++ b/modules/integerconfiguration/build.gradle.kts @@ -2,16 +2,11 @@ plugins { id("toggles.android.module-conventions") id("toggles.ownership-conventions") alias(libs.plugins.com.google.devtools.ksp) + id("org.jetbrains.kotlin.plugin.compose") } android { namespace = "se.eelde.toggles.integerconfiguration" - buildFeatures { - compose = true - } - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() - } } dependencies { @@ -19,6 +14,7 @@ dependencies { implementation(projects.modules.composeTheme) implementation(projects.modules.database) implementation(projects.modules.provider) + implementation(projects.modules.routes) implementation(libs.androidx.core.core.ktx) implementation(libs.androidx.appcompat) implementation(libs.androidx.navigation.navigation.compose) diff --git a/modules/integerconfiguration/src/main/java/se/eelde/toggles/integerconfiguration/IntegerValueView.kt b/modules/integerconfiguration/src/main/java/se/eelde/toggles/integerconfiguration/IntegerValueView.kt index aca18cba..0a2e195a 100644 --- a/modules/integerconfiguration/src/main/java/se/eelde/toggles/integerconfiguration/IntegerValueView.kt +++ b/modules/integerconfiguration/src/main/java/se/eelde/toggles/integerconfiguration/IntegerValueView.kt @@ -25,13 +25,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.launch @Preview @Composable -fun IntegerValueViewPreview() { +internal fun IntegerValueViewPreview() { IntegerValueView( uiState = ViewState( title = "Integer value", @@ -49,8 +48,8 @@ fun IntegerValueViewPreview() { @OptIn(ExperimentalMaterial3Api::class) @Composable fun IntegerValueView( + viewModel: IntegerValueViewModel, modifier: Modifier = Modifier, - viewModel: FragmentIntegerValueViewModel = hiltViewModel(), back: () -> Unit, ) { val viewState by viewModel.state.collectAsStateWithLifecycle() diff --git a/modules/integerconfiguration/src/main/java/se/eelde/toggles/integerconfiguration/IntegerValueViewModel.kt b/modules/integerconfiguration/src/main/java/se/eelde/toggles/integerconfiguration/IntegerValueViewModel.kt index df7826bc..52c6550d 100644 --- a/modules/integerconfiguration/src/main/java/se/eelde/toggles/integerconfiguration/IntegerValueViewModel.kt +++ b/modules/integerconfiguration/src/main/java/se/eelde/toggles/integerconfiguration/IntegerValueViewModel.kt @@ -1,9 +1,11 @@ package se.eelde.toggles.integerconfiguration import android.app.Application -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -12,12 +14,12 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import se.eelde.toggles.core.TogglesProviderContract -import se.eelde.toggles.database.WrenchConfigurationDao import se.eelde.toggles.database.WrenchConfigurationValue -import se.eelde.toggles.database.WrenchConfigurationValueDao +import se.eelde.toggles.database.dao.application.TogglesConfigurationDao +import se.eelde.toggles.database.dao.application.TogglesConfigurationValueDao import se.eelde.toggles.provider.notifyUpdate +import se.eelde.toggles.routes.IntegerConfiguration import java.util.Date -import javax.inject.Inject data class ViewState( val title: String? = null, @@ -34,21 +36,27 @@ private sealed class PartialViewState { object Reverting : PartialViewState() } -@HiltViewModel -class FragmentIntegerValueViewModel @Inject internal constructor( - savedStateHandle: SavedStateHandle, +@HiltViewModel(assistedFactory = IntegerValueViewModel.Factory::class) +class IntegerValueViewModel @AssistedInject internal constructor( private val application: Application, - private val configurationDao: WrenchConfigurationDao, - private val configurationValueDao: WrenchConfigurationValueDao + private val configurationDao: TogglesConfigurationDao, + private val configurationValueDao: TogglesConfigurationValueDao, + @Assisted integerConfiguration: IntegerConfiguration, ) : ViewModel() { + @AssistedFactory + interface Factory { + fun create( + integerConfiguration: IntegerConfiguration + ): IntegerValueViewModel + } private val _state = MutableStateFlow(reduce(ViewState(), PartialViewState.Empty)) val state: StateFlow get() = _state - private val configurationId: Long = savedStateHandle.get("configurationId")!! - private val scopeId: Long = savedStateHandle.get("scopeId")!! + private val configurationId: Long = integerConfiguration.configurationId + private val scopeId: Long = integerConfiguration.scopeId private var selectedConfigurationValue: WrenchConfigurationValue? = null diff --git a/modules/oss/build.gradle.kts b/modules/oss/build.gradle.kts index d5af9f62..b362e668 100644 --- a/modules/oss/build.gradle.kts +++ b/modules/oss/build.gradle.kts @@ -2,16 +2,11 @@ plugins { id("toggles.android.module-conventions") id("toggles.ownership-conventions") alias(libs.plugins.com.google.devtools.ksp) + id("org.jetbrains.kotlin.plugin.compose") } android { namespace = "se.eelde.toggles.oss" - buildFeatures { - compose = true - } - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() - } } dependencies { diff --git a/modules/oss/src/main/java/se/eelde/toggles/oss/OssView.kt b/modules/oss/src/main/java/se/eelde/toggles/oss/OssView.kt index 3ed3bcb0..7bfc11ae 100644 --- a/modules/oss/src/main/java/se/eelde/toggles/oss/OssView.kt +++ b/modules/oss/src/main/java/se/eelde/toggles/oss/OssView.kt @@ -56,7 +56,7 @@ fun OssView(viewModel: OssProjectViewModel, modifier: Modifier = Modifier) { } @Composable -fun OssView(artifacts: ImmutableList) { +fun OssView(artifacts: ImmutableList, modifier: Modifier = Modifier) { val licenses: SnapshotStateList = remember { mutableStateListOf() } var alertTitle by remember { mutableStateOf("") } LicenseSelector(alertTitle, licenses.toPersistentList()) { @@ -64,7 +64,7 @@ fun OssView(artifacts: ImmutableList) { } LazyColumn( - modifier = Modifier.fillMaxSize(), + modifier = modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { @@ -107,9 +107,9 @@ fun OssView(artifacts: ImmutableList) { } @Composable -fun CharacterHeader(initial: String) { +fun CharacterHeader(initial: String, modifier: Modifier = Modifier) { Text( - modifier = Modifier.padding( + modifier = modifier.padding( start = 16.dp, top = 16.dp, end = 16.dp, @@ -121,7 +121,7 @@ fun CharacterHeader(initial: String) { @Preview(showSystemUi = true) @Composable -fun LicenseSelectorPreview() { +internal fun LicenseSelectorPreview() { Column(Modifier.fillMaxSize()) { LicenseSelector("Licenses", persistentListOf(License("aaa", "http://google.se"))) { } diff --git a/modules/provider/build.gradle.kts b/modules/provider/build.gradle.kts index daf67d98..d3b8946c 100644 --- a/modules/provider/build.gradle.kts +++ b/modules/provider/build.gradle.kts @@ -2,17 +2,11 @@ plugins { id("toggles.android.module-conventions") id("toggles.ownership-conventions") alias(libs.plugins.com.google.devtools.ksp) + id("org.jetbrains.kotlin.plugin.compose") } android { namespace = "se.eelde.toggles.provider" - buildFeatures { - - compose = true - } - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() - } } dependencies { diff --git a/modules/provider/src/main/java/se/eelde/toggles/provider/TogglesProvider.kt b/modules/provider/src/main/java/se/eelde/toggles/provider/TogglesProvider.kt index 660bc955..fbd86da4 100644 --- a/modules/provider/src/main/java/se/eelde/toggles/provider/TogglesProvider.kt +++ b/modules/provider/src/main/java/se/eelde/toggles/provider/TogglesProvider.kt @@ -13,38 +13,39 @@ import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent import se.eelde.toggles.core.Toggle +import se.eelde.toggles.core.TogglesConfiguration import se.eelde.toggles.database.WrenchApplication -import se.eelde.toggles.database.WrenchApplicationDao import se.eelde.toggles.database.WrenchConfiguration -import se.eelde.toggles.database.WrenchConfigurationDao import se.eelde.toggles.database.WrenchConfigurationValue -import se.eelde.toggles.database.WrenchConfigurationValueDao import se.eelde.toggles.database.WrenchPredefinedConfigurationValue -import se.eelde.toggles.database.WrenchPredefinedConfigurationValueDao import se.eelde.toggles.database.WrenchScope -import se.eelde.toggles.database.WrenchScopeDao +import se.eelde.toggles.database.dao.provider.ProviderApplicationDao +import se.eelde.toggles.database.dao.provider.ProviderConfigurationDao +import se.eelde.toggles.database.dao.provider.ProviderConfigurationValueDao +import se.eelde.toggles.database.dao.provider.ProviderPredefinedConfigurationValueDao +import se.eelde.toggles.database.dao.provider.ProviderScopeDao import se.eelde.toggles.prefs.TogglesPreferences import java.util.Date class TogglesProvider : ContentProvider() { - private val applicationDao: WrenchApplicationDao by lazy { - applicationEntryPoint.provideWrenchApplicationDao() + private val applicationDao: ProviderApplicationDao by lazy { + applicationEntryPoint.provideProviderApplicationDao() } - private val scopeDao: WrenchScopeDao by lazy { - applicationEntryPoint.provideWrenchScopeDao() + private val scopeDao: ProviderScopeDao by lazy { + applicationEntryPoint.provideProviderScopeDao() } - val configurationDao: WrenchConfigurationDao by lazy { - applicationEntryPoint.provideWrenchConfigurationDao() + val configurationDao: ProviderConfigurationDao by lazy { + applicationEntryPoint.provideProviderConfigurationDao() } - val configurationValueDao: WrenchConfigurationValueDao by lazy { - applicationEntryPoint.provideWrenchConfigurationValueDao() + val configurationValueDao: ProviderConfigurationValueDao by lazy { + applicationEntryPoint.provideProviderConfigurationValueDao() } - private val predefinedConfigurationDao: WrenchPredefinedConfigurationValueDao by lazy { + private val predefinedConfigurationDao: ProviderPredefinedConfigurationValueDao by lazy { applicationEntryPoint.providePredefinedConfigurationValueDao() } @@ -53,11 +54,11 @@ class TogglesProvider : ContentProvider() { } private val togglesPreferences: TogglesPreferences by lazy { - applicationEntryPoint.providesWrenchPreferences() + applicationEntryPoint.provideTogglesPreferences() } private val togglesUriMatcher: TogglesUriMatcher by lazy { - applicationEntryPoint.providesTogglesUriMatcher() + applicationEntryPoint.provideTogglesUriMatcher() } private val applicationEntryPoint: TogglesProviderEntryPoint by lazy { @@ -67,17 +68,17 @@ class TogglesProvider : ContentProvider() { @EntryPoint @InstallIn(SingletonComponent::class) interface TogglesProviderEntryPoint { - fun provideWrenchApplicationDao(): WrenchApplicationDao - fun provideWrenchConfigurationDao(): WrenchConfigurationDao - fun provideWrenchConfigurationValueDao(): WrenchConfigurationValueDao - fun provideWrenchScopeDao(): WrenchScopeDao - fun providePredefinedConfigurationValueDao(): WrenchPredefinedConfigurationValueDao + fun provideProviderApplicationDao(): ProviderApplicationDao + fun provideProviderConfigurationDao(): ProviderConfigurationDao + fun provideProviderConfigurationValueDao(): ProviderConfigurationValueDao + fun provideProviderScopeDao(): ProviderScopeDao + fun providePredefinedConfigurationValueDao(): ProviderPredefinedConfigurationValueDao fun providePackageManagerWrapper(): IPackageManagerWrapper - fun providesWrenchPreferences(): TogglesPreferences - fun providesTogglesUriMatcher(): TogglesUriMatcher + fun provideTogglesPreferences(): TogglesPreferences + fun provideTogglesUriMatcher(): TogglesUriMatcher } - private fun getCallingApplication(applicationDao: WrenchApplicationDao): WrenchApplication = + private fun getCallingApplication(applicationDao: ProviderApplicationDao): WrenchApplication = synchronized(this) { var wrenchApplication: WrenchApplication? = applicationDao.loadByPackageName(packageManagerWrapper.callingApplicationPackageName!!) @@ -114,7 +115,7 @@ class TogglesProvider : ContentProvider() { var cursor: Cursor? when (togglesUriMatcher.match(uri)) { - togglesUriMatcher.currentConfigurationId -> { + UriMatch.CURRENT_CONFIGURATION_ID -> { val scope = getSelectedScope(context, scopeDao, callingApplication.id) cursor = configurationDao.getToggle( java.lang.Long.valueOf(uri.lastPathSegment!!), @@ -132,7 +133,7 @@ class TogglesProvider : ContentProvider() { } } - togglesUriMatcher.currentConfigurationKey -> { + UriMatch.CURRENT_CONFIGURATION_KEY -> { // this change is experimental and might be a way // for consumers to @Suppress("ConstantConditionIf") @@ -157,6 +158,38 @@ class TogglesProvider : ContentProvider() { } } + UriMatch.CONFIGURATIONS -> { + cursor = configurationDao.getConfigurationCursor(callingApplication.id) + } + + UriMatch.CONFIGURATION_KEY -> { + cursor = configurationDao.getConfigurationCursor( + callingApplication.id, + uri.lastPathSegment!! + ) + } + + UriMatch.CONFIGURATION_ID -> { + cursor = configurationDao.getConfigurationCursor( + callingApplication.id, + uri.lastPathSegment!!.toLong() + ) + } + + UriMatch.CONFIGURATION_VALUE_KEY -> { + cursor = configurationDao.getConfigurationValueCursor( + callingApplication.id, + uri.pathSegments.get(uri.pathSegments.size - 2) + ) + } + + UriMatch.CONFIGURATION_VALUE_ID -> { + cursor = configurationDao.getConfigurationValueCursor( + callingApplication.id, + uri.pathSegments.get(uri.pathSegments.size - 2).toLong() + ) + } + else -> { throw UnsupportedOperationException("Not yet implemented $uri") } @@ -173,6 +206,7 @@ class TogglesProvider : ContentProvider() { return callingApplication.packageName == context!!.packageName } + @Suppress("LongMethod") override fun insert(uri: Uri, values: ContentValues?): Uri { val callingApplication = getCallingApplication(applicationDao) @@ -182,7 +216,7 @@ class TogglesProvider : ContentProvider() { val insertId: Long when (togglesUriMatcher.match(uri)) { - togglesUriMatcher.currentConfigurations -> { + UriMatch.CURRENT_CONFIGURATIONS -> { val toggle = Toggle.fromContentValues(values!!) var wrenchConfiguration: WrenchConfiguration? = @@ -219,7 +253,7 @@ class TogglesProvider : ContentProvider() { insertId = wrenchConfiguration.id } - togglesUriMatcher.predefinedConfigurationValues -> { + UriMatch.PREDEFINED_CONFIGURATION_VALUES -> { val fullConfig = WrenchPredefinedConfigurationValue.fromContentValues(values!!) insertId = try { predefinedConfigurationDao.insert(fullConfig) @@ -231,6 +265,17 @@ class TogglesProvider : ContentProvider() { } } + UriMatch.CONFIGURATIONS -> { + val togglesConfiguration = TogglesConfiguration.fromContentValues(values!!) + val wrenchConfiguration = WrenchConfiguration( + id = togglesConfiguration.id, + applicationId = callingApplication.id, + key = togglesConfiguration.key, + type = togglesConfiguration.type + ) + insertId = configurationDao.insert(wrenchConfiguration) + } + else -> { throw UnsupportedOperationException("Not yet implemented $uri") } @@ -265,7 +310,7 @@ class TogglesProvider : ContentProvider() { val updatedRows: Int when (togglesUriMatcher.match(uri)) { - togglesUriMatcher.currentConfigurationId -> { + UriMatch.CURRENT_CONFIGURATION_ID -> { val toggle = Toggle.fromContentValues(values!!) val scope = getSelectedScope(context, scopeDao, callingApplication.id) updatedRows = configurationValueDao.updateConfigurationValueSync( @@ -284,6 +329,16 @@ class TogglesProvider : ContentProvider() { } } + UriMatch.CONFIGURATION_ID -> { + val fromContentValues = TogglesConfiguration.fromContentValues(values!!) + updatedRows = configurationDao.updateConfiguration( + callingApplication = callingApplication.id, + id = uri.lastPathSegment!!.toLong(), + key = fromContentValues.key, + type = fromContentValues.type + ) + } + else -> { throw UnsupportedOperationException("Not yet implemented $uri") } @@ -303,7 +358,35 @@ class TogglesProvider : ContentProvider() { assertValidApiVersion(togglesPreferences, uri) } - throw UnsupportedOperationException("Not yet implemented") + when (togglesUriMatcher.match(uri)) { + UriMatch.CONFIGURATION_ID -> { + val deletedRows = + configurationDao.deleteConfiguration( + callingApplication.id, + uri.lastPathSegment!!.toLong() + ) + if (deletedRows > 0) { + context!!.contentResolver.notifyChange(uri, null) + } + return deletedRows + } + + UriMatch.CONFIGURATION_KEY -> { + val deletedRows = + configurationDao.deleteConfiguration( + callingApplication.id, + uri.lastPathSegment!! + ) + if (deletedRows > 0) { + context!!.contentResolver.notifyChange(uri, null) + } + return deletedRows + } + + else -> { + throw UnsupportedOperationException("Not yet implemented $uri") + } + } } override fun getType(uri: Uri): String { @@ -314,25 +397,30 @@ class TogglesProvider : ContentProvider() { } return when (togglesUriMatcher.match(uri)) { - togglesUriMatcher.currentConfigurations -> { + UriMatch.CURRENT_CONFIGURATIONS -> "vnd.android.cursor.dir/vnd.${context!!.packageName}.currentConfiguration" - } - togglesUriMatcher.currentConfigurationId -> { + UriMatch.CURRENT_CONFIGURATION_ID -> "vnd.android.cursor.item/vnd.${context!!.packageName}.currentConfiguration" - } - togglesUriMatcher.currentConfigurationKey -> { - "vnd.android.cursor.dir/vnd.${context!!.packageName}.currentConfiguration" - } + UriMatch.CURRENT_CONFIGURATION_KEY -> + "vnd.android.cursor.item/vnd.${context!!.packageName}.currentConfiguration" + + UriMatch.CONFIGURATIONS -> + "vnd.android.cursor.dir/vnd.${context!!.packageName}.configuration" - togglesUriMatcher.predefinedConfigurationValues -> { + UriMatch.CONFIGURATION_ID -> + "vnd.android.cursor.item/vnd.${context!!.packageName}.configuration" + + UriMatch.CONFIGURATION_KEY -> + "vnd.android.cursor.item/vnd.${context!!.packageName}.configuration" + + UriMatch.PREDEFINED_CONFIGURATION_VALUES -> "vnd.android.cursor.dir/vnd.${context!!.packageName}.predefinedConfigurationValue" - } - else -> { - throw UnsupportedOperationException("Not yet implemented") - } + UriMatch.CONFIGURATION_VALUE_ID -> "vnd.android.cursor.dir/vnd.${context!!.packageName}.configurationValue" + UriMatch.CONFIGURATION_VALUE_KEY -> "vnd.android.cursor.dir/vnd.${context!!.packageName}.configurationValue" + UriMatch.UNKNOWN -> TODO() } } @@ -343,7 +431,7 @@ class TogglesProvider : ContentProvider() { @Synchronized private fun getDefaultScope( context: Context?, - scopeDao: WrenchScopeDao?, + scopeDao: ProviderScopeDao?, applicationId: Long ): WrenchScope? { if (context == null) { @@ -364,7 +452,7 @@ class TogglesProvider : ContentProvider() { @Synchronized private fun getSelectedScope( context: Context?, - scopeDao: WrenchScopeDao?, + scopeDao: ProviderScopeDao?, applicationId: Long ): WrenchScope? { if (context == null) { diff --git a/modules/provider/src/main/java/se/eelde/toggles/provider/TogglesUriMatcher.kt b/modules/provider/src/main/java/se/eelde/toggles/provider/TogglesUriMatcher.kt index eb55604a..91c5f16a 100644 --- a/modules/provider/src/main/java/se/eelde/toggles/provider/TogglesUriMatcher.kt +++ b/modules/provider/src/main/java/se/eelde/toggles/provider/TogglesUriMatcher.kt @@ -3,51 +3,76 @@ package se.eelde.toggles.provider import android.content.UriMatcher import android.net.Uri -class TogglesUriMatcher constructor(providerAuthority: String) { - @Suppress("MagicNumber") - internal val currentConfigurationId = 1 - - @Suppress("MagicNumber") - internal val currentConfigurationKey = 2 - - @Suppress("MagicNumber") - internal val currentConfigurations = 3 - - @Suppress("MagicNumber") - internal val predefinedConfigurationValues = 5 - - @Suppress("MagicNumber") - private val applicationId = 6 +enum class UriMatch { + CURRENT_CONFIGURATION_ID, + CURRENT_CONFIGURATION_KEY, + CURRENT_CONFIGURATIONS, + CONFIGURATIONS, + CONFIGURATION_ID, + CONFIGURATION_KEY, + CONFIGURATION_VALUE_ID, + CONFIGURATION_VALUE_KEY, + PREDEFINED_CONFIGURATION_VALUES, + UNKNOWN, +} +class TogglesUriMatcher(providerAuthority: String) { private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH) - fun match(uri: Uri) = uriMatcher.match(uri) + fun match(uri: Uri): UriMatch { + val match = uriMatcher.match(uri) + return if (match == -1) { + UriMatch.UNKNOWN + } else { + UriMatch.entries[match] + } + } init { - uriMatcher.addURI( - providerAuthority, - "application/#", - applicationId - ) uriMatcher.addURI( providerAuthority, "currentConfiguration/#", - currentConfigurationId + UriMatch.CURRENT_CONFIGURATION_ID.ordinal ) uriMatcher.addURI( providerAuthority, "currentConfiguration/*", - currentConfigurationKey + UriMatch.CURRENT_CONFIGURATION_KEY.ordinal ) uriMatcher.addURI( providerAuthority, "currentConfiguration", - currentConfigurations + UriMatch.CURRENT_CONFIGURATIONS.ordinal + ) + uriMatcher.addURI( + providerAuthority, + "configuration", + UriMatch.CONFIGURATIONS.ordinal + ) + uriMatcher.addURI( + providerAuthority, + "configuration/#", + UriMatch.CONFIGURATION_ID.ordinal + ) + uriMatcher.addURI( + providerAuthority, + "configuration/*", + UriMatch.CONFIGURATION_KEY.ordinal + ) + uriMatcher.addURI( + providerAuthority, + "configuration/#/values", + UriMatch.CONFIGURATION_VALUE_ID.ordinal + ) + uriMatcher.addURI( + providerAuthority, + "configuration/*/values", + UriMatch.CONFIGURATION_VALUE_KEY.ordinal ) uriMatcher.addURI( providerAuthority, "predefinedConfigurationValue", - predefinedConfigurationValues + UriMatch.PREDEFINED_CONFIGURATION_VALUES.ordinal ) } } diff --git a/modules/routes/.gitignore b/modules/routes/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/modules/routes/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/modules/routes/OWNERSHIP.toml b/modules/routes/OWNERSHIP.toml new file mode 100644 index 00000000..9e1cee56 --- /dev/null +++ b/modules/routes/OWNERSHIP.toml @@ -0,0 +1,4 @@ +version = 1 + +[owner] +user = "@erikeelde" \ No newline at end of file diff --git a/modules/routes/build.gradle.kts b/modules/routes/build.gradle.kts new file mode 100644 index 00000000..7011d3ca --- /dev/null +++ b/modules/routes/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("toggles.android.module-conventions") + id("toggles.ownership-conventions") + alias(libs.plugins.org.jetbrains.kotlin.plugin.serialization) +} + +android { + namespace = "se.eelde.toggles.routes" +} + +dependencies { + implementation(libs.org.jetbrains.kotlinx.kotlinx.serialization.json) +} \ No newline at end of file diff --git a/modules/routes/lint-baseline.xml b/modules/routes/lint-baseline.xml new file mode 100644 index 00000000..f32fed49 --- /dev/null +++ b/modules/routes/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/modules/routes/src/main/AndroidManifest.xml b/modules/routes/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8072ee00 --- /dev/null +++ b/modules/routes/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/modules/routes/src/main/java/se/eelde/toggles/routes/Routes.kt b/modules/routes/src/main/java/se/eelde/toggles/routes/Routes.kt new file mode 100644 index 00000000..a010fe10 --- /dev/null +++ b/modules/routes/src/main/java/se/eelde/toggles/routes/Routes.kt @@ -0,0 +1,30 @@ +package se.eelde.toggles.routes + +import kotlinx.serialization.Serializable + +@Serializable +object Applications + +@Serializable +object Oss + +@Serializable +object Help + +@Serializable +data class Configurations(val applicationId: Long) + +@Serializable +data class BooleanConfiguration(val configurationId: Long, val scopeId: Long) + +@Serializable +data class IntegerConfiguration(val configurationId: Long, val scopeId: Long) + +@Serializable +data class EnumConfiguration(val configurationId: Long, val scopeId: Long) + +@Serializable +data class StringConfiguration(val configurationId: Long, val scopeId: Long) + +@Serializable +data class Scope(val applicationId: Long) diff --git a/modules/stringconfiguration/build.gradle.kts b/modules/stringconfiguration/build.gradle.kts index 4aa9d1d3..380b048e 100644 --- a/modules/stringconfiguration/build.gradle.kts +++ b/modules/stringconfiguration/build.gradle.kts @@ -2,16 +2,11 @@ plugins { id("toggles.android.module-conventions") id("toggles.ownership-conventions") alias(libs.plugins.com.google.devtools.ksp) + id("org.jetbrains.kotlin.plugin.compose") } android { namespace = "se.eelde.toggles.stringconfiguration" - buildFeatures { - compose = true - } - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() - } } dependencies { @@ -19,6 +14,7 @@ dependencies { implementation(projects.modules.composeTheme) implementation(projects.modules.database) implementation(projects.modules.provider) + implementation(projects.modules.routes) implementation(libs.androidx.core.core.ktx) implementation(libs.androidx.appcompat) implementation(libs.androidx.navigation.navigation.compose) diff --git a/modules/stringconfiguration/src/main/java/se/eelde/toggles/stringconfiguration/StringValueView.kt b/modules/stringconfiguration/src/main/java/se/eelde/toggles/stringconfiguration/StringValueView.kt index 1fc72214..c6b6fd2b 100644 --- a/modules/stringconfiguration/src/main/java/se/eelde/toggles/stringconfiguration/StringValueView.kt +++ b/modules/stringconfiguration/src/main/java/se/eelde/toggles/stringconfiguration/StringValueView.kt @@ -1,12 +1,13 @@ package se.eelde.toggles.stringconfiguration +import android.annotation.SuppressLint import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -27,13 +28,14 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.launch import se.eelde.toggles.composetheme.TogglesTheme +import se.eelde.toggles.routes.StringConfiguration @Preview @Composable -fun StringValueViewPreview() { +internal fun StringValueViewPreview() { TogglesTheme { StringValueView( - state = ViewState(title = "The title", stringValue = "This is value"), + viewState = ViewState(title = "The title", stringValue = "This is value"), setStringValue = {}, save = {}, revert = {}, @@ -42,23 +44,52 @@ fun StringValueViewPreview() { } } -@OptIn(ExperimentalMaterial3Api::class) +@SuppressLint("ComposeViewModelInjection") @Composable fun StringValueView( - modifier: Modifier = Modifier, - viewModel: FragmentStringValueViewModel = hiltViewModel(), + stringConfiguration: StringConfiguration, back: () -> Unit, ) { + val viewModel: StringValueViewModel = + hiltViewModel( + creationCallback = { factory -> + factory.create(stringConfiguration) + } + ) + val viewState by viewModel.state.collectAsStateWithLifecycle() + + val scope = rememberCoroutineScope() + + StringValueView( + viewState = viewState, + setStringValue = { viewModel.setStringValue(it) }, + save = { scope.launch { viewModel.saveClick() } }, + revert = { viewModel.revertClick() }, + popBackStack = back, + ) +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +@Suppress("LongParameterList") +internal fun StringValueView( + viewState: ViewState, + setStringValue: (String) -> Unit, + save: suspend () -> Unit, + revert: suspend () -> Unit, + popBackStack: () -> Unit, + modifier: Modifier = Modifier, +) { Scaffold( topBar = { TopAppBar( title = { Text("String configuration") }, navigationIcon = { - IconButton(onClick = { back() }) { + IconButton(onClick = { popBackStack() }) { Icon( - imageVector = Icons.Filled.ArrowBack, + imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null ) } @@ -66,59 +97,39 @@ fun StringValueView( ) }, ) { paddingValues -> - StringValueView( - state = viewState, - popBackStack = { back() }, - revert = { viewModel.revertClick() }, - save = { viewModel.saveClick() }, - setStringValue = { viewModel.setStringValue(it) }, - modifier = modifier.padding(paddingValues), - ) - } -} + val scope = rememberCoroutineScope() -@Composable -@Suppress("LongParameterList") -internal fun StringValueView( - state: ViewState, - setStringValue: (String) -> Unit, - save: suspend () -> Unit, - revert: suspend () -> Unit, - popBackStack: () -> Unit, - modifier: Modifier = Modifier, -) { - val scope = rememberCoroutineScope() - - Surface(modifier = modifier.padding(16.dp)) { - Column { - Text( - modifier = Modifier.padding(8.dp), - style = MaterialTheme.typography.headlineMedium, - text = state.title ?: "" - ) - OutlinedTextField( - modifier = Modifier - .fillMaxWidth(), - value = state.stringValue ?: "", - onValueChange = { setStringValue(it) }, - ) - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { - Button(modifier = Modifier.padding(8.dp), onClick = { - scope.launch { - revert() - popBackStack() + Surface(modifier = modifier.padding(paddingValues)) { + Column { + Text( + modifier = Modifier.padding(8.dp), + style = MaterialTheme.typography.headlineMedium, + text = viewState.title ?: "" + ) + OutlinedTextField( + modifier = Modifier + .fillMaxWidth(), + value = viewState.stringValue ?: "", + onValueChange = { setStringValue(it) }, + ) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + Button(modifier = Modifier.padding(8.dp), onClick = { + scope.launch { + revert() + popBackStack() + } + }) { + Text("Revert") } - }) { - Text("Revert") - } - Button(modifier = Modifier.padding(8.dp), onClick = { - scope.launch { - save() - popBackStack() + Button(modifier = Modifier.padding(8.dp), onClick = { + scope.launch { + save() + popBackStack() + } + }) { + Text("Save") } - }) { - Text("Save") } } } diff --git a/modules/stringconfiguration/src/main/java/se/eelde/toggles/stringconfiguration/StringValueViewModel.kt b/modules/stringconfiguration/src/main/java/se/eelde/toggles/stringconfiguration/StringValueViewModel.kt index ba8b8bf0..f2399287 100644 --- a/modules/stringconfiguration/src/main/java/se/eelde/toggles/stringconfiguration/StringValueViewModel.kt +++ b/modules/stringconfiguration/src/main/java/se/eelde/toggles/stringconfiguration/StringValueViewModel.kt @@ -1,9 +1,11 @@ package se.eelde.toggles.stringconfiguration import android.app.Application -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -12,12 +14,12 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import se.eelde.toggles.core.TogglesProviderContract -import se.eelde.toggles.database.WrenchConfigurationDao import se.eelde.toggles.database.WrenchConfigurationValue -import se.eelde.toggles.database.WrenchConfigurationValueDao +import se.eelde.toggles.database.dao.application.TogglesConfigurationDao +import se.eelde.toggles.database.dao.application.TogglesConfigurationValueDao import se.eelde.toggles.provider.notifyUpdate +import se.eelde.toggles.routes.StringConfiguration import java.util.Date -import javax.inject.Inject data class ViewState( val title: String? = null, @@ -34,23 +36,29 @@ private sealed class PartialViewState { object Reverting : PartialViewState() } -@HiltViewModel -class FragmentStringValueViewModel -@Inject internal constructor( - savedStateHandle: SavedStateHandle, +@HiltViewModel(assistedFactory = StringValueViewModel.Factory::class) +class StringValueViewModel +@AssistedInject internal constructor( private val application: Application, - private val configurationDao: WrenchConfigurationDao, - private val configurationValueDao: WrenchConfigurationValueDao + private val configurationDao: TogglesConfigurationDao, + private val configurationValueDao: TogglesConfigurationValueDao, + @Assisted stringConfiguration: StringConfiguration ) : ViewModel() { + @AssistedFactory + interface Factory { + fun create( + stringConfiguration: StringConfiguration + ): StringValueViewModel + } + + private val configurationId = stringConfiguration.configurationId + private val scopeId = stringConfiguration.scopeId private val _state = MutableStateFlow(reduce(ViewState(), PartialViewState.Empty)) val state: StateFlow get() = _state - private val configurationId: Long = savedStateHandle.get("configurationId")!! - private val scopeId: Long = savedStateHandle.get("scopeId")!! - private var selectedConfigurationValue: WrenchConfigurationValue? = null init { @@ -78,15 +86,19 @@ class FragmentStringValueViewModel is PartialViewState.NewConfiguration -> { previousState.copy(title = partialViewState.title) } + is PartialViewState.Empty -> { previousState } + is PartialViewState.Saving -> { previousState.copy(saving = true) } + is PartialViewState.Reverting -> { previousState.copy(reverting = true) } + is PartialViewState.NewConfigurationValue -> { previousState.copy(stringValue = partialViewState.value) } diff --git a/scripts/pr_check.sh b/scripts/pr_check.sh new file mode 100755 index 00000000..735c3753 --- /dev/null +++ b/scripts/pr_check.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +cd "$SCRIPT_DIR"/.. || exit + +./gradlew detekt +./gradlew versionCatalogFormat +./gradlew test -Pshared-tests-are-android-tests=false +./gradlew test -Pshared-tests-are-android-tests=true +./gradlew assembleAndroidTest -Pshared-tests-are-android-tests=false +./gradlew assembleAndroidTest -Pshared-tests-are-android-tests=true +./gradlew check +./gradlew :toggles-core:check +./gradlew :toggles-flow:check --no-configuration-cache +./gradlew :toggles-flow-noop:check +./gradlew :toggles-prefs:check --no-configuration-cache +./gradlew :toggles-prefs-noop:check diff --git a/settings.gradle.kts b/settings.gradle.kts index d596e8c9..9e1b39bc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,8 +10,15 @@ pluginManagement { } plugins { - id("com.gradle.enterprise") version "3.16.1" - id("org.gradle.toolchains.foojay-resolver-convention") version("0.7.0") + id("com.gradle.develocity") version "3.18" + id("org.gradle.toolchains.foojay-resolver-convention") version ("0.8.0") +} + +develocity { + buildScan { + termsOfUseUrl.set("https://gradle.com/help/legal-terms-of-use") + termsOfUseAgree.set("yes") + } } buildCache { @@ -25,41 +32,43 @@ buildCache { } } -private val localLibraries = true +val localLibaries = providers.gradleProperty("se-eelde-toggles-use-local-libraries") + .map { it.toBoolean() } + .orElse(false) rootProject.name = "Toggles" includeBuild("build-logic/conventions") includeBuild("toggles-core") { dependencySubstitution { - if (localLibraries) { + if (localLibaries.get()) { substitute(module("se.eelde.toggles:toggles-core")).using(project(":")) } } } includeBuild("toggles-flow") { dependencySubstitution { - if (localLibraries) { + if (localLibaries.get()) { substitute(module("se.eelde.toggles:toggles-flow")).using(project(":")) } } } includeBuild("toggles-flow-noop") { dependencySubstitution { - if (localLibraries) { + if (localLibaries.get()) { substitute(module("se.eelde.toggles:toggles-flow-noop")).using(project(":")) } } } includeBuild("toggles-prefs") { dependencySubstitution { - if (localLibraries) { + if (localLibaries.get()) { substitute(module("se.eelde.toggles:toggles-prefs")).using(project(":")) } } } includeBuild("toggles-prefs-noop") { dependencySubstitution { - if (localLibraries) { + if (localLibaries.get()) { substitute(module("se.eelde.toggles:toggles-prefs-noop")).using(project(":")) } } @@ -74,6 +83,7 @@ include( ":modules:configurations", ":modules:oss", ":modules:help", + ":modules:routes", ":modules:stringconfiguration", ":modules:booleanconfiguration", ":modules:integerconfiguration", diff --git a/toggles-app/build.gradle.kts b/toggles-app/build.gradle.kts index 01730a1b..8073e3ed 100644 --- a/toggles-app/build.gradle.kts +++ b/toggles-app/build.gradle.kts @@ -96,6 +96,26 @@ android { versionNameSuffix = " debug" } } + testOptions { + animationsDisabled = true + execution = "ANDROIDX_TEST_ORCHESTRATOR" + unitTests { + isReturnDefaultValues = true + isIncludeAndroidResources = true + } + managedDevices { + devices { + maybeCreate("pixel2api30").apply { + // Use device profiles you typically see in Android Studio. + device = "Pixel 2" + // Use only API levels 30 and higher. + apiLevel = 30 + // To include Google services, use "google". + systemImageSource = "aosp" + } + } + } + } } dependencies { @@ -116,6 +136,7 @@ dependencies { implementation(libs.androidx.compose.runtime) implementation(libs.androidx.compose.ui) implementation(libs.androidx.activity.activity.compose) + implementation(libs.androidx.core.core.splashscreen) implementation(libs.androidx.compose.foundation.foundation.layout) implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material.material.icons.extended) @@ -127,24 +148,11 @@ dependencies { implementation(libs.androidx.hilt.hilt.navigation.compose) implementation(libs.androidx.navigation.navigation.compose) implementation(libs.androidx.startup.startup.runtime) - testImplementation(libs.junit) - - testImplementation(libs.androidx.test.core.ktx) - testImplementation(libs.androidx.test.ext.truth) - testImplementation(libs.androidx.test.rules) - testImplementation(libs.androidx.test.runner) - testImplementation(libs.androidx.test.ext.junit) - testImplementation(libs.org.robolectric) - testImplementation(libs.androidx.test.espresso.espresso.core) - testImplementation(libs.androidx.arch.core.core.testing) - + implementation(projects.modules.routes) implementation(libs.com.google.dagger.hilt.android) ksp(libs.com.google.dagger.hilt.android.compiler) ksp(libs.androidx.hilt.hilt.compiler) - testImplementation(libs.com.google.dagger.hilt.android.testing) - kspTest(libs.com.google.dagger.hilt.android.compiler) - implementation(libs.androidx.lifecycle.lifecycle.common.java8) implementation(libs.com.google.dagger) @@ -163,6 +171,22 @@ dependencies { debugImplementation(libs.com.squareup.leakcanary.leakcanary.android) + testImplementation(libs.junit) + + + testImplementation(libs.androidx.test.core.ktx) + testImplementation(libs.androidx.test.ext.truth) + testImplementation(libs.androidx.test.rules) + testImplementation(libs.androidx.test.runner) + testImplementation(libs.androidx.test.ext.junit) + testImplementation(libs.org.robolectric) + testImplementation(libs.androidx.test.espresso.espresso.core) + testImplementation(libs.androidx.arch.core.core.testing) + testImplementation(libs.com.google.dagger.hilt.android.testing) + testImplementation(libs.org.jetbrains.kotlinx.kotlinx.coroutines.test) + testImplementation(libs.app.cash.turbine) + kspTest(libs.com.google.dagger.hilt.android.compiler) + androidTestImplementation(libs.androidx.test.core.ktx) androidTestImplementation(libs.androidx.test.ext.junit) androidTestImplementation(platform(libs.androidx.compose.bom)) diff --git a/toggles-app/src/main/java/se/eelde/toggles/MainActivity.kt b/toggles-app/src/main/java/se/eelde/toggles/MainActivity.kt index 0c794b15..643f3365 100644 --- a/toggles-app/src/main/java/se/eelde/toggles/MainActivity.kt +++ b/toggles-app/src/main/java/se/eelde/toggles/MainActivity.kt @@ -3,6 +3,7 @@ package se.eelde.toggles import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack @@ -14,22 +15,35 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController -import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument +import androidx.navigation.toRoute import dagger.hilt.android.AndroidEntryPoint import se.eelde.toggles.applications.applicationNavigations import se.eelde.toggles.booleanconfiguration.BooleanValueView import se.eelde.toggles.composetheme.TogglesTheme import se.eelde.toggles.configurations.configurationsNavigations import se.eelde.toggles.dialogs.scope.ScopeValueView +import se.eelde.toggles.dialogs.scope.ScopeViewModel import se.eelde.toggles.enumconfiguration.EnumValueView +import se.eelde.toggles.enumconfiguration.EnumValueViewModel import se.eelde.toggles.help.HelpView import se.eelde.toggles.integerconfiguration.IntegerValueView +import se.eelde.toggles.integerconfiguration.IntegerValueViewModel import se.eelde.toggles.oss.OssView +import se.eelde.toggles.routes.Applications +import se.eelde.toggles.routes.BooleanConfiguration +import se.eelde.toggles.routes.Configurations +import se.eelde.toggles.routes.EnumConfiguration +import se.eelde.toggles.routes.Help +import se.eelde.toggles.routes.IntegerConfiguration +import se.eelde.toggles.routes.Oss +import se.eelde.toggles.routes.Scope +import se.eelde.toggles.routes.StringConfiguration import se.eelde.toggles.stringconfiguration.StringValueView @OptIn(ExperimentalMaterial3Api::class) @@ -41,79 +55,82 @@ fun Navigation( ) { NavHost( navController = navController, - startDestination = "applications", + startDestination = Applications, modifier = modifier ) { applicationNavigations( navigateToConfigurations = { applicationId -> - navController.navigate("configurations/$applicationId") + navController.navigate(Configurations(applicationId)) }, - navigateToApplications = { navController.navigate("applications") }, - navigateToOss = { navController.navigate("oss") }, - navigateToHelp = { navController.navigate("help") }, + navigateToApplications = { navController.navigate(Applications) }, + navigateToOss = { navController.navigate(Oss) }, + navigateToHelp = { navController.navigate(Help) }, ) configurationsNavigations( navigateToBooleanConfiguration = { scopeId: Long, configurationId: Long -> - navController.navigate("configuration/$configurationId/$scopeId/boolean") + navController.navigate(BooleanConfiguration(configurationId, scopeId)) }, navigateToIntegerConfiguration = { scopeId: Long, configurationId: Long -> - navController.navigate("configuration/$configurationId/$scopeId/integer") + navController.navigate(IntegerConfiguration(configurationId, scopeId)) }, navigateToStringConfiguration = { scopeId: Long, configurationId: Long -> - navController.navigate("configuration/$configurationId/$scopeId/string") + navController.navigate( + StringConfiguration( + configurationId = configurationId, + scopeId = scopeId + ) + ) }, navigateToEnumConfiguration = { scopeId: Long, configurationId: Long -> - navController.navigate("configuration/$configurationId/$scopeId/enum") + navController.navigate(EnumConfiguration(configurationId, scopeId)) }, navigateToScopeView = { applicationId: Long -> - navController.navigate("scopes/$applicationId") + navController.navigate(Scope(applicationId)) } ) { navController.popBackStack() } - composable( - "configuration/{configurationId}/{scopeId}/boolean", - arguments = listOf( - navArgument("configurationId") { type = NavType.LongType }, - navArgument("scopeId") { type = NavType.LongType } - ) - ) { - BooleanValueView { navController.popBackStack() } + composable { backStackEntry -> + val booleanConfiguration: BooleanConfiguration = backStackEntry.toRoute() + + BooleanValueView(booleanConfiguration) { navController.popBackStack() } } - composable( - "scopes/{applicationId}", - arguments = listOf(navArgument("applicationId") { type = NavType.LongType }) - ) { - ScopeValueView { navController.popBackStack() } + composable { backStackEntry -> + val scope: Scope = backStackEntry.toRoute() + + ScopeValueView( + viewModel = hiltViewModel( + creationCallback = { factory -> + factory.create(scope) + } + ) + ) { navController.popBackStack() } } - composable( - "configuration/{configurationId}/{scopeId}/integer", - arguments = listOf( - navArgument("configurationId") { type = NavType.LongType }, - navArgument("scopeId") { type = NavType.LongType } - ) - ) { - IntegerValueView { navController.popBackStack() } + composable { backStackEntry -> + val integerConfiguration: IntegerConfiguration = backStackEntry.toRoute() + IntegerValueView( + viewModel = hiltViewModel( + creationCallback = { factory -> + factory.create(integerConfiguration) + } + ), + ) { navController.popBackStack() } } - composable( - "configuration/{configurationId}/{scopeId}/string", - arguments = listOf( - navArgument("configurationId") { type = NavType.LongType }, - navArgument("scopeId") { type = NavType.LongType } - ) - ) { - StringValueView { navController.popBackStack() } + composable { backStackEntry -> + val stringConfiguration: StringConfiguration = backStackEntry.toRoute() + + StringValueView(stringConfiguration) { navController.popBackStack() } } - composable( - "configuration/{configurationId}/{scopeId}/enum", - arguments = listOf( - navArgument("configurationId") { type = NavType.LongType }, - navArgument("scopeId") { type = NavType.LongType } - ) - ) { - EnumValueView { navController.popBackStack() } + composable { backStackEntry -> + val enumConfiguration: EnumConfiguration = backStackEntry.toRoute() + + EnumValueView( + viewModel = hiltViewModel( + creationCallback = { factory -> + factory.create(enumConfiguration) + } + ) + ) { navController.popBackStack() } } - composable( - "oss", - ) { + composable { Scaffold( topBar = { TopAppBar( @@ -133,9 +150,7 @@ fun Navigation( OssView(modifier = Modifier.padding(paddingValues)) } } - composable( - "help", - ) { + composable { HelpView { navController.popBackStack() } } } @@ -143,10 +158,13 @@ fun Navigation( @AndroidEntryPoint class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen() + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { TogglesTheme { val navController: NavHostController = rememberNavController() diff --git a/toggles-app/src/main/java/se/eelde/toggles/di/ApplicationDaoModule.kt b/toggles-app/src/main/java/se/eelde/toggles/di/ApplicationDaoModule.kt new file mode 100644 index 00000000..4a671540 --- /dev/null +++ b/toggles-app/src/main/java/se/eelde/toggles/di/ApplicationDaoModule.kt @@ -0,0 +1,30 @@ +package se.eelde.toggles.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import se.eelde.toggles.database.WrenchDatabase + +@Module +@InstallIn(SingletonComponent::class) +object ApplicationDaoModule { + @Provides + fun provideTogglesApplicationDao(wrenchDatabase: WrenchDatabase) = + wrenchDatabase.togglesApplicationDao() + + @Provides + fun provideTogglesConfigurationDao(wrenchDatabase: WrenchDatabase) = + wrenchDatabase.togglesConfigurationDao() + + @Provides + fun provideTogglesConfigurationValueDao(wrenchDatabase: WrenchDatabase) = + wrenchDatabase.togglesConfigurationValueDao() + + @Provides + fun provideTogglesScopeDao(wrenchDatabase: WrenchDatabase) = wrenchDatabase.togglesScopeDao() + + @Provides + fun providePredefinedConfigurationValueDao(wrenchDatabase: WrenchDatabase) = + wrenchDatabase.togglesPredefinedConfigurationValueDao() +} diff --git a/toggles-app/src/main/java/se/eelde/toggles/di/DatabaseModule.kt b/toggles-app/src/main/java/se/eelde/toggles/di/DatabaseModule.kt index 81a46209..20949891 100644 --- a/toggles-app/src/main/java/se/eelde/toggles/di/DatabaseModule.kt +++ b/toggles-app/src/main/java/se/eelde/toggles/di/DatabaseModule.kt @@ -26,26 +26,3 @@ object DatabaseModule { .build() } } - -@Module -@InstallIn(SingletonComponent::class) -object DaoModule { - @Provides - fun provideWrenchApplicationDao(wrenchDatabase: WrenchDatabase) = - wrenchDatabase.applicationDao() - - @Provides - fun provideWrenchConfigurationDao(wrenchDatabase: WrenchDatabase) = - wrenchDatabase.configurationDao() - - @Provides - fun provideWrenchConfigurationValueDao(wrenchDatabase: WrenchDatabase) = - wrenchDatabase.configurationValueDao() - - @Provides - fun provideWrenchScopeDao(wrenchDatabase: WrenchDatabase) = wrenchDatabase.scopeDao() - - @Provides - fun providePredefinedConfigurationValueDao(wrenchDatabase: WrenchDatabase) = - wrenchDatabase.predefinedConfigurationValueDao() -} diff --git a/toggles-app/src/main/java/se/eelde/toggles/di/ProviderDaoModule.kt b/toggles-app/src/main/java/se/eelde/toggles/di/ProviderDaoModule.kt new file mode 100644 index 00000000..f1b67320 --- /dev/null +++ b/toggles-app/src/main/java/se/eelde/toggles/di/ProviderDaoModule.kt @@ -0,0 +1,30 @@ +package se.eelde.toggles.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import se.eelde.toggles.database.WrenchDatabase + +@Module +@InstallIn(SingletonComponent::class) +object ProviderDaoModule { + @Provides + fun provideProviderApplicationDao(wrenchDatabase: WrenchDatabase) = + wrenchDatabase.providerApplicationDao() + + @Provides + fun provideProviderConfigurationDao(wrenchDatabase: WrenchDatabase) = + wrenchDatabase.providerConfigurationDao() + + @Provides + fun provideProviderConfigurationValueDao(wrenchDatabase: WrenchDatabase) = + wrenchDatabase.providerConfigurationValueDao() + + @Provides + fun provideProviderScopeDao(wrenchDatabase: WrenchDatabase) = wrenchDatabase.providerScopeDao() + + @Provides + fun provideProviderPredefinedConfigurationValueDao(wrenchDatabase: WrenchDatabase) = + wrenchDatabase.providerPredefinedConfigurationValueDao() +} diff --git a/toggles-app/src/main/java/se/eelde/toggles/dialogs/scope/ScopeView.kt b/toggles-app/src/main/java/se/eelde/toggles/dialogs/scope/ScopeView.kt index f089780c..de54af23 100644 --- a/toggles-app/src/main/java/se/eelde/toggles/dialogs/scope/ScopeView.kt +++ b/toggles-app/src/main/java/se/eelde/toggles/dialogs/scope/ScopeView.kt @@ -29,7 +29,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog -import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import se.eelde.toggles.R import se.eelde.toggles.database.WrenchScope @@ -37,8 +36,8 @@ import se.eelde.toggles.database.WrenchScope @OptIn(ExperimentalMaterial3Api::class) @Composable fun ScopeValueView( + viewModel: ScopeViewModel, modifier: Modifier = Modifier, - viewModel: ScopeViewModel = hiltViewModel(), back: () -> Unit ) { val uiState = viewModel.state.collectAsStateWithLifecycle() diff --git a/toggles-app/src/main/java/se/eelde/toggles/dialogs/scope/ScopeViewModel.kt b/toggles-app/src/main/java/se/eelde/toggles/dialogs/scope/ScopeViewModel.kt index 9dd4f4d7..1a4a310d 100644 --- a/toggles-app/src/main/java/se/eelde/toggles/dialogs/scope/ScopeViewModel.kt +++ b/toggles-app/src/main/java/se/eelde/toggles/dialogs/scope/ScopeViewModel.kt @@ -1,8 +1,10 @@ package se.eelde.toggles.dialogs.scope -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -10,9 +12,9 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import se.eelde.toggles.database.WrenchScope -import se.eelde.toggles.database.WrenchScopeDao +import se.eelde.toggles.database.dao.application.TogglesScopeDao +import se.eelde.toggles.routes.Scope import java.util.Date -import javax.inject.Inject internal data class ViewState( val title: String? = null, @@ -30,13 +32,20 @@ private sealed class PartialViewState { object Reverting : PartialViewState() } -@HiltViewModel -class ScopeViewModel @Inject internal constructor( - private val savedStateHandle: SavedStateHandle, - private val scopeDao: WrenchScopeDao +@HiltViewModel(assistedFactory = ScopeViewModel.Factory::class) +class ScopeViewModel @AssistedInject internal constructor( + private val scopeDao: TogglesScopeDao, + @Assisted scope: Scope, ) : ViewModel() { - private val applicationId: Long = savedStateHandle.get("applicationId")!! + @AssistedFactory + interface Factory { + fun create( + scope: Scope + ): ScopeViewModel + } + + private val applicationId: Long = scope.applicationId private val _state = MutableStateFlow(reduce(ViewState(), PartialViewState.Empty)) diff --git a/toggles-app/src/main/res/values-v31/styles.xml b/toggles-app/src/main/res/values-v31/styles.xml new file mode 100644 index 00000000..0dc40ac5 --- /dev/null +++ b/toggles-app/src/main/res/values-v31/styles.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/toggles-app/src/main/res/values/styles.xml b/toggles-app/src/main/res/values/styles.xml index 7a084a9c..c9ddd2f9 100644 --- a/toggles-app/src/main/res/values/styles.xml +++ b/toggles-app/src/main/res/values/styles.xml @@ -1,3 +1,4 @@ + + \ No newline at end of file diff --git a/toggles-sample/src/main/res/values/styles.xml b/toggles-sample/src/main/res/values/styles.xml index 7a084a9c..c9ddd2f9 100644 --- a/toggles-sample/src/main/res/values/styles.xml +++ b/toggles-sample/src/main/res/values/styles.xml @@ -1,3 +1,4 @@ +