diff --git a/.github/version_information/action.yml b/.github/version_information/action.yml new file mode 100644 index 00000000..20266d04 --- /dev/null +++ b/.github/version_information/action.yml @@ -0,0 +1,29 @@ +name: "Generate version information" +description: "Generate version information and expose using outputs" + +outputs: + mobile-version-code: + description: "Version code of application" + value: ${{ steps.mobile_version_code.outputs.mobile_version_code }} + + mobile-version-name: + description: "Version name of application" + value: ${{ steps.mobile_version_name.outputs.mobile_version_name }} + +runs: + using: "composite" + steps: + - name: "Generate versions" + id: version_code + run: ./scripts/generate_versions.sh + shell: bash + + - name: "Get mobile version code" + id: mobile_version_code + run: echo "mobile_version_code=$(grep 'V_VERSION_CODE=' versions.properties | cut -d'=' -f2)" >> $GITHUB_OUTPUT + shell: bash + + - name: "Get mobile version name" + id: mobile_version_name + run: echo "mobile_version_name=$(grep 'V_VERSION=' versions.properties | cut -d'=' -f2)" >> $GITHUB_OUTPUT + shell: bash diff --git a/.github/workflows/app-release.yml b/.github/workflows/app-release.yml index 9f847338..084f9640 100644 --- a/.github/workflows/app-release.yml +++ b/.github/workflows/app-release.yml @@ -7,7 +7,7 @@ jobs: # This workflow contains a single job called "build" build: # The type of runner that the job will run on - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: @@ -28,6 +28,10 @@ jobs: echo "${{ secrets.SERVICE_ACCOUNT }}" > service_account.json.asc gpg -d --passphrase "${{ secrets.GPG_ENCRYPTION_KEY }}" --batch service_account.json.asc > service_account.json + - name: "Calculate build number" + id: version_information + uses: ./.github/actions/version_information + - name: Check out java uses: actions/setup-java@v3 with: diff --git a/.github/workflows/core-release.yml b/.github/workflows/core-release.yml index f4e57e53..7d13e1a3 100644 --- a/.github/workflows/core-release.yml +++ b/.github/workflows/core-release.yml @@ -7,7 +7,7 @@ jobs: # This workflow contains a single job called "build" build: # The type of runner that the job will run on - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: diff --git a/.github/workflows/flow-noop-release.yml b/.github/workflows/flow-noop-release.yml index 1afe261f..c3816a0a 100644 --- a/.github/workflows/flow-noop-release.yml +++ b/.github/workflows/flow-noop-release.yml @@ -7,7 +7,7 @@ jobs: # This workflow contains a single job called "build" build: # The type of runner that the job will run on - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: diff --git a/.github/workflows/flow-release.yml b/.github/workflows/flow-release.yml index f27f5397..6d0a6c86 100644 --- a/.github/workflows/flow-release.yml +++ b/.github/workflows/flow-release.yml @@ -7,7 +7,7 @@ jobs: # This workflow contains a single job called "build" build: # The type of runner that the job will run on - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: diff --git a/.github/workflows/post-merge.yml b/.github/workflows/post-merge.yml index 800f597d..2ba8bc87 100644 --- a/.github/workflows/post-merge.yml +++ b/.github/workflows/post-merge.yml @@ -8,7 +8,7 @@ jobs: # This workflow contains a single job called "build" build: # The type of runner that the job will run on - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: diff --git a/.github/workflows/prefs-noop-release.yml b/.github/workflows/prefs-noop-release.yml index b06c65f2..e15effa7 100644 --- a/.github/workflows/prefs-noop-release.yml +++ b/.github/workflows/prefs-noop-release.yml @@ -7,7 +7,7 @@ jobs: # This workflow contains a single job called "build" build: # The type of runner that the job will run on - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: diff --git a/.github/workflows/prefs-release.yml b/.github/workflows/prefs-release.yml index d36f2eda..4780f024 100644 --- a/.github/workflows/prefs-release.yml +++ b/.github/workflows/prefs-release.yml @@ -7,7 +7,7 @@ jobs: # This workflow contains a single job called "build" build: # The type of runner that the job will run on - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 516cbac0..04cebfee 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -8,7 +8,7 @@ jobs: # This workflow contains a single job called "build" build: # The type of runner that the job will run on - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: @@ -30,8 +30,23 @@ jobs: - name: Setup Gradle uses: gradle/gradle-build-action@v2 - - name: Run Checks - run: ./gradlew check :toggles-core:check :toggles-flow:check :toggles-flow-noop:check :toggles-prefs:check :toggles-prefs-noop:check + - name: Run app Checks + run: ./gradlew check + + - name: Run core Checks + run: ./gradlew :toggles-core:check + + - name: Run flow Checks + run: ./gradlew :toggles-flow:check --no-configuration-cache + + - name: Run flow noop Checks + run: ./gradlew :toggles-flow-noop:check + + - name: Run prefs Checks + run: ./gradlew :toggles-prefs:check --no-configuration-cache + + - name: Run prefs noop Checks + run: ./gradlew :toggles-prefs-noop:check - name: Upload reports uses: actions/upload-artifact@v3 diff --git a/build-logic/conventions/build.gradle.kts b/build-logic/conventions/build.gradle.kts index 7f1c5fdf..3349beba 100644 --- a/build-logic/conventions/build.gradle.kts +++ b/build-logic/conventions/build.gradle.kts @@ -11,10 +11,11 @@ java { dependencies { // implementation("gradle.plugin.com.github.spotbugs.snom:spotbugs-gradle-plugin:4.7.2") - implementation("se.premex:ownership-gradle-plugin:0.0.7") + implementation("se.premex:ownership-gradle-plugin:0.0.11") 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") // https://github.com/gradle/gradle/issues/15383 implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) 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 4f0bec3f..219b6e77 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 @@ -1,3 +1,4 @@ + import org.gradle.accessors.dm.LibrariesForLibs import org.gradle.kotlin.dsl.kotlin @@ -6,19 +7,10 @@ val libs = the() plugins { id("com.android.application") kotlin("android") - kotlin("kapt") id("kotlin-android") id("toggles.detekt-conventions") } -kapt { - javacOptions { - // Increase the max count of errors from annotation processors. - // Default is 100. - option("-Xmaxerrs", 500) - } -} - android { compileSdk = 34 @@ -37,6 +29,7 @@ android { kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() } + @Suppress("UnstableApiUsage") testOptions { unitTests.isIncludeAndroidResources = true } 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 43e29f6f..5bf141ff 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 @@ -28,8 +28,8 @@ android { } } -java { - toolchain { +kotlin { + jvmToolchain { languageVersion.set(JavaLanguageVersion.of(JavaVersion.VERSION_17.toString())) vendor.set(JvmVendorSpec.AZUL) } diff --git a/build.gradle.kts b/build.gradle.kts index 69e25dfe..3801ec3e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,6 +25,8 @@ plugins { // https://github.com/Kotlin/KEEP/blob/master/proposals/explicit-api-mode.md alias(libs.plugins.com.github.triplet.play) apply (false) id("toggles.ownership-conventions") + alias(libs.plugins.com.google.devtools.ksp) apply false + alias(libs.plugins.com.autonomousapps.dependency.analysis) } fun isNonStable(version: String): Boolean { diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index 73b92489..fcb07430 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -1,3 +1,4 @@ naming: FunctionNaming: ignoreAnnotated: ['Composable'] + diff --git a/gradle.properties b/gradle.properties index 5edc5ca7..0227d7ee 100644 --- a/gradle.properties +++ b/gradle.properties @@ -37,11 +37,8 @@ android.enableJetifier=false #kotlin.caching.enabled=true # default false #kotlin.incremental.usePreciseJavaTracking=true #default false -#kapt.use.worker.api=true -#kapt.include.compile.classpath=false -#kapt.incremental.apt=true #kotlin.parallel.tasks.in.project=true org.gradle.caching=true -org.gradle.configuration-cache=false +org.gradle.configuration-cache=true org.gradle.configuration-cache.max-problems=5 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 556b1779..3e4cdd61 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,8 +3,8 @@ androidx-activity = "1.7.2" androidx-appcompat = "1.6.1" androidx-arch-core = "2.2.0" androidx-collection = "1.1.0" -androidx-compose-bom = "2023.08.00" -androidx-compose-compiler = "1.5.1" +androidx-compose-bom = "2023.10.01" +androidx-compose-compiler = "1.5.5" androidx-core = "1.9.0" androidx-hilt = "1.0.0" androidx-legacy = "1.0.0" @@ -13,12 +13,18 @@ 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.47" +com-google-dagger = "2.48" com-squareup-leakcanary = "2.8.1" com-squareup-moshi = "1.15.0" -dokka = "1.8.20" -kotlin = "1.9.0" -org-jetbrains-kotlinx = "1.7.1" +dokka = "1.9.0" +kotlin = "1.9.20" +org-jetbrains-kotlinx = "1.7.3" +agp = "8.1.2" +org-jetbrains-kotlin-android = "1.9.20" +junit = "4.13.2" +androidx-test-ext-junit = "1.1.5" +espresso-core = "3.5.1" +material = "1.9.20" [libraries] androidx-activity = { module = "androidx.activity:activity", version.ref = "androidx-activity" } @@ -53,6 +59,7 @@ androidx-compose-ui-ui-tooling-data = { module = "androidx.compose.ui:ui-tooling 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-compose-ui-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } androidx-concurrent-concurrent-futures = "androidx.concurrent:concurrent-futures:1.1.0" androidx-constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4" androidx-core = { module = "androidx.core:core", version.ref = "androidx-core" } @@ -111,7 +118,7 @@ androidx-vectordrawable-vectordrawable-animated = { module = "androidx.vectordra 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.7.0" -com-android-tools-build-gradle = "com.android.tools.build:gradle:8.1.0" +com-android-tools-build-gradle = "com.android.tools.build:gradle:8.2.0" com-google-android-datatransport-transport-api = "com.google.android.datatransport:transport-api:3.0.0" com-google-android-gms-play-services-ads-identifier = "com.google.android.gms:play-services-ads-identifier:18.0.0" com-google-android-gms-play-services-basement = "com.google.android.gms:play-services-basement:18.1.0" @@ -131,62 +138,47 @@ com-google-firebase-firebase-bom = "com.google.firebase:firebase-bom:32.2.2" com-google-firebase-firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" } com-google-firebase-firebase-crashlytics-gradle = "com.google.firebase:firebase-crashlytics-gradle:2.9.8" com-google-firebase-firebase-crashlytics-ktx = { module = "com.google.firebase:firebase-crashlytics-ktx" } -com-google-firebase-firebase-encoders = "com.google.firebase:firebase-encoders:17.0.0" -com-google-firebase-firebase-encoders-json = "com.google.firebase:firebase-encoders-json:18.0.0" -com-google-firebase-firebase-encoders-proto = "com.google.firebase:firebase-encoders-proto:16.0.0" -com-google-firebase-firebase-measurement-connector = "com.google.firebase:firebase-measurement-connector:19.0.0" 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-squareup-curtains = "com.squareup.curtains:curtains:1.2.3" com-squareup-leakcanary-leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "com-squareup-leakcanary" } -com-squareup-leakcanary-leakcanary-android-core = { module = "com.squareup.leakcanary:leakcanary-android-core", version.ref = "com-squareup-leakcanary" } -com-squareup-leakcanary-leakcanary-android-utils = { module = "com.squareup.leakcanary:leakcanary-android-utils", version.ref = "com-squareup-leakcanary" } -com-squareup-leakcanary-leakcanary-object-watcher = { module = "com.squareup.leakcanary:leakcanary-object-watcher", version.ref = "com-squareup-leakcanary" } -com-squareup-leakcanary-leakcanary-object-watcher-android = { module = "com.squareup.leakcanary:leakcanary-object-watcher-android", version.ref = "com-squareup-leakcanary" } -com-squareup-leakcanary-leakcanary-object-watcher-android-androidx = { module = "com.squareup.leakcanary:leakcanary-object-watcher-android-androidx", version.ref = "com-squareup-leakcanary" } -com-squareup-leakcanary-leakcanary-object-watcher-android-core = { module = "com.squareup.leakcanary:leakcanary-object-watcher-android-core", version.ref = "com-squareup-leakcanary" } -com-squareup-leakcanary-leakcanary-object-watcher-android-support-fragments = { module = "com.squareup.leakcanary:leakcanary-object-watcher-android-support-fragments", version.ref = "com-squareup-leakcanary" } -com-squareup-leakcanary-plumber-android = { module = "com.squareup.leakcanary:plumber-android", version.ref = "com-squareup-leakcanary" } -com-squareup-leakcanary-plumber-android-core = { module = "com.squareup.leakcanary:plumber-android-core", version.ref = "com-squareup-leakcanary" } -com-squareup-leakcanary-shark = { module = "com.squareup.leakcanary:shark", version.ref = "com-squareup-leakcanary" } -com-squareup-leakcanary-shark-android = { module = "com.squareup.leakcanary:shark-android", version.ref = "com-squareup-leakcanary" } -com-squareup-leakcanary-shark-graph = { module = "com.squareup.leakcanary:shark-graph", version.ref = "com-squareup-leakcanary" } -com-squareup-leakcanary-shark-hprof = { module = "com.squareup.leakcanary:shark-hprof", version.ref = "com-squareup-leakcanary" } -com-squareup-leakcanary-shark-log = { module = "com.squareup.leakcanary:shark-log", 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" io-gitlab-arturbosch-detekt-detekt-gradle-plugin = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.1" io-gitlab-arturbosch-detekt-detekt-formatting = "io.gitlab.arturbosch.detekt:detekt-formatting:1.23.1" io-gitlab-arturbosch-detekt-detekt-rules-libraries = "io.gitlab.arturbosch.detekt:detekt-rules-libraries:1.23.1" -javax-inject-javax-inject = "javax.inject:javax.inject:1" -junit = "junit:junit:4.13.2" -org-apache-logging-log4j-log4j-core = "org.apache.logging.log4j:log4j-core:2.17.1" -org-jetbrains-annotations = "org.jetbrains:annotations:23.0.0" +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-kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } org-jetbrains-kotlin-kotlin-parcelize-runtime = { module = "org.jetbrains.kotlin:kotlin-parcelize-runtime", version.ref = "kotlin" } org-jetbrains-kotlin-kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } org-jetbrains-kotlin-kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } org-jetbrains-kotlinx-kotlinx-collections-immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5" -org-jetbrains-kotlinx-kotlinx-coroutines-android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3" +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-jetbrains-kotlinx-kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "org-jetbrains-kotlinx" } -org-jetbrains-kotlinx-kotlinx-coroutines-core-jvm = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm", version.ref = "org-jetbrains-kotlinx" } +org-jetbrains-kotlinx-kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test" } org-robolectric = "org.robolectric:robolectric:4.10.3" -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" +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" } +espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } +#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" [plugins] +com-autonomousapps-dependency-analysis = { id = "com.autonomousapps.dependency-analysis", version = "1.26.0" } com-github-ben-manes-versions = "com.github.ben-manes.versions:0.47.0" com-github-triplet-play = "com.github.triplet.play:3.8.4" -com-gladed-androidgitversion = "com.gladed.androidgitversion:0.4.14" -com-google-dagger-hilt-android = "com.google.dagger.hilt.android:2.47" +com-google-devtools-ksp = { id = "com.google.devtools.ksp", version = "1.9.20-1.0.14" } +com-google-dagger-hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "com-google-dagger" } 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-vanniktech-maven-publish = "com.vanniktech.maven.publish:0.25.3" -dagger-hilt-android-plugin = "dagger.hilt.android.plugin:2.47" +dagger-hilt-android-plugin = { id = "dagger.hilt.android.plugin:2.48", version.ref = "com-google-dagger" } nl-littlerobots-version-catalog-update = "nl.littlerobots.version-catalog-update:0.8.1" 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" +com-android-library = { id = "com.android.library", version.ref = "agp" } +org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "org-jetbrains-kotlin-android" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180f..d64cd491 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index db9a6b82..1af9e093 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c7873..1aa94a42 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +80,11 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +131,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,11 +198,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ @@ -205,6 +214,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index ac1b06f9..6689b85b 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/modules/applications/build.gradle.kts b/modules/applications/build.gradle.kts index c5d422c8..41e51775 100644 --- a/modules/applications/build.gradle.kts +++ b/modules/applications/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("toggles.android.module-conventions") id("toggles.ownership-conventions") + id("com.google.devtools.ksp") } android { @@ -27,5 +28,5 @@ dependencies { implementation(libs.androidx.compose.ui.ui.tooling.preview) implementation(libs.androidx.startup.startup.runtime) implementation(libs.com.google.dagger.hilt.android) - kapt(libs.com.google.dagger.hilt.compiler) + ksp(libs.com.google.dagger.hilt.compiler) } \ No newline at end of file diff --git a/modules/applications/consumer-rules.pro b/modules/applications/consumer-rules.pro deleted file mode 100644 index e69de29b..00000000 diff --git a/modules/applications/proguard-rules.pro b/modules/applications/proguard-rules.pro deleted file mode 100644 index 481bb434..00000000 --- a/modules/applications/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file 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 92014259..3b705497 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 @@ -67,7 +67,7 @@ fun NavGraphBuilder.applicationNavigations( val scope = rememberCoroutineScope() TopAppBar( - title = { "Applications" }, + title = { Text("Applications") }, navigationIcon = { IconButton(onClick = { scope.launch { drawerState.open() } }) { diff --git a/modules/booleanconfiguration/.gitignore b/modules/booleanconfiguration/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/modules/booleanconfiguration/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/modules/booleanconfiguration/OWNERSHIP.toml b/modules/booleanconfiguration/OWNERSHIP.toml new file mode 100644 index 00000000..9e1cee56 --- /dev/null +++ b/modules/booleanconfiguration/OWNERSHIP.toml @@ -0,0 +1,4 @@ +version = 1 + +[owner] +user = "@erikeelde" \ No newline at end of file diff --git a/modules/booleanconfiguration/build.gradle.kts b/modules/booleanconfiguration/build.gradle.kts new file mode 100644 index 00000000..c7dcd7cc --- /dev/null +++ b/modules/booleanconfiguration/build.gradle.kts @@ -0,0 +1,35 @@ +plugins { + id("toggles.android.module-conventions") + id("toggles.ownership-conventions") + id("com.google.devtools.ksp") +} + +android { + namespace = "se.eelde.toggles.booleanconfiguration" + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() + } +} + +dependencies { + implementation(platform(libs.androidx.compose.bom)) + implementation(projects.modules.composeTheme) + implementation(projects.modules.database) + implementation(projects.modules.provider) + implementation(libs.androidx.core.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.navigation.navigation.compose) + implementation(libs.androidx.hilt.hilt.navigation.compose) + implementation(libs.androidx.lifecycle.lifecycle.runtime.compose) + implementation(libs.androidx.compose.runtime) + implementation(libs.androidx.compose.material3) + debugImplementation(libs.androidx.compose.ui.ui.tooling) + implementation(libs.androidx.compose.ui.ui.tooling.preview) + implementation(libs.androidx.startup.startup.runtime) + implementation(libs.com.google.dagger.hilt.android) + implementation(libs.se.eelde.toggles.toggles.core) + ksp(libs.com.google.dagger.hilt.compiler) +} \ No newline at end of file diff --git a/modules/booleanconfiguration/lint-baseline.xml b/modules/booleanconfiguration/lint-baseline.xml new file mode 100644 index 00000000..f32fed49 --- /dev/null +++ b/modules/booleanconfiguration/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/modules/booleanconfiguration/src/main/AndroidManifest.xml b/modules/booleanconfiguration/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/modules/booleanconfiguration/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/toggles-app/src/main/java/se/eelde/toggles/dialogs/booleanvalue/BooleanValueView.kt b/modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration/BooleanValueView.kt similarity index 56% rename from toggles-app/src/main/java/se/eelde/toggles/dialogs/booleanvalue/BooleanValueView.kt rename to modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration/BooleanValueView.kt index 62a5ce1d..a94a76e9 100644 --- a/toggles-app/src/main/java/se/eelde/toggles/dialogs/booleanvalue/BooleanValueView.kt +++ b/modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration/BooleanValueView.kt @@ -1,22 +1,67 @@ -package se.eelde.toggles.dialogs.booleanvalue +package se.eelde.toggles.booleanconfiguration 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.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment.Companion.End import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.launch +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BooleanValueView( + modifier: Modifier = Modifier, + viewModel: FragmentBooleanValueViewModel = hiltViewModel(), + back: () -> Unit, +) { + val viewState by viewModel.state.collectAsStateWithLifecycle() + Scaffold( + topBar = { + TopAppBar( + title = { Text("Boolean configuration") }, + navigationIcon = + { + IconButton(onClick = { back() }) { + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = null + ) + } + } + ) + }, + ) { paddingValues -> + BooleanValueView( + uiState = viewState, + popBackStack = { back() }, + revert = { viewModel.revertClick() }, + save = { viewModel.saveClick() }, + setBooleanValue = { viewModel.checkedChanged(it) }, + modifier = modifier.padding(paddingValues), + ) + } +} + @Composable @Suppress("LongParameterList") internal fun BooleanValueView( @@ -29,7 +74,7 @@ internal fun BooleanValueView( ) { val scope = rememberCoroutineScope() - Surface(modifier = modifier) { + Surface(modifier = modifier.padding(16.dp)) { Column { Text( modifier = Modifier.padding(8.dp), diff --git a/toggles-app/src/main/java/se/eelde/toggles/dialogs/booleanvalue/BooleanValueViewModel.kt b/modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration/BooleanValueViewModel.kt similarity index 86% rename from toggles-app/src/main/java/se/eelde/toggles/dialogs/booleanvalue/BooleanValueViewModel.kt rename to modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration/BooleanValueViewModel.kt index e2f09dda..b3ee2a74 100644 --- a/toggles-app/src/main/java/se/eelde/toggles/dialogs/booleanvalue/BooleanValueViewModel.kt +++ b/modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration/BooleanValueViewModel.kt @@ -1,4 +1,4 @@ -package se.eelde.toggles.dialogs.booleanvalue +package se.eelde.toggles.booleanconfiguration import android.app.Application import androidx.lifecycle.SavedStateHandle @@ -19,7 +19,7 @@ import se.eelde.toggles.provider.notifyUpdate import java.util.Date import javax.inject.Inject -internal data class ViewState( +data class ViewState( val title: String? = null, val checked: Boolean? = null, val saving: Boolean = false, @@ -44,7 +44,7 @@ class FragmentBooleanValueViewModel @Inject internal constructor( private val _state = MutableStateFlow(reduce(ViewState(), PartialViewState.Empty)) - internal val state: StateFlow + val state: StateFlow get() = _state private val configurationId: Long = savedStateHandle.get("configurationId")!! @@ -63,7 +63,10 @@ class FragmentBooleanValueViewModel @Inject internal constructor( if (it != null) { selectedConfigurationValue = it // viewEffects.value = Event(ViewEffect.CheckedChanged(it.value!!.toBoolean())) - _state.value = reduce(state.value, PartialViewState.NewConfigurationValue(it.value!!.toBoolean())) + _state.value = reduce( + state.value, + PartialViewState.NewConfigurationValue(it.value!!.toBoolean()) + ) } } } @@ -78,26 +81,34 @@ class FragmentBooleanValueViewModel @Inject internal constructor( 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(checked = partialViewState.checked) } } - internal suspend fun saveClick() { + suspend fun saveClick() { _state.value = reduce(state.value, PartialViewState.Saving) - updateConfigurationValue(state.value.checked.toString()).join() + state.value.checked?.let { checkedState -> + updateConfigurationValue(checkedState.toString()).join() + } ?: run { + deleteConfigurationValue() + } } - internal suspend fun revertClick() { + suspend fun revertClick() { _state.value = reduce(state.value, PartialViewState.Reverting) deleteConfigurationValue().join() } @@ -113,7 +124,11 @@ class FragmentBooleanValueViewModel @Inject internal constructor( } configurationDao.touch(configurationId, Date()) - application.contentResolver.notifyUpdate(TogglesProviderContract.toggleUri(configurationId)) + application.contentResolver.notifyUpdate( + TogglesProviderContract.toggleUri( + configurationId + ) + ) } } diff --git a/modules/compose-theme/src/main/java/se/eelde/toggles/composetheme/AppState.kt b/modules/compose-theme/src/main/java/se/eelde/toggles/composetheme/AppState.kt deleted file mode 100644 index 261835b7..00000000 --- a/modules/compose-theme/src/main/java/se/eelde/toggles/composetheme/AppState.kt +++ /dev/null @@ -1,41 +0,0 @@ -package se.eelde.toggles.composetheme - -import androidx.compose.foundation.layout.RowScope -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.navigation.NavDestination -import androidx.navigation.NavHostController -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.rememberNavController - -@Composable -fun rememberAppState( - navController: NavHostController = rememberNavController(), -): AppState { - return remember(navController) { - AppState(navController) - } -} - -data class AppBarState( - val title: String = "", - val actions: (@Composable RowScope.() -> Unit)? = null -) - -class AppState constructor( - private val navController: NavHostController, -) { - val currentDestination: NavDestination? - @Composable get() = navController - .currentBackStackEntryAsState().value?.destination - - val isRootDestination: Boolean - @Composable get() = when (currentDestination?.route) { - "applications" -> true - "help" -> true - "oss" -> true - else -> false - } - fun navigateUp() = navController.navigateUp() -} diff --git a/modules/compose-theme/src/main/java/se/eelde/toggles/composetheme/MaterialColors.kt b/modules/compose-theme/src/main/java/se/eelde/toggles/composetheme/MaterialColors.kt deleted file mode 100644 index 9c73d03b..00000000 --- a/modules/compose-theme/src/main/java/se/eelde/toggles/composetheme/MaterialColors.kt +++ /dev/null @@ -1,8 +0,0 @@ -package se.eelde.toggles.composetheme - -import androidx.compose.ui.graphics.Color - -object MaterialColors { - val black = Color(color = 0xFF000000) - val white = Color(color = 0xFFFFFFFF) -} diff --git a/modules/configurations/.gitignore b/modules/configurations/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/modules/configurations/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/modules/configurations/OWNERSHIP.toml b/modules/configurations/OWNERSHIP.toml new file mode 100644 index 00000000..9e1cee56 --- /dev/null +++ b/modules/configurations/OWNERSHIP.toml @@ -0,0 +1,4 @@ +version = 1 + +[owner] +user = "@erikeelde" \ No newline at end of file diff --git a/modules/configurations/build.gradle.kts b/modules/configurations/build.gradle.kts new file mode 100644 index 00000000..d920d38c --- /dev/null +++ b/modules/configurations/build.gradle.kts @@ -0,0 +1,35 @@ +plugins { + id("toggles.android.module-conventions") + id("toggles.ownership-conventions") + id("com.google.devtools.ksp") +} + +android { + namespace = "se.eelde.toggles.configurations" + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() + } +} + +dependencies { + implementation(platform(libs.androidx.compose.bom)) + implementation(projects.modules.composeTheme) + implementation(projects.modules.database) + implementation(libs.se.eelde.toggles.toggles.core) + implementation(libs.androidx.core.core.ktx) + implementation(libs.androidx.lifecycle.lifecycle.runtime.compose) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.navigation.navigation.compose) + implementation(libs.androidx.hilt.hilt.navigation.compose) + implementation(libs.androidx.compose.runtime) + implementation(libs.androidx.compose.material3) + debugImplementation(libs.androidx.compose.ui.ui.tooling) + implementation(libs.androidx.compose.material.material.icons.extended) + implementation(libs.androidx.compose.ui.ui.tooling.preview) + implementation(libs.androidx.startup.startup.runtime) + implementation(libs.com.google.dagger.hilt.android) + ksp(libs.com.google.dagger.hilt.compiler) +} \ No newline at end of file diff --git a/modules/configurations/lint-baseline.xml b/modules/configurations/lint-baseline.xml new file mode 100644 index 00000000..f32fed49 --- /dev/null +++ b/modules/configurations/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/modules/configurations/src/main/AndroidManifest.xml b/modules/configurations/src/main/AndroidManifest.xml new file mode 100644 index 00000000..aa656a10 --- /dev/null +++ b/modules/configurations/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/toggles-app/src/main/java/se/eelde/toggles/configurationlist/ConfigurationListView.kt b/modules/configurations/src/main/java/se/eelde/toggles/configurations/ConfigurationListView.kt similarity index 77% rename from toggles-app/src/main/java/se/eelde/toggles/configurationlist/ConfigurationListView.kt rename to modules/configurations/src/main/java/se/eelde/toggles/configurations/ConfigurationListView.kt index b9c4de94..8ffe9692 100644 --- a/toggles-app/src/main/java/se/eelde/toggles/configurationlist/ConfigurationListView.kt +++ b/modules/configurations/src/main/java/se/eelde/toggles/configurations/ConfigurationListView.kt @@ -1,4 +1,4 @@ -package se.eelde.toggles.configurationlist +package se.eelde.toggles.configurations import android.text.TextUtils import android.util.Log @@ -18,16 +18,18 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import kotlinx.coroutines.ExperimentalCoroutinesApi import se.eelde.toggles.core.Toggle import se.eelde.toggles.database.WrenchConfigurationValue import se.eelde.toggles.database.WrenchConfigurationWithValues import se.eelde.toggles.database.WrenchScope @Composable +@Suppress("LongParameterList") internal fun ConfigurationListView( - navController: NavController, + navigateToBooleanConfiguration: (scopeId: Long, configurationId: Long) -> Unit, + navigateToIntegerConfiguration: (scopeId: Long, configurationId: Long) -> Unit, + navigateToStringConfiguration: (scopeId: Long, configurationId: Long) -> Unit, + navigateToEnumConfiguration: (scopeId: Long, configurationId: Long) -> Unit, uiState: State, modifier: Modifier = Modifier, ) { @@ -46,7 +48,10 @@ internal fun ConfigurationListView( .clickable { Log.w("Clicked configuration", "") configurationClicked( - navController = navController, + navigateToBooleanConfiguration = navigateToBooleanConfiguration, + navigateToIntegerConfiguration = navigateToIntegerConfiguration, + navigateToStringConfiguration = navigateToStringConfiguration, + navigateToEnumConfiguration = navigateToEnumConfiguration, configuration = configuration, selectedScope = uiState.value.selectedScope ) @@ -107,10 +112,12 @@ private fun getItemForScope( return null } -@Suppress("LongMethod") -@OptIn(ExperimentalCoroutinesApi::class) +@Suppress("LongMethod", "LongParameterList") fun configurationClicked( - navController: NavController, + navigateToBooleanConfiguration: (scopeId: Long, configurationId: Long) -> Unit, + navigateToIntegerConfiguration: (scopeId: Long, configurationId: Long) -> Unit, + navigateToStringConfiguration: (scopeId: Long, configurationId: Long) -> Unit, + navigateToEnumConfiguration: (scopeId: Long, configurationId: Long) -> Unit, configuration: WrenchConfigurationWithValues, selectedScope: WrenchScope? ) { @@ -124,25 +131,25 @@ fun configurationClicked( configuration.type ) || TextUtils.equals(Toggle.TYPE.STRING, configuration.type) ) { - navController.navigate("configuration/${configuration.id}/${selectedScope!!.id}/string") + navigateToStringConfiguration(selectedScope!!.id, configuration.id) } else if (TextUtils.equals(Int::class.java.name, configuration.type) || TextUtils.equals( Toggle.TYPE.INTEGER, configuration.type ) ) { - navController.navigate("configuration/${configuration.id}/${selectedScope!!.id}/integer") + navigateToIntegerConfiguration(selectedScope!!.id, configuration.id) } else if (TextUtils.equals( Boolean::class.java.name, configuration.type ) || TextUtils.equals(Toggle.TYPE.BOOLEAN, configuration.type) ) { - navController.navigate("configuration/${configuration.id}/${selectedScope!!.id}/boolean") + navigateToBooleanConfiguration(selectedScope!!.id, configuration.id) } else if (TextUtils.equals(Enum::class.java.name, configuration.type) || TextUtils.equals( Toggle.TYPE.ENUM, configuration.type ) ) { - navController.navigate("configuration/${configuration.id}/${selectedScope!!.id}/enum") + navigateToEnumConfiguration(selectedScope!!.id, configuration.id) } else { // Snackbar.make( // binding.animator, diff --git a/toggles-app/src/main/java/se/eelde/toggles/configurationlist/ConfigurationViewModel.kt b/modules/configurations/src/main/java/se/eelde/toggles/configurations/ConfigurationViewModel.kt similarity index 99% rename from toggles-app/src/main/java/se/eelde/toggles/configurationlist/ConfigurationViewModel.kt rename to modules/configurations/src/main/java/se/eelde/toggles/configurations/ConfigurationViewModel.kt index ae49e5ea..97ca2f51 100644 --- a/toggles-app/src/main/java/se/eelde/toggles/configurationlist/ConfigurationViewModel.kt +++ b/modules/configurations/src/main/java/se/eelde/toggles/configurations/ConfigurationViewModel.kt @@ -1,4 +1,4 @@ -package se.eelde.toggles.configurationlist +package se.eelde.toggles.configurations import android.app.ActivityManager import android.content.Context @@ -107,7 +107,6 @@ 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 new file mode 100644 index 00000000..7665b9a7 --- /dev/null +++ b/modules/configurations/src/main/java/se/eelde/toggles/configurations/ConfigurationsEntry.kt @@ -0,0 +1,177 @@ +package se.eelde.toggles.configurations + +import android.content.Intent +import android.net.Uri +import android.provider.Settings +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.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 +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SearchBar +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +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 + +@Suppress("LongMethod", "LongParameterList") +@OptIn(ExperimentalMaterial3Api::class) +fun NavGraphBuilder.configurationsNavigations( + navigateToBooleanConfiguration: (scopeId: Long, configurationId: Long) -> Unit, + navigateToIntegerConfiguration: (scopeId: Long, configurationId: Long) -> Unit, + navigateToStringConfiguration: (scopeId: Long, configurationId: Long) -> Unit, + navigateToEnumConfiguration: (scopeId: Long, configurationId: Long) -> Unit, + navigateToScopeView: (Long) -> Unit, + back: () -> Unit, +) { + composable( + "configurations/{applicationId}", + arguments = listOf(navArgument("applicationId") { type = NavType.LongType }) + ) { + val viewModel: ConfigurationViewModel = hiltViewModel() + val uiState = viewModel.state.collectAsStateWithLifecycle() + + val launcher = + rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) {} + + val query = viewModel.getQuery().collectAsState().value + var searching = query.isNotEmpty() + + Scaffold( + topBar = { + TopAppBar( + title = { + SearchBar( + query = query, + onQueryChange = { + viewModel.setQuery(it) + }, + onSearch = {}, + placeholder = { Text("Search") }, + active = false, // active, + trailingIcon = { + if (searching) { + IconButton(onClick = { + viewModel.setQuery("") + searching = false + }) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = null + ) + } + } + }, + onActiveChange = {} + ) { + } + }, + navigationIcon = + { + IconButton(onClick = { back() }) { + Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = null) + } + }, + actions = { + var showMenu by rememberSaveable { mutableStateOf(false) } + + DropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false } + ) { + DropdownMenuItem( + text = { Text("Start application") }, + onClick = { viewModel.restartApplication(uiState.value.application!!) }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.Cyclone, + contentDescription = null + ) + } + ) + + DropdownMenuItem( + text = { Text("Appinfo") }, + onClick = { + launcher.launch( + Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts( + "package", + uiState.value.application!!.packageName, + null + ) + ) + ) + }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.Info, + contentDescription = null + ) + } + ) + DropdownMenuItem( + text = { Text("Scopes") }, + onClick = { navigateToScopeView(uiState.value.application!!.id) }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.List, + contentDescription = null + ) + } + ) + DropdownMenuItem( + text = { Text("Delete") }, + onClick = { viewModel.deleteApplication(uiState.value.application!!) }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = null + ) + } + ) + } + IconButton(onClick = { showMenu = !showMenu }) { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = null + ) + } + } + ) + }, + ) { paddingValues -> + ConfigurationListView( + navigateToBooleanConfiguration = navigateToBooleanConfiguration, + navigateToIntegerConfiguration = navigateToIntegerConfiguration, + navigateToStringConfiguration = navigateToStringConfiguration, + navigateToEnumConfiguration = navigateToEnumConfiguration, + uiState = uiState, + modifier = Modifier.padding(paddingValues), + ) + } + } +} diff --git a/modules/database/build.gradle.kts b/modules/database/build.gradle.kts index d43d1ebe..3bb82660 100644 --- a/modules/database/build.gradle.kts +++ b/modules/database/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("toggles.android.module-conventions") id("toggles.ownership-conventions") + id("com.google.devtools.ksp") } class RoomSchemaArgProvider( @@ -10,25 +11,12 @@ class RoomSchemaArgProvider( ) : CommandLineArgumentProvider { override fun asArguments(): Iterable { - // Note: If you're using KSP, you should change the line below to return - // listOf("room.schemaLocation=${schemaDir.path}") - return listOf("-Aroom.schemaLocation=${schemaDir.path}") + return listOf("room.schemaLocation=${schemaDir.path}") } } android { namespace = "se.eelde.toggles.database" - defaultConfig { - javaCompileOptions { - annotationProcessorOptions { - compilerArgumentProviders( - RoomSchemaArgProvider(File(projectDir, "schemas")) - ) - arguments["room.incremental"] = "true" - arguments["room.expandProjection"] = "true" - } - } - } testOptions { unitTests{ isIncludeAndroidResources = true @@ -39,11 +27,15 @@ android { } } +ksp { + arg(RoomSchemaArgProvider(File(projectDir, "schemas"))) +} + dependencies { implementation(libs.androidx.room.room.paging) implementation(libs.androidx.room.room.runtime) implementation(libs.androidx.room.room.ktx) - kapt(libs.androidx.room.room.compiler) + ksp(libs.androidx.room.room.compiler) implementation(libs.se.eelde.toggles.toggles.core) implementation(libs.androidx.core.core.ktx) implementation(libs.androidx.appcompat) diff --git a/modules/database/consumer-rules.pro b/modules/database/consumer-rules.pro deleted file mode 100644 index e69de29b..00000000 diff --git a/modules/database/schemas/se.eelde.toggles.database.WrenchDatabase/6.json b/modules/database/schemas/se.eelde.toggles.database.WrenchDatabase/6.json new file mode 100644 index 00000000..b5e2ac09 --- /dev/null +++ b/modules/database/schemas/se.eelde.toggles.database.WrenchDatabase/6.json @@ -0,0 +1,307 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "07a93116d6d96c975cc2ce7266b873a2", + "entities": [ + { + "tableName": "application", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `shortcutId` TEXT NOT NULL, `packageName` TEXT NOT NULL, `applicationLabel` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortcutId", + "columnName": "shortcutId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "applicationLabel", + "columnName": "applicationLabel", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_application_packageName", + "unique": true, + "columnNames": [ + "packageName" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_application_packageName` ON `${TABLE_NAME}` (`packageName`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "configuration", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `applicationId` INTEGER NOT NULL, `configurationKey` TEXT, `configurationType` TEXT NOT NULL, `lastUse` INTEGER NOT NULL, FOREIGN KEY(`applicationId`) REFERENCES `application`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "applicationId", + "columnName": "applicationId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "configurationKey", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "configurationType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUse", + "columnName": "lastUse", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_configuration_applicationId_configurationKey", + "unique": true, + "columnNames": [ + "applicationId", + "configurationKey" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_configuration_applicationId_configurationKey` ON `${TABLE_NAME}` (`applicationId`, `configurationKey`)" + } + ], + "foreignKeys": [ + { + "table": "application", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "applicationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "configurationValue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `configurationId` INTEGER NOT NULL, `value` TEXT, `scope` INTEGER NOT NULL, FOREIGN KEY(`configurationId`) REFERENCES `configuration`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "configurationId", + "columnName": "configurationId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "scope", + "columnName": "scope", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_configurationValue_configurationId_value_scope", + "unique": true, + "columnNames": [ + "configurationId", + "value", + "scope" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_configurationValue_configurationId_value_scope` ON `${TABLE_NAME}` (`configurationId`, `value`, `scope`)" + } + ], + "foreignKeys": [ + { + "table": "configuration", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "configurationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "predefinedConfigurationValue", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `configurationId` INTEGER NOT NULL, `value` TEXT, FOREIGN KEY(`configurationId`) REFERENCES `configuration`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "configurationId", + "columnName": "configurationId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_predefinedConfigurationValue_configurationId_value", + "unique": true, + "columnNames": [ + "configurationId", + "value" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_predefinedConfigurationValue_configurationId_value` ON `${TABLE_NAME}` (`configurationId`, `value`)" + } + ], + "foreignKeys": [ + { + "table": "configuration", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "configurationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "scope", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `applicationId` INTEGER NOT NULL, `name` TEXT NOT NULL, `selectedTimestamp` INTEGER NOT NULL, FOREIGN KEY(`applicationId`) REFERENCES `application`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "applicationId", + "columnName": "applicationId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timeStamp", + "columnName": "selectedTimestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_scope_applicationId_name", + "unique": true, + "columnNames": [ + "applicationId", + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_scope_applicationId_name` ON `${TABLE_NAME}` (`applicationId`, `name`)" + } + ], + "foreignKeys": [ + { + "table": "application", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "applicationId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '07a93116d6d96c975cc2ce7266b873a2')" + ] + } +} \ No newline at end of file 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 87d986e7..450644ea 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 @@ -12,7 +12,7 @@ import androidx.room.TypeConverters WrenchPredefinedConfigurationValue::class, WrenchScope::class, ], - version = 5 + version = 6 ) @TypeConverters(RoomDateConverter::class) abstract class WrenchDatabase : RoomDatabase() { diff --git a/modules/database/src/main/java/se/eelde/toggles/database/WrenchPredefinedConfigurationValue.kt b/modules/database/src/main/java/se/eelde/toggles/database/WrenchPredefinedConfigurationValue.kt index 0215d3c8..2468dc56 100644 --- a/modules/database/src/main/java/se/eelde/toggles/database/WrenchPredefinedConfigurationValue.kt +++ b/modules/database/src/main/java/se/eelde/toggles/database/WrenchPredefinedConfigurationValue.kt @@ -11,7 +11,15 @@ import se.eelde.toggles.database.tables.PredefinedConfigurationValueTable @Entity( tableName = PredefinedConfigurationValueTable.TABLE_NAME, - indices = [Index(value = arrayOf(PredefinedConfigurationValueTable.COL_CONFIG_ID))], + indices = [ + Index( + value = arrayOf( + PredefinedConfigurationValueTable.COL_CONFIG_ID, + PredefinedConfigurationValueTable.COL_VALUE + ), + unique = true + ) + ], foreignKeys = [ ForeignKey( entity = WrenchConfiguration::class, diff --git a/modules/database/src/main/java/se/eelde/toggles/database/WrenchPredefinedConfigurationValueDao.kt b/modules/database/src/main/java/se/eelde/toggles/database/WrenchPredefinedConfigurationValueDao.kt index de64213c..a3bbad85 100644 --- a/modules/database/src/main/java/se/eelde/toggles/database/WrenchPredefinedConfigurationValueDao.kt +++ b/modules/database/src/main/java/se/eelde/toggles/database/WrenchPredefinedConfigurationValueDao.kt @@ -18,10 +18,18 @@ interface WrenchPredefinedConfigurationValueDao { fun getLiveDataByConfigurationId(configurationId: Long): LiveData> @Query( - "SELECT * FROM " + PredefinedConfigurationValueTable.TABLE_NAME + " WHERE " + PredefinedConfigurationValueTable.COL_CONFIG_ID + " = (:configurationId)" + """SELECT * FROM ${PredefinedConfigurationValueTable.TABLE_NAME} WHERE ${PredefinedConfigurationValueTable.COL_CONFIG_ID} = (:configurationId)""" ) fun getByConfigurationId(configurationId: Long): Flow> + @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/migrations/Migrations.kt b/modules/database/src/main/java/se/eelde/toggles/database/migrations/Migrations.kt index 2794ba54..925cd569 100644 --- a/modules/database/src/main/java/se/eelde/toggles/database/migrations/Migrations.kt +++ b/modules/database/src/main/java/se/eelde/toggles/database/migrations/Migrations.kt @@ -11,133 +11,134 @@ object Migrations { const val databaseVersion3 = 3 const val databaseVersion4 = 4 const val databaseVersion5 = 5 + const val databaseVersion6 = 6 val MIGRATION_1_2: Migration = object : Migration(databaseVersion1, databaseVersion2) { - override fun migrate(database: SupportSQLiteDatabase) { + override fun migrate(db: SupportSQLiteDatabase) { run { val tableName = "application" val tableNameTemp = tableName + "_temp" // create new table with temp name and temp index - database.execSQL( + db.execSQL( "CREATE TABLE `$tableNameTemp` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `packageName` TEXT, `applicationLabel` TEXT)" ) - database.execSQL( + db.execSQL( "CREATE UNIQUE INDEX `index_application_temp_packageName` ON `$tableNameTemp` (`packageName`)" ) // copy data from old table + drop it - database.execSQL("INSERT INTO $tableNameTemp SELECT * FROM $tableName") - database.execSQL("DROP TABLE $tableName") + db.execSQL("INSERT INTO $tableNameTemp SELECT * FROM $tableName") + db.execSQL("DROP TABLE $tableName") // recreate index with correct name - database.execSQL("DROP INDEX `index_application_temp_packageName`") - database.execSQL( + db.execSQL("DROP INDEX `index_application_temp_packageName`") + db.execSQL( "CREATE UNIQUE INDEX `index_application_packageName` ON `$tableNameTemp` (`packageName`)" ) // rename database - database.execSQL("ALTER TABLE $tableNameTemp RENAME TO $tableName") + db.execSQL("ALTER TABLE $tableNameTemp RENAME TO $tableName") } run { val tableName = "configuration" val tableNameTemp = tableName + "_temp" - database.execSQL( + db.execSQL( "CREATE TABLE `$tableNameTemp` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `applicationId` INTEGER NOT NULL, `configurationKey` TEXT, `configurationType` TEXT, FOREIGN KEY(`applicationId`) REFERENCES `application`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )" ) - database.execSQL( + db.execSQL( "CREATE UNIQUE INDEX `index_configuration_temp_applicationId_configurationKey` ON `$tableNameTemp` (`applicationId`, `configurationKey`)" ) - database.execSQL("INSERT INTO $tableNameTemp SELECT * FROM $tableName") - database.execSQL("DROP TABLE $tableName") + db.execSQL("INSERT INTO $tableNameTemp SELECT * FROM $tableName") + db.execSQL("DROP TABLE $tableName") - database.execSQL("DROP INDEX `index_configuration_temp_applicationId_configurationKey`") - database.execSQL( + db.execSQL("DROP INDEX `index_configuration_temp_applicationId_configurationKey`") + db.execSQL( "CREATE UNIQUE INDEX `index_configuration_applicationId_configurationKey` ON `$tableNameTemp` (`applicationId`, `configurationKey`)" ) - database.execSQL("ALTER TABLE $tableNameTemp RENAME TO $tableName") + db.execSQL("ALTER TABLE $tableNameTemp RENAME TO $tableName") } run { val tableName = "configurationValue" val tableNameTemp = tableName + "_temp" - database.execSQL( + db.execSQL( "CREATE TABLE `$tableNameTemp` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `configurationId` INTEGER NOT NULL, `value` TEXT, `scope` INTEGER NOT NULL, FOREIGN KEY(`configurationId`) REFERENCES `configuration`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )" ) - database.execSQL( + db.execSQL( "CREATE UNIQUE INDEX `index_configurationValue_temp_configurationId_value_scope` ON `$tableNameTemp` (`configurationId`, `value`, `scope`)" ) - database.execSQL("INSERT INTO $tableNameTemp SELECT * FROM $tableName") - database.execSQL("DROP TABLE $tableName") + db.execSQL("INSERT INTO $tableNameTemp SELECT * FROM $tableName") + db.execSQL("DROP TABLE $tableName") - database.execSQL("DROP INDEX `index_configurationValue_temp_configurationId_value_scope`") - database.execSQL( + db.execSQL("DROP INDEX `index_configurationValue_temp_configurationId_value_scope`") + db.execSQL( "CREATE UNIQUE INDEX `index_configurationValue_configurationId_value_scope` ON `$tableNameTemp` (`configurationId`, `value`, `scope`)" ) - database.execSQL("ALTER TABLE $tableNameTemp RENAME TO $tableName") + db.execSQL("ALTER TABLE $tableNameTemp RENAME TO $tableName") } run { val tableName = "predefinedConfigurationValue" val tableNameTemp = tableName + "_temp" - database.execSQL( + db.execSQL( "CREATE TABLE `$tableNameTemp` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `configurationId` INTEGER NOT NULL, `value` TEXT, FOREIGN KEY(`configurationId`) REFERENCES `configuration`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )" ) - database.execSQL( + db.execSQL( "CREATE INDEX `index_predefinedConfigurationValue_temp_configurationId` ON `$tableNameTemp` (`configurationId`)" ) - database.execSQL("INSERT INTO $tableNameTemp SELECT * FROM $tableName") - database.execSQL("DROP TABLE $tableName") + db.execSQL("INSERT INTO $tableNameTemp SELECT * FROM $tableName") + db.execSQL("DROP TABLE $tableName") - database.execSQL("DROP INDEX `index_predefinedConfigurationValue_temp_configurationId`") - database.execSQL( + db.execSQL("DROP INDEX `index_predefinedConfigurationValue_temp_configurationId`") + db.execSQL( "CREATE INDEX `index_predefinedConfigurationValue_configurationId` ON `$tableNameTemp` (`configurationId`)" ) - database.execSQL("ALTER TABLE $tableNameTemp RENAME TO $tableName") + db.execSQL("ALTER TABLE $tableNameTemp RENAME TO $tableName") } run { val tableName = "scope" val tableNameTemp = tableName + "_temp" - database.execSQL( + db.execSQL( "CREATE TABLE `$tableNameTemp` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `applicationId` INTEGER NOT NULL, `name` TEXT, `selectedTimestamp` INTEGER, FOREIGN KEY(`applicationId`) REFERENCES `application`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )" ) - database.execSQL( + db.execSQL( "CREATE UNIQUE INDEX `index_scope_temp_applicationId_name` ON `$tableNameTemp` (`applicationId`, `name`)" ) - database.execSQL("INSERT INTO $tableNameTemp SELECT * FROM $tableName") - database.execSQL("DROP TABLE $tableName") + db.execSQL("INSERT INTO $tableNameTemp SELECT * FROM $tableName") + db.execSQL("DROP TABLE $tableName") - database.execSQL("DROP INDEX `index_scope_temp_applicationId_name`") - database.execSQL( + db.execSQL("DROP INDEX `index_scope_temp_applicationId_name`") + db.execSQL( "CREATE UNIQUE INDEX `index_scope_applicationId_name` ON `$tableNameTemp` (`applicationId`, `name`)" ) - database.execSQL("ALTER TABLE $tableNameTemp RENAME TO $tableName") + db.execSQL("ALTER TABLE $tableNameTemp RENAME TO $tableName") } } } val MIGRATION_2_3: Migration = object : Migration(databaseVersion2, databaseVersion3) { - override fun migrate(database: SupportSQLiteDatabase) { + override fun migrate(db: SupportSQLiteDatabase) { run { // Reinstate indexes - due to a bug in a previous migration (1 -> 2) these indexes may be missing. // This will recreate them in case they were missing so that migration can progress - database.execSQL("DROP INDEX IF EXISTS `index_configurationValue_configurationId_value_scope`") - database.execSQL( + db.execSQL("DROP INDEX IF EXISTS `index_configurationValue_configurationId_value_scope`") + db.execSQL( "CREATE UNIQUE INDEX `index_configurationValue_configurationId_value_scope` ON `configurationValue` (`configurationId`, `value`, `scope`)" ) - database.execSQL("DROP INDEX IF EXISTS `index_predefinedConfigurationValue_configurationId`") - database.execSQL( + db.execSQL("DROP INDEX IF EXISTS `index_predefinedConfigurationValue_configurationId`") + db.execSQL( "CREATE INDEX `index_predefinedConfigurationValue_configurationId` ON `predefinedConfigurationValue` (`configurationId`)" ) } @@ -146,88 +147,88 @@ object Migrations { val tableName = "application" val tableNameTemp = tableName + "_temp" - database.execSQL( + db.execSQL( "CREATE TABLE IF NOT EXISTS `$tableNameTemp` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `packageName` TEXT NOT NULL, `applicationLabel` TEXT NOT NULL)" ) - database.execSQL( + db.execSQL( "CREATE UNIQUE INDEX `index_application_temp_packageName` ON `$tableNameTemp` (`packageName`)" ) - database.execSQL("INSERT INTO $tableNameTemp SELECT * FROM $tableName") - database.execSQL("DROP TABLE $tableName") + db.execSQL("INSERT INTO $tableNameTemp SELECT * FROM $tableName") + db.execSQL("DROP TABLE $tableName") // recreate index with correct name - database.execSQL("DROP INDEX `index_application_temp_packageName`") - database.execSQL( + db.execSQL("DROP INDEX `index_application_temp_packageName`") + db.execSQL( "CREATE UNIQUE INDEX `index_application_packageName` ON `$tableNameTemp` (`packageName`)" ) - database.execSQL("ALTER TABLE $tableNameTemp RENAME TO $tableName") + db.execSQL("ALTER TABLE $tableNameTemp RENAME TO $tableName") } run { val tableName = "configuration" val tableNameTemp = tableName + "_temp" - database.execSQL( + db.execSQL( "CREATE TABLE IF NOT EXISTS `$tableNameTemp` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `applicationId` INTEGER NOT NULL, `configurationKey` TEXT, `configurationType` TEXT NOT NULL, `lastUse` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`applicationId`) REFERENCES `application`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )" ) - database.execSQL( + db.execSQL( "CREATE UNIQUE INDEX `index_configuration_temp_applicationId_configurationKey` ON `$tableNameTemp` (`applicationId`, `configurationKey`)" ) - database.execSQL( + db.execSQL( "INSERT INTO $tableNameTemp SELECT id, applicationId, configurationKey, configurationType, 0 FROM $tableName" ) - database.execSQL( + db.execSQL( "UPDATE $tableNameTemp SET configurationType='integer' WHERE configurationType='java.lang.Integer'" ) - database.execSQL( + db.execSQL( "UPDATE $tableNameTemp SET configurationType='string' WHERE configurationType='java.lang.String'" ) - database.execSQL( + db.execSQL( "UPDATE $tableNameTemp SET configurationType='boolean' WHERE configurationType='java.lang.Boolean'" ) - database.execSQL( + db.execSQL( "UPDATE $tableNameTemp SET configurationType='enum' WHERE configurationType='java.lang.Enum'" ) - database.execSQL("DROP TABLE $tableName") + db.execSQL("DROP TABLE $tableName") - database.execSQL("DROP INDEX `index_configuration_temp_applicationId_configurationKey`") - database.execSQL( + db.execSQL("DROP INDEX `index_configuration_temp_applicationId_configurationKey`") + db.execSQL( "CREATE UNIQUE INDEX `index_configuration_applicationId_configurationKey` ON `$tableNameTemp` (`applicationId`, `configurationKey`)" ) - database.execSQL("ALTER TABLE $tableNameTemp RENAME TO $tableName") + db.execSQL("ALTER TABLE $tableNameTemp RENAME TO $tableName") } run { val tableName = "scope" val tableNameTemp = tableName + "_temp" - database.execSQL( + db.execSQL( "CREATE TABLE IF NOT EXISTS `$tableNameTemp` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `applicationId` INTEGER NOT NULL, `name` TEXT NOT NULL, `selectedTimestamp` INTEGER NOT NULL, FOREIGN KEY(`applicationId`) REFERENCES `application`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )" ) - database.execSQL( + db.execSQL( "CREATE UNIQUE INDEX `index_scope_temp_applicationId_name` ON `$tableNameTemp` (`applicationId`, `name`)" ) - database.execSQL("INSERT INTO $tableNameTemp SELECT * FROM $tableName") - database.execSQL("DROP TABLE $tableName") + db.execSQL("INSERT INTO $tableNameTemp SELECT * FROM $tableName") + db.execSQL("DROP TABLE $tableName") - database.execSQL("DROP INDEX `index_scope_temp_applicationId_name`") - database.execSQL( + db.execSQL("DROP INDEX `index_scope_temp_applicationId_name`") + db.execSQL( "CREATE UNIQUE INDEX `index_scope_applicationId_name` ON `$tableNameTemp` (`applicationId`, `name`)" ) - database.execSQL("ALTER TABLE $tableNameTemp RENAME TO $tableName") + db.execSQL("ALTER TABLE $tableNameTemp RENAME TO $tableName") } } } val MIGRATION_3_4: Migration = object : Migration(databaseVersion3, databaseVersion4) { - override fun migrate(database: SupportSQLiteDatabase) { + override fun migrate(db: SupportSQLiteDatabase) { run { val tableName = "TogglesNotification" - database.execSQL( + db.execSQL( "CREATE TABLE IF NOT EXISTS `$tableName` (`id` INTEGER NOT NULL, `applicationId` INTEGER NOT NULL, `applicationPackageName` TEXT NOT NULL, `configurationId` INTEGER NOT NULL, `configurationKey` TEXT NOT NULL, `configurationValue` TEXT NOT NULL, `added` INTEGER NOT NULL, PRIMARY KEY(`id`))" ) } @@ -235,33 +236,58 @@ object Migrations { val tableName = "application" val tableNameTemp = tableName + "_temp" - database.execSQL( + db.execSQL( "CREATE TABLE IF NOT EXISTS `$tableNameTemp` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `shortcutId` TEXT NOT NULL, `packageName` TEXT NOT NULL, `applicationLabel` TEXT NOT NULL)" ) - database.execSQL( + db.execSQL( "CREATE UNIQUE INDEX `index_application_temp_packageName` ON `$tableNameTemp` (`packageName`)" ) - database.execSQL( + db.execSQL( "INSERT INTO $tableNameTemp (id, shortcutId, packageName, applicationLabel) SELECT id, packageName, packageName, applicationLabel FROM $tableName" ) - database.execSQL("DROP TABLE $tableName") + db.execSQL("DROP TABLE $tableName") // recreate index with correct name - database.execSQL("DROP INDEX `index_application_temp_packageName`") - database.execSQL( + db.execSQL("DROP INDEX `index_application_temp_packageName`") + db.execSQL( "CREATE UNIQUE INDEX `index_application_packageName` ON `$tableNameTemp` (`packageName`)" ) - database.execSQL("ALTER TABLE $tableNameTemp RENAME TO $tableName") + db.execSQL("ALTER TABLE $tableNameTemp RENAME TO $tableName") } } } val MIGRATION_4_5: Migration = object : Migration(databaseVersion4, databaseVersion5) { - override fun migrate(database: SupportSQLiteDatabase) { + override fun migrate(db: SupportSQLiteDatabase) { run { val tableName = "TogglesNotification" - database.execSQL("DROP TABLE IF EXISTS `$tableName`") + db.execSQL("DROP TABLE IF EXISTS `$tableName`") + } + } + } + val MIGRATION_5_6: Migration = object : Migration(databaseVersion5, databaseVersion6) { + override fun migrate(db: SupportSQLiteDatabase) { + run { + val tableName = "predefinedConfigurationValue" + val tableNameTemp = tableName + "_temp" + + val newIndexName = "index_predefinedConfigurationValue_configurationId_value" + + // create new table with temp name and temp index + db.execSQL( + "CREATE TABLE IF NOT EXISTS `$tableNameTemp` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `configurationId` INTEGER NOT NULL, `value` TEXT, FOREIGN KEY(`configurationId`) REFERENCES `configuration`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )" + ) + db.execSQL( + "CREATE UNIQUE INDEX IF NOT EXISTS `$newIndexName` ON `$tableNameTemp` (`configurationId`, `value`);" + ) + + // copy data from old table + drop it + db.execSQL("INSERT INTO $tableNameTemp SELECT * FROM $tableName GROUP BY configurationId, value") + db.execSQL("DROP TABLE $tableName") + + // rename database + db.execSQL("ALTER TABLE $tableNameTemp RENAME TO $tableName") } } } diff --git a/modules/database/src/test/java/se/eelde/toggles/database/DatabaseHelper.kt b/modules/database/src/test/java/se/eelde/toggles/database/DatabaseHelper.kt index 140abf13..6ce88a3d 100644 --- a/modules/database/src/test/java/se/eelde/toggles/database/DatabaseHelper.kt +++ b/modules/database/src/test/java/se/eelde/toggles/database/DatabaseHelper.kt @@ -1,3 +1,5 @@ +@file:Suppress("MaximumLineLength") + package se.eelde.toggles.database import android.content.ContentValues @@ -7,9 +9,51 @@ import androidx.sqlite.db.SupportSQLiteDatabase import org.junit.Assert.assertNotNull import se.eelde.toggles.database.tables.ApplicationTable import se.eelde.toggles.database.tables.ConfigurationTable +import se.eelde.toggles.database.tables.PredefinedConfigurationValueTable object DatabaseHelper { - fun insertWrenchConfiguration( + fun insertPredefinedConfigurationValue( + db: SupportSQLiteDatabase, + configurationId: Long, + value: String, + ): Long { + val configurationValues = ContentValues() + configurationValues.put(PredefinedConfigurationValueTable.COL_CONFIG_ID, configurationId) + configurationValues.put(PredefinedConfigurationValueTable.COL_VALUE, value) + configurationValues.put(PredefinedConfigurationValueTable.COL_VALUE, value) + return db.insert( + PredefinedConfigurationValueTable.TABLE_NAME, + CONFLICT_FAIL, + configurationValues + ) + } + + fun getPredefinedConfigurationValueByConfigurationId( + db: SupportSQLiteDatabase, + configId: Long + ): List { + @Suppress("MaxLineLength") + val query = db.query( + "SELECT * FROM ${PredefinedConfigurationValueTable.TABLE_NAME} WHERE ${PredefinedConfigurationValueTable.COL_CONFIG_ID} = ?", + arrayOf(configId) + ) + assertNotNull(query) + + val values = mutableListOf() + while (query.moveToNext()) { + val id = + query.getLong(query.getColumnIndexOrThrow(PredefinedConfigurationValueTable.COL_ID)) + val configurationId = + query.getLong(query.getColumnIndexOrThrow(PredefinedConfigurationValueTable.COL_CONFIG_ID)) + val value = + query.getString(query.getColumnIndexOrThrow(PredefinedConfigurationValueTable.COL_VALUE)) + values.add(WrenchPredefinedConfigurationValue(id, configurationId, value)) + } + + return values.toList() + } + + fun insertConfigurationPre3( db: SupportSQLiteDatabase, applicationId: Long, key: String, @@ -22,7 +66,22 @@ object DatabaseHelper { return db.insert(ConfigurationTable.TABLE_NAME, CONFLICT_FAIL, configurationValues) } - fun getWrenchConfigurationByKey(db: SupportSQLiteDatabase, key: String): Cursor { + fun insertConfiguration( + db: SupportSQLiteDatabase, + applicationId: Long, + key: String, + type: String, + lastUse: Long + ): Long { + val configurationValues = ContentValues() + configurationValues.put(ConfigurationTable.COL_APP_ID, applicationId) + configurationValues.put(ConfigurationTable.COL_KEY, key) + configurationValues.put(ConfigurationTable.COL_TYPE, type) + configurationValues.put("lastUse", lastUse) + return db.insert(ConfigurationTable.TABLE_NAME, CONFLICT_FAIL, configurationValues) + } + + fun getConfigurationByKey(db: SupportSQLiteDatabase, key: String): Cursor { val query = db.query( "SELECT * FROM " + ConfigurationTable.TABLE_NAME + " WHERE " + ConfigurationTable.COL_KEY + "=?", arrayOf(key) @@ -31,7 +90,7 @@ object DatabaseHelper { return query } - fun insertWrenchApplication( + fun insertApplicationPre4( db: SupportSQLiteDatabase, applicationLabel: String, packageName: String @@ -42,7 +101,20 @@ object DatabaseHelper { return db.insert(ApplicationTable.TABLE_NAME, CONFLICT_FAIL, applicationValues) } - fun getWrenchApplication(db: SupportSQLiteDatabase, applicationId: Long): WrenchApplication { + fun insertApplication( + db: SupportSQLiteDatabase, + applicationLabel: String, + packageName: String, + shortcutId: String, + ): Long { + val applicationValues = ContentValues() + applicationValues.put(ApplicationTable.COL_APP_LABEL, applicationLabel) + applicationValues.put(ApplicationTable.COL_PACK_NAME, packageName) + applicationValues.put(ApplicationTable.COL_SHORTCUT_ID, shortcutId) + return db.insert(ApplicationTable.TABLE_NAME, CONFLICT_FAIL, applicationValues) + } + + fun getApplication(db: SupportSQLiteDatabase, applicationId: Long): WrenchApplication { val cursor = db.query( "SELECT * FROM " + ApplicationTable.TABLE_NAME + " WHERE " + ApplicationTable.COL_ID + "=?", arrayOf(applicationId) diff --git a/modules/database/src/test/java/se/eelde/toggles/database/MigrationTests.kt b/modules/database/src/test/java/se/eelde/toggles/database/MigrationTests.kt index 38d7e7dd..7ffcc4d5 100644 --- a/modules/database/src/test/java/se/eelde/toggles/database/MigrationTests.kt +++ b/modules/database/src/test/java/se/eelde/toggles/database/MigrationTests.kt @@ -15,6 +15,7 @@ import se.eelde.toggles.database.migrations.Migrations.MIGRATION_1_2 import se.eelde.toggles.database.migrations.Migrations.MIGRATION_2_3 import se.eelde.toggles.database.migrations.Migrations.MIGRATION_3_4 import se.eelde.toggles.database.migrations.Migrations.MIGRATION_4_5 +import se.eelde.toggles.database.migrations.Migrations.MIGRATION_5_6 import se.eelde.toggles.database.tables.ConfigurationTable import java.io.IOException @@ -50,32 +51,32 @@ class MigrationTests { // Create the database with version 2 val originalDb = testHelper.createDatabase(TEST_DB_NAME, 2) - val testApplicationId = DatabaseHelper.insertWrenchApplication( + val testApplicationId = DatabaseHelper.insertApplicationPre4( originalDb, "TestApplication", - "com.izettle.wrench.testapplication" + "se.eelde.toggles.application" ) // insert data - DatabaseHelper.insertWrenchConfiguration( + DatabaseHelper.insertConfigurationPre3( originalDb, testApplicationId, "Integerkey", Toggle.TYPE.INTEGER ) - DatabaseHelper.insertWrenchConfiguration( + DatabaseHelper.insertConfigurationPre3( originalDb, testApplicationId, "Stringkey", Toggle.TYPE.STRING ) - DatabaseHelper.insertWrenchConfiguration( + DatabaseHelper.insertConfigurationPre3( originalDb, testApplicationId, "Booleankey", Toggle.TYPE.BOOLEAN ) - DatabaseHelper.insertWrenchConfiguration( + DatabaseHelper.insertConfigurationPre3( originalDb, testApplicationId, "Enumkey", @@ -86,7 +87,7 @@ class MigrationTests { val migratedDb = testHelper.runMigrationsAndValidate(TEST_DB_NAME, 3, true, MIGRATION_2_3) - var cursor = DatabaseHelper.getWrenchConfigurationByKey(migratedDb, "Integerkey") + var cursor = DatabaseHelper.getConfigurationByKey(migratedDb, "Integerkey") assertTrue(cursor.moveToFirst()) assertEquals( Toggle.TYPE.INTEGER, @@ -94,7 +95,7 @@ class MigrationTests { ) cursor.close() - cursor = DatabaseHelper.getWrenchConfigurationByKey(migratedDb, "Stringkey") + cursor = DatabaseHelper.getConfigurationByKey(migratedDb, "Stringkey") assertTrue(cursor.moveToFirst()) assertEquals( Toggle.TYPE.STRING, @@ -102,7 +103,7 @@ class MigrationTests { ) cursor.close() - cursor = DatabaseHelper.getWrenchConfigurationByKey(migratedDb, "Booleankey") + cursor = DatabaseHelper.getConfigurationByKey(migratedDb, "Booleankey") assertTrue(cursor.moveToFirst()) assertEquals( Toggle.TYPE.BOOLEAN, @@ -110,7 +111,7 @@ class MigrationTests { ) cursor.close() - cursor = DatabaseHelper.getWrenchConfigurationByKey(migratedDb, "Enumkey") + cursor = DatabaseHelper.getConfigurationByKey(migratedDb, "Enumkey") assertTrue(cursor.moveToFirst()) assertEquals( Toggle.TYPE.ENUM, @@ -124,7 +125,7 @@ class MigrationTests { fun test3to4() { val originalDb = testHelper.createDatabase(TEST_DB_NAME, 3) - val testApplicationId = DatabaseHelper.insertWrenchApplication( + DatabaseHelper.insertApplicationPre4( originalDb, "TestApplication", "se.eelde.toggles.testapplication" @@ -132,7 +133,7 @@ class MigrationTests { val migratedDb = testHelper.runMigrationsAndValidate(TEST_DB_NAME, 4, true, MIGRATION_3_4) - val application = DatabaseHelper.getWrenchApplication(migratedDb, testApplicationId) + val application = DatabaseHelper.getApplication(migratedDb, 1) assertEquals("TestApplication", application.applicationLabel) assertEquals("se.eelde.toggles.testapplication", application.shortcutId) @@ -146,6 +147,64 @@ class MigrationTests { testHelper.runMigrationsAndValidate(TEST_DB_NAME, 5, true, MIGRATION_4_5) } + @Test + @Throws(IOException::class) + fun test5to6() { + testHelper.createDatabase(TEST_DB_NAME, 5) + testHelper.runMigrationsAndValidate(TEST_DB_NAME, 6, true, MIGRATION_5_6) + } + + @Test + @Throws(IOException::class) + fun test5to6WithDuplicates() { + val originalDb = testHelper.createDatabase(TEST_DB_NAME, 5) + assertEquals( + 1, + DatabaseHelper.insertApplication( + originalDb, + "TestApplication", + "se.eelde.toggles.application", + "se.eelde.toggles.application", + ) + ) + + // insert data + assertEquals( + 1, + DatabaseHelper.insertConfiguration( + originalDb, + 1, + "MyEnum", + Toggle.TYPE.ENUM, + 0, + ) + ) + + assertEquals(1, DatabaseHelper.insertPredefinedConfigurationValue(originalDb, 1, "a")) + assertEquals(2, DatabaseHelper.insertPredefinedConfigurationValue(originalDb, 1, "a")) + assertEquals(3, DatabaseHelper.insertPredefinedConfigurationValue(originalDb, 1, "b")) + assertEquals(4, DatabaseHelper.insertPredefinedConfigurationValue(originalDb, 1, "b")) + assertEquals(5, DatabaseHelper.insertPredefinedConfigurationValue(originalDb, 1, "b")) + assertEquals(6, DatabaseHelper.insertPredefinedConfigurationValue(originalDb, 1, "c")) + + val valuesBefore = + DatabaseHelper.getPredefinedConfigurationValueByConfigurationId( + db = originalDb, + configId = 1 + ) + assertEquals(6, valuesBefore.size) + + val migratedDb = testHelper.runMigrationsAndValidate(TEST_DB_NAME, 6, true, MIGRATION_5_6) + + val values = + DatabaseHelper.getPredefinedConfigurationValueByConfigurationId( + db = migratedDb, + configId = 1 + ) + + assertEquals(3, values.size) + } + companion object { private const val TEST_DB_NAME = "test_db" } diff --git a/modules/enumconfiguration/.gitignore b/modules/enumconfiguration/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/modules/enumconfiguration/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/modules/enumconfiguration/OWNERSHIP.toml b/modules/enumconfiguration/OWNERSHIP.toml new file mode 100644 index 00000000..9e1cee56 --- /dev/null +++ b/modules/enumconfiguration/OWNERSHIP.toml @@ -0,0 +1,4 @@ +version = 1 + +[owner] +user = "@erikeelde" \ No newline at end of file diff --git a/modules/enumconfiguration/build.gradle.kts b/modules/enumconfiguration/build.gradle.kts new file mode 100644 index 00000000..112937a3 --- /dev/null +++ b/modules/enumconfiguration/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + id("toggles.android.module-conventions") + id("toggles.ownership-conventions") + id("com.google.devtools.ksp") +} + +android { + namespace = "se.eelde.toggles.enumconfiguration" + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() + } +} + +dependencies { + implementation(platform(libs.androidx.compose.bom)) + implementation(projects.modules.composeTheme) + implementation(projects.modules.database) + implementation(projects.modules.provider) + implementation(libs.androidx.core.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.navigation.navigation.compose) + implementation(libs.androidx.hilt.hilt.navigation.compose) + implementation(libs.androidx.lifecycle.lifecycle.runtime.compose) + implementation(libs.androidx.compose.runtime) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material.material.icons.extended) + debugImplementation(libs.androidx.compose.ui.ui.tooling) + implementation(libs.androidx.compose.ui.ui.tooling.preview) + implementation(libs.androidx.startup.startup.runtime) + implementation(libs.com.google.dagger.hilt.android) + implementation(libs.se.eelde.toggles.toggles.core) + ksp(libs.com.google.dagger.hilt.compiler) +} \ No newline at end of file diff --git a/modules/enumconfiguration/lint-baseline.xml b/modules/enumconfiguration/lint-baseline.xml new file mode 100644 index 00000000..f32fed49 --- /dev/null +++ b/modules/enumconfiguration/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/modules/enumconfiguration/src/main/AndroidManifest.xml b/modules/enumconfiguration/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/modules/enumconfiguration/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file 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 new file mode 100644 index 00000000..04df1c05 --- /dev/null +++ b/modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration/EnumValueView.kt @@ -0,0 +1,121 @@ +package se.eelde.toggles.enumconfiguration + +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.foundation.lazy.LazyColumn +import androidx.compose.foundation.selection.selectable +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Link +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun EnumValueView( + modifier: Modifier = Modifier, + viewModel: FragmentEnumValueViewModel = hiltViewModel(), + back: () -> Unit, +) { + val viewState by viewModel.state.collectAsStateWithLifecycle() + Scaffold( + topBar = { + TopAppBar( + title = { Text("Enum configuration") }, + navigationIcon = + { + IconButton(onClick = { back() }) { + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = null + ) + } + } + ) + }, + ) { paddingValues -> + EnumValueView( + state = viewState, + setEnumValue = { viewModel.saveClick(it) }, + revert = { viewModel.revertClick() }, + popBackStack = { back() }, + modifier = modifier.padding(paddingValues) + ) + } +} + +@Composable +internal fun EnumValueView( + state: ViewState, + setEnumValue: suspend (String) -> 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 ?: "" + ) + LazyColumn { + state.configurationValues.forEach { wrenchPredefinedConfigurationValue -> + item { + val selected = + wrenchPredefinedConfigurationValue.value == state.selectedConfigurationValue?.value + ListItem( + modifier = Modifier.selectable( + selected = selected + ) { + scope.launch { + setEnumValue(wrenchPredefinedConfigurationValue.value.toString()) + } + }, + headlineContent = { Text(text = wrenchPredefinedConfigurationValue.value.toString()) }, + leadingContent = { + if (selected) { + Icon( + imageVector = Icons.Filled.Link, + contentDescription = null + ) + } + } + ) + } + } + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + Button(modifier = Modifier.padding(8.dp), onClick = { + scope.launch { + revert() + popBackStack() + } + }) { + Text("Revert") + } + } + } + } +} diff --git a/toggles-app/src/main/java/se/eelde/toggles/dialogs/enumvalue/EnumValueViewModel.kt b/modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration/EnumValueViewModel.kt similarity index 85% rename from toggles-app/src/main/java/se/eelde/toggles/dialogs/enumvalue/EnumValueViewModel.kt rename to modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration/EnumValueViewModel.kt index 19b0bcae..186403a9 100644 --- a/toggles-app/src/main/java/se/eelde/toggles/dialogs/enumvalue/EnumValueViewModel.kt +++ b/modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration/EnumValueViewModel.kt @@ -1,7 +1,6 @@ -package se.eelde.toggles.dialogs.enumvalue +package se.eelde.toggles.enumconfiguration import android.app.Application -import androidx.lifecycle.LiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -23,21 +22,25 @@ import se.eelde.toggles.provider.notifyUpdate import java.util.Date import javax.inject.Inject -internal data class ViewState( +data class ViewState( val title: String? = null, + val selectedConfigurationValue: WrenchConfigurationValue? = null, val configurationValues: List = listOf(), val saving: Boolean = false, val reverting: Boolean = false ) internal sealed class PartialViewState { - object Empty : PartialViewState() + data object Empty : PartialViewState() data class NewConfiguration(val title: String?) : PartialViewState() - class ConfigurationValues(val configurationValues: List) : + data class ConfigurationValues(val configurationValues: List) : PartialViewState() - object Saving : PartialViewState() - object Reverting : PartialViewState() + data class SelectedConfigurationValue(val selectedConfigurationValue: WrenchConfigurationValue) : + PartialViewState() + + data object Saving : PartialViewState() + data object Reverting : PartialViewState() } @HiltViewModel @@ -49,13 +52,9 @@ class FragmentEnumValueViewModel @Inject internal constructor( private val predefinedConfigurationValueDao: WrenchPredefinedConfigurationValueDao ) : ViewModel() { - internal val predefinedValues: LiveData> by lazy { - predefinedConfigurationValueDao.getLiveDataByConfigurationId(configurationId) - } - private val _state = MutableStateFlow(reduce(ViewState(), PartialViewState.Empty)) - internal val state: StateFlow + val state: StateFlow get() = _state private val configurationId: Long = savedStateHandle.get("configurationId")!! @@ -79,6 +78,8 @@ class FragmentEnumValueViewModel @Inject internal constructor( configurationValueDao.getConfigurationValue(configurationId, scopeId).collect { if (it != null) { selectedConfigurationValue = it + _state.value = + reduce(_state.value, PartialViewState.SelectedConfigurationValue(it)) } } } @@ -89,22 +90,30 @@ class FragmentEnumValueViewModel @Inject internal constructor( 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.ConfigurationValues -> { previousState.copy(configurationValues = partialViewState.configurationValues) } + + is PartialViewState.SelectedConfigurationValue -> { + previousState.copy(selectedConfigurationValue = partialViewState.selectedConfigurationValue) + } } } - internal suspend fun saveClick(value: String) { + suspend fun saveClick(value: String) { _state.value = reduce(_state.value, PartialViewState.Saving) updateConfigurationValue(value).join() application.contentResolver.notifyUpdate( @@ -114,7 +123,7 @@ class FragmentEnumValueViewModel @Inject internal constructor( ) } - internal suspend fun revertClick() { + suspend fun revertClick() { _state.value = reduce(_state.value, PartialViewState.Reverting) deleteConfigurationValue().join() application.contentResolver.notifyInsert( diff --git a/modules/help/.gitignore b/modules/help/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/modules/help/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/modules/help/OWNERSHIP.toml b/modules/help/OWNERSHIP.toml new file mode 100644 index 00000000..9e1cee56 --- /dev/null +++ b/modules/help/OWNERSHIP.toml @@ -0,0 +1,4 @@ +version = 1 + +[owner] +user = "@erikeelde" \ No newline at end of file diff --git a/modules/help/build.gradle.kts b/modules/help/build.gradle.kts new file mode 100644 index 00000000..bef1b2e1 --- /dev/null +++ b/modules/help/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + id("toggles.android.module-conventions") + id("toggles.ownership-conventions") + id("com.google.devtools.ksp") +} + +android { + namespace = "se.eelde.toggles.help" + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() + } +} + +dependencies { + implementation(platform(libs.androidx.compose.bom)) + implementation(projects.modules.composeTheme) + implementation(libs.androidx.core.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.navigation.navigation.compose) + implementation(libs.androidx.hilt.hilt.navigation.compose) + implementation(libs.androidx.compose.runtime) + implementation(libs.androidx.compose.material3) + debugImplementation(libs.androidx.compose.ui.ui.tooling) + implementation(libs.androidx.compose.ui.ui.tooling.preview) + implementation(libs.androidx.startup.startup.runtime) + implementation(libs.com.google.dagger.hilt.android) + ksp(libs.com.google.dagger.hilt.compiler) +} \ No newline at end of file diff --git a/modules/help/lint-baseline.xml b/modules/help/lint-baseline.xml new file mode 100644 index 00000000..f32fed49 --- /dev/null +++ b/modules/help/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/modules/help/src/main/AndroidManifest.xml b/modules/help/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/modules/help/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/modules/help/src/main/java/se/eelde/toggles/help/HelpView.kt b/modules/help/src/main/java/se/eelde/toggles/help/HelpView.kt new file mode 100644 index 00000000..66ad120b --- /dev/null +++ b/modules/help/src/main/java/se/eelde/toggles/help/HelpView.kt @@ -0,0 +1,46 @@ +package se.eelde.toggles.help + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HelpView(modifier: Modifier = Modifier, back: () -> Unit) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(id = R.string.help)) }, + navigationIcon = + { + IconButton(onClick = { back() }) { + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = null + ) + } + } + ) + }, + ) { paddingValues -> + Box( + modifier + .fillMaxSize() + .padding(paddingValues) + ) { + Text(text = "Implementation", color = Color.White) + } + } +} diff --git a/modules/help/src/main/res/values/strings.xml b/modules/help/src/main/res/values/strings.xml new file mode 100644 index 00000000..190d4ef5 --- /dev/null +++ b/modules/help/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Help + \ No newline at end of file diff --git a/modules/integerconfiguration/.gitignore b/modules/integerconfiguration/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/modules/integerconfiguration/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/modules/integerconfiguration/OWNERSHIP.toml b/modules/integerconfiguration/OWNERSHIP.toml new file mode 100644 index 00000000..9e1cee56 --- /dev/null +++ b/modules/integerconfiguration/OWNERSHIP.toml @@ -0,0 +1,4 @@ +version = 1 + +[owner] +user = "@erikeelde" \ No newline at end of file diff --git a/modules/integerconfiguration/build.gradle.kts b/modules/integerconfiguration/build.gradle.kts new file mode 100644 index 00000000..0ed1d402 --- /dev/null +++ b/modules/integerconfiguration/build.gradle.kts @@ -0,0 +1,35 @@ +plugins { + id("toggles.android.module-conventions") + id("toggles.ownership-conventions") + id("com.google.devtools.ksp") +} + +android { + namespace = "se.eelde.toggles.integerconfiguration" + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() + } +} + +dependencies { + implementation(platform(libs.androidx.compose.bom)) + implementation(projects.modules.composeTheme) + implementation(projects.modules.database) + implementation(projects.modules.provider) + implementation(libs.androidx.core.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.navigation.navigation.compose) + implementation(libs.androidx.hilt.hilt.navigation.compose) + implementation(libs.androidx.lifecycle.lifecycle.runtime.compose) + implementation(libs.androidx.compose.runtime) + implementation(libs.androidx.compose.material3) + debugImplementation(libs.androidx.compose.ui.ui.tooling) + implementation(libs.androidx.compose.ui.ui.tooling.preview) + implementation(libs.androidx.startup.startup.runtime) + implementation(libs.com.google.dagger.hilt.android) + implementation(libs.se.eelde.toggles.toggles.core) + ksp(libs.com.google.dagger.hilt.compiler) +} \ No newline at end of file diff --git a/modules/integerconfiguration/lint-baseline.xml b/modules/integerconfiguration/lint-baseline.xml new file mode 100644 index 00000000..f32fed49 --- /dev/null +++ b/modules/integerconfiguration/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/modules/integerconfiguration/src/main/AndroidManifest.xml b/modules/integerconfiguration/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/modules/integerconfiguration/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/toggles-app/src/main/java/se/eelde/toggles/dialogs/integervalue/IntegerValueView.kt b/modules/integerconfiguration/src/main/java/se/eelde/toggles/integerconfiguration/IntegerValueView.kt similarity index 55% rename from toggles-app/src/main/java/se/eelde/toggles/dialogs/integervalue/IntegerValueView.kt rename to modules/integerconfiguration/src/main/java/se/eelde/toggles/integerconfiguration/IntegerValueView.kt index 49ce1e27..aca18cba 100644 --- a/toggles-app/src/main/java/se/eelde/toggles/dialogs/integervalue/IntegerValueView.kt +++ b/modules/integerconfiguration/src/main/java/se/eelde/toggles/integerconfiguration/IntegerValueView.kt @@ -1,4 +1,4 @@ -package se.eelde.toggles.dialogs.integervalue +package se.eelde.toggles.integerconfiguration import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -6,24 +6,39 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope 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() { IntegerValueView( - uiState = ViewState(title = "Integer value", integerValue = 5, saving = false, reverting = false), + uiState = ViewState( + title = "Integer value", + integerValue = 5, + saving = false, + reverting = false + ), popBackStack = {}, revert = {}, save = {}, @@ -31,6 +46,41 @@ fun IntegerValueViewPreview() { ) } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun IntegerValueView( + modifier: Modifier = Modifier, + viewModel: FragmentIntegerValueViewModel = hiltViewModel(), + back: () -> Unit, +) { + val viewState by viewModel.state.collectAsStateWithLifecycle() + Scaffold( + topBar = { + TopAppBar( + title = { Text("Integer configuration") }, + navigationIcon = + { + IconButton(onClick = { back() }) { + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = null + ) + } + } + ) + }, + ) { paddingValues -> + IntegerValueView( + uiState = viewState, + popBackStack = { back() }, + revert = { viewModel.revertClick() }, + save = { viewModel.saveClick() }, + setIntegerValue = { viewModel.setIntegerValue(it) }, + modifier = modifier.padding(paddingValues), + ) + } +} + @Composable @Suppress("LongParameterList") internal fun IntegerValueView( @@ -43,7 +93,7 @@ internal fun IntegerValueView( ) { val scope = rememberCoroutineScope() - Surface(modifier = modifier) { + Surface(modifier = modifier.padding(16.dp)) { Column { Text( modifier = Modifier.padding(8.dp), @@ -55,7 +105,11 @@ internal fun IntegerValueView( modifier = Modifier .fillMaxWidth(), value = if (uiState.integerValue != null) uiState.integerValue.toString() else "", - onValueChange = { setIntegerValue(it.toInt()) }, + onValueChange = { + try { + setIntegerValue(it.toInt()) + } catch (_: NumberFormatException) { } + }, ) Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { Button(modifier = Modifier.padding(8.dp), onClick = { diff --git a/toggles-app/src/main/java/se/eelde/toggles/dialogs/integervalue/IntegerValueViewModel.kt b/modules/integerconfiguration/src/main/java/se/eelde/toggles/integerconfiguration/IntegerValueViewModel.kt similarity index 96% rename from toggles-app/src/main/java/se/eelde/toggles/dialogs/integervalue/IntegerValueViewModel.kt rename to modules/integerconfiguration/src/main/java/se/eelde/toggles/integerconfiguration/IntegerValueViewModel.kt index 26e6a648..df7826bc 100644 --- a/toggles-app/src/main/java/se/eelde/toggles/dialogs/integervalue/IntegerValueViewModel.kt +++ b/modules/integerconfiguration/src/main/java/se/eelde/toggles/integerconfiguration/IntegerValueViewModel.kt @@ -1,4 +1,4 @@ -package se.eelde.toggles.dialogs.integervalue +package se.eelde.toggles.integerconfiguration import android.app.Application import androidx.lifecycle.SavedStateHandle @@ -19,7 +19,7 @@ import se.eelde.toggles.provider.notifyUpdate import java.util.Date import javax.inject.Inject -internal data class ViewState( +data class ViewState( val title: String? = null, val integerValue: Int? = null, val saving: Boolean = false, @@ -44,7 +44,7 @@ class FragmentIntegerValueViewModel @Inject internal constructor( private val _state = MutableStateFlow(reduce(ViewState(), PartialViewState.Empty)) - internal val state: StateFlow + val state: StateFlow get() = _state private val configurationId: Long = savedStateHandle.get("configurationId")!! @@ -101,7 +101,7 @@ class FragmentIntegerValueViewModel @Inject internal constructor( _state.value = reduce(state.value, PartialViewState.NewConfigurationValue(newValue)) } - internal suspend fun saveClick() { + suspend fun saveClick() { _state.value = reduce(state.value, PartialViewState.Saving) // updateConfigurationValue(state.value.integerValue).join() @@ -112,7 +112,7 @@ class FragmentIntegerValueViewModel @Inject internal constructor( } } - internal suspend fun revertClick() { + suspend fun revertClick() { _state.value = reduce(state.value, PartialViewState.Reverting) deleteConfigurationValue().join() } diff --git a/modules/oss/build.gradle.kts b/modules/oss/build.gradle.kts index 5e2516d0..383e6a41 100644 --- a/modules/oss/build.gradle.kts +++ b/modules/oss/build.gradle.kts @@ -3,6 +3,7 @@ plugins { id("toggles.ownership-conventions") //id("app.cash.licensee") //id("se.premex.gross") version "0.1.0" + id("com.google.devtools.ksp") } android { @@ -16,7 +17,7 @@ android { } dependencies { - kapt(libs.com.squareup.moshi.moshi.kotlin.codegen) + ksp(libs.com.squareup.moshi.moshi.kotlin.codegen) implementation(libs.androidx.lifecycle.lifecycle.viewmodel.ktx) implementation(platform(libs.androidx.compose.bom)) @@ -38,7 +39,7 @@ dependencies { implementation(libs.androidx.navigation.navigation.compose) implementation(libs.com.google.dagger.hilt.android) - kapt(libs.com.google.dagger.hilt.compiler) + ksp(libs.com.google.dagger.hilt.compiler) implementation(libs.androidx.hilt.hilt.navigation.compose) implementation(libs.com.squareup.moshi) diff --git a/modules/provider/.gitignore b/modules/provider/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/modules/provider/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/modules/provider/OWNERSHIP.toml b/modules/provider/OWNERSHIP.toml new file mode 100644 index 00000000..9e1cee56 --- /dev/null +++ b/modules/provider/OWNERSHIP.toml @@ -0,0 +1,4 @@ +version = 1 + +[owner] +user = "@erikeelde" \ No newline at end of file diff --git a/modules/provider/build.gradle.kts b/modules/provider/build.gradle.kts new file mode 100644 index 00000000..27bd1b02 --- /dev/null +++ b/modules/provider/build.gradle.kts @@ -0,0 +1,35 @@ +plugins { + id("toggles.android.module-conventions") + id("toggles.ownership-conventions") + id("com.google.devtools.ksp") +} + +android { + namespace = "se.eelde.toggles.provider" + buildFeatures { + + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() + } +} + +dependencies { + implementation(platform(libs.androidx.compose.bom)) + implementation(projects.modules.composeTheme) + implementation(projects.modules.database) + implementation(libs.androidx.core.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.se.eelde.toggles.toggles.core) + implementation(libs.se.eelde.toggles.toggles.prefs) + implementation(libs.androidx.navigation.navigation.compose) + implementation(libs.androidx.hilt.hilt.navigation.compose) + implementation(libs.androidx.compose.runtime) + implementation(libs.androidx.compose.material3) + debugImplementation(libs.androidx.compose.ui.ui.tooling) + implementation(libs.androidx.compose.ui.ui.tooling.preview) + implementation(libs.androidx.startup.startup.runtime) + implementation(libs.com.google.dagger.hilt.android) + ksp(libs.com.google.dagger.hilt.compiler) +} \ No newline at end of file diff --git a/modules/provider/lint-baseline.xml b/modules/provider/lint-baseline.xml new file mode 100644 index 00000000..f32fed49 --- /dev/null +++ b/modules/provider/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/modules/provider/src/main/AndroidManifest.xml b/modules/provider/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/modules/provider/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/toggles-app/src/main/java/se/eelde/toggles/provider/ContentResolverExtensions.kt b/modules/provider/src/main/java/se/eelde/toggles/provider/ContentResolverExtensions.kt similarity index 100% rename from toggles-app/src/main/java/se/eelde/toggles/provider/ContentResolverExtensions.kt rename to modules/provider/src/main/java/se/eelde/toggles/provider/ContentResolverExtensions.kt diff --git a/toggles-app/src/main/java/se/eelde/toggles/provider/PackageManagerWrapper.kt b/modules/provider/src/main/java/se/eelde/toggles/provider/PackageManagerWrapper.kt similarity index 100% rename from toggles-app/src/main/java/se/eelde/toggles/provider/PackageManagerWrapper.kt rename to modules/provider/src/main/java/se/eelde/toggles/provider/PackageManagerWrapper.kt diff --git a/toggles-app/src/main/java/se/eelde/toggles/provider/TogglesApiVersion.kt b/modules/provider/src/main/java/se/eelde/toggles/provider/TogglesApiVersion.kt similarity index 100% rename from toggles-app/src/main/java/se/eelde/toggles/provider/TogglesApiVersion.kt rename to modules/provider/src/main/java/se/eelde/toggles/provider/TogglesApiVersion.kt diff --git a/toggles-app/src/main/java/se/eelde/toggles/provider/TogglesProvider.kt b/modules/provider/src/main/java/se/eelde/toggles/provider/TogglesProvider.kt similarity index 88% rename from toggles-app/src/main/java/se/eelde/toggles/provider/TogglesProvider.kt rename to modules/provider/src/main/java/se/eelde/toggles/provider/TogglesProvider.kt index 9b9c805a..660bc955 100644 --- a/toggles-app/src/main/java/se/eelde/toggles/provider/TogglesProvider.kt +++ b/modules/provider/src/main/java/se/eelde/toggles/provider/TogglesProvider.kt @@ -12,12 +12,6 @@ import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent -import se.eelde.toggles.BuildConfig -import se.eelde.toggles.TogglesUriMatcher -import se.eelde.toggles.TogglesUriMatcher.Companion.CURRENT_CONFIGURATIONS -import se.eelde.toggles.TogglesUriMatcher.Companion.CURRENT_CONFIGURATION_ID -import se.eelde.toggles.TogglesUriMatcher.Companion.CURRENT_CONFIGURATION_KEY -import se.eelde.toggles.TogglesUriMatcher.Companion.PREDEFINED_CONFIGURATION_VALUES import se.eelde.toggles.core.Toggle import se.eelde.toggles.database.WrenchApplication import se.eelde.toggles.database.WrenchApplicationDao @@ -62,6 +56,10 @@ class TogglesProvider : ContentProvider() { applicationEntryPoint.providesWrenchPreferences() } + private val togglesUriMatcher: TogglesUriMatcher by lazy { + applicationEntryPoint.providesTogglesUriMatcher() + } + private val applicationEntryPoint: TogglesProviderEntryPoint by lazy { EntryPointAccessors.fromApplication(context!!, TogglesProviderEntryPoint::class.java) } @@ -76,6 +74,7 @@ class TogglesProvider : ContentProvider() { fun providePredefinedConfigurationValueDao(): WrenchPredefinedConfigurationValueDao fun providePackageManagerWrapper(): IPackageManagerWrapper fun providesWrenchPreferences(): TogglesPreferences + fun providesTogglesUriMatcher(): TogglesUriMatcher } private fun getCallingApplication(applicationDao: WrenchApplicationDao): WrenchApplication = @@ -114,8 +113,8 @@ class TogglesProvider : ContentProvider() { var cursor: Cursor? - when (uriMatcher.match(uri)) { - CURRENT_CONFIGURATION_ID -> { + when (togglesUriMatcher.match(uri)) { + togglesUriMatcher.currentConfigurationId -> { val scope = getSelectedScope(context, scopeDao, callingApplication.id) cursor = configurationDao.getToggle( java.lang.Long.valueOf(uri.lastPathSegment!!), @@ -133,7 +132,7 @@ class TogglesProvider : ContentProvider() { } } - CURRENT_CONFIGURATION_KEY -> { + togglesUriMatcher.currentConfigurationKey -> { // this change is experimental and might be a way // for consumers to @Suppress("ConstantConditionIf") @@ -152,7 +151,8 @@ class TogglesProvider : ContentProvider() { cursor.close() val defaultScope = getDefaultScope(context, scopeDao, callingApplication.id) - cursor = configurationDao.getToggle(uri.lastPathSegment!!, defaultScope!!.id) + cursor = + configurationDao.getToggle(uri.lastPathSegment!!, defaultScope!!.id) } } } @@ -170,7 +170,7 @@ class TogglesProvider : ContentProvider() { } private fun isTogglesApplication(callingApplication: WrenchApplication): Boolean { - return callingApplication.packageName == BuildConfig.APPLICATION_ID + return callingApplication.packageName == context!!.packageName } override fun insert(uri: Uri, values: ContentValues?): Uri { @@ -181,8 +181,8 @@ class TogglesProvider : ContentProvider() { } val insertId: Long - when (uriMatcher.match(uri)) { - CURRENT_CONFIGURATIONS -> { + when (togglesUriMatcher.match(uri)) { + togglesUriMatcher.currentConfigurations -> { val toggle = Toggle.fromContentValues(values!!) var wrenchConfiguration: WrenchConfiguration? = @@ -219,9 +219,16 @@ class TogglesProvider : ContentProvider() { insertId = wrenchConfiguration.id } - PREDEFINED_CONFIGURATION_VALUES -> { + togglesUriMatcher.predefinedConfigurationValues -> { val fullConfig = WrenchPredefinedConfigurationValue.fromContentValues(values!!) - insertId = predefinedConfigurationDao.insert(fullConfig) + insertId = try { + predefinedConfigurationDao.insert(fullConfig) + } catch (_: SQLiteConstraintException) { + predefinedConfigurationDao.getByConfigurationAndValueId( + fullConfig.configurationId, + fullConfig.value!! + ).id + } } else -> { @@ -257,8 +264,8 @@ class TogglesProvider : ContentProvider() { } val updatedRows: Int - when (uriMatcher.match(uri)) { - CURRENT_CONFIGURATION_ID -> { + when (togglesUriMatcher.match(uri)) { + togglesUriMatcher.currentConfigurationId -> { val toggle = Toggle.fromContentValues(values!!) val scope = getSelectedScope(context, scopeDao, callingApplication.id) updatedRows = configurationValueDao.updateConfigurationValueSync( @@ -306,21 +313,21 @@ class TogglesProvider : ContentProvider() { assertValidApiVersion(togglesPreferences, uri) } - return when (uriMatcher.match(uri)) { - CURRENT_CONFIGURATIONS -> { - "vnd.android.cursor.dir/vnd.${BuildConfig.APPLICATION_ID}.currentConfiguration" + return when (togglesUriMatcher.match(uri)) { + togglesUriMatcher.currentConfigurations -> { + "vnd.android.cursor.dir/vnd.${context!!.packageName}.currentConfiguration" } - CURRENT_CONFIGURATION_ID -> { - "vnd.android.cursor.item/vnd.${BuildConfig.APPLICATION_ID}.currentConfiguration" + togglesUriMatcher.currentConfigurationId -> { + "vnd.android.cursor.item/vnd.${context!!.packageName}.currentConfiguration" } - CURRENT_CONFIGURATION_KEY -> { - "vnd.android.cursor.dir/vnd.${BuildConfig.APPLICATION_ID}.currentConfiguration" + togglesUriMatcher.currentConfigurationKey -> { + "vnd.android.cursor.dir/vnd.${context!!.packageName}.currentConfiguration" } - PREDEFINED_CONFIGURATION_VALUES -> { - "vnd.android.cursor.dir/vnd.${BuildConfig.APPLICATION_ID}.predefinedConfigurationValue" + togglesUriMatcher.predefinedConfigurationValues -> { + "vnd.android.cursor.dir/vnd.${context!!.packageName}.predefinedConfigurationValue" } else -> { @@ -331,8 +338,6 @@ class TogglesProvider : ContentProvider() { companion object { - private val uriMatcher = TogglesUriMatcher.getTogglesUriMatcher() - private const val oneSecond = 1000 @Synchronized 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 new file mode 100644 index 00000000..eb55604a --- /dev/null +++ b/modules/provider/src/main/java/se/eelde/toggles/provider/TogglesUriMatcher.kt @@ -0,0 +1,53 @@ +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 + + private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH) + + fun match(uri: Uri) = uriMatcher.match(uri) + + init { + uriMatcher.addURI( + providerAuthority, + "application/#", + applicationId + ) + uriMatcher.addURI( + providerAuthority, + "currentConfiguration/#", + currentConfigurationId + ) + uriMatcher.addURI( + providerAuthority, + "currentConfiguration/*", + currentConfigurationKey + ) + uriMatcher.addURI( + providerAuthority, + "currentConfiguration", + currentConfigurations + ) + uriMatcher.addURI( + providerAuthority, + "predefinedConfigurationValue", + predefinedConfigurationValues + ) + } +} diff --git a/modules/stringconfiguration/.gitignore b/modules/stringconfiguration/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/modules/stringconfiguration/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/modules/stringconfiguration/OWNERSHIP.toml b/modules/stringconfiguration/OWNERSHIP.toml new file mode 100644 index 00000000..9e1cee56 --- /dev/null +++ b/modules/stringconfiguration/OWNERSHIP.toml @@ -0,0 +1,4 @@ +version = 1 + +[owner] +user = "@erikeelde" \ No newline at end of file diff --git a/modules/stringconfiguration/build.gradle.kts b/modules/stringconfiguration/build.gradle.kts new file mode 100644 index 00000000..fe9b0e36 --- /dev/null +++ b/modules/stringconfiguration/build.gradle.kts @@ -0,0 +1,35 @@ +plugins { + id("toggles.android.module-conventions") + id("toggles.ownership-conventions") + id("com.google.devtools.ksp") +} + +android { + namespace = "se.eelde.toggles.stringconfiguration" + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() + } +} + +dependencies { + implementation(platform(libs.androidx.compose.bom)) + implementation(projects.modules.composeTheme) + implementation(projects.modules.database) + implementation(projects.modules.provider) + implementation(libs.androidx.core.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.navigation.navigation.compose) + implementation(libs.androidx.hilt.hilt.navigation.compose) + implementation(libs.androidx.lifecycle.lifecycle.runtime.compose) + implementation(libs.androidx.compose.runtime) + implementation(libs.androidx.compose.material3) + debugImplementation(libs.androidx.compose.ui.ui.tooling) + implementation(libs.androidx.compose.ui.ui.tooling.preview) + implementation(libs.androidx.startup.startup.runtime) + implementation(libs.com.google.dagger.hilt.android) + implementation(libs.se.eelde.toggles.toggles.core) + ksp(libs.com.google.dagger.hilt.compiler) +} \ No newline at end of file diff --git a/modules/stringconfiguration/lint-baseline.xml b/modules/stringconfiguration/lint-baseline.xml new file mode 100644 index 00000000..f32fed49 --- /dev/null +++ b/modules/stringconfiguration/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/modules/stringconfiguration/src/main/AndroidManifest.xml b/modules/stringconfiguration/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/modules/stringconfiguration/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/toggles-app/src/main/java/se/eelde/toggles/dialogs/stringvalue/StringValueView.kt b/modules/stringconfiguration/src/main/java/se/eelde/toggles/stringconfiguration/StringValueView.kt similarity index 60% rename from toggles-app/src/main/java/se/eelde/toggles/dialogs/stringvalue/StringValueView.kt rename to modules/stringconfiguration/src/main/java/se/eelde/toggles/stringconfiguration/StringValueView.kt index 655188ca..1fc72214 100644 --- a/toggles-app/src/main/java/se/eelde/toggles/dialogs/stringvalue/StringValueView.kt +++ b/modules/stringconfiguration/src/main/java/se/eelde/toggles/stringconfiguration/StringValueView.kt @@ -1,20 +1,30 @@ -package se.eelde.toggles.dialogs.stringvalue +package se.eelde.toggles.stringconfiguration 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.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier 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 import se.eelde.toggles.composetheme.TogglesTheme @@ -32,6 +42,41 @@ fun StringValueViewPreview() { } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun StringValueView( + modifier: Modifier = Modifier, + viewModel: FragmentStringValueViewModel = hiltViewModel(), + back: () -> Unit, +) { + val viewState by viewModel.state.collectAsStateWithLifecycle() + Scaffold( + topBar = { + TopAppBar( + title = { Text("String configuration") }, + navigationIcon = + { + IconButton(onClick = { back() }) { + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = null + ) + } + } + ) + }, + ) { paddingValues -> + StringValueView( + state = viewState, + popBackStack = { back() }, + revert = { viewModel.revertClick() }, + save = { viewModel.saveClick() }, + setStringValue = { viewModel.setStringValue(it) }, + modifier = modifier.padding(paddingValues), + ) + } +} + @Composable @Suppress("LongParameterList") internal fun StringValueView( @@ -44,7 +89,7 @@ internal fun StringValueView( ) { val scope = rememberCoroutineScope() - Surface(modifier = modifier) { + Surface(modifier = modifier.padding(16.dp)) { Column { Text( modifier = Modifier.padding(8.dp), diff --git a/toggles-app/src/main/java/se/eelde/toggles/dialogs/stringvalue/StringValueViewModel.kt b/modules/stringconfiguration/src/main/java/se/eelde/toggles/stringconfiguration/StringValueViewModel.kt similarity index 96% rename from toggles-app/src/main/java/se/eelde/toggles/dialogs/stringvalue/StringValueViewModel.kt rename to modules/stringconfiguration/src/main/java/se/eelde/toggles/stringconfiguration/StringValueViewModel.kt index 6ec453bf..ba8b8bf0 100644 --- a/toggles-app/src/main/java/se/eelde/toggles/dialogs/stringvalue/StringValueViewModel.kt +++ b/modules/stringconfiguration/src/main/java/se/eelde/toggles/stringconfiguration/StringValueViewModel.kt @@ -1,4 +1,4 @@ -package se.eelde.toggles.dialogs.stringvalue +package se.eelde.toggles.stringconfiguration import android.app.Application import androidx.lifecycle.SavedStateHandle @@ -19,7 +19,7 @@ import se.eelde.toggles.provider.notifyUpdate import java.util.Date import javax.inject.Inject -internal data class ViewState( +data class ViewState( val title: String? = null, val stringValue: String? = null, val saving: Boolean = false, @@ -45,7 +45,7 @@ class FragmentStringValueViewModel private val _state = MutableStateFlow(reduce(ViewState(), PartialViewState.Empty)) - internal val state: StateFlow + val state: StateFlow get() = _state private val configurationId: Long = savedStateHandle.get("configurationId")!! @@ -97,7 +97,7 @@ class FragmentStringValueViewModel _state.value = reduce(state.value, PartialViewState.NewConfigurationValue(newValue)) } - internal suspend fun saveClick() { + suspend fun saveClick() { _state.value = reduce(state.value, PartialViewState.Saving) state.value.stringValue?.let { updateConfigurationValue(it).join() @@ -106,7 +106,7 @@ class FragmentStringValueViewModel } } - internal suspend fun revertClick() { + suspend fun revertClick() { _state.value = reduce(state.value, PartialViewState.Reverting) deleteConfigurationValue() } diff --git a/scripts/generate_versions.sh b/scripts/generate_versions.sh new file mode 100755 index 00000000..30c25516 --- /dev/null +++ b/scripts/generate_versions.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +# Overwrite versions.properties at the beginning +: > versions.properties + +# Define the prefixes +prefixes=("v") + +# Get the latest commit hash +hash=$(git rev-parse --short HEAD) + +# Check if the working directory is dirty +git diff --quiet || dirty=true +dirty=${dirty:-false} + +# Loop over the prefixes +for prefix in "${prefixes[@]}"; do + + # Get the latest git tag from current branch that matches the current prefix + tag=$(git describe --tags --match "${prefix}[0-9]*" --abbrev=0) + + # Trim the prefix from the start of the tag name + version="${tag:1}" + + # Split the version into major, minor, and patch parts + IFS='.' read -r -a version_parts <<< "$version" + major="${version_parts[0]}" + minor="${version_parts[1]}" + patch="${version_parts[2]}" + + # Calculate the version code using arithmetic expansion + version_code=$((major * 100000 + minor * 1000 + patch * 10)) + + # Get the number of commits since the tag + commit_count=$(git rev-list --count "${tag}"..HEAD) + + # Create the debug version string and append -dirty if necessary + debug_version="${version}-${commit_count}-g${hash}" + debug_version_suffix="${commit_count}-g${hash}" + [[ $dirty == true ]] && debug_version+="-dirty" + [[ $dirty == true ]] && debug_version_suffix+="-dirty" + + # Convert prefix to uppercase for property names + prefix_upper=$(echo "$prefix" | tr '[:lower:]' '[:upper:]') + + # Print the result + printf "%s_VERSION=%s\n" "$prefix_upper" "$version" + printf "%s_DEBUG_VERSION=%s\n" "$prefix_upper" "$debug_version" + printf "%s_DEBUG_VERSION_SUFFIX=%s\n" "$prefix_upper" "$debug_version_suffix" + printf "%s_VERSION_CODE=%d\n" "$prefix_upper" "$version_code" + + # Write to versions.properties + { + printf "%s_VERSION=%s\n" "$prefix_upper" "$version" + printf "%s_VERSION_CODE=%d\n" "$prefix_upper" "$version_code" + printf "%s_DEBUG_VERSION_SUFFIX=%s\n" "$prefix_upper" "$debug_version_suffix" + printf "%s_DEBUG_VERSION=%s\n" "$prefix_upper" "$debug_version" + } >> versions.properties + +done + +# Print more result +printf "HASH=%s\n" "$hash" +printf "DIRTY=%s\n" "$dirty" + +# Write the hash to versions.properties after the loop +printf "HASH=%s\n" "$hash" >> versions.properties +printf "DIRTY=%s\n" "$dirty" >> versions.properties \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index adb29564..258300f9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -25,7 +25,7 @@ buildCache { } } -private val localLibraries = false +private val localLibraries = true rootProject.name = "Toggles" includeBuild("build-logic/conventions") @@ -69,8 +69,15 @@ include( ":toggles-sample", ":modules:compose-theme", ":modules:database", + ":modules:provider", ":modules:applications", + ":modules:configurations", ":modules:oss", + ":modules:help", + ":modules:stringconfiguration", + ":modules:booleanconfiguration", + ":modules:integerconfiguration", + ":modules:enumconfiguration", ) dependencyResolutionManagement { diff --git a/toggles-app/build.gradle.kts b/toggles-app/build.gradle.kts index 03aa8594..a1165b56 100644 --- a/toggles-app/build.gradle.kts +++ b/toggles-app/build.gradle.kts @@ -1,6 +1,5 @@ import java.io.FileInputStream import java.util.Properties -//import se.eelde.toggles.licenseeassetplugin.CopyLicenseeReportPlugin plugins { id("toggles.android.application-conventions") @@ -8,10 +7,19 @@ plugins { id("kotlin-parcelize") id("dagger.hilt.android.plugin") id("com.github.triplet.play") - id("com.gladed.androidgitversion") version "0.4.14" id("com.google.firebase.crashlytics") id("app.cash.licensee") id("se.premex.gross") version "0.1.0" + id("com.google.devtools.ksp") +} + +val versionFile = File("versions.properties") +val versions = Properties().apply { + if (versionFile.exists()) { + FileInputStream(versionFile).use { + load(it) + } + } } licensee { @@ -23,10 +31,7 @@ licensee { // try remove or ping developer later // allowUrl("http://www.opensource.org/licenses/mit-license.php") - allowUrl("https://raw.githubusercontent.com/erikeelde/toggles/master/LICENCE")} - -androidGitVersion { - tagPattern = "^v[0-9]+.*" + allowUrl("https://raw.githubusercontent.com/erikeelde/toggles/master/LICENCE") } play { @@ -60,8 +65,8 @@ android { defaultConfig { applicationId = "se.eelde.toggles" - versionName = androidGitVersion.name() - versionCode = androidGitVersion.code() + versionName = versions.getProperty("V_VERSION", "0.0.1") + versionCode = versions.getProperty("V_VERSION_CODE", "1").toInt() vectorDrawables.useSupportLibrary = true @@ -72,6 +77,8 @@ android { manifestPlaceholders["togglesPermission"] = togglesPermission buildConfigField("String", "CONFIG_AUTHORITY", "\"$togglesAuthority\"") + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } packaging { resources { @@ -95,8 +102,15 @@ android { dependencies { implementation(projects.modules.composeTheme) implementation(projects.modules.database) + implementation(projects.modules.provider) implementation(projects.modules.applications) + implementation(projects.modules.configurations) implementation(projects.modules.oss) + implementation(projects.modules.help) + implementation(projects.modules.booleanconfiguration) + implementation(projects.modules.integerconfiguration) + implementation(projects.modules.stringconfiguration) + implementation(projects.modules.enumconfiguration) implementation(libs.androidx.ui.ui.tooling) implementation(platform(libs.androidx.compose.bom)) @@ -132,16 +146,16 @@ dependencies { implementation(libs.com.google.firebase.firebase.analytics.ktx) implementation(libs.com.google.dagger.hilt.android) - kapt(libs.com.google.dagger.hilt.android.compiler) - kapt(libs.androidx.hilt.hilt.compiler) + ksp(libs.com.google.dagger.hilt.android.compiler) + ksp(libs.androidx.hilt.hilt.compiler) testImplementation(libs.com.google.dagger.hilt.android.testing) - kaptTest(libs.com.google.dagger.hilt.android.compiler) + kspTest(libs.com.google.dagger.hilt.android.compiler) implementation(libs.androidx.lifecycle.lifecycle.common.java8) implementation(libs.com.google.dagger) - kapt(libs.com.google.dagger.dagger.compiler) + ksp(libs.com.google.dagger.dagger.compiler) implementation(libs.androidx.appcompat) implementation(libs.androidx.recyclerview) @@ -156,12 +170,11 @@ dependencies { implementation(libs.androidx.room.room.ktx) implementation(libs.androidx.paging.paging.runtime.ktx) - implementation(libs.androidx.paging.paging.runtime.ktx) - implementation(libs.se.eelde.toggles.toggles.core) implementation(libs.se.eelde.toggles.toggles.flow) implementation(libs.se.eelde.toggles.toggles.prefs) + implementation(platform(libs.org.jetbrains.kotlinx.kotlinx.coroutines.bom)) implementation(libs.org.jetbrains.kotlinx.kotlinx.coroutines.android) implementation(libs.androidx.core.core.ktx) @@ -169,4 +182,10 @@ dependencies { implementation(libs.com.squareup.okio) debugImplementation(libs.com.squareup.leakcanary.leakcanary.android) + + + androidTestImplementation(libs.androidx.test.core.ktx) + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.compose.ui.ui.test.junit4) } diff --git a/toggles-app/src/androidTest/java/se/eelde/toggles/ExampleInstrumentationTest.kt b/toggles-app/src/androidTest/java/se/eelde/toggles/ExampleInstrumentationTest.kt new file mode 100644 index 00000000..760b916d --- /dev/null +++ b/toggles-app/src/androidTest/java/se/eelde/toggles/ExampleInstrumentationTest.kt @@ -0,0 +1,17 @@ +package se.eelde.toggles + +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 + +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentationTest { + @Test + fun useAppContext() { + val appContext = ApplicationProvider.getApplicationContext() + assertEquals("se.eelde.toggles", appContext.packageName) + } +} diff --git a/toggles-app/src/androidTest/java/se/eelde/toggles/ExampleInstrumentationTest2.kt b/toggles-app/src/androidTest/java/se/eelde/toggles/ExampleInstrumentationTest2.kt new file mode 100644 index 00000000..4024f2fb --- /dev/null +++ b/toggles-app/src/androidTest/java/se/eelde/toggles/ExampleInstrumentationTest2.kt @@ -0,0 +1,24 @@ +package se.eelde.toggles + +import androidx.compose.ui.test.junit4.createEmptyComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.test.core.app.launchActivity +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentationTest2 { + + @get:Rule + val composeTestRule = createEmptyComposeRule() + + @Test + fun useAppContext() = runTest { + launchActivity().use { + composeTestRule.onNodeWithText("No applications found.").assertExists() + } + } +} diff --git a/toggles-app/src/main/AndroidManifest.xml b/toggles-app/src/main/AndroidManifest.xml index f0b09093..eb987516 100644 --- a/toggles-app/src/main/AndroidManifest.xml +++ b/toggles-app/src/main/AndroidManifest.xml @@ -9,7 +9,6 @@ android:protectionLevel="normal" /> - + navController.navigate("configuration/$configurationId/$scopeId/boolean") + }, + navigateToIntegerConfiguration = { scopeId: Long, configurationId: Long -> + navController.navigate("configuration/$configurationId/$scopeId/integer") + }, + navigateToStringConfiguration = { scopeId: Long, configurationId: Long -> + navController.navigate("configuration/$configurationId/$scopeId/string") + }, + navigateToEnumConfiguration = { scopeId: Long, configurationId: Long -> + navController.navigate("configuration/$configurationId/$scopeId/enum") + }, + navigateToScopeView = { applicationId: Long -> + navController.navigate("scopes/$applicationId") + } + ) { navController.popBackStack() } composable( "configuration/{configurationId}/{scopeId}/boolean", arguments = listOf( @@ -67,40 +76,13 @@ fun Navigation( navArgument("scopeId") { type = NavType.LongType } ) ) { - val viewModel: FragmentBooleanValueViewModel = hiltViewModel() - val state = viewModel.state.collectAsStateWithLifecycle().value - - Scaffold( - topBar = { - TopAppBar( - title = { Text("") }, - navigationIcon = - { - IconButton(onClick = { navController.popBackStack() }) { - Icon( - imageVector = Icons.Filled.ArrowBack, - contentDescription = null - ) - } - } - ) - }, - ) { paddingValues -> - BooleanValueView( - uiState = state, - popBackStack = { navController.popBackStack() }, - revert = { - viewModel.revertClick() - navController.popBackStack() - }, - save = { - viewModel.saveClick() - navController.popBackStack() - }, - setBooleanValue = { viewModel.checkedChanged(it) }, - modifier = Modifier.padding(paddingValues), - ) - } + BooleanValueView { navController.popBackStack() } + } + composable( + "scopes/{applicationId}", + arguments = listOf(navArgument("applicationId") { type = NavType.LongType }) + ) { + ScopeValueView { navController.popBackStack() } } composable( "configuration/{configurationId}/{scopeId}/integer", @@ -109,34 +91,7 @@ fun Navigation( navArgument("scopeId") { type = NavType.LongType } ) ) { - val viewModel: FragmentIntegerValueViewModel = hiltViewModel() - val state = viewModel.state.collectAsStateWithLifecycle().value - - Scaffold( - topBar = { - TopAppBar( - title = { Text("") }, - navigationIcon = - { - IconButton(onClick = { navController.popBackStack() }) { - Icon( - imageVector = Icons.Filled.ArrowBack, - contentDescription = null - ) - } - } - ) - }, - ) { paddingValues -> - IntegerValueView( - uiState = state, - popBackStack = { navController.popBackStack() }, - revert = { viewModel.revertClick() }, - save = { viewModel.saveClick() }, - setIntegerValue = { viewModel.setIntegerValue(it) }, - modifier = Modifier.padding(paddingValues), - ) - } + IntegerValueView { navController.popBackStack() } } composable( "configuration/{configurationId}/{scopeId}/string", @@ -145,34 +100,7 @@ fun Navigation( navArgument("scopeId") { type = NavType.LongType } ) ) { - val viewModel: FragmentStringValueViewModel = hiltViewModel() - val state = viewModel.state.collectAsStateWithLifecycle().value - - Scaffold( - topBar = { - TopAppBar( - title = { Text("") }, - navigationIcon = - { - IconButton(onClick = { navController.popBackStack() }) { - Icon( - imageVector = Icons.Filled.ArrowBack, - contentDescription = null - ) - } - } - ) - }, - ) { paddingValues -> - StringValueView( - state = state, - popBackStack = { navController.popBackStack() }, - revert = { viewModel.revertClick() }, - save = { viewModel.saveClick() }, - setStringValue = { viewModel.setStringValue(it) }, - modifier = Modifier.padding(paddingValues), - ) - } + StringValueView { navController.popBackStack() } } composable( "configuration/{configurationId}/{scopeId}/enum", @@ -181,32 +109,7 @@ fun Navigation( navArgument("scopeId") { type = NavType.LongType } ) ) { - val viewModel: FragmentEnumValueViewModel = hiltViewModel() - val state = viewModel.state.collectAsStateWithLifecycle().value - Scaffold( - topBar = { - TopAppBar( - title = { Text("") }, - navigationIcon = - { - IconButton(onClick = { navController.popBackStack() }) { - Icon( - imageVector = Icons.Filled.ArrowBack, - contentDescription = null - ) - } - } - ) - }, - ) { paddingValues -> - EnumValueView( - state = state, - popBackStack = { navController.popBackStack() }, - revert = { viewModel.revertClick() }, - setEnumValue = { viewModel.saveClick(it) }, - modifier = Modifier.padding(paddingValues), - ) - } + EnumValueView { navController.popBackStack() } } composable( "oss", @@ -233,24 +136,7 @@ fun Navigation( composable( "help", ) { - Scaffold( - topBar = { - TopAppBar( - title = { Text("") }, - navigationIcon = - { - IconButton(onClick = { navController.popBackStack() }) { - Icon( - imageVector = Icons.Filled.ArrowBack, - contentDescription = null - ) - } - } - ) - }, - ) { paddingValues -> - HelpView(modifier = Modifier.padding(paddingValues)) - } + HelpView { navController.popBackStack() } } } } @@ -264,8 +150,6 @@ class MainActivity : ComponentActivity() { setContent { TogglesTheme { val navController: NavHostController = rememberNavController() - val appState = rememberAppState(navController) - Log.e("appState", "current destination: ${appState.currentDestination}") Navigation(navController = navController) } diff --git a/toggles-app/src/main/java/se/eelde/toggles/TogglesUriMatcher.kt b/toggles-app/src/main/java/se/eelde/toggles/TogglesUriMatcher.kt deleted file mode 100644 index 416db2de..00000000 --- a/toggles-app/src/main/java/se/eelde/toggles/TogglesUriMatcher.kt +++ /dev/null @@ -1,45 +0,0 @@ -package se.eelde.toggles - -import android.content.UriMatcher - -class TogglesUriMatcher private constructor() { - companion object { - internal const val CURRENT_CONFIGURATION_ID = 1 - internal const val CURRENT_CONFIGURATION_KEY = 2 - internal const val CURRENT_CONFIGURATIONS = 3 - internal const val PREDEFINED_CONFIGURATION_VALUES = 5 - private const val APPLICATION_ID = 6 - - private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH) - - fun getTogglesUriMatcher() = uriMatcher - - init { - uriMatcher.addURI( - BuildConfig.CONFIG_AUTHORITY, - "application/#", - APPLICATION_ID - ) - uriMatcher.addURI( - BuildConfig.CONFIG_AUTHORITY, - "currentConfiguration/#", - CURRENT_CONFIGURATION_ID - ) - uriMatcher.addURI( - BuildConfig.CONFIG_AUTHORITY, - "currentConfiguration/*", - CURRENT_CONFIGURATION_KEY - ) - uriMatcher.addURI( - BuildConfig.CONFIG_AUTHORITY, - "currentConfiguration", - CURRENT_CONFIGURATIONS - ) - uriMatcher.addURI( - BuildConfig.CONFIG_AUTHORITY, - "predefinedConfigurationValue", - PREDEFINED_CONFIGURATION_VALUES - ) - } - } -} diff --git a/toggles-app/src/main/java/se/eelde/toggles/configurationlist/ConfigurationsEntry.kt b/toggles-app/src/main/java/se/eelde/toggles/configurationlist/ConfigurationsEntry.kt deleted file mode 100644 index a2faa76e..00000000 --- a/toggles-app/src/main/java/se/eelde/toggles/configurationlist/ConfigurationsEntry.kt +++ /dev/null @@ -1,133 +0,0 @@ -package se.eelde.toggles.configurationlist - -import android.content.Intent -import android.net.Uri -import android.provider.Settings -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.filled.Close -import androidx.compose.material.icons.outlined.Cyclone -import androidx.compose.material.icons.outlined.Delete -import androidx.compose.material.icons.outlined.Info -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SearchBar -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Modifier -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavType -import androidx.navigation.compose.composable -import androidx.navigation.navArgument - -@Suppress("LongMethod") -@OptIn(ExperimentalMaterial3Api::class) -fun NavGraphBuilder.configurationsNavigations( - navController: NavController, - back: () -> Unit, -) { - composable( - "configurations/{applicationId}", - arguments = listOf(navArgument("applicationId") { type = NavType.LongType }) - ) { - val viewModel: ConfigurationViewModel = hiltViewModel() - val uiState = viewModel.state.collectAsStateWithLifecycle() - - val launcher = - rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) {} - - val query = viewModel.getQuery().collectAsState().value - var searching = query.isNotEmpty() - - Scaffold( - topBar = { - TopAppBar( - title = { - SearchBar( - query = query, - onQueryChange = { - viewModel.setQuery(it) - }, - onSearch = {}, - placeholder = { Text("Search") }, - active = false, // active, - trailingIcon = { - if (searching) { - IconButton(onClick = { - viewModel.setQuery("") - searching = false - }) { - Icon( - imageVector = Icons.Filled.Close, - contentDescription = null - ) - } - } - }, - onActiveChange = {} - ) { - } - }, - navigationIcon = - { - IconButton(onClick = { back() }) { - Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = null) - } - }, - actions = { - IconButton(onClick = { - viewModel.restartApplication(uiState.value.application!!) - }) { - Icon( - imageVector = Icons.Outlined.Cyclone, - contentDescription = null - ) - } - if (!searching) { - IconButton(onClick = { - launcher.launch( - Intent( - Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - Uri.fromParts( - "package", - uiState.value.application!!.packageName, - null - ) - ) - ) - }) { - Icon( - imageVector = Icons.Outlined.Info, - contentDescription = null - ) - } - IconButton(onClick = { - viewModel.deleteApplication(uiState.value.application!!) - }) { - Icon( - imageVector = Icons.Outlined.Delete, - contentDescription = null - ) - } - } - } - ) - }, - ) { paddingValues -> - ConfigurationListView( - navController = navController, - uiState = uiState, - modifier = Modifier.padding(paddingValues), - ) - } - } -} diff --git a/toggles-app/src/main/java/se/eelde/toggles/di/ApplicationModule.kt b/toggles-app/src/main/java/se/eelde/toggles/di/ApplicationModule.kt index e64e313b..1b799722 100644 --- a/toggles-app/src/main/java/se/eelde/toggles/di/ApplicationModule.kt +++ b/toggles-app/src/main/java/se/eelde/toggles/di/ApplicationModule.kt @@ -7,14 +7,19 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.Dispatchers +import se.eelde.toggles.BuildConfig import se.eelde.toggles.prefs.TogglesPreferences import se.eelde.toggles.prefs.TogglesPreferencesImpl import se.eelde.toggles.provider.IPackageManagerWrapper import se.eelde.toggles.provider.PackageManagerWrapper +import se.eelde.toggles.provider.TogglesUriMatcher @Module @InstallIn(SingletonComponent::class) object ApplicationModule { + @Provides + fun provideTogglesUriMatcher() = TogglesUriMatcher(BuildConfig.CONFIG_AUTHORITY) + @Provides fun provideIoDispatcher() = Dispatchers.IO 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 2eab2f09..81a46209 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 @@ -22,6 +22,7 @@ object DatabaseModule { .addMigrations(Migrations.MIGRATION_2_3) .addMigrations(Migrations.MIGRATION_3_4) .addMigrations(Migrations.MIGRATION_4_5) + .addMigrations(Migrations.MIGRATION_5_6) .build() } } diff --git a/toggles-app/src/main/java/se/eelde/toggles/dialogs/enumvalue/EnumValueView.kt b/toggles-app/src/main/java/se/eelde/toggles/dialogs/enumvalue/EnumValueView.kt deleted file mode 100644 index b917acbf..00000000 --- a/toggles-app/src/main/java/se/eelde/toggles/dialogs/enumvalue/EnumValueView.kt +++ /dev/null @@ -1,65 +0,0 @@ -package se.eelde.toggles.dialogs.enumvalue - -import androidx.compose.foundation.clickable -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.foundation.lazy.LazyColumn -import androidx.compose.material3.Button -import androidx.compose.material3.ListItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import kotlinx.coroutines.launch - -@Composable -internal fun EnumValueView( - state: ViewState, - setEnumValue: suspend (String) -> Unit, - revert: suspend () -> Unit, - popBackStack: () -> Unit, - modifier: Modifier = Modifier, -) { - val scope = rememberCoroutineScope() - - Surface(modifier = modifier) { - Column { - Text( - modifier = Modifier.padding(8.dp), - style = MaterialTheme.typography.headlineMedium, - text = state.title ?: "" - ) - LazyColumn { - state.configurationValues.forEach { wrenchPredefinedConfigurationValue -> - item { - ListItem( - modifier = Modifier.clickable { - scope.launch { - setEnumValue(wrenchPredefinedConfigurationValue.value.toString()) - popBackStack() - } - }, - headlineContent = { Text(text = wrenchPredefinedConfigurationValue.value.toString()) } - ) - } - } - } - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { - Button(modifier = Modifier.padding(8.dp), onClick = { - scope.launch { - revert() - popBackStack() - } - }) { - Text("Revert") - } - } - } - } -} 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 ce189208..f089780c 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 @@ -1,66 +1,192 @@ package se.eelde.toggles.dialogs.scope -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.selection.selectable +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Link import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable 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 androidx.navigation.NavController -import kotlinx.coroutines.launch import se.eelde.toggles.R +import se.eelde.toggles.database.WrenchScope +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun ScopeValueView(navController: NavController, viewModel: ScopeFragmentViewModel) { +fun ScopeValueView( + modifier: Modifier = Modifier, + viewModel: ScopeViewModel = hiltViewModel(), + back: () -> Unit +) { val uiState = viewModel.state.collectAsStateWithLifecycle() - val scope = rememberCoroutineScope() + Scaffold( + topBar = { + TopAppBar( + title = { Text("Scopes") }, + navigationIcon = + { + IconButton(onClick = { back() }) { + Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = null) + } + } + ) + }, + ) { paddingValues -> + ScopeValueView( + viewState = uiState.value, + selectScope = { scope -> viewModel.selectScope(scope) }, + deleteScope = { scope -> viewModel.removeScope(scope) }, + createScope = { viewModel.createScope(it) }, + modifier = modifier.padding(paddingValues) + ) + } +} - uiState.value.let { - Surface(modifier = Modifier.padding(16.dp)) { - Column { - Text( - modifier = Modifier.padding(8.dp), - style = MaterialTheme.typography.titleLarge, - text = stringResource(id = R.string.select_scope) - ) - LazyColumn { - uiState.value.scopes.forEach { - 0 - item { - ListItem( - modifier = Modifier.clickable { - scope.launch { - viewModel.selectScope(it) - navController.popBackStack() - } +@Suppress("LongMethod") +@Composable +internal fun ScopeValueView( + viewState: ViewState, + selectScope: (scope: WrenchScope) -> Unit, + deleteScope: (scope: WrenchScope) -> Unit, + createScope: (scopeName: String) -> Unit, + modifier: Modifier = Modifier, +) { + Surface(modifier = modifier.fillMaxSize()) { + Column { + Text( + modifier = Modifier.padding(16.dp), + style = MaterialTheme.typography.bodyLarge, + text = stringResource(id = R.string.scope_information) + ) + LazyColumn { + viewState.scopes.forEach { scope -> + item { + val selected = scope.id == viewState.selectedScope?.id + ListItem( + modifier = Modifier + .selectable(selected = selected) { + selectScope(scope) }, - headlineContent = { Text(text = it.name) } - ) - } + leadingContent = { + if (selected) { + Icon( + imageVector = Icons.Filled.Link, + contentDescription = null + ) + } + }, + headlineContent = { Text(text = scope.name) } + ) } } - Row { - Button(modifier = Modifier.padding(8.dp), onClick = { - TODO() - }) { - Text("Delete") - } - Button(modifier = Modifier.padding(8.dp), onClick = { - TODO() - }) { - Text("Add") - } + } + + val showAddScopeView = rememberSaveable { mutableStateOf(false) } + val showDeleteScopeView = rememberSaveable { mutableStateOf(false) } + + Row { + Button( + modifier = Modifier.padding(16.dp), + onClick = { showAddScopeView.value = true } + ) { + Text("Add") + } + OutlinedButton( + modifier = Modifier.padding(16.dp), + enabled = viewState.scopes.size > 1, + onClick = { showDeleteScopeView.value = true } + ) { + Text("Delete") + } + } + + if (showAddScopeView.value) { + AddScopeView( + addScope = { + createScope(it) + showAddScopeView.value = false + }, + dismiss = { showAddScopeView.value = false } + ) + } + if (showDeleteScopeView.value) { + DeleteScopeView( + scope = viewState.selectedScope!!, + deleteScope = { scope: WrenchScope -> + deleteScope(scope) + showDeleteScopeView.value = false + }, + dismiss = { showDeleteScopeView.value = false } + ) + } + } + } +} + +@Composable +fun AddScopeView( + addScope: (String) -> Unit, + dismiss: () -> Unit, + modifier: Modifier = Modifier +) { + val scopeName = rememberSaveable { mutableStateOf("") } + Dialog(onDismissRequest = { dismiss() }) { + Card(modifier = modifier) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + modifier = Modifier.padding(16.dp), + style = MaterialTheme.typography.titleMedium, + text = stringResource(id = R.string.scope_add_information) + ) + TextField( + value = scopeName.value, + onValueChange = { scopeName.value = it } + ) + Button(onClick = { addScope(scopeName.value) }) { + Text("Add") + } + } + } + } +} + +@Composable +fun DeleteScopeView( + scope: WrenchScope, + deleteScope: (WrenchScope) -> Unit, + dismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + Dialog(onDismissRequest = { dismiss() }) { + Card(modifier = modifier) { + Column(modifier = Modifier.padding(16.dp)) { + Text("Are you sure you want to delete the scope: ${scope.name}") + Button(onClick = { deleteScope(scope) }) { + Text("Delete") } } } 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 ab112786..9dd4f4d7 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 @@ -4,9 +4,11 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow 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 java.util.Date @@ -15,6 +17,7 @@ import javax.inject.Inject internal data class ViewState( val title: String? = null, val scopes: List = listOf(), + val selectedScope: WrenchScope? = null, val saving: Boolean = false, val reverting: Boolean = false ) @@ -28,7 +31,7 @@ private sealed class PartialViewState { } @HiltViewModel -class ScopeFragmentViewModel @Inject internal constructor( +class ScopeViewModel @Inject internal constructor( private val savedStateHandle: SavedStateHandle, private val scopeDao: WrenchScopeDao ) : ViewModel() { @@ -56,7 +59,12 @@ class ScopeFragmentViewModel @Inject internal constructor( is PartialViewState.NewConfiguration -> previousState PartialViewState.Reverting -> previousState PartialViewState.Saving -> previousState - is PartialViewState.Scopes -> previousState.copy(scopes = partialViewState.scopes) + is PartialViewState.Scopes -> { + previousState.copy( + selectedScope = partialViewState.scopes.sortedByDescending { it.timeStamp }.first(), + scopes = partialViewState.scopes + ) + } } } @@ -72,13 +80,17 @@ class ScopeFragmentViewModel @Inject internal constructor( val wrenchScope = WrenchScope.newWrenchScope() wrenchScope.name = scopeName wrenchScope.applicationId = applicationId - wrenchScope.id = scopeDao.insert(wrenchScope) + withContext(Dispatchers.IO) { + wrenchScope.id = scopeDao.insert(wrenchScope) + } } } internal fun removeScope(scope: WrenchScope) { viewModelScope.launch { - scopeDao.delete(scope) + withContext(Dispatchers.IO) { + scopeDao.delete(scope) + } } } } diff --git a/toggles-app/src/main/java/se/eelde/toggles/help/HelpView.kt b/toggles-app/src/main/java/se/eelde/toggles/help/HelpView.kt deleted file mode 100644 index 728774f8..00000000 --- a/toggles-app/src/main/java/se/eelde/toggles/help/HelpView.kt +++ /dev/null @@ -1,15 +0,0 @@ -package se.eelde.toggles.help - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color - -@Composable -fun HelpView(modifier: Modifier = Modifier) { - Box(modifier.fillMaxSize()) { - Text(text = "Implementation", color = Color.White) - } -} diff --git a/toggles-app/src/main/res/values/strings.xml b/toggles-app/src/main/res/values/strings.xml index 2b554bb3..6ca1279c 100644 --- a/toggles-app/src/main/res/values/strings.xml +++ b/toggles-app/src/main/res/values/strings.xml @@ -8,7 +8,6 @@ Go to application settings Filter configurations revert - Select scope Application icon Access Toggles Access to request configuration from Toggles @@ -21,4 +20,6 @@ Loading bubble Add Delete + Scopes allow us to define a set of configurations that we can switch between quickly. Your selected scope is indicated by a link. + Add a new scope diff --git a/toggles-app/src/test/java/se/eelde/toggles/provider/TogglesProviderTest.kt b/toggles-app/src/test/java/se/eelde/toggles/provider/TogglesProviderTest.kt index 992fb5cc..548ff19f 100644 --- a/toggles-app/src/test/java/se/eelde/toggles/provider/TogglesProviderTest.kt +++ b/toggles-app/src/test/java/se/eelde/toggles/provider/TogglesProviderTest.kt @@ -136,12 +136,12 @@ class TogglesProviderTest { Assert.assertEquals(insertToggle.value, providerToggle.value) Assert.assertEquals(insertToggle.type, providerToggle.type) - val updateToggle = Toggle( - providerToggle.id, - providerToggle.type, - providerToggle.key, - providerToggle.value!! + providerToggle.value!! - ) + val updateToggle = Toggle { + id = providerToggle.id + type = providerToggle.type + key = providerToggle.key + value = providerToggle.value!! + providerToggle.value!! + } val update = togglesProvider.update( TogglesProviderContract.toggleUri(updateToggle.id), @@ -253,10 +253,18 @@ class TogglesProviderTest { } private fun getToggleValue(value: String): ToggleValue { - return ToggleValue(0, value) + return ToggleValue { + id = 0 + this.value = value + } } private fun getToggle(key: String): Toggle { - return Toggle(0L, "toggletype", key, "togglevalue") + return Toggle { + id = 0L + type = "toggletype" + this.key = key + value = "togglevalue" + } } } diff --git a/toggles-core/api/toggles-core.api b/toggles-core/api/toggles-core.api index f99fa57a..96fd5ce3 100644 --- a/toggles-core/api/toggles-core.api +++ b/toggles-core/api/toggles-core.api @@ -1,5 +1,5 @@ public final class se/eelde/toggles/core/ColumnNames { - public fun ()V + public static final field INSTANCE Lse/eelde/toggles/core/ColumnNames; } public final class se/eelde/toggles/core/ColumnNames$Toggle { @@ -19,12 +19,7 @@ public final class se/eelde/toggles/core/ColumnNames$ToggleValue { public final class se/eelde/toggles/core/Toggle { public static final field Companion Lse/eelde/toggles/core/Toggle$Companion; - public fun (JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V - public synthetic fun (JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()J - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Ljava/lang/String; - public final fun component4 ()Ljava/lang/String; + public synthetic fun (JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun copy (JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lse/eelde/toggles/core/Toggle; public static synthetic fun copy$default (Lse/eelde/toggles/core/Toggle;JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lse/eelde/toggles/core/Toggle; public fun equals (Ljava/lang/Object;)Z @@ -40,6 +35,23 @@ public final class se/eelde/toggles/core/Toggle { public fun toString ()Ljava/lang/String; } +public final class se/eelde/toggles/core/Toggle$Builder { + public fun ()V + public final fun build ()Lse/eelde/toggles/core/Toggle; + public final fun getId ()J + public final fun getKey ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; + public final fun getValue ()Ljava/lang/String; + public final fun setId (J)Lse/eelde/toggles/core/Toggle$Builder; + public final synthetic fun setId (J)V + public final fun setKey (Ljava/lang/String;)Lse/eelde/toggles/core/Toggle$Builder; + public final synthetic fun setKey (Ljava/lang/String;)V + public final fun setType (Ljava/lang/String;)Lse/eelde/toggles/core/Toggle$Builder; + public final synthetic fun setType (Ljava/lang/String;)V + public final fun setValue (Ljava/lang/String;)Lse/eelde/toggles/core/Toggle$Builder; + public final synthetic fun setValue (Ljava/lang/String;)V +} + public final class se/eelde/toggles/core/Toggle$Companion { public final fun fromContentValues (Landroid/content/ContentValues;)Lse/eelde/toggles/core/Toggle; public final fun fromCursor (Landroid/database/Cursor;)Lse/eelde/toggles/core/Toggle; @@ -57,15 +69,7 @@ public abstract interface annotation class se/eelde/toggles/core/Toggle$ToggleTy } public final class se/eelde/toggles/core/ToggleValue { - public fun ()V - public fun (JJLjava/lang/String;)V - public synthetic fun (JJLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun (JLjava/lang/String;)V - public final fun component1 ()J - public final fun component2 ()J - public final fun component3 ()Ljava/lang/String; - public final fun copy (JJLjava/lang/String;)Lse/eelde/toggles/core/ToggleValue; - public static synthetic fun copy$default (Lse/eelde/toggles/core/ToggleValue;JJLjava/lang/String;ILjava/lang/Object;)Lse/eelde/toggles/core/ToggleValue; + public synthetic fun (JJLjava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun equals (Ljava/lang/Object;)Z public final fun getConfigurationId ()J public final fun getId ()J @@ -75,6 +79,20 @@ public final class se/eelde/toggles/core/ToggleValue { public fun toString ()Ljava/lang/String; } +public final class se/eelde/toggles/core/ToggleValue$Builder { + public fun ()V + public final fun build ()Lse/eelde/toggles/core/ToggleValue; + public final fun getConfigurationId ()J + public final fun getId ()J + public final fun getValue ()Ljava/lang/String; + public final fun setConfigurationId (J)Lse/eelde/toggles/core/ToggleValue$Builder; + public final synthetic fun setConfigurationId (J)V + public final fun setId (J)Lse/eelde/toggles/core/ToggleValue$Builder; + public final synthetic fun setId (J)V + public final fun setValue (Ljava/lang/String;)Lse/eelde/toggles/core/ToggleValue$Builder; + public final synthetic fun setValue (Ljava/lang/String;)V +} + public final class se/eelde/toggles/core/TogglesProviderContract { public static final field INSTANCE Lse/eelde/toggles/core/TogglesProviderContract; public static final fun applicationUri (J)Landroid/net/Uri; @@ -84,3 +102,8 @@ public final class se/eelde/toggles/core/TogglesProviderContract { public static final fun toggleValueUri ()Landroid/net/Uri; } +public final class se/eelde/toggles/core/TogglesProviderContractKt { + public static final synthetic fun Toggle (Lkotlin/jvm/functions/Function1;)Lse/eelde/toggles/core/Toggle; + public static final synthetic fun ToggleValue (Lkotlin/jvm/functions/Function1;)Lse/eelde/toggles/core/ToggleValue; +} + diff --git a/toggles-core/gradle.properties b/toggles-core/gradle.properties index db030065..5e25a3b1 100644 --- a/toggles-core/gradle.properties +++ b/toggles-core/gradle.properties @@ -29,5 +29,5 @@ android.useAndroidX=true android.enableJetifier=false org.gradle.caching=true -org.gradle.configuration-cache=false +org.gradle.configuration-cache=true org.gradle.configuration-cache.max-problems=5 diff --git a/toggles-core/src/main/java/se/eelde/toggles/core/TogglesProviderContract.kt b/toggles-core/src/main/java/se/eelde/toggles/core/TogglesProviderContract.kt index 09d046c4..26f0c305 100644 --- a/toggles-core/src/main/java/se/eelde/toggles/core/TogglesProviderContract.kt +++ b/toggles-core/src/main/java/se/eelde/toggles/core/TogglesProviderContract.kt @@ -6,7 +6,7 @@ import android.net.Uri import androidx.annotation.StringDef @Suppress("LibraryEntitiesShouldNotBePublic") -public class ColumnNames { +public object ColumnNames { public object Toggle { public const val COL_KEY: String = "configurationKey" public const val COL_ID: String = "id" @@ -21,35 +21,111 @@ public class ColumnNames { } } -@Suppress("ForbiddenPublicDataClass", "LibraryEntitiesShouldNotBePublic") -public data class ToggleValue( - val id: Long = 0, - val configurationId: Long = 0, - val value: String? = null +@Suppress("LibraryEntitiesShouldNotBePublic") +public class ToggleValue private constructor( + public val id: Long = 0, + public val configurationId: Long = 0, + public val value: String? = null ) { - public constructor(configurationId: Long, value: String?) : this(0, configurationId, value) + public class Builder { + @set:JvmSynthetic + public var id: Long = 0 + + @set:JvmSynthetic + public var configurationId: Long = 0 + + @set:JvmSynthetic + public var value: String? = null + + public fun setId(id: Long): Builder = apply { this.id = id } + public fun setConfigurationId(configurationId: Long): Builder = + apply { this.configurationId = configurationId } + + public fun setValue(value: String?): Builder = apply { this.value = value } - public fun toContentValues(): ContentValues { - val contentValues = ContentValues() + public fun build(): ToggleValue = + ToggleValue(id = id, configurationId = configurationId, value = value) + } + + public fun toContentValues(): ContentValues = ContentValues().apply { if (id > 0) { - contentValues.put(ColumnNames.ToggleValue.COL_ID, id) + put(ColumnNames.ToggleValue.COL_ID, id) } - contentValues.put(ColumnNames.ToggleValue.COL_CONFIG_ID, configurationId) - contentValues.put(ColumnNames.ToggleValue.COL_VALUE, value) + put(ColumnNames.ToggleValue.COL_CONFIG_ID, configurationId) + put(ColumnNames.ToggleValue.COL_VALUE, value) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false - return contentValues + other as ToggleValue + + if (id != other.id) return false + if (configurationId != other.configurationId) return false + if (value != other.value) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + configurationId.hashCode() + result = 31 * result + (value?.hashCode() ?: 0) + return result + } + + override fun toString(): String { + return "ToggleValue(id=$id, configurationId=$configurationId, value=$value)" } } -@Suppress("ForbiddenPublicDataClass", "LibraryEntitiesShouldNotBePublic") -public data class Toggle( - var id: Long = 0, - @ToggleType val type: String, - val key: String = "", - val value: String? = null, -// val scope: String? = null, +@JvmSynthetic +@Suppress("LibraryEntitiesShouldNotBePublic") +public fun ToggleValue(initializer: ToggleValue.Builder.() -> Unit): ToggleValue { + return ToggleValue.Builder().apply(initializer).build() +} + +@Suppress("LibraryEntitiesShouldNotBePublic") +public class Toggle private constructor( + public var id: Long = 0, + @ToggleType public val type: String, + public val key: String = "", + public val value: String? = null, ) { + public class Builder { + @set:JvmSynthetic + public var id: Long = 0 + + @set:JvmSynthetic + @ToggleType + public var type: String = "" + + @set:JvmSynthetic + public var key: String = "" + + @set:JvmSynthetic + public var value: String? = null + + public fun setId(id: Long): Builder = apply { this.id = id } + public fun setType(@ToggleType type: String): Builder = + apply { this.type = type } + + public fun setKey(key: String): Builder = apply { this.key = key } + public fun setValue(value: String?): Builder = apply { this.value = value } + + public fun build(): Toggle = + Toggle(id = id, type = type, key = key, value = value) + } + + public fun copy( + id: Long = this.id, + type: String = this.type, + key: String = this.key, + value: String? = this.value + ): Toggle = + Toggle(id = id, type = type, key = key, value = value) public fun toContentValues(): ContentValues = ContentValues().apply { put(ColumnNames.Toggle.COL_ID, id) @@ -58,6 +134,32 @@ public data class Toggle( put(ColumnNames.Toggle.COL_TYPE, type) } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Toggle + + if (id != other.id) return false + if (type != other.type) return false + if (key != other.key) return false + if (value != other.value) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + type.hashCode() + result = 31 * result + key.hashCode() + result = 31 * result + (value?.hashCode() ?: 0) + return result + } + + override fun toString(): String { + return "Toggle(id=$id, type='$type', key='$key', value=$value)" + } + @StringDef(TYPE.BOOLEAN, TYPE.STRING, TYPE.INTEGER, TYPE.ENUM) @Retention(AnnotationRetention.SOURCE) public annotation class ToggleType @@ -87,12 +189,17 @@ public data class Toggle( type = cursor.getStringOrThrow(ColumnNames.Toggle.COL_TYPE), key = cursor.getStringOrThrow(ColumnNames.Toggle.COL_KEY), value = cursor.getStringOrNull(ColumnNames.Toggle.COL_VALUE), - // scope = cursor.getStringOrNull("scope"), ) } } } +@JvmSynthetic +@Suppress("LibraryEntitiesShouldNotBePublic") +public fun Toggle(initializer: Toggle.Builder.() -> Unit): Toggle { + return Toggle.Builder().apply(initializer).build() +} + private fun Cursor.getStringOrThrow(columnName: String): String = getStringOrNull(columnName)!! private fun Cursor.getStringOrNull(columnName: String): String? { @@ -114,7 +221,8 @@ public object TogglesProviderContract { private val applicationUri = Uri.parse("content://$TOGGLES_AUTHORITY/application") private val configurationUri = Uri.parse("content://$TOGGLES_AUTHORITY/currentConfiguration") - private val configurationValueUri = Uri.parse("content://$TOGGLES_AUTHORITY/predefinedConfigurationValue") + private val configurationValueUri = + Uri.parse("content://$TOGGLES_AUTHORITY/predefinedConfigurationValue") @JvmStatic public fun applicationUri(id: Long): Uri { diff --git a/toggles-flow-noop/build.gradle.kts b/toggles-flow-noop/build.gradle.kts index 7817904e..78ecff74 100644 --- a/toggles-flow-noop/build.gradle.kts +++ b/toggles-flow-noop/build.gradle.kts @@ -14,5 +14,6 @@ dependencies { implementation(libs.se.eelde.toggles.toggles.core) implementation(libs.androidx.annotation) + implementation(platform(libs.org.jetbrains.kotlinx.kotlinx.coroutines.bom)) implementation(libs.org.jetbrains.kotlinx.kotlinx.coroutines.android) } diff --git a/toggles-flow-noop/gradle.properties b/toggles-flow-noop/gradle.properties index 5f7e1688..829c958c 100644 --- a/toggles-flow-noop/gradle.properties +++ b/toggles-flow-noop/gradle.properties @@ -29,5 +29,5 @@ android.useAndroidX=true android.enableJetifier=false org.gradle.caching=true -org.gradle.configuration-cache=false +org.gradle.configuration-cache=true org.gradle.configuration-cache.max-problems=5 diff --git a/toggles-flow/build.gradle.kts b/toggles-flow/build.gradle.kts index d8513119..e16816f1 100644 --- a/toggles-flow/build.gradle.kts +++ b/toggles-flow/build.gradle.kts @@ -23,5 +23,6 @@ dependencies { implementation(libs.se.eelde.toggles.toggles.core) implementation(libs.androidx.annotation) + implementation(platform(libs.org.jetbrains.kotlinx.kotlinx.coroutines.bom)) implementation(libs.org.jetbrains.kotlinx.kotlinx.coroutines.android) } diff --git a/toggles-flow/gradle.properties b/toggles-flow/gradle.properties index 580fe004..d323a277 100644 --- a/toggles-flow/gradle.properties +++ b/toggles-flow/gradle.properties @@ -29,5 +29,5 @@ android.useAndroidX=true android.enableJetifier=false org.gradle.caching=true -org.gradle.configuration-cache=false +org.gradle.configuration-cache=true org.gradle.configuration-cache.max-problems=5 diff --git a/toggles-flow/src/main/java/se/eelde/toggles/flow/TogglesImpl.kt b/toggles-flow/src/main/java/se/eelde/toggles/flow/TogglesImpl.kt index 96b17ef3..6a6a2093 100644 --- a/toggles-flow/src/main/java/se/eelde/toggles/flow/TogglesImpl.kt +++ b/toggles-flow/src/main/java/se/eelde/toggles/flow/TogglesImpl.kt @@ -39,6 +39,7 @@ public class TogglesImpl(context: Context) : Toggles { ) defaultValue } + toggle.value == null -> defaultValue else -> toggle.value!!.toBoolean() } @@ -56,6 +57,7 @@ public class TogglesImpl(context: Context) : Toggles { ) defaultValue } + toggle.value == null -> defaultValue else -> toggle.value!!.toInt() } @@ -73,6 +75,7 @@ public class TogglesImpl(context: Context) : Toggles { ) defaultValue } + toggle.value == null -> defaultValue else -> toggle.value!! } @@ -98,14 +101,15 @@ public class TogglesImpl(context: Context) : Toggles { for (enumConstant in type.enumConstants!!) { contentResolver.insert( toggleValueUri(), - ToggleValue( - configurationId = configurationId, + ToggleValue { + this.configurationId = configurationId value = enumConstant.toString() - ).toContentValues() + }.toContentValues() ) } defaultValue } + toggle.value == null -> defaultValue else -> java.lang.Enum.valueOf(type, toggle.value!!) } @@ -172,7 +176,13 @@ public class TogglesImpl(context: Context) : Toggles { cursor.close() } } - return@withContext Toggle(0L, type, key, "") + + return@withContext Toggle { + id = 0L + this.type = type + this.key = key + value = "" + } } private class ToggleContentObserver( diff --git a/toggles-prefs-noop/build.gradle.kts b/toggles-prefs-noop/build.gradle.kts index 428594bd..050ff594 100644 --- a/toggles-prefs-noop/build.gradle.kts +++ b/toggles-prefs-noop/build.gradle.kts @@ -14,5 +14,4 @@ dependencies { implementation(libs.se.eelde.toggles.toggles.core) implementation(libs.androidx.annotation) - implementation(libs.org.jetbrains.kotlinx.kotlinx.coroutines.android) } diff --git a/toggles-prefs-noop/gradle.properties b/toggles-prefs-noop/gradle.properties index 1ae96d63..0f25c8b6 100644 --- a/toggles-prefs-noop/gradle.properties +++ b/toggles-prefs-noop/gradle.properties @@ -29,5 +29,5 @@ android.useAndroidX=true android.enableJetifier=false org.gradle.caching=true -org.gradle.configuration-cache=false +org.gradle.configuration-cache=true org.gradle.configuration-cache.max-problems=5 diff --git a/toggles-prefs/gradle.properties b/toggles-prefs/gradle.properties index 234dc945..85b9837e 100644 --- a/toggles-prefs/gradle.properties +++ b/toggles-prefs/gradle.properties @@ -29,5 +29,5 @@ android.useAndroidX=true android.enableJetifier=false org.gradle.caching=true -org.gradle.configuration-cache=false +org.gradle.configuration-cache=true org.gradle.configuration-cache.max-problems=5 diff --git a/toggles-prefs/src/main/java/se/eelde/toggles/prefs/TogglesPreferencesImpl.kt b/toggles-prefs/src/main/java/se/eelde/toggles/prefs/TogglesPreferencesImpl.kt index 2e3fb6ef..fe1e50b3 100644 --- a/toggles-prefs/src/main/java/se/eelde/toggles/prefs/TogglesPreferencesImpl.kt +++ b/toggles-prefs/src/main/java/se/eelde/toggles/prefs/TogglesPreferencesImpl.kt @@ -78,11 +78,10 @@ public class TogglesPreferencesImpl(context: Context) : TogglesPreferences { for (enumConstant in type.enumConstants!!) { contentResolver.insert( toggleValueUri(), - ToggleValue( - configurationId = toggle.id, + ToggleValue { + configurationId = toggle.id value = enumConstant.toString() - ) - .toContentValues() + }.toContentValues() ) } } @@ -106,7 +105,11 @@ public class TogglesPreferencesImpl(context: Context) : TogglesPreferences { return Toggle.fromCursor(cursor) } } - - return Toggle(0, toggleType, key, null) + return Toggle { + id = 0 + type = toggleType + this.key = key + value = null + } } } diff --git a/toggles-sample/build.gradle.kts b/toggles-sample/build.gradle.kts index 72bd7a16..607b12da 100644 --- a/toggles-sample/build.gradle.kts +++ b/toggles-sample/build.gradle.kts @@ -1,10 +1,22 @@ +import java.io.FileInputStream +import java.util.Properties + plugins { id("toggles.android.application-conventions") id("dagger.hilt.android.plugin") - id("com.gladed.androidgitversion") version "0.4.14" id("toggles.ownership-conventions") id("app.cash.licensee") id("se.premex.gross") version "0.1.0" + id("com.google.devtools.ksp") +} + +val versionFile = File("versions.properties") +val versions = Properties().apply { + if (versionFile.exists()) { + FileInputStream(versionFile).use { + load(it) + } + } } licensee { @@ -19,18 +31,11 @@ licensee { allowUrl("https://raw.githubusercontent.com/erikeelde/toggles/master/LICENCE") } -androidGitVersion { - tagPattern = "^v[0-9]+.*" -} - android { - buildFeatures { - viewBinding = true - } defaultConfig { applicationId = "se.eelde.toggles.example" - versionName = androidGitVersion.name() - versionCode = androidGitVersion.code() + versionName = versions.getProperty("V_VERSION", "0.0.1") + versionCode = versions.getProperty("V_VERSION_CODE", "1").toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true @@ -48,6 +53,13 @@ android { namespace = "com.example.toggles" } +kotlin { + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(JavaVersion.VERSION_17.toString())) + vendor.set(JvmVendorSpec.AZUL) + } +} + dependencies { implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.compose.runtime) @@ -76,13 +88,14 @@ dependencies { testImplementation(libs.org.robolectric) implementation(libs.com.google.dagger.hilt.android) - kapt(libs.com.google.dagger.hilt.android.compiler) + ksp(libs.com.google.dagger.hilt.android.compiler) testImplementation(libs.com.google.dagger.hilt.android.testing) - kaptTest(libs.com.google.dagger.hilt.android.compiler) + kspTest(libs.com.google.dagger.hilt.android.compiler) implementation(libs.se.eelde.toggles.toggles.flow) implementation(libs.se.eelde.toggles.toggles.prefs) + implementation(platform(libs.org.jetbrains.kotlinx.kotlinx.coroutines.bom)) implementation(libs.org.jetbrains.kotlinx.kotlinx.coroutines.android) implementation(libs.androidx.core.core.ktx) diff --git a/toggles-sample/src/main/java/com/example/toggles/MainActivity.kt b/toggles-sample/src/main/java/com/example/toggles/MainActivity.kt index 584d03b1..c14d9773 100644 --- a/toggles-sample/src/main/java/com/example/toggles/MainActivity.kt +++ b/toggles-sample/src/main/java/com/example/toggles/MainActivity.kt @@ -1,7 +1,6 @@ package com.example.toggles import android.os.Bundle -import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize @@ -27,7 +26,6 @@ import com.example.toggles.flow.FlowView import com.example.toggles.prefs.PrefsView import dagger.hilt.android.AndroidEntryPoint import se.eelde.toggles.composetheme.TogglesTheme -import se.eelde.toggles.composetheme.rememberAppState import se.eelde.toggles.oss.OssView @AndroidEntryPoint @@ -38,8 +36,6 @@ class MainActivity : ComponentActivity() { setContent { TogglesTheme { val navController: NavHostController = rememberNavController() - val appState = rememberAppState(navController) - Log.e("appState", "current destination: ${appState.currentDestination}") Navigation(navController = navController) } diff --git a/toggles-sample/src/main/java/com/example/toggles/prefs/TogglesPrefsViewModel.kt b/toggles-sample/src/main/java/com/example/toggles/prefs/TogglesPrefsViewModel.kt index 3841d7b1..af77982b 100644 --- a/toggles-sample/src/main/java/com/example/toggles/prefs/TogglesPrefsViewModel.kt +++ b/toggles-sample/src/main/java/com/example/toggles/prefs/TogglesPrefsViewModel.kt @@ -79,10 +79,6 @@ class TogglesPrefsViewModel @Inject internal constructor( private fun getEnumConfiguration(): Config = Config.Success( title = resources.getString(R.string.enum_configuration), - value = togglesPreferences.getEnum( - resources.getString(R.string.enum_configuration), - MyEnum::class.java, - MyEnum.SECOND - ) + value = MyEnum.FIRST ) } diff --git a/versions.properties b/versions.properties new file mode 100644 index 00000000..b8c62057 --- /dev/null +++ b/versions.properties @@ -0,0 +1,6 @@ +V_VERSION=1.02.00 +V_VERSION_CODE=102000 +V_DEBUG_VERSION_SUFFIX=34-gc9aafc1-dirty +V_DEBUG_VERSION=1.02.00-34-gc9aafc1-dirty +HASH=c9aafc1 +DIRTY=true