From c1a5845958a55e56247e61de9ca9069eca66b83f Mon Sep 17 00:00:00 2001 From: Erik Eelde Date: Sun, 24 Sep 2023 11:55:16 +0200 Subject: [PATCH 01/15] WIP --- .github/version_information/action.yml | 29 ++++++++ .github/workflows/app-release.yml | 6 +- .github/workflows/core-release.yml | 2 +- .github/workflows/flow-noop-release.yml | 2 +- .github/workflows/flow-release.yml | 2 +- .github/workflows/post-merge.yml | 2 +- .github/workflows/prefs-noop-release.yml | 2 +- .github/workflows/prefs-release.yml | 2 +- .github/workflows/pull-request.yml | 2 +- ...gles.android.module-conventions.gradle.kts | 4 +- gradle/libs.versions.toml | 2 +- scripts/generate_versions.sh | 68 +++++++++++++++++++ toggles-app/build.gradle.kts | 20 +++--- .../java/se/eelde/toggles/MainActivity.kt | 10 +-- toggles-sample/build.gradle.kts | 31 ++++++--- .../toggles/prefs/TogglesPrefsViewModel.kt | 6 +- 16 files changed, 148 insertions(+), 42 deletions(-) create mode 100644 .github/version_information/action.yml create mode 100755 scripts/generate_versions.sh 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..1fc11596 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: 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/gradle/libs.versions.toml b/gradle/libs.versions.toml index 556b1779..9ad52352 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -111,7 +111,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.1.1" 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" 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/toggles-app/build.gradle.kts b/toggles-app/build.gradle.kts index 03aa8594..8fd9b802 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,12 +7,20 @@ 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" } +val versionFile = File("versions.properties") +val versions = Properties().apply { + if (versionFile.exists()) { + FileInputStream(versionFile).use { + load(it) + } + } +} + licensee { allow("Apache-2.0") @@ -23,10 +30,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 +64,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 diff --git a/toggles-app/src/main/java/se/eelde/toggles/MainActivity.kt b/toggles-app/src/main/java/se/eelde/toggles/MainActivity.kt index 201386ec..43fe9065 100644 --- a/toggles-app/src/main/java/se/eelde/toggles/MainActivity.kt +++ b/toggles-app/src/main/java/se/eelde/toggles/MainActivity.kt @@ -89,14 +89,8 @@ fun Navigation( BooleanValueView( uiState = state, popBackStack = { navController.popBackStack() }, - revert = { - viewModel.revertClick() - navController.popBackStack() - }, - save = { - viewModel.saveClick() - navController.popBackStack() - }, + revert = { viewModel.revertClick() }, + save = { viewModel.saveClick() }, setBooleanValue = { viewModel.checkedChanged(it) }, modifier = Modifier.padding(paddingValues), ) diff --git a/toggles-sample/build.gradle.kts b/toggles-sample/build.gradle.kts index 72bd7a16..d58cc561 100644 --- a/toggles-sample/build.gradle.kts +++ b/toggles-sample/build.gradle.kts @@ -1,12 +1,23 @@ +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" } +val versionFile = File("versions.properties") +val versions = Properties().apply { + if (versionFile.exists()) { + FileInputStream(versionFile).use { + load(it) + } + } +} + licensee { allow("Apache-2.0") @@ -19,18 +30,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 +52,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) 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 ) } From 421f7ce0f475cda0a92b1205af406a9cb1e3f94c Mon Sep 17 00:00:00 2001 From: Erik Eelde Date: Wed, 27 Sep 2023 00:17:34 +0200 Subject: [PATCH 02/15] WIP 2 --- .../6.json | 307 ++++++++++++++++++ .../eelde/toggles/database/WrenchDatabase.kt | 2 +- .../WrenchPredefinedConfigurationValue.kt | 8 +- .../WrenchPredefinedConfigurationValueDao.kt | 10 +- .../toggles/database/migrations/Migrations.kt | 26 ++ .../eelde/toggles/database/DatabaseHelper.kt | 77 ++++- .../eelde/toggles/database/MigrationTests.kt | 78 ++++- .../se/eelde/toggles/di/DatabaseModule.kt | 1 + .../dialogs/enumvalue/EnumValueViewModel.kt | 4 - .../eelde/toggles/provider/TogglesProvider.kt | 14 +- 10 files changed, 501 insertions(+), 26 deletions(-) create mode 100644 modules/database/schemas/se.eelde.toggles.database.WrenchDatabase/6.json 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..20b80dfc 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,13 @@ 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..6b293ae4 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,6 +11,7 @@ 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) { @@ -265,4 +266,29 @@ object Migrations { } } } + val MIGRATION_5_6: Migration = object : Migration(databaseVersion5, databaseVersion6) { + override fun migrate(database: SupportSQLiteDatabase) { + run { + val tableName = "predefinedConfigurationValue" + val tableNameTemp = tableName + "_temp" + + val newIndexName = "index_predefinedConfigurationValue_configurationId_value" + + // create new table with temp name and temp index + database.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 )" + ) + database.execSQL( + "CREATE UNIQUE INDEX IF NOT EXISTS `$newIndexName` ON `$tableNameTemp` (`configurationId`, `value`);" + ) + + // copy data from old table + drop it + database.execSQL("INSERT INTO $tableNameTemp SELECT * FROM $tableName GROUP BY configurationId, value") + database.execSQL("DROP TABLE $tableName") + + // rename database + database.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..9e7ff104 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 @@ -7,9 +7,50 @@ 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 { + 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 +63,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 +87,7 @@ object DatabaseHelper { return query } - fun insertWrenchApplication( + fun insertApplicationPre4( db: SupportSQLiteDatabase, applicationLabel: String, packageName: String @@ -42,7 +98,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..4bea49f5 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,59 @@ 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/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/EnumValueViewModel.kt b/toggles-app/src/main/java/se/eelde/toggles/dialogs/enumvalue/EnumValueViewModel.kt index 19b0bcae..b85242cc 100644 --- a/toggles-app/src/main/java/se/eelde/toggles/dialogs/enumvalue/EnumValueViewModel.kt +++ b/toggles-app/src/main/java/se/eelde/toggles/dialogs/enumvalue/EnumValueViewModel.kt @@ -49,10 +49,6 @@ 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 diff --git a/toggles-app/src/main/java/se/eelde/toggles/provider/TogglesProvider.kt b/toggles-app/src/main/java/se/eelde/toggles/provider/TogglesProvider.kt index 9b9c805a..fc00892a 100644 --- a/toggles-app/src/main/java/se/eelde/toggles/provider/TogglesProvider.kt +++ b/toggles-app/src/main/java/se/eelde/toggles/provider/TogglesProvider.kt @@ -152,7 +152,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) } } } @@ -221,7 +222,14 @@ class TogglesProvider : ContentProvider() { PREDEFINED_CONFIGURATION_VALUES -> { val fullConfig = WrenchPredefinedConfigurationValue.fromContentValues(values!!) - insertId = predefinedConfigurationDao.insert(fullConfig) + insertId = try { + predefinedConfigurationDao.insert(fullConfig) + } catch (exception: SQLiteConstraintException) { + predefinedConfigurationDao.getByConfigurationAndValueId( + fullConfig.configurationId, + fullConfig.value!! + ).id + } } else -> { @@ -406,7 +414,7 @@ class TogglesProvider : ContentProvider() { if (strictApiVersion) { throw IllegalArgumentException( "This content provider requires you to provide a " + - "valid api-version in a queryParameter" + "valid api-version in a queryParameter" ) } } From 78ec0d28937b530d37dc95c0b95a36c6d386e188 Mon Sep 17 00:00:00 2001 From: Erik Eelde Date: Wed, 27 Sep 2023 22:43:36 +0200 Subject: [PATCH 03/15] Better core --- toggles-core/api/toggles-core.api | 43 +++--- .../toggles/core/TogglesProviderContract.kt | 144 +++++++++++++++--- 2 files changed, 147 insertions(+), 40 deletions(-) diff --git a/toggles-core/api/toggles-core.api b/toggles-core/api/toggles-core.api index f99fa57a..af57db77 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,14 +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 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 synthetic fun (JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun equals (Ljava/lang/Object;)Z public static final fun fromContentValues (Landroid/content/ContentValues;)Lse/eelde/toggles/core/Toggle; public static final fun fromCursor (Landroid/database/Cursor;)Lse/eelde/toggles/core/Toggle; @@ -40,6 +33,15 @@ 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 setId (J)Lse/eelde/toggles/core/Toggle$Builder; + public final fun setKey (Ljava/lang/String;)Lse/eelde/toggles/core/Toggle$Builder; + public final fun setType (Ljava/lang/String;)Lse/eelde/toggles/core/Toggle$Builder; + public final fun setValue (Ljava/lang/String;)Lse/eelde/toggles/core/Toggle$Builder; +} + 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 +59,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 +69,14 @@ 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 setConfigurationId (J)Lse/eelde/toggles/core/ToggleValue$Builder; + public final fun setId (J)Lse/eelde/toggles/core/ToggleValue$Builder; + public final fun setValue (Ljava/lang/String;)Lse/eelde/toggles/core/ToggleValue$Builder; +} + 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 +86,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/src/main/java/se/eelde/toggles/core/TogglesProviderContract.kt b/toggles-core/src/main/java/se/eelde/toggles/core/TogglesProviderContract.kt index 09d046c4..4c37af22 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,103 @@ 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 + private var id: Long = 0 + + @set:JvmSynthetic + private var configurationId: Long = 0 + + @set:JvmSynthetic + private 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 + private var id: Long = 0 + + @set:JvmSynthetic + @ToggleType + private var type: String = "" + + @set:JvmSynthetic + private var key: String = "" + + @set:JvmSynthetic + private 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 toContentValues(): ContentValues = ContentValues().apply { put(ColumnNames.Toggle.COL_ID, id) @@ -58,6 +126,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 +181,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 +213,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 { From 992d36d928bb63cf513ee91bb90306113dd72a93 Mon Sep 17 00:00:00 2001 From: Erik Eelde Date: Wed, 27 Sep 2023 23:49:58 +0200 Subject: [PATCH 04/15] WIUP --- config/detekt/detekt.yml | 1 + gradle/libs.versions.toml | 9 +- .../WrenchPredefinedConfigurationValue.kt | 16 +- .../toggles/database/migrations/Migrations.kt | 166 +++++++++--------- .../eelde/toggles/database/DatabaseHelper.kt | 3 + .../eelde/toggles/database/MigrationTests.kt | 31 ++-- settings.gradle.kts | 2 +- .../dialogs/enumvalue/EnumValueViewModel.kt | 1 - .../eelde/toggles/dialogs/scope/ScopeView.kt | 1 - .../eelde/toggles/provider/TogglesProvider.kt | 4 +- .../toggles/core/TogglesProviderContract.kt | 14 +- .../java/se/eelde/toggles/flow/Toggles.kt | 12 +- .../java/se/eelde/toggles/flow/TogglesImpl.kt | 4 +- .../eelde/toggles/prefs/TogglesPreferences.kt | 12 +- .../toggles/prefs/TogglesPreferencesImpl.kt | 4 +- .../eelde/toggles/prefs/TogglesPreferences.kt | 12 +- .../toggles/prefs/TogglesPreferencesImpl.kt | 20 ++- 17 files changed, 164 insertions(+), 148 deletions(-) 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/libs.versions.toml b/gradle/libs.versions.toml index 9ad52352..a33fdfce 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -174,9 +174,12 @@ org-jetbrains-kotlinx-kotlinx-coroutines-bom = { module = "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-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" } +#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-github-ben-manes-versions = "com.github.ben-manes.versions:0.47.0" 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 20b80dfc..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,13 +11,15 @@ import se.eelde.toggles.database.tables.PredefinedConfigurationValueTable @Entity( tableName = PredefinedConfigurationValueTable.TABLE_NAME, - indices = [Index( - value = arrayOf( - PredefinedConfigurationValueTable.COL_CONFIG_ID, - PredefinedConfigurationValueTable.COL_VALUE - ), - unique = true - )], + 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/migrations/Migrations.kt b/modules/database/src/main/java/se/eelde/toggles/database/migrations/Migrations.kt index 6b293ae4..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 @@ -14,131 +14,131 @@ object Migrations { 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`)" ) } @@ -147,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`))" ) } @@ -236,38 +236,38 @@ 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(database: SupportSQLiteDatabase) { + override fun migrate(db: SupportSQLiteDatabase) { run { val tableName = "predefinedConfigurationValue" val tableNameTemp = tableName + "_temp" @@ -275,19 +275,19 @@ object Migrations { val newIndexName = "index_predefinedConfigurationValue_configurationId_value" // create new table with temp name and temp index - database.execSQL( + 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 )" ) - database.execSQL( + db.execSQL( "CREATE UNIQUE INDEX IF NOT EXISTS `$newIndexName` ON `$tableNameTemp` (`configurationId`, `value`);" ) // copy data from old table + drop it - database.execSQL("INSERT INTO $tableNameTemp SELECT * FROM $tableName GROUP BY configurationId, value") - database.execSQL("DROP TABLE $tableName") + db.execSQL("INSERT INTO $tableNameTemp SELECT * FROM $tableName GROUP BY configurationId, value") + db.execSQL("DROP TABLE $tableName") // rename database - database.execSQL("ALTER TABLE $tableNameTemp RENAME TO $tableName") + 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 9e7ff104..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 @@ -30,6 +32,7 @@ object DatabaseHelper { db: SupportSQLiteDatabase, configId: Long ): List { + @Suppress("MaxLineLength") val query = db.query( "SELECT * FROM ${PredefinedConfigurationValueTable.TABLE_NAME} WHERE ${PredefinedConfigurationValueTable.COL_CONFIG_ID} = ?", arrayOf(configId) 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 4bea49f5..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 @@ -158,21 +158,27 @@ class MigrationTests { @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", - )) + assertEquals( + 1, + DatabaseHelper.insertApplication( + originalDb, + "TestApplication", + "se.eelde.toggles.application", + "se.eelde.toggles.application", + ) + ) // insert data - assertEquals(1, DatabaseHelper.insertConfiguration( - originalDb, + assertEquals( 1, - "MyEnum", - Toggle.TYPE.ENUM, - 0, - )) + DatabaseHelper.insertConfiguration( + originalDb, + 1, + "MyEnum", + Toggle.TYPE.ENUM, + 0, + ) + ) assertEquals(1, DatabaseHelper.insertPredefinedConfigurationValue(originalDb, 1, "a")) assertEquals(2, DatabaseHelper.insertPredefinedConfigurationValue(originalDb, 1, "a")) @@ -197,7 +203,6 @@ class MigrationTests { ) assertEquals(3, values.size) - } companion object { diff --git a/settings.gradle.kts b/settings.gradle.kts index adb29564..b95704d6 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") diff --git a/toggles-app/src/main/java/se/eelde/toggles/dialogs/enumvalue/EnumValueViewModel.kt b/toggles-app/src/main/java/se/eelde/toggles/dialogs/enumvalue/EnumValueViewModel.kt index b85242cc..4ea5f0f4 100644 --- a/toggles-app/src/main/java/se/eelde/toggles/dialogs/enumvalue/EnumValueViewModel.kt +++ b/toggles-app/src/main/java/se/eelde/toggles/dialogs/enumvalue/EnumValueViewModel.kt @@ -1,7 +1,6 @@ package se.eelde.toggles.dialogs.enumvalue import android.app.Application -import androidx.lifecycle.LiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope 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..c334ebb5 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 @@ -36,7 +36,6 @@ fun ScopeValueView(navController: NavController, viewModel: ScopeFragmentViewMod ) LazyColumn { uiState.value.scopes.forEach { - 0 item { ListItem( modifier = Modifier.clickable { diff --git a/toggles-app/src/main/java/se/eelde/toggles/provider/TogglesProvider.kt b/toggles-app/src/main/java/se/eelde/toggles/provider/TogglesProvider.kt index fc00892a..4bdceee3 100644 --- a/toggles-app/src/main/java/se/eelde/toggles/provider/TogglesProvider.kt +++ b/toggles-app/src/main/java/se/eelde/toggles/provider/TogglesProvider.kt @@ -224,7 +224,7 @@ class TogglesProvider : ContentProvider() { val fullConfig = WrenchPredefinedConfigurationValue.fromContentValues(values!!) insertId = try { predefinedConfigurationDao.insert(fullConfig) - } catch (exception: SQLiteConstraintException) { + } catch (_: SQLiteConstraintException) { predefinedConfigurationDao.getByConfigurationAndValueId( fullConfig.configurationId, fullConfig.value!! @@ -414,7 +414,7 @@ class TogglesProvider : ContentProvider() { if (strictApiVersion) { throw IllegalArgumentException( "This content provider requires you to provide a " + - "valid api-version in a queryParameter" + "valid api-version in a queryParameter" ) } } 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 4c37af22..945ff3c5 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 @@ -30,13 +30,13 @@ public class ToggleValue private constructor( public class Builder { @set:JvmSynthetic - private var id: Long = 0 + public var id: Long = 0 @set:JvmSynthetic - private var configurationId: Long = 0 + public var configurationId: Long = 0 @set:JvmSynthetic - private var value: String? = null + public var value: String? = null public fun setId(id: Long): Builder = apply { this.id = id } public fun setConfigurationId(configurationId: Long): Builder = @@ -96,17 +96,17 @@ public class Toggle private constructor( ) { public class Builder { @set:JvmSynthetic - private var id: Long = 0 + public var id: Long = 0 @set:JvmSynthetic @ToggleType - private var type: String = "" + public var type: String = "" @set:JvmSynthetic - private var key: String = "" + public var key: String = "" @set:JvmSynthetic - private var value: String? = null + public var value: String? = null public fun setId(id: Long): Builder = apply { this.id = id } public fun setType(@ToggleType type: String): Builder = diff --git a/toggles-flow-noop/src/main/java/se/eelde/toggles/flow/Toggles.kt b/toggles-flow-noop/src/main/java/se/eelde/toggles/flow/Toggles.kt index c3c1c3ce..3f505980 100644 --- a/toggles-flow-noop/src/main/java/se/eelde/toggles/flow/Toggles.kt +++ b/toggles-flow-noop/src/main/java/se/eelde/toggles/flow/Toggles.kt @@ -2,12 +2,12 @@ package se.eelde.toggles.flow import kotlinx.coroutines.flow.Flow -@Suppress("LibraryEntitiesShouldNotBePublic") -public interface Toggles { - public fun toggle(key: String, defaultValue: Boolean): Flow - public fun toggle(key: String, defaultValue: String): Flow - public fun toggle(key: String, defaultValue: Int): Flow - public fun > toggle( +@PublishedApi +internal interface Toggles { + fun toggle(key: String, defaultValue: Boolean): Flow + fun toggle(key: String, defaultValue: String): Flow + fun toggle(key: String, defaultValue: Int): Flow + fun > toggle( key: String, type: Class, defaultValue: T diff --git a/toggles-flow-noop/src/main/java/se/eelde/toggles/flow/TogglesImpl.kt b/toggles-flow-noop/src/main/java/se/eelde/toggles/flow/TogglesImpl.kt index 35d43217..4485f964 100644 --- a/toggles-flow-noop/src/main/java/se/eelde/toggles/flow/TogglesImpl.kt +++ b/toggles-flow-noop/src/main/java/se/eelde/toggles/flow/TogglesImpl.kt @@ -4,8 +4,8 @@ import android.content.Context import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf -@Suppress("LibraryEntitiesShouldNotBePublic") -public class TogglesImpl(@Suppress("UNUSED_PARAMETER") context: Context) : Toggles { +@PublishedApi +internal class TogglesImpl(@Suppress("UNUSED_PARAMETER") context: Context) : Toggles { override fun toggle(key: String, defaultValue: Boolean): Flow = flowOf(defaultValue) diff --git a/toggles-prefs-noop/src/main/java/se/eelde/toggles/prefs/TogglesPreferences.kt b/toggles-prefs-noop/src/main/java/se/eelde/toggles/prefs/TogglesPreferences.kt index 54e1b618..c7fdd978 100644 --- a/toggles-prefs-noop/src/main/java/se/eelde/toggles/prefs/TogglesPreferences.kt +++ b/toggles-prefs-noop/src/main/java/se/eelde/toggles/prefs/TogglesPreferences.kt @@ -1,9 +1,9 @@ package se.eelde.toggles.prefs -@Suppress("LibraryEntitiesShouldNotBePublic") -public interface TogglesPreferences { - public fun getBoolean(key: String, defValue: Boolean): Boolean - public fun getInt(key: String, defValue: Int): Int - public fun getString(key: String, defValue: String): String - public fun > getEnum(key: String, type: Class, defValue: T): T +@PublishedApi +internal interface TogglesPreferences { + fun getBoolean(key: String, defValue: Boolean): Boolean + fun getInt(key: String, defValue: Int): Int + fun getString(key: String, defValue: String): String + fun > getEnum(key: String, type: Class, defValue: T): T } diff --git a/toggles-prefs-noop/src/main/java/se/eelde/toggles/prefs/TogglesPreferencesImpl.kt b/toggles-prefs-noop/src/main/java/se/eelde/toggles/prefs/TogglesPreferencesImpl.kt index cc4117a3..fbc66954 100644 --- a/toggles-prefs-noop/src/main/java/se/eelde/toggles/prefs/TogglesPreferencesImpl.kt +++ b/toggles-prefs-noop/src/main/java/se/eelde/toggles/prefs/TogglesPreferencesImpl.kt @@ -2,8 +2,8 @@ package se.eelde.toggles.prefs import android.content.Context -@Suppress("LibraryEntitiesShouldNotBePublic") -public class TogglesPreferencesImpl(@Suppress("UNUSED_PARAMETER") context: Context) : TogglesPreferences { +@PublishedApi +internal class TogglesPreferencesImpl(@Suppress("UNUSED_PARAMETER") context: Context) : TogglesPreferences { override fun getBoolean(key: String, defValue: Boolean): Boolean = defValue diff --git a/toggles-prefs/src/main/java/se/eelde/toggles/prefs/TogglesPreferences.kt b/toggles-prefs/src/main/java/se/eelde/toggles/prefs/TogglesPreferences.kt index 54e1b618..c7fdd978 100644 --- a/toggles-prefs/src/main/java/se/eelde/toggles/prefs/TogglesPreferences.kt +++ b/toggles-prefs/src/main/java/se/eelde/toggles/prefs/TogglesPreferences.kt @@ -1,9 +1,9 @@ package se.eelde.toggles.prefs -@Suppress("LibraryEntitiesShouldNotBePublic") -public interface TogglesPreferences { - public fun getBoolean(key: String, defValue: Boolean): Boolean - public fun getInt(key: String, defValue: Int): Int - public fun getString(key: String, defValue: String): String - public fun > getEnum(key: String, type: Class, defValue: T): T +@PublishedApi +internal interface TogglesPreferences { + fun getBoolean(key: String, defValue: Boolean): Boolean + fun getInt(key: String, defValue: Int): Int + fun getString(key: String, defValue: String): String + fun > getEnum(key: String, type: Class, defValue: T): T } 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..52618bf8 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 @@ -7,11 +7,12 @@ import se.eelde.toggles.core.ToggleValue import se.eelde.toggles.core.TogglesProviderContract.toggleUri import se.eelde.toggles.core.TogglesProviderContract.toggleValueUri -@Suppress("LibraryEntitiesShouldNotBePublic") -public class TogglesPreferencesImpl(context: Context) : TogglesPreferences { +@PublishedApi +internal class TogglesPreferencesImpl(context: Context) : TogglesPreferences { private val context = context.applicationContext private val contentResolver: ContentResolver = this.context.contentResolver + override fun getBoolean(key: String, defValue: Boolean): Boolean { var toggle = getToggle( contentResolver = contentResolver, @@ -78,11 +79,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 +106,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 + } } } From a33789998e8d853535d85a9a7d66b8782b0aa112 Mon Sep 17 00:00:00 2001 From: Erik Eelde Date: Fri, 29 Sep 2023 14:32:41 +0200 Subject: [PATCH 05/15] Fixi --- gradle/libs.versions.toml | 8 ++++---- .../toggles/core/TogglesProviderContract.kt | 8 ++++++++ .../main/java/se/eelde/toggles/flow/Toggles.kt | 12 ++++++------ .../java/se/eelde/toggles/flow/TogglesImpl.kt | 4 ++-- .../java/se/eelde/toggles/flow/TogglesImpl.kt | 18 ++++++++++++++---- .../eelde/toggles/prefs/TogglesPreferences.kt | 4 ++-- .../toggles/prefs/TogglesPreferencesImpl.kt | 4 ++-- .../eelde/toggles/prefs/TogglesPreferences.kt | 12 ++++++------ .../toggles/prefs/TogglesPreferencesImpl.kt | 4 ++-- 9 files changed, 46 insertions(+), 28 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a33fdfce..089aa180 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.09.02" +androidx-compose-compiler = "1.5.3" androidx-core = "1.9.0" androidx-hilt = "1.0.0" androidx-legacy = "1.0.0" @@ -17,7 +17,7 @@ com-google-dagger = "2.47" com-squareup-leakcanary = "2.8.1" com-squareup-moshi = "1.15.0" dokka = "1.8.20" -kotlin = "1.9.0" +kotlin = "1.9.10" org-jetbrains-kotlinx = "1.7.1" [libraries] @@ -111,7 +111,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.1" +com-android-tools-build-gradle = "com.android.tools.build:gradle:8.1.2" 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" 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 945ff3c5..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 @@ -119,6 +119,14 @@ public class Toggle private constructor( 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) put(ColumnNames.Toggle.COL_KEY, key) diff --git a/toggles-flow-noop/src/main/java/se/eelde/toggles/flow/Toggles.kt b/toggles-flow-noop/src/main/java/se/eelde/toggles/flow/Toggles.kt index 3f505980..c3c1c3ce 100644 --- a/toggles-flow-noop/src/main/java/se/eelde/toggles/flow/Toggles.kt +++ b/toggles-flow-noop/src/main/java/se/eelde/toggles/flow/Toggles.kt @@ -2,12 +2,12 @@ package se.eelde.toggles.flow import kotlinx.coroutines.flow.Flow -@PublishedApi -internal interface Toggles { - fun toggle(key: String, defaultValue: Boolean): Flow - fun toggle(key: String, defaultValue: String): Flow - fun toggle(key: String, defaultValue: Int): Flow - fun > toggle( +@Suppress("LibraryEntitiesShouldNotBePublic") +public interface Toggles { + public fun toggle(key: String, defaultValue: Boolean): Flow + public fun toggle(key: String, defaultValue: String): Flow + public fun toggle(key: String, defaultValue: Int): Flow + public fun > toggle( key: String, type: Class, defaultValue: T diff --git a/toggles-flow-noop/src/main/java/se/eelde/toggles/flow/TogglesImpl.kt b/toggles-flow-noop/src/main/java/se/eelde/toggles/flow/TogglesImpl.kt index 4485f964..35d43217 100644 --- a/toggles-flow-noop/src/main/java/se/eelde/toggles/flow/TogglesImpl.kt +++ b/toggles-flow-noop/src/main/java/se/eelde/toggles/flow/TogglesImpl.kt @@ -4,8 +4,8 @@ import android.content.Context import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf -@PublishedApi -internal class TogglesImpl(@Suppress("UNUSED_PARAMETER") context: Context) : Toggles { +@Suppress("LibraryEntitiesShouldNotBePublic") +public class TogglesImpl(@Suppress("UNUSED_PARAMETER") context: Context) : Toggles { override fun toggle(key: String, defaultValue: Boolean): Flow = flowOf(defaultValue) 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/src/main/java/se/eelde/toggles/prefs/TogglesPreferences.kt b/toggles-prefs-noop/src/main/java/se/eelde/toggles/prefs/TogglesPreferences.kt index c7fdd978..d26568a1 100644 --- a/toggles-prefs-noop/src/main/java/se/eelde/toggles/prefs/TogglesPreferences.kt +++ b/toggles-prefs-noop/src/main/java/se/eelde/toggles/prefs/TogglesPreferences.kt @@ -1,7 +1,7 @@ package se.eelde.toggles.prefs -@PublishedApi -internal interface TogglesPreferences { +@Suppress("LibraryEntitiesShouldNotBePublic") +public interface TogglesPreferences { fun getBoolean(key: String, defValue: Boolean): Boolean fun getInt(key: String, defValue: Int): Int fun getString(key: String, defValue: String): String diff --git a/toggles-prefs-noop/src/main/java/se/eelde/toggles/prefs/TogglesPreferencesImpl.kt b/toggles-prefs-noop/src/main/java/se/eelde/toggles/prefs/TogglesPreferencesImpl.kt index fbc66954..cc4117a3 100644 --- a/toggles-prefs-noop/src/main/java/se/eelde/toggles/prefs/TogglesPreferencesImpl.kt +++ b/toggles-prefs-noop/src/main/java/se/eelde/toggles/prefs/TogglesPreferencesImpl.kt @@ -2,8 +2,8 @@ package se.eelde.toggles.prefs import android.content.Context -@PublishedApi -internal class TogglesPreferencesImpl(@Suppress("UNUSED_PARAMETER") context: Context) : TogglesPreferences { +@Suppress("LibraryEntitiesShouldNotBePublic") +public class TogglesPreferencesImpl(@Suppress("UNUSED_PARAMETER") context: Context) : TogglesPreferences { override fun getBoolean(key: String, defValue: Boolean): Boolean = defValue diff --git a/toggles-prefs/src/main/java/se/eelde/toggles/prefs/TogglesPreferences.kt b/toggles-prefs/src/main/java/se/eelde/toggles/prefs/TogglesPreferences.kt index c7fdd978..54e1b618 100644 --- a/toggles-prefs/src/main/java/se/eelde/toggles/prefs/TogglesPreferences.kt +++ b/toggles-prefs/src/main/java/se/eelde/toggles/prefs/TogglesPreferences.kt @@ -1,9 +1,9 @@ package se.eelde.toggles.prefs -@PublishedApi -internal interface TogglesPreferences { - fun getBoolean(key: String, defValue: Boolean): Boolean - fun getInt(key: String, defValue: Int): Int - fun getString(key: String, defValue: String): String - fun > getEnum(key: String, type: Class, defValue: T): T +@Suppress("LibraryEntitiesShouldNotBePublic") +public interface TogglesPreferences { + public fun getBoolean(key: String, defValue: Boolean): Boolean + public fun getInt(key: String, defValue: Int): Int + public fun getString(key: String, defValue: String): String + public fun > getEnum(key: String, type: Class, defValue: T): T } 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 52618bf8..3569644f 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 @@ -7,8 +7,8 @@ import se.eelde.toggles.core.ToggleValue import se.eelde.toggles.core.TogglesProviderContract.toggleUri import se.eelde.toggles.core.TogglesProviderContract.toggleValueUri -@PublishedApi -internal class TogglesPreferencesImpl(context: Context) : TogglesPreferences { +@Suppress("LibraryEntitiesShouldNotBePublic") +public class TogglesPreferencesImpl(context: Context) : TogglesPreferences { private val context = context.applicationContext private val contentResolver: ContentResolver = this.context.contentResolver From b46b013c6591b3e29610d881d6f3e9f4fcf3016e Mon Sep 17 00:00:00 2001 From: Erik Eelde Date: Sat, 30 Sep 2023 01:07:19 +0200 Subject: [PATCH 06/15] Modularize a bit --- build-logic/conventions/build.gradle.kts | 2 +- build.gradle.kts | 2 + gradle.properties | 2 +- gradle/libs.versions.toml | 12 +++++ modules/applications/consumer-rules.pro | 0 modules/applications/proguard-rules.pro | 21 -------- modules/booleanconfiguration/.gitignore | 1 + modules/booleanconfiguration/OWNERSHIP.toml | 4 ++ modules/booleanconfiguration/build.gradle.kts | 33 ++++++++++++ .../booleanconfiguration/lint-baseline.xml | 4 ++ .../src/main/AndroidManifest.xml | 4 ++ .../booleanconfiguration}/BooleanValueView.kt | 4 +- .../BooleanValueViewModel.kt | 15 +++--- modules/configurations/.gitignore | 1 + modules/configurations/OWNERSHIP.toml | 4 ++ modules/configurations/build.gradle.kts | 34 ++++++++++++ modules/configurations/lint-baseline.xml | 4 ++ .../src/main/AndroidManifest.xml | 4 ++ .../configurations}/ConfigurationListView.kt | 31 ++++++----- .../configurations}/ConfigurationViewModel.kt | 3 +- .../configurations}/ConfigurationsEntry.kt | 13 +++-- modules/database/consumer-rules.pro | 0 modules/enumconfiguration/.gitignore | 1 + modules/enumconfiguration/OWNERSHIP.toml | 4 ++ modules/enumconfiguration/build.gradle.kts | 33 ++++++++++++ modules/enumconfiguration/lint-baseline.xml | 4 ++ .../src/main/AndroidManifest.xml | 4 ++ .../enumconfiguration}/EnumValueView.kt | 4 +- .../enumconfiguration}/EnumValueViewModel.kt | 10 ++-- modules/help/.gitignore | 1 + modules/help/OWNERSHIP.toml | 4 ++ modules/help/build.gradle.kts | 30 +++++++++++ modules/help/lint-baseline.xml | 4 ++ modules/help/src/main/AndroidManifest.xml | 4 ++ .../java/se/eelde/toggles/help/HelpView.kt | 0 modules/integerconfiguration/.gitignore | 1 + modules/integerconfiguration/OWNERSHIP.toml | 4 ++ modules/integerconfiguration/build.gradle.kts | 33 ++++++++++++ .../integerconfiguration/lint-baseline.xml | 4 ++ .../src/main/AndroidManifest.xml | 4 ++ .../integerconfiguration}/IntegerValueView.kt | 4 +- .../IntegerValueViewModel.kt | 10 ++-- modules/provider/.gitignore | 1 + modules/provider/OWNERSHIP.toml | 4 ++ modules/provider/build.gradle.kts | 34 ++++++++++++ modules/provider/lint-baseline.xml | 4 ++ modules/provider/src/main/AndroidManifest.xml | 4 ++ .../provider/ContentResolverExtensions.kt | 0 .../toggles/provider/PackageManagerWrapper.kt | 0 .../toggles/provider/TogglesApiVersion.kt | 0 .../eelde/toggles/provider/TogglesProvider.kt | 49 ++++++++--------- .../toggles/provider/TogglesUriMatcher.kt | 53 +++++++++++++++++++ modules/stringconfiguration/.gitignore | 1 + modules/stringconfiguration/OWNERSHIP.toml | 4 ++ modules/stringconfiguration/build.gradle.kts | 33 ++++++++++++ modules/stringconfiguration/lint-baseline.xml | 4 ++ .../src/main/AndroidManifest.xml | 4 ++ .../stringconfiguration}/StringValueView.kt | 4 +- .../StringValueViewModel.kt | 10 ++-- settings.gradle.kts | 7 +++ toggles-app/build.gradle.kts | 7 +++ toggles-app/src/main/AndroidManifest.xml | 1 - .../java/se/eelde/toggles/MainActivity.kt | 36 ++++++++----- .../se/eelde/toggles/TogglesUriMatcher.kt | 45 ---------------- .../se/eelde/toggles/di/ApplicationModule.kt | 5 ++ .../toggles/provider/TogglesProviderTest.kt | 24 ++++++--- toggles-core/gradle.properties | 2 +- toggles-flow-noop/gradle.properties | 2 +- toggles-flow/gradle.properties | 2 +- toggles-prefs-noop/gradle.properties | 2 +- toggles-prefs/gradle.properties | 2 +- versions.properties | 6 +++ 72 files changed, 544 insertions(+), 167 deletions(-) delete mode 100644 modules/applications/consumer-rules.pro delete mode 100644 modules/applications/proguard-rules.pro create mode 100644 modules/booleanconfiguration/.gitignore create mode 100644 modules/booleanconfiguration/OWNERSHIP.toml create mode 100644 modules/booleanconfiguration/build.gradle.kts create mode 100644 modules/booleanconfiguration/lint-baseline.xml create mode 100644 modules/booleanconfiguration/src/main/AndroidManifest.xml rename {toggles-app/src/main/java/se/eelde/toggles/dialogs/booleanvalue => modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration}/BooleanValueView.kt (96%) rename {toggles-app/src/main/java/se/eelde/toggles/dialogs/booleanvalue => modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration}/BooleanValueViewModel.kt (93%) create mode 100644 modules/configurations/.gitignore create mode 100644 modules/configurations/OWNERSHIP.toml create mode 100644 modules/configurations/build.gradle.kts create mode 100644 modules/configurations/lint-baseline.xml create mode 100644 modules/configurations/src/main/AndroidManifest.xml rename {toggles-app/src/main/java/se/eelde/toggles/configurationlist => modules/configurations/src/main/java/se/eelde/toggles/configurations}/ConfigurationListView.kt (77%) rename {toggles-app/src/main/java/se/eelde/toggles/configurationlist => modules/configurations/src/main/java/se/eelde/toggles/configurations}/ConfigurationViewModel.kt (99%) rename {toggles-app/src/main/java/se/eelde/toggles/configurationlist => modules/configurations/src/main/java/se/eelde/toggles/configurations}/ConfigurationsEntry.kt (88%) delete mode 100644 modules/database/consumer-rules.pro create mode 100644 modules/enumconfiguration/.gitignore create mode 100644 modules/enumconfiguration/OWNERSHIP.toml create mode 100644 modules/enumconfiguration/build.gradle.kts create mode 100644 modules/enumconfiguration/lint-baseline.xml create mode 100644 modules/enumconfiguration/src/main/AndroidManifest.xml rename {toggles-app/src/main/java/se/eelde/toggles/dialogs/enumvalue => modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration}/EnumValueView.kt (97%) rename {toggles-app/src/main/java/se/eelde/toggles/dialogs/enumvalue => modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration}/EnumValueViewModel.kt (96%) create mode 100644 modules/help/.gitignore create mode 100644 modules/help/OWNERSHIP.toml create mode 100644 modules/help/build.gradle.kts create mode 100644 modules/help/lint-baseline.xml create mode 100644 modules/help/src/main/AndroidManifest.xml rename {toggles-app => modules/help}/src/main/java/se/eelde/toggles/help/HelpView.kt (100%) create mode 100644 modules/integerconfiguration/.gitignore create mode 100644 modules/integerconfiguration/OWNERSHIP.toml create mode 100644 modules/integerconfiguration/build.gradle.kts create mode 100644 modules/integerconfiguration/lint-baseline.xml create mode 100644 modules/integerconfiguration/src/main/AndroidManifest.xml rename {toggles-app/src/main/java/se/eelde/toggles/dialogs/integervalue => modules/integerconfiguration/src/main/java/se/eelde/toggles/integerconfiguration}/IntegerValueView.kt (97%) rename {toggles-app/src/main/java/se/eelde/toggles/dialogs/integervalue => modules/integerconfiguration/src/main/java/se/eelde/toggles/integerconfiguration}/IntegerValueViewModel.kt (96%) create mode 100644 modules/provider/.gitignore create mode 100644 modules/provider/OWNERSHIP.toml create mode 100644 modules/provider/build.gradle.kts create mode 100644 modules/provider/lint-baseline.xml create mode 100644 modules/provider/src/main/AndroidManifest.xml rename {toggles-app => modules/provider}/src/main/java/se/eelde/toggles/provider/ContentResolverExtensions.kt (100%) rename {toggles-app => modules/provider}/src/main/java/se/eelde/toggles/provider/PackageManagerWrapper.kt (100%) rename {toggles-app => modules/provider}/src/main/java/se/eelde/toggles/provider/TogglesApiVersion.kt (100%) rename {toggles-app => modules/provider}/src/main/java/se/eelde/toggles/provider/TogglesProvider.kt (90%) create mode 100644 modules/provider/src/main/java/se/eelde/toggles/provider/TogglesUriMatcher.kt create mode 100644 modules/stringconfiguration/.gitignore create mode 100644 modules/stringconfiguration/OWNERSHIP.toml create mode 100644 modules/stringconfiguration/build.gradle.kts create mode 100644 modules/stringconfiguration/lint-baseline.xml create mode 100644 modules/stringconfiguration/src/main/AndroidManifest.xml rename {toggles-app/src/main/java/se/eelde/toggles/dialogs/stringvalue => modules/stringconfiguration/src/main/java/se/eelde/toggles/stringconfiguration}/StringValueView.kt (97%) rename {toggles-app/src/main/java/se/eelde/toggles/dialogs/stringvalue => modules/stringconfiguration/src/main/java/se/eelde/toggles/stringconfiguration}/StringValueViewModel.kt (96%) delete mode 100644 toggles-app/src/main/java/se/eelde/toggles/TogglesUriMatcher.kt create mode 100644 versions.properties diff --git a/build-logic/conventions/build.gradle.kts b/build-logic/conventions/build.gradle.kts index 7f1c5fdf..5826a3fb 100644 --- a/build-logic/conventions/build.gradle.kts +++ b/build-logic/conventions/build.gradle.kts @@ -11,7 +11,7 @@ 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) diff --git a/build.gradle.kts b/build.gradle.kts index 69e25dfe..659b875b 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.android.library) apply false + alias(libs.plugins.org.jetbrains.kotlin.android) apply false } fun isNonStable(version: String): Boolean { diff --git a/gradle.properties b/gradle.properties index 5edc5ca7..56cd9917 100644 --- a/gradle.properties +++ b/gradle.properties @@ -43,5 +43,5 @@ android.enableJetifier=false #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 089aa180..14725e8b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,6 +19,12 @@ com-squareup-moshi = "1.15.0" dokka = "1.8.20" kotlin = "1.9.10" org-jetbrains-kotlinx = "1.7.1" +agp = "8.1.2" +org-jetbrains-kotlin-android = "1.9.0" +junit = "4.13.2" +androidx-test-ext-junit = "1.1.5" +espresso-core = "3.5.1" +material = "1.9.0" [libraries] androidx-activity = { module = "androidx.activity:activity", version.ref = "androidx-activity" } @@ -177,6 +183,10 @@ org-robolectric = "org.robolectric:robolectric:4.10.3" se-eelde-toggles-toggles-core = { module = "se.eelde.toggles:toggles-core" } se-eelde-toggles-toggles-flow = { module = "se.eelde.toggles:toggles-flow" } se-eelde-toggles-toggles-prefs = { module = "se.eelde.toggles:toggles-prefs" } +junit-junit = { group = "junit", name = "junit", version.ref = "junit" } +androidx-test-ext-junit115 = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } +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" @@ -193,3 +203,5 @@ dagger-hilt-android-plugin = "dagger.hilt.android.plugin:2.47" 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/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/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..50d3d89d --- /dev/null +++ b/modules/booleanconfiguration/build.gradle.kts @@ -0,0 +1,33 @@ +plugins { + id("toggles.android.module-conventions") + id("toggles.ownership-conventions") +} + +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.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) + kapt(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 96% 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..369635a4 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,4 +1,4 @@ -package se.eelde.toggles.dialogs.booleanvalue +package se.eelde.toggles.booleanconfiguration import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -19,7 +19,7 @@ import kotlinx.coroutines.launch @Composable @Suppress("LongParameterList") -internal fun BooleanValueView( +fun BooleanValueView( uiState: ViewState, popBackStack: () -> Unit, setBooleanValue: (Boolean) -> Unit, 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 93% 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..6ef25e2c 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()) + ) } } } @@ -92,12 +95,12 @@ class FragmentBooleanValueViewModel @Inject internal constructor( } } - internal suspend fun saveClick() { + suspend fun saveClick() { _state.value = reduce(state.value, PartialViewState.Saving) updateConfigurationValue(state.value.checked.toString()).join() } - internal suspend fun revertClick() { + suspend fun revertClick() { _state.value = reduce(state.value, PartialViewState.Reverting) deleteConfigurationValue().join() } 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..8db64397 --- /dev/null +++ b/modules/configurations/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + id("toggles.android.module-conventions") + id("toggles.ownership-conventions") +} + +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) + kapt(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/toggles-app/src/main/java/se/eelde/toggles/configurationlist/ConfigurationsEntry.kt b/modules/configurations/src/main/java/se/eelde/toggles/configurations/ConfigurationsEntry.kt similarity index 88% rename from toggles-app/src/main/java/se/eelde/toggles/configurationlist/ConfigurationsEntry.kt rename to modules/configurations/src/main/java/se/eelde/toggles/configurations/ConfigurationsEntry.kt index a2faa76e..a2837dcb 100644 --- a/toggles-app/src/main/java/se/eelde/toggles/configurationlist/ConfigurationsEntry.kt +++ b/modules/configurations/src/main/java/se/eelde/toggles/configurations/ConfigurationsEntry.kt @@ -1,4 +1,4 @@ -package se.eelde.toggles.configurationlist +package se.eelde.toggles.configurations import android.content.Intent import android.net.Uri @@ -23,7 +23,6 @@ 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 @@ -32,7 +31,10 @@ import androidx.navigation.navArgument @Suppress("LongMethod") @OptIn(ExperimentalMaterial3Api::class) fun NavGraphBuilder.configurationsNavigations( - 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, back: () -> Unit, ) { composable( @@ -124,7 +126,10 @@ fun NavGraphBuilder.configurationsNavigations( }, ) { paddingValues -> ConfigurationListView( - navController = navController, + navigateToBooleanConfiguration = navigateToBooleanConfiguration, + navigateToIntegerConfiguration = navigateToIntegerConfiguration, + navigateToStringConfiguration = navigateToStringConfiguration, + navigateToEnumConfiguration = navigateToEnumConfiguration, uiState = uiState, modifier = Modifier.padding(paddingValues), ) 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/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..50d3d89d --- /dev/null +++ b/modules/enumconfiguration/build.gradle.kts @@ -0,0 +1,33 @@ +plugins { + id("toggles.android.module-conventions") + id("toggles.ownership-conventions") +} + +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.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) + kapt(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/toggles-app/src/main/java/se/eelde/toggles/dialogs/enumvalue/EnumValueView.kt b/modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration/EnumValueView.kt similarity index 97% rename from toggles-app/src/main/java/se/eelde/toggles/dialogs/enumvalue/EnumValueView.kt rename to modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration/EnumValueView.kt index b917acbf..1e9ec425 100644 --- a/toggles-app/src/main/java/se/eelde/toggles/dialogs/enumvalue/EnumValueView.kt +++ b/modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration/EnumValueView.kt @@ -1,4 +1,4 @@ -package se.eelde.toggles.dialogs.enumvalue +package se.eelde.toggles.enumconfiguration import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -19,7 +19,7 @@ import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch @Composable -internal fun EnumValueView( +fun EnumValueView( state: ViewState, setEnumValue: suspend (String) -> Unit, revert: suspend () -> Unit, 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 96% 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 4ea5f0f4..0be8c600 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,4 +1,4 @@ -package se.eelde.toggles.dialogs.enumvalue +package se.eelde.toggles.enumconfiguration import android.app.Application import androidx.lifecycle.SavedStateHandle @@ -22,7 +22,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 configurationValues: List = listOf(), val saving: Boolean = false, @@ -50,7 +50,7 @@ class FragmentEnumValueViewModel @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")!! @@ -99,7 +99,7 @@ class FragmentEnumValueViewModel @Inject internal constructor( } } - internal suspend fun saveClick(value: String) { + suspend fun saveClick(value: String) { _state.value = reduce(_state.value, PartialViewState.Saving) updateConfigurationValue(value).join() application.contentResolver.notifyUpdate( @@ -109,7 +109,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..762c9503 --- /dev/null +++ b/modules/help/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + id("toggles.android.module-conventions") + id("toggles.ownership-conventions") +} + +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) + kapt(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/toggles-app/src/main/java/se/eelde/toggles/help/HelpView.kt b/modules/help/src/main/java/se/eelde/toggles/help/HelpView.kt similarity index 100% rename from toggles-app/src/main/java/se/eelde/toggles/help/HelpView.kt rename to modules/help/src/main/java/se/eelde/toggles/help/HelpView.kt 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..50d3d89d --- /dev/null +++ b/modules/integerconfiguration/build.gradle.kts @@ -0,0 +1,33 @@ +plugins { + id("toggles.android.module-conventions") + id("toggles.ownership-conventions") +} + +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.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) + kapt(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 97% 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..e8b41737 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 @@ -33,7 +33,7 @@ fun IntegerValueViewPreview() { @Composable @Suppress("LongParameterList") -internal fun IntegerValueView( +fun IntegerValueView( uiState: ViewState, popBackStack: () -> Unit, revert: suspend () -> Unit, 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/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..1e3705f3 --- /dev/null +++ b/modules/provider/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + id("toggles.android.module-conventions") + id("toggles.ownership-conventions") +} + +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(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) + kapt(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 90% 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 4bdceee3..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") @@ -171,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 { @@ -182,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? = @@ -220,7 +219,7 @@ class TogglesProvider : ContentProvider() { insertId = wrenchConfiguration.id } - PREDEFINED_CONFIGURATION_VALUES -> { + togglesUriMatcher.predefinedConfigurationValues -> { val fullConfig = WrenchPredefinedConfigurationValue.fromContentValues(values!!) insertId = try { predefinedConfigurationDao.insert(fullConfig) @@ -265,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( @@ -314,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 -> { @@ -339,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..50d3d89d --- /dev/null +++ b/modules/stringconfiguration/build.gradle.kts @@ -0,0 +1,33 @@ +plugins { + id("toggles.android.module-conventions") + id("toggles.ownership-conventions") +} + +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.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) + kapt(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 97% 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..70460281 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,4 +1,4 @@ -package se.eelde.toggles.dialogs.stringvalue +package se.eelde.toggles.stringconfiguration import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -34,7 +34,7 @@ fun StringValueViewPreview() { @Composable @Suppress("LongParameterList") -internal fun StringValueView( +fun StringValueView( state: ViewState, setStringValue: (String) -> Unit, save: suspend () -> Unit, 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/settings.gradle.kts b/settings.gradle.kts index b95704d6..258300f9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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 8fd9b802..fb809d4a 100644 --- a/toggles-app/build.gradle.kts +++ b/toggles-app/build.gradle.kts @@ -99,8 +99,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)) 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") + } + ) { navController.popBackStack() } composable( "configuration/{configurationId}/{scopeId}/boolean", arguments = listOf( @@ -243,7 +255,7 @@ fun Navigation( ) }, ) { paddingValues -> - HelpView(modifier = Modifier.padding(paddingValues)) + se.eelde.toggles.help.HelpView(modifier = Modifier.padding(paddingValues)) } } } 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/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/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/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-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/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-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/versions.properties b/versions.properties new file mode 100644 index 00000000..e9750eab --- /dev/null +++ b/versions.properties @@ -0,0 +1,6 @@ +V_VERSION=1.02.00 +V_VERSION_CODE=102000 +V_DEBUG_VERSION_SUFFIX=25-g6026e2e +V_DEBUG_VERSION=1.02.00-25-g6026e2e +HASH=6026e2e +DIRTY=false From 3f983633fe3d0b4522ea3e4ee4ca7ccd23cf3a26 Mon Sep 17 00:00:00 2001 From: Erik Eelde Date: Sat, 30 Sep 2023 08:58:21 +0200 Subject: [PATCH 07/15] More fixi --- build-logic/conventions/build.gradle.kts | 1 + build.gradle.kts | 3 +-- gradle/libs.versions.toml | 8 ++++---- modules/applications/build.gradle.kts | 3 ++- modules/booleanconfiguration/build.gradle.kts | 3 ++- modules/configurations/build.gradle.kts | 3 ++- modules/database/build.gradle.kts | 3 ++- modules/enumconfiguration/build.gradle.kts | 3 ++- modules/help/build.gradle.kts | 3 ++- modules/integerconfiguration/build.gradle.kts | 3 ++- modules/provider/build.gradle.kts | 3 ++- modules/stringconfiguration/build.gradle.kts | 3 ++- toggles-app/build.gradle.kts | 9 +++++---- toggles-core/api/toggles-core.api | 16 ++++++++++++++++ .../se/eelde/toggles/prefs/TogglesPreferences.kt | 8 ++++---- .../toggles/prefs/TogglesPreferencesImpl.kt | 1 - 16 files changed, 49 insertions(+), 24 deletions(-) diff --git a/build-logic/conventions/build.gradle.kts b/build-logic/conventions/build.gradle.kts index 5826a3fb..3349beba 100644 --- a/build-logic/conventions/build.gradle.kts +++ b/build-logic/conventions/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { 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.gradle.kts b/build.gradle.kts index 659b875b..ae119cd9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,8 +25,7 @@ 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.android.library) apply false - alias(libs.plugins.org.jetbrains.kotlin.android) apply false + id("com.google.devtools.ksp") version "1.9.10-1.0.13" apply false } fun isNonStable(version: String): Boolean { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 14725e8b..224dc043 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,12 +13,12 @@ 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.10" -org-jetbrains-kotlinx = "1.7.1" +org-jetbrains-kotlinx = "1.7.3" agp = "8.1.2" org-jetbrains-kotlin-android = "1.9.0" junit = "4.13.2" @@ -195,11 +195,11 @@ material = { group = "com.google.android.material", name = "material", version.r 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-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" 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/booleanconfiguration/build.gradle.kts b/modules/booleanconfiguration/build.gradle.kts index 50d3d89d..d9d37f39 100644 --- a/modules/booleanconfiguration/build.gradle.kts +++ b/modules/booleanconfiguration/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("toggles.android.module-conventions") id("toggles.ownership-conventions") + id("com.google.devtools.ksp") } android { @@ -29,5 +30,5 @@ dependencies { implementation(libs.androidx.startup.startup.runtime) implementation(libs.com.google.dagger.hilt.android) implementation(libs.se.eelde.toggles.toggles.core) - kapt(libs.com.google.dagger.hilt.compiler) + ksp(libs.com.google.dagger.hilt.compiler) } \ No newline at end of file diff --git a/modules/configurations/build.gradle.kts b/modules/configurations/build.gradle.kts index 8db64397..d920d38c 100644 --- a/modules/configurations/build.gradle.kts +++ b/modules/configurations/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("toggles.android.module-conventions") id("toggles.ownership-conventions") + id("com.google.devtools.ksp") } android { @@ -30,5 +31,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/database/build.gradle.kts b/modules/database/build.gradle.kts index d43d1ebe..730f2c2d 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( @@ -43,7 +44,7 @@ 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/enumconfiguration/build.gradle.kts b/modules/enumconfiguration/build.gradle.kts index 50d3d89d..d9d37f39 100644 --- a/modules/enumconfiguration/build.gradle.kts +++ b/modules/enumconfiguration/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("toggles.android.module-conventions") id("toggles.ownership-conventions") + id("com.google.devtools.ksp") } android { @@ -29,5 +30,5 @@ dependencies { implementation(libs.androidx.startup.startup.runtime) implementation(libs.com.google.dagger.hilt.android) implementation(libs.se.eelde.toggles.toggles.core) - kapt(libs.com.google.dagger.hilt.compiler) + ksp(libs.com.google.dagger.hilt.compiler) } \ No newline at end of file diff --git a/modules/help/build.gradle.kts b/modules/help/build.gradle.kts index 762c9503..bef1b2e1 100644 --- a/modules/help/build.gradle.kts +++ b/modules/help/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("toggles.android.module-conventions") id("toggles.ownership-conventions") + id("com.google.devtools.ksp") } android { @@ -26,5 +27,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/integerconfiguration/build.gradle.kts b/modules/integerconfiguration/build.gradle.kts index 50d3d89d..d9d37f39 100644 --- a/modules/integerconfiguration/build.gradle.kts +++ b/modules/integerconfiguration/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("toggles.android.module-conventions") id("toggles.ownership-conventions") + id("com.google.devtools.ksp") } android { @@ -29,5 +30,5 @@ dependencies { implementation(libs.androidx.startup.startup.runtime) implementation(libs.com.google.dagger.hilt.android) implementation(libs.se.eelde.toggles.toggles.core) - kapt(libs.com.google.dagger.hilt.compiler) + ksp(libs.com.google.dagger.hilt.compiler) } \ No newline at end of file diff --git a/modules/provider/build.gradle.kts b/modules/provider/build.gradle.kts index 1e3705f3..3b12ae51 100644 --- a/modules/provider/build.gradle.kts +++ b/modules/provider/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("toggles.android.module-conventions") id("toggles.ownership-conventions") + id("com.google.devtools.ksp") } android { @@ -30,5 +31,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/stringconfiguration/build.gradle.kts b/modules/stringconfiguration/build.gradle.kts index 50d3d89d..d9d37f39 100644 --- a/modules/stringconfiguration/build.gradle.kts +++ b/modules/stringconfiguration/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("toggles.android.module-conventions") id("toggles.ownership-conventions") + id("com.google.devtools.ksp") } android { @@ -29,5 +30,5 @@ dependencies { implementation(libs.androidx.startup.startup.runtime) implementation(libs.com.google.dagger.hilt.android) implementation(libs.se.eelde.toggles.toggles.core) - kapt(libs.com.google.dagger.hilt.compiler) + ksp(libs.com.google.dagger.hilt.compiler) } \ No newline at end of file diff --git a/toggles-app/build.gradle.kts b/toggles-app/build.gradle.kts index fb809d4a..e0bea97d 100644 --- a/toggles-app/build.gradle.kts +++ b/toggles-app/build.gradle.kts @@ -10,6 +10,7 @@ plugins { 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") @@ -143,16 +144,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) diff --git a/toggles-core/api/toggles-core.api b/toggles-core/api/toggles-core.api index af57db77..96fd5ce3 100644 --- a/toggles-core/api/toggles-core.api +++ b/toggles-core/api/toggles-core.api @@ -20,6 +20,8 @@ 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 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 public static final fun fromContentValues (Landroid/content/ContentValues;)Lse/eelde/toggles/core/Toggle; public static final fun fromCursor (Landroid/database/Cursor;)Lse/eelde/toggles/core/Toggle; @@ -36,10 +38,18 @@ public final class se/eelde/toggles/core/Toggle { 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 { @@ -72,9 +82,15 @@ public final class se/eelde/toggles/core/ToggleValue { 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 { diff --git a/toggles-prefs-noop/src/main/java/se/eelde/toggles/prefs/TogglesPreferences.kt b/toggles-prefs-noop/src/main/java/se/eelde/toggles/prefs/TogglesPreferences.kt index d26568a1..54e1b618 100644 --- a/toggles-prefs-noop/src/main/java/se/eelde/toggles/prefs/TogglesPreferences.kt +++ b/toggles-prefs-noop/src/main/java/se/eelde/toggles/prefs/TogglesPreferences.kt @@ -2,8 +2,8 @@ package se.eelde.toggles.prefs @Suppress("LibraryEntitiesShouldNotBePublic") public interface TogglesPreferences { - fun getBoolean(key: String, defValue: Boolean): Boolean - fun getInt(key: String, defValue: Int): Int - fun getString(key: String, defValue: String): String - fun > getEnum(key: String, type: Class, defValue: T): T + public fun getBoolean(key: String, defValue: Boolean): Boolean + public fun getInt(key: String, defValue: Int): Int + public fun getString(key: String, defValue: String): String + public fun > getEnum(key: String, type: Class, defValue: T): T } 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 3569644f..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 @@ -12,7 +12,6 @@ public class TogglesPreferencesImpl(context: Context) : TogglesPreferences { private val context = context.applicationContext private val contentResolver: ContentResolver = this.context.contentResolver - override fun getBoolean(key: String, defValue: Boolean): Boolean { var toggle = getToggle( contentResolver = contentResolver, From f6499b9d1bf36857fb12b08b00c6435c905a9054 Mon Sep 17 00:00:00 2001 From: Erik Eelde Date: Sat, 30 Sep 2023 14:39:05 +0200 Subject: [PATCH 08/15] More fixi --- .github/workflows/pull-request.yml | 19 +++++++++++++++++-- gradle/libs.versions.toml | 2 +- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 1fc11596..04cebfee 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -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/gradle/libs.versions.toml b/gradle/libs.versions.toml index 224dc043..37e6d580 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,7 @@ androidx-vectordrawable = "1.1.0" com-google-dagger = "2.48" com-squareup-leakcanary = "2.8.1" com-squareup-moshi = "1.15.0" -dokka = "1.8.20" +dokka = "1.9.0" kotlin = "1.9.10" org-jetbrains-kotlinx = "1.7.3" agp = "8.1.2" From c9aafc1eaca621733a9d281f7c419e1cd306b400 Mon Sep 17 00:00:00 2001 From: Erik Eelde Date: Sat, 30 Sep 2023 16:02:49 +0200 Subject: [PATCH 09/15] Tidy up --- ...android.application-conventions.gradle.kts | 9 -- .../toggles/applications/ApplicationEntry.kt | 2 +- modules/booleanconfiguration/build.gradle.kts | 1 + .../booleanconfiguration/BooleanValueView.kt | 46 +++++- .../se/eelde/toggles/composetheme/AppState.kt | 41 ----- .../toggles/composetheme/MaterialColors.kt | 8 - modules/enumconfiguration/build.gradle.kts | 1 + .../enumconfiguration/EnumValueView.kt | 44 ++++++ .../java/se/eelde/toggles/help/HelpView.kt | 37 ++++- modules/help/src/main/res/values/strings.xml | 4 + modules/integerconfiguration/build.gradle.kts | 3 +- .../integerconfiguration/IntegerValueView.kt | 47 +++++- modules/oss/build.gradle.kts | 5 +- modules/stringconfiguration/build.gradle.kts | 1 + .../stringconfiguration/StringValueView.kt | 47 +++++- .../java/se/eelde/toggles/MainActivity.kt | 144 +----------------- toggles-sample/build.gradle.kts | 5 +- .../java/com/example/toggles/MainActivity.kt | 4 - 18 files changed, 237 insertions(+), 212 deletions(-) delete mode 100644 modules/compose-theme/src/main/java/se/eelde/toggles/composetheme/AppState.kt delete mode 100644 modules/compose-theme/src/main/java/se/eelde/toggles/composetheme/MaterialColors.kt create mode 100644 modules/help/src/main/res/values/strings.xml 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..70b7357b 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 @@ -6,19 +6,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 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/build.gradle.kts b/modules/booleanconfiguration/build.gradle.kts index d9d37f39..c7dcd7cc 100644 --- a/modules/booleanconfiguration/build.gradle.kts +++ b/modules/booleanconfiguration/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { 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) diff --git a/modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration/BooleanValueView.kt b/modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration/BooleanValueView.kt index 369635a4..11fd972b 100644 --- a/modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration/BooleanValueView.kt +++ b/modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration/BooleanValueView.kt @@ -5,21 +5,65 @@ 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("") }, + 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") -fun BooleanValueView( +internal fun BooleanValueView( uiState: ViewState, popBackStack: () -> Unit, setBooleanValue: (Boolean) -> Unit, 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/enumconfiguration/build.gradle.kts b/modules/enumconfiguration/build.gradle.kts index d9d37f39..c7dcd7cc 100644 --- a/modules/enumconfiguration/build.gradle.kts +++ b/modules/enumconfiguration/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { 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) diff --git a/modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration/EnumValueView.kt b/modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration/EnumValueView.kt index 1e9ec425..30300dad 100644 --- a/modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration/EnumValueView.kt +++ b/modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration/EnumValueView.kt @@ -7,19 +7,63 @@ 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.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.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("") }, + 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, 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 index 728774f8..66ad120b 100644 --- a/modules/help/src/main/java/se/eelde/toggles/help/HelpView.kt +++ b/modules/help/src/main/java/se/eelde/toggles/help/HelpView.kt @@ -2,14 +2,45 @@ 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) { - Box(modifier.fillMaxSize()) { - Text(text = "Implementation", color = Color.White) +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/build.gradle.kts b/modules/integerconfiguration/build.gradle.kts index d9d37f39..0ed1d402 100644 --- a/modules/integerconfiguration/build.gradle.kts +++ b/modules/integerconfiguration/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } android { - namespace = "se.eelde.toggles.booleanconfiguration" + namespace = "se.eelde.toggles.integerconfiguration" buildFeatures { compose = true } @@ -23,6 +23,7 @@ dependencies { 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) diff --git a/modules/integerconfiguration/src/main/java/se/eelde/toggles/integerconfiguration/IntegerValueView.kt b/modules/integerconfiguration/src/main/java/se/eelde/toggles/integerconfiguration/IntegerValueView.kt index e8b41737..c40524f0 100644 --- a/modules/integerconfiguration/src/main/java/se/eelde/toggles/integerconfiguration/IntegerValueView.kt +++ b/modules/integerconfiguration/src/main/java/se/eelde/toggles/integerconfiguration/IntegerValueView.kt @@ -6,17 +6,27 @@ 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 @@ -31,9 +41,44 @@ fun IntegerValueViewPreview() { ) } +@OptIn(ExperimentalMaterial3Api::class) @Composable -@Suppress("LongParameterList") fun IntegerValueView( + modifier: Modifier = Modifier, + viewModel: FragmentIntegerValueViewModel = hiltViewModel(), + back: () -> Unit, +) { + val viewState by viewModel.state.collectAsStateWithLifecycle() + Scaffold( + topBar = { + TopAppBar( + title = { Text("") }, + 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( uiState: ViewState, popBackStack: () -> Unit, revert: suspend () -> Unit, 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/stringconfiguration/build.gradle.kts b/modules/stringconfiguration/build.gradle.kts index d9d37f39..c7dcd7cc 100644 --- a/modules/stringconfiguration/build.gradle.kts +++ b/modules/stringconfiguration/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { 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) diff --git a/modules/stringconfiguration/src/main/java/se/eelde/toggles/stringconfiguration/StringValueView.kt b/modules/stringconfiguration/src/main/java/se/eelde/toggles/stringconfiguration/StringValueView.kt index 70460281..79bb5b27 100644 --- a/modules/stringconfiguration/src/main/java/se/eelde/toggles/stringconfiguration/StringValueView.kt +++ b/modules/stringconfiguration/src/main/java/se/eelde/toggles/stringconfiguration/StringValueView.kt @@ -5,16 +5,26 @@ 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,9 +42,44 @@ fun StringValueViewPreview() { } } +@OptIn(ExperimentalMaterial3Api::class) @Composable -@Suppress("LongParameterList") fun StringValueView( + modifier: Modifier = Modifier, + viewModel: FragmentStringValueViewModel = hiltViewModel(), + back: () -> Unit, +) { + val viewState by viewModel.state.collectAsStateWithLifecycle() + Scaffold( + topBar = { + TopAppBar( + title = { Text("") }, + 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( state: ViewState, setStringValue: (String) -> Unit, save: suspend () -> Unit, diff --git a/toggles-app/src/main/java/se/eelde/toggles/MainActivity.kt b/toggles-app/src/main/java/se/eelde/toggles/MainActivity.kt index f6058a77..fd7215aa 100644 --- a/toggles-app/src/main/java/se/eelde/toggles/MainActivity.kt +++ b/toggles-app/src/main/java/se/eelde/toggles/MainActivity.kt @@ -1,7 +1,6 @@ package se.eelde.toggles import android.os.Bundle -import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.padding @@ -15,8 +14,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.NavHost @@ -26,16 +23,12 @@ import androidx.navigation.navArgument import dagger.hilt.android.AndroidEntryPoint import se.eelde.toggles.applications.applicationNavigations import se.eelde.toggles.booleanconfiguration.BooleanValueView -import se.eelde.toggles.booleanconfiguration.FragmentBooleanValueViewModel import se.eelde.toggles.composetheme.TogglesTheme -import se.eelde.toggles.composetheme.rememberAppState import se.eelde.toggles.configurations.configurationsNavigations import se.eelde.toggles.enumconfiguration.EnumValueView -import se.eelde.toggles.enumconfiguration.FragmentEnumValueViewModel -import se.eelde.toggles.integerconfiguration.FragmentIntegerValueViewModel +import se.eelde.toggles.help.HelpView import se.eelde.toggles.integerconfiguration.IntegerValueView import se.eelde.toggles.oss.OssView -import se.eelde.toggles.stringconfiguration.FragmentStringValueViewModel import se.eelde.toggles.stringconfiguration.StringValueView @OptIn(ExperimentalMaterial3Api::class) @@ -79,34 +72,7 @@ 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() }, - save = { viewModel.saveClick() }, - setBooleanValue = { viewModel.checkedChanged(it) }, - modifier = Modifier.padding(paddingValues), - ) - } + BooleanValueView { navController.popBackStack() } } composable( "configuration/{configurationId}/{scopeId}/integer", @@ -115,34 +81,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", @@ -151,34 +90,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", @@ -187,32 +99,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", @@ -239,24 +126,7 @@ fun Navigation( composable( "help", ) { - Scaffold( - topBar = { - TopAppBar( - title = { Text("") }, - navigationIcon = - { - IconButton(onClick = { navController.popBackStack() }) { - Icon( - imageVector = Icons.Filled.ArrowBack, - contentDescription = null - ) - } - } - ) - }, - ) { paddingValues -> - se.eelde.toggles.help.HelpView(modifier = Modifier.padding(paddingValues)) - } + HelpView { navController.popBackStack() } } } } @@ -270,8 +140,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/build.gradle.kts b/toggles-sample/build.gradle.kts index d58cc561..a35d67c2 100644 --- a/toggles-sample/build.gradle.kts +++ b/toggles-sample/build.gradle.kts @@ -7,6 +7,7 @@ plugins { 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") @@ -87,9 +88,9 @@ 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) 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) } From 2b26af788ec7284f0251956fa3d6114a8b2feb87 Mon Sep 17 00:00:00 2001 From: Erik Eelde Date: Thu, 30 Nov 2023 11:39:18 +0100 Subject: [PATCH 10/15] Bump versions --- gradle/libs.versions.toml | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 37e6d580..e2c8bbb3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -117,7 +117,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.2" +com-android-tools-build-gradle = "com.android.tools.build:gradle:8.1.4" 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" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index db9a6b82..e411586a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ 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.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 6ac23a642a9623f902ca3ea3f1fb27625047c2b5 Mon Sep 17 00:00:00 2001 From: Erik Eelde Date: Fri, 1 Dec 2023 09:07:15 +0100 Subject: [PATCH 11/15] Bump hedggehog --- ...android.application-conventions.gradle.kts | 2 + build.gradle.kts | 3 +- gradle.properties | 3 -- gradle/libs.versions.toml | 46 +++++-------------- gradle/wrapper/gradle-wrapper.properties | 3 +- .../BooleanValueViewModel.kt | 16 ++++++- modules/database/build.gradle.kts | 19 ++------ modules/enumconfiguration/build.gradle.kts | 2 +- modules/provider/build.gradle.kts | 2 +- modules/stringconfiguration/build.gradle.kts | 2 +- toggles-app/build.gradle.kts | 1 + toggles-flow-noop/build.gradle.kts | 1 + toggles-flow/build.gradle.kts | 1 + toggles-prefs-noop/build.gradle.kts | 1 - toggles-sample/build.gradle.kts | 1 + versions.properties | 8 ++-- 16 files changed, 47 insertions(+), 64 deletions(-) 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 70b7357b..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 @@ -28,6 +29,7 @@ android { kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() } + @Suppress("UnstableApiUsage") testOptions { unitTests.isIncludeAndroidResources = true } diff --git a/build.gradle.kts b/build.gradle.kts index ae119cd9..3801ec3e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,7 +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") - id("com.google.devtools.ksp") version "1.9.10-1.0.13" apply false + alias(libs.plugins.com.google.devtools.ksp) apply false + alias(libs.plugins.com.autonomousapps.dependency.analysis) } fun isNonStable(version: String): Boolean { diff --git a/gradle.properties b/gradle.properties index 56cd9917..0227d7ee 100644 --- a/gradle.properties +++ b/gradle.properties @@ -37,9 +37,6 @@ 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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e2c8bbb3..e4cd6026 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.09.02" -androidx-compose-compiler = "1.5.3" +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" @@ -17,14 +17,14 @@ com-google-dagger = "2.48" com-squareup-leakcanary = "2.8.1" com-squareup-moshi = "1.15.0" dokka = "1.9.0" -kotlin = "1.9.10" +kotlin = "1.9.20" org-jetbrains-kotlinx = "1.7.3" agp = "8.1.2" -org-jetbrains-kotlin-android = "1.9.0" +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.0" +material = "1.9.20" [libraries] androidx-activity = { module = "androidx.activity:activity", version.ref = "androidx-activity" } @@ -117,7 +117,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.4" +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" @@ -137,54 +137,29 @@ 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 = { 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" } -junit-junit = { group = "junit", name = "junit", version.ref = "junit" } -androidx-test-ext-junit115 = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } 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" @@ -192,9 +167,10 @@ material = { group = "com.google.android.material", name = "material", version.r #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-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" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e411586a..d3ca171f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration/BooleanValueViewModel.kt b/modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration/BooleanValueViewModel.kt index 6ef25e2c..b3ee2a74 100644 --- a/modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration/BooleanValueViewModel.kt +++ b/modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration/BooleanValueViewModel.kt @@ -81,15 +81,19 @@ 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) } @@ -97,7 +101,11 @@ class FragmentBooleanValueViewModel @Inject internal constructor( 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() + } } suspend fun revertClick() { @@ -116,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/database/build.gradle.kts b/modules/database/build.gradle.kts index 730f2c2d..3bb82660 100644 --- a/modules/database/build.gradle.kts +++ b/modules/database/build.gradle.kts @@ -11,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 @@ -40,6 +27,10 @@ android { } } +ksp { + arg(RoomSchemaArgProvider(File(projectDir, "schemas"))) +} + dependencies { implementation(libs.androidx.room.room.paging) implementation(libs.androidx.room.room.runtime) diff --git a/modules/enumconfiguration/build.gradle.kts b/modules/enumconfiguration/build.gradle.kts index c7dcd7cc..133f4f90 100644 --- a/modules/enumconfiguration/build.gradle.kts +++ b/modules/enumconfiguration/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } android { - namespace = "se.eelde.toggles.booleanconfiguration" + namespace = "se.eelde.toggles.enumconfiguration" buildFeatures { compose = true } diff --git a/modules/provider/build.gradle.kts b/modules/provider/build.gradle.kts index 3b12ae51..27bd1b02 100644 --- a/modules/provider/build.gradle.kts +++ b/modules/provider/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } android { - namespace = "se.eelde.toggles.help" + namespace = "se.eelde.toggles.provider" buildFeatures { compose = true diff --git a/modules/stringconfiguration/build.gradle.kts b/modules/stringconfiguration/build.gradle.kts index c7dcd7cc..fe9b0e36 100644 --- a/modules/stringconfiguration/build.gradle.kts +++ b/modules/stringconfiguration/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } android { - namespace = "se.eelde.toggles.booleanconfiguration" + namespace = "se.eelde.toggles.stringconfiguration" buildFeatures { compose = true } diff --git a/toggles-app/build.gradle.kts b/toggles-app/build.gradle.kts index e0bea97d..daaa3a88 100644 --- a/toggles-app/build.gradle.kts +++ b/toggles-app/build.gradle.kts @@ -174,6 +174,7 @@ dependencies { 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-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/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-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-sample/build.gradle.kts b/toggles-sample/build.gradle.kts index a35d67c2..607b12da 100644 --- a/toggles-sample/build.gradle.kts +++ b/toggles-sample/build.gradle.kts @@ -95,6 +95,7 @@ dependencies { 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/versions.properties b/versions.properties index e9750eab..b8c62057 100644 --- a/versions.properties +++ b/versions.properties @@ -1,6 +1,6 @@ V_VERSION=1.02.00 V_VERSION_CODE=102000 -V_DEBUG_VERSION_SUFFIX=25-g6026e2e -V_DEBUG_VERSION=1.02.00-25-g6026e2e -HASH=6026e2e -DIRTY=false +V_DEBUG_VERSION_SUFFIX=34-gc9aafc1-dirty +V_DEBUG_VERSION=1.02.00-34-gc9aafc1-dirty +HASH=c9aafc1 +DIRTY=true From 5fef04834d0d79bfe51ba990be5986c07ffc6aab Mon Sep 17 00:00:00 2001 From: Erik Eelde Date: Fri, 1 Dec 2023 09:09:59 +0100 Subject: [PATCH 12/15] New gradle files --- gradle/wrapper/gradle-wrapper.jar | Bin 59536 -> 43462 bytes gradle/wrapper/gradle-wrapper.properties | 1 + gradlew | 41 ++++++++++++++++------- gradlew.bat | 15 +++++---- 4 files changed, 38 insertions(+), 19 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180f2ae8848c63b8b4dea2cb829da983f2fa..d64cd4917707c1f8861d8cb53dd15194d4248596 100644 GIT binary patch literal 43462 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 literal 59536 zcma&NbC71ylI~qywr$(CZQJHswz}-9F59+k+g;UV+cs{`J?GrGXYR~=-ydruB3JCa zB64N^cILAcWk5iofq)<(fq;O7{th4@;QxID0)qN`mJ?GIqLY#rX8-|G{5M0pdVW5^ zzXk$-2kQTAC?_N@B`&6-N-rmVFE=$QD?>*=4<|!MJu@}isLc4AW#{m2if&A5T5g&~ ziuMQeS*U5sL6J698wOd)K@oK@1{peP5&Esut<#VH^u)gp`9H4)`uE!2$>RTctN+^u z=ASkePDZA-X8)rp%D;p*~P?*a_=*Kwc<^>QSH|^<0>o37lt^+Mj1;4YvJ(JR-Y+?%Nu}JAYj5 z_Qc5%Ao#F?q32i?ZaN2OSNhWL;2oDEw_({7ZbgUjna!Fqn3NzLM@-EWFPZVmc>(fZ z0&bF-Ch#p9C{YJT9Rcr3+Y_uR^At1^BxZ#eo>$PLJF3=;t_$2|t+_6gg5(j{TmjYU zK12c&lE?Eh+2u2&6Gf*IdKS&6?rYbSEKBN!rv{YCm|Rt=UlPcW9j`0o6{66#y5t9C zruFA2iKd=H%jHf%ypOkxLnO8#H}#Zt{8p!oi6)7#NqoF({t6|J^?1e*oxqng9Q2Cc zg%5Vu!em)}Yuj?kaP!D?b?(C*w!1;>R=j90+RTkyEXz+9CufZ$C^umX^+4|JYaO<5 zmIM3#dv`DGM;@F6;(t!WngZSYzHx?9&$xEF70D1BvfVj<%+b#)vz)2iLCrTeYzUcL z(OBnNoG6Le%M+@2oo)&jdOg=iCszzv59e zDRCeaX8l1hC=8LbBt|k5?CXgep=3r9BXx1uR8!p%Z|0+4Xro=xi0G!e{c4U~1j6!) zH6adq0}#l{%*1U(Cb%4AJ}VLWKBPi0MoKFaQH6x?^hQ!6em@993xdtS%_dmevzeNl z(o?YlOI=jl(`L9^ z0O+H9k$_@`6L13eTT8ci-V0ljDMD|0ifUw|Q-Hep$xYj0hTO@0%IS^TD4b4n6EKDG z??uM;MEx`s98KYN(K0>c!C3HZdZ{+_53DO%9k5W%pr6yJusQAv_;IA}925Y%;+!tY z%2k!YQmLLOr{rF~!s<3-WEUs)`ix_mSU|cNRBIWxOox_Yb7Z=~Q45ZNe*u|m^|)d* zog=i>`=bTe!|;8F+#H>EjIMcgWcG2ORD`w0WD;YZAy5#s{65~qfI6o$+Ty&-hyMyJ z3Ra~t>R!p=5ZpxA;QkDAoPi4sYOP6>LT+}{xp}tk+<0k^CKCFdNYG(Es>p0gqD)jP zWOeX5G;9(m@?GOG7g;e74i_|SmE?`B2i;sLYwRWKLy0RLW!Hx`=!LH3&k=FuCsM=9M4|GqzA)anEHfxkB z?2iK-u(DC_T1};KaUT@3nP~LEcENT^UgPvp!QC@Dw&PVAhaEYrPey{nkcn(ro|r7XUz z%#(=$7D8uP_uU-oPHhd>>^adbCSQetgSG`e$U|7mr!`|bU0aHl_cmL)na-5x1#OsVE#m*+k84Y^+UMeSAa zbrVZHU=mFwXEaGHtXQq`2ZtjfS!B2H{5A<3(nb-6ARVV8kEmOkx6D2x7~-6hl;*-*}2Xz;J#a8Wn;_B5=m zl3dY;%krf?i-Ok^Pal-}4F`{F@TYPTwTEhxpZK5WCpfD^UmM_iYPe}wpE!Djai6_{ z*pGO=WB47#Xjb7!n2Ma)s^yeR*1rTxp`Mt4sfA+`HwZf%!7ZqGosPkw69`Ix5Ku6G z@Pa;pjzV&dn{M=QDx89t?p?d9gna*}jBly*#1!6}5K<*xDPJ{wv4& zM$17DFd~L*Te3A%yD;Dp9UGWTjRxAvMu!j^Tbc}2v~q^59d4bz zvu#!IJCy(BcWTc`;v$9tH;J%oiSJ_i7s;2`JXZF+qd4C)vY!hyCtl)sJIC{ebI*0> z@x>;EzyBv>AI-~{D6l6{ST=em*U( z(r$nuXY-#CCi^8Z2#v#UXOt`dbYN1z5jzNF2 z411?w)whZrfA20;nl&C1Gi+gk<`JSm+{|*2o<< zqM#@z_D`Cn|0H^9$|Tah)0M_X4c37|KQ*PmoT@%xHc3L1ZY6(p(sNXHa&49Frzto& zR`c~ClHpE~4Z=uKa5S(-?M8EJ$zt0&fJk~p$M#fGN1-y$7!37hld`Uw>Urri(DxLa;=#rK0g4J)pXMC zxzraOVw1+kNWpi#P=6(qxf`zSdUC?D$i`8ZI@F>k6k zz21?d+dw7b&i*>Kv5L(LH-?J%@WnqT7j#qZ9B>|Zl+=> z^U-pV@1y_ptHo4hl^cPRWewbLQ#g6XYQ@EkiP z;(=SU!yhjHp%1&MsU`FV1Z_#K1&(|5n(7IHbx&gG28HNT)*~-BQi372@|->2Aw5It z0CBpUcMA*QvsPy)#lr!lIdCi@1k4V2m!NH)%Px(vu-r(Q)HYc!p zJ^$|)j^E#q#QOgcb^pd74^JUi7fUmMiNP_o*lvx*q%_odv49Dsv$NV;6J z9GOXKomA{2Pb{w}&+yHtH?IkJJu~}Z?{Uk++2mB8zyvh*xhHKE``99>y#TdD z&(MH^^JHf;g(Tbb^&8P*;_i*2&fS$7${3WJtV7K&&(MBV2~)2KB3%cWg#1!VE~k#C z!;A;?p$s{ihyojEZz+$I1)L}&G~ml=udD9qh>Tu(ylv)?YcJT3ihapi!zgPtWb*CP zlLLJSRCj-^w?@;RU9aL2zDZY1`I3d<&OMuW=c3$o0#STpv_p3b9Wtbql>w^bBi~u4 z3D8KyF?YE?=HcKk!xcp@Cigvzy=lnFgc^9c%(^F22BWYNAYRSho@~*~S)4%AhEttv zvq>7X!!EWKG?mOd9&n>vvH1p4VzE?HCuxT-u+F&mnsfDI^}*-d00-KAauEaXqg3k@ zy#)MGX!X;&3&0s}F3q40ZmVM$(H3CLfpdL?hB6nVqMxX)q=1b}o_PG%r~hZ4gUfSp zOH4qlEOW4OMUc)_m)fMR_rl^pCfXc{$fQbI*E&mV77}kRF z&{<06AJyJ!e863o-V>FA1a9Eemx6>^F$~9ppt()ZbPGfg_NdRXBWoZnDy2;#ODgf! zgl?iOcF7Meo|{AF>KDwTgYrJLb$L2%%BEtO>T$C?|9bAB&}s;gI?lY#^tttY&hfr# zKhC+&b-rpg_?~uVK%S@mQleU#_xCsvIPK*<`E0fHE1&!J7!xD#IB|SSPW6-PyuqGn3^M^Rz%WT{e?OI^svARX&SAdU77V(C~ zM$H{Kg59op{<|8ry9ecfP%=kFm(-!W&?U0@<%z*+!*<e0XesMxRFu9QnGqun6R_%T+B%&9Dtk?*d$Q zb~>84jEAPi@&F@3wAa^Lzc(AJz5gsfZ7J53;@D<;Klpl?sK&u@gie`~vTsbOE~Cd4 z%kr56mI|#b(Jk&;p6plVwmNB0H@0SmgdmjIn5Ne@)}7Vty(yb2t3ev@22AE^s!KaN zyQ>j+F3w=wnx7w@FVCRe+`vUH)3gW%_72fxzqX!S&!dchdkRiHbXW1FMrIIBwjsai8`CB2r4mAbwp%rrO>3B$Zw;9=%fXI9B{d(UzVap7u z6piC-FQ)>}VOEuPpuqznpY`hN4dGa_1Xz9rVg(;H$5Te^F0dDv*gz9JS<|>>U0J^# z6)(4ICh+N_Q`Ft0hF|3fSHs*?a=XC;e`sJaU9&d>X4l?1W=|fr!5ShD|nv$GK;j46@BV6+{oRbWfqOBRb!ir88XD*SbC(LF}I1h#6@dvK%Toe%@ zhDyG$93H8Eu&gCYddP58iF3oQH*zLbNI;rN@E{T9%A8!=v#JLxKyUe}e}BJpB{~uN zqgxRgo0*-@-iaHPV8bTOH(rS(huwK1Xg0u+e!`(Irzu@Bld&s5&bWgVc@m7;JgELd zimVs`>vQ}B_1(2#rv#N9O`fJpVfPc7V2nv34PC);Dzbb;p!6pqHzvy?2pD&1NE)?A zt(t-ucqy@wn9`^MN5apa7K|L=9>ISC>xoc#>{@e}m#YAAa1*8-RUMKwbm|;5p>T`Z zNf*ph@tnF{gmDa3uwwN(g=`Rh)4!&)^oOy@VJaK4lMT&5#YbXkl`q?<*XtsqD z9PRK6bqb)fJw0g-^a@nu`^?71k|m3RPRjt;pIkCo1{*pdqbVs-Yl>4E>3fZx3Sv44grW=*qdSoiZ9?X0wWyO4`yDHh2E!9I!ZFi zVL8|VtW38}BOJHW(Ax#KL_KQzarbuE{(%TA)AY)@tY4%A%P%SqIU~8~-Lp3qY;U-} z`h_Gel7;K1h}7$_5ZZT0&%$Lxxr-<89V&&TCsu}LL#!xpQ1O31jaa{U34~^le*Y%L za?7$>Jk^k^pS^_M&cDs}NgXlR>16AHkSK-4TRaJSh#h&p!-!vQY%f+bmn6x`4fwTp z$727L^y`~!exvmE^W&#@uY!NxJi`g!i#(++!)?iJ(1)2Wk;RN zFK&O4eTkP$Xn~4bB|q8y(btx$R#D`O@epi4ofcETrx!IM(kWNEe42Qh(8*KqfP(c0 zouBl6>Fc_zM+V;F3znbo{x#%!?mH3`_ANJ?y7ppxS@glg#S9^MXu|FM&ynpz3o&Qh z2ujAHLF3($pH}0jXQsa#?t--TnF1P73b?4`KeJ9^qK-USHE)4!IYgMn-7z|=ALF5SNGkrtPG@Y~niUQV2?g$vzJN3nZ{7;HZHzWAeQ;5P|@Tl3YHpyznGG4-f4=XflwSJY+58-+wf?~Fg@1p1wkzuu-RF3j2JX37SQUc? zQ4v%`V8z9ZVZVqS8h|@@RpD?n0W<=hk=3Cf8R?d^9YK&e9ZybFY%jdnA)PeHvtBe- zhMLD+SSteHBq*q)d6x{)s1UrsO!byyLS$58WK;sqip$Mk{l)Y(_6hEIBsIjCr5t>( z7CdKUrJTrW%qZ#1z^n*Lb8#VdfzPw~OIL76aC+Rhr<~;4Tl!sw?Rj6hXj4XWa#6Tp z@)kJ~qOV)^Rh*-?aG>ic2*NlC2M7&LUzc9RT6WM%Cpe78`iAowe!>(T0jo&ivn8-7 zs{Qa@cGy$rE-3AY0V(l8wjI^uB8Lchj@?L}fYal^>T9z;8juH@?rG&g-t+R2dVDBe zq!K%{e-rT5jX19`(bP23LUN4+_zh2KD~EAYzhpEO3MUG8@}uBHH@4J zd`>_(K4q&>*k82(dDuC)X6JuPrBBubOg7qZ{?x!r@{%0);*`h*^F|%o?&1wX?Wr4b z1~&cy#PUuES{C#xJ84!z<1tp9sfrR(i%Tu^jnXy;4`Xk;AQCdFC@?V%|; zySdC7qS|uQRcH}EFZH%mMB~7gi}a0utE}ZE_}8PQH8f;H%PN41Cb9R%w5Oi5el^fd z$n{3SqLCnrF##x?4sa^r!O$7NX!}&}V;0ZGQ&K&i%6$3C_dR%I7%gdQ;KT6YZiQrW zk%q<74oVBV>@}CvJ4Wj!d^?#Zwq(b$E1ze4$99DuNg?6t9H}k_|D7KWD7i0-g*EO7 z;5{hSIYE4DMOK3H%|f5Edx+S0VI0Yw!tsaRS2&Il2)ea^8R5TG72BrJue|f_{2UHa z@w;^c|K3da#$TB0P3;MPlF7RuQeXT$ zS<<|C0OF(k)>fr&wOB=gP8!Qm>F41u;3esv7_0l%QHt(~+n; zf!G6%hp;Gfa9L9=AceiZs~tK+Tf*Wof=4!u{nIO90jH@iS0l+#%8=~%ASzFv7zqSB^?!@N7)kp0t&tCGLmzXSRMRyxCmCYUD2!B`? zhs$4%KO~m=VFk3Buv9osha{v+mAEq=ik3RdK@;WWTV_g&-$U4IM{1IhGX{pAu%Z&H zFfwCpUsX%RKg);B@7OUzZ{Hn{q6Vv!3#8fAg!P$IEx<0vAx;GU%}0{VIsmFBPq_mb zpe^BChDK>sc-WLKl<6 zwbW|e&d&dv9Wu0goueyu>(JyPx1mz0v4E?cJjFuKF71Q1)AL8jHO$!fYT3(;U3Re* zPPOe%*O+@JYt1bW`!W_1!mN&=w3G9ru1XsmwfS~BJ))PhD(+_J_^N6j)sx5VwbWK| zwRyC?W<`pOCY)b#AS?rluxuuGf-AJ=D!M36l{ua?@SJ5>e!IBr3CXIxWw5xUZ@Xrw z_R@%?{>d%Ld4p}nEsiA@v*nc6Ah!MUs?GA7e5Q5lPpp0@`%5xY$C;{%rz24$;vR#* zBP=a{)K#CwIY%p} zXVdxTQ^HS@O&~eIftU+Qt^~(DGxrdi3k}DdT^I7Iy5SMOp$QuD8s;+93YQ!OY{eB24%xY7ml@|M7I(Nb@K_-?F;2?et|CKkuZK_>+>Lvg!>JE~wN`BI|_h6$qi!P)+K-1Hh(1;a`os z55)4Q{oJiA(lQM#;w#Ta%T0jDNXIPM_bgESMCDEg6rM33anEr}=|Fn6)|jBP6Y}u{ zv9@%7*#RI9;fv;Yii5CI+KrRdr0DKh=L>)eO4q$1zmcSmglsV`*N(x=&Wx`*v!!hn6X-l0 zP_m;X??O(skcj+oS$cIdKhfT%ABAzz3w^la-Ucw?yBPEC+=Pe_vU8nd-HV5YX6X8r zZih&j^eLU=%*;VzhUyoLF;#8QsEfmByk+Y~caBqSvQaaWf2a{JKB9B>V&r?l^rXaC z8)6AdR@Qy_BxQrE2Fk?ewD!SwLuMj@&d_n5RZFf7=>O>hzVE*seW3U?_p|R^CfoY`?|#x9)-*yjv#lo&zP=uI`M?J zbzC<^3x7GfXA4{FZ72{PE*-mNHyy59Q;kYG@BB~NhTd6pm2Oj=_ zizmD?MKVRkT^KmXuhsk?eRQllPo2Ubk=uCKiZ&u3Xjj~<(!M94c)Tez@9M1Gfs5JV z->@II)CDJOXTtPrQudNjE}Eltbjq>6KiwAwqvAKd^|g!exgLG3;wP+#mZYr`cy3#39e653d=jrR-ulW|h#ddHu(m9mFoW~2yE zz5?dB%6vF}+`-&-W8vy^OCxm3_{02royjvmwjlp+eQDzFVEUiyO#gLv%QdDSI#3W* z?3!lL8clTaNo-DVJw@ynq?q!%6hTQi35&^>P85G$TqNt78%9_sSJt2RThO|JzM$iL zg|wjxdMC2|Icc5rX*qPL(coL!u>-xxz-rFiC!6hD1IR%|HSRsV3>Kq~&vJ=s3M5y8SG%YBQ|{^l#LGlg!D?E>2yR*eV%9m$_J6VGQ~AIh&P$_aFbh zULr0Z$QE!QpkP=aAeR4ny<#3Fwyw@rZf4?Ewq`;mCVv}xaz+3ni+}a=k~P+yaWt^L z@w67!DqVf7D%7XtXX5xBW;Co|HvQ8WR1k?r2cZD%U;2$bsM%u8{JUJ5Z0k= zZJARv^vFkmWx15CB=rb=D4${+#DVqy5$C%bf`!T0+epLJLnh1jwCdb*zuCL}eEFvE z{rO1%gxg>1!W(I!owu*mJZ0@6FM(?C+d*CeceZRW_4id*D9p5nzMY&{mWqrJomjIZ z97ZNnZ3_%Hx8dn;H>p8m7F#^2;T%yZ3H;a&N7tm=Lvs&lgJLW{V1@h&6Vy~!+Ffbb zv(n3+v)_D$}dqd!2>Y2B)#<+o}LH#%ogGi2-?xRIH)1!SD)u-L65B&bsJTC=LiaF+YOCif2dUX6uAA|#+vNR z>U+KQekVGon)Yi<93(d!(yw1h3&X0N(PxN2{%vn}cnV?rYw z$N^}_o!XUB!mckL`yO1rnUaI4wrOeQ(+&k?2mi47hzxSD`N#-byqd1IhEoh!PGq>t z_MRy{5B0eKY>;Ao3z$RUU7U+i?iX^&r739F)itdrTpAi-NN0=?^m%?{A9Ly2pVv>Lqs6moTP?T2-AHqFD-o_ znVr|7OAS#AEH}h8SRPQ@NGG47dO}l=t07__+iK8nHw^(AHx&Wb<%jPc$$jl6_p(b$ z)!pi(0fQodCHfM)KMEMUR&UID>}m^(!{C^U7sBDOA)$VThRCI0_+2=( zV8mMq0R(#z;C|7$m>$>`tX+T|xGt(+Y48@ZYu#z;0pCgYgmMVbFb!$?%yhZqP_nhn zy4<#3P1oQ#2b51NU1mGnHP$cf0j-YOgAA}A$QoL6JVLcmExs(kU{4z;PBHJD%_=0F z>+sQV`mzijSIT7xn%PiDKHOujX;n|M&qr1T@rOxTdxtZ!&u&3HHFLYD5$RLQ=heur zb>+AFokUVQeJy-#LP*^)spt{mb@Mqe=A~-4p0b+Bt|pZ+@CY+%x}9f}izU5;4&QFE zO1bhg&A4uC1)Zb67kuowWY4xbo&J=%yoXlFB)&$d*-}kjBu|w!^zbD1YPc0-#XTJr z)pm2RDy%J3jlqSMq|o%xGS$bPwn4AqitC6&e?pqWcjWPt{3I{>CBy;hg0Umh#c;hU3RhCUX=8aR>rmd` z7Orw(5tcM{|-^J?ZAA9KP|)X6n9$-kvr#j5YDecTM6n z&07(nD^qb8hpF0B^z^pQ*%5ePYkv&FabrlI61ntiVp!!C8y^}|<2xgAd#FY=8b*y( zuQOuvy2`Ii^`VBNJB&R!0{hABYX55ooCAJSSevl4RPqEGb)iy_0H}v@vFwFzD%>#I>)3PsouQ+_Kkbqy*kKdHdfkN7NBcq%V{x^fSxgXpg7$bF& zj!6AQbDY(1u#1_A#1UO9AxiZaCVN2F0wGXdY*g@x$ByvUA?ePdide0dmr#}udE%K| z3*k}Vv2Ew2u1FXBaVA6aerI36R&rzEZeDDCl5!t0J=ug6kuNZzH>3i_VN`%BsaVB3 zQYw|Xub_SGf{)F{$ZX5`Jc!X!;eybjP+o$I{Z^Hsj@D=E{MnnL+TbC@HEU2DjG{3-LDGIbq()U87x4eS;JXnSh;lRlJ z>EL3D>wHt-+wTjQF$fGyDO$>d+(fq@bPpLBS~xA~R=3JPbS{tzN(u~m#Po!?H;IYv zE;?8%^vle|%#oux(Lj!YzBKv+Fd}*Ur-dCBoX*t{KeNM*n~ZPYJ4NNKkI^MFbz9!v z4(Bvm*Kc!-$%VFEewYJKz-CQN{`2}KX4*CeJEs+Q(!kI%hN1!1P6iOq?ovz}X0IOi z)YfWpwW@pK08^69#wSyCZkX9?uZD?C^@rw^Y?gLS_xmFKkooyx$*^5#cPqntNTtSG zlP>XLMj2!VF^0k#ole7`-c~*~+_T5ls?x4)ah(j8vo_ zwb%S8qoaZqY0-$ZI+ViIA_1~~rAH7K_+yFS{0rT@eQtTAdz#8E5VpwnW!zJ_^{Utv zlW5Iar3V5t&H4D6A=>?mq;G92;1cg9a2sf;gY9pJDVKn$DYdQlvfXq}zz8#LyPGq@ z+`YUMD;^-6w&r-82JL7mA8&M~Pj@aK!m{0+^v<|t%APYf7`}jGEhdYLqsHW-Le9TL z_hZZ1gbrz7$f9^fAzVIP30^KIz!!#+DRLL+qMszvI_BpOSmjtl$hh;&UeM{ER@INV zcI}VbiVTPoN|iSna@=7XkP&-4#06C};8ajbxJ4Gcq8(vWv4*&X8bM^T$mBk75Q92j z1v&%a;OSKc8EIrodmIiw$lOES2hzGDcjjB`kEDfJe{r}yE6`eZL zEB`9u>Cl0IsQ+t}`-cx}{6jqcANucqIB>Qmga_&<+80E2Q|VHHQ$YlAt{6`Qu`HA3 z03s0-sSlwbvgi&_R8s={6<~M^pGvBNjKOa>tWenzS8s zR>L7R5aZ=mSU{f?ib4Grx$AeFvtO5N|D>9#)ChH#Fny2maHWHOf2G=#<9Myot#+4u zWVa6d^Vseq_0=#AYS(-m$Lp;*8nC_6jXIjEM`omUmtH@QDs3|G)i4j*#_?#UYVZvJ z?YjT-?!4Q{BNun;dKBWLEw2C-VeAz`%?A>p;)PL}TAZn5j~HK>v1W&anteARlE+~+ zj>c(F;?qO3pXBb|#OZdQnm<4xWmn~;DR5SDMxt0UK_F^&eD|KZ=O;tO3vy4@4h^;2 zUL~-z`-P1aOe?|ZC1BgVsL)2^J-&vIFI%q@40w0{jjEfeVl)i9(~bt2z#2Vm)p`V_ z1;6$Ae7=YXk#=Qkd24Y23t&GvRxaOoad~NbJ+6pxqzJ>FY#Td7@`N5xp!n(c!=RE& z&<<@^a$_Ys8jqz4|5Nk#FY$~|FPC0`*a5HH!|Gssa9=~66&xG9)|=pOOJ2KE5|YrR zw!w6K2aC=J$t?L-;}5hn6mHd%hC;p8P|Dgh6D>hGnXPgi;6r+eA=?f72y9(Cf_ho{ zH6#)uD&R=73^$$NE;5piWX2bzR67fQ)`b=85o0eOLGI4c-Tb@-KNi2pz=Ke@SDcPn za$AxXib84`!Sf;Z3B@TSo`Dz7GM5Kf(@PR>Ghzi=BBxK8wRp>YQoXm+iL>H*Jo9M3 z6w&E?BC8AFTFT&Tv8zf+m9<&S&%dIaZ)Aoqkak_$r-2{$d~0g2oLETx9Y`eOAf14QXEQw3tJne;fdzl@wV#TFXSLXM2428F-Q}t+n2g%vPRMUzYPvzQ9f# zu(liiJem9P*?0%V@RwA7F53r~|I!Ty)<*AsMX3J{_4&}{6pT%Tpw>)^|DJ)>gpS~1rNEh z0$D?uO8mG?H;2BwM5a*26^7YO$XjUm40XmBsb63MoR;bJh63J;OngS5sSI+o2HA;W zdZV#8pDpC9Oez&L8loZO)MClRz!_!WD&QRtQxnazhT%Vj6Wl4G11nUk8*vSeVab@N#oJ}`KyJv+8Mo@T1-pqZ1t|?cnaVOd;1(h9 z!$DrN=jcGsVYE-0-n?oCJ^4x)F}E;UaD-LZUIzcD?W^ficqJWM%QLy6QikrM1aKZC zi{?;oKwq^Vsr|&`i{jIphA8S6G4)$KGvpULjH%9u(Dq247;R#l&I0{IhcC|oBF*Al zvLo7Xte=C{aIt*otJD}BUq)|_pdR>{zBMT< z(^1RpZv*l*m*OV^8>9&asGBo8h*_4q*)-eCv*|Pq=XNGrZE)^(SF7^{QE_~4VDB(o zVcPA_!G+2CAtLbl+`=Q~9iW`4ZRLku!uB?;tWqVjB0lEOf}2RD7dJ=BExy=<9wkb- z9&7{XFA%n#JsHYN8t5d~=T~5DcW4$B%3M+nNvC2`0!#@sckqlzo5;hhGi(D9=*A4` z5ynobawSPRtWn&CDLEs3Xf`(8^zDP=NdF~F^s&={l7(aw&EG}KWpMjtmz7j_VLO;@ zM2NVLDxZ@GIv7*gzl1 zjq78tv*8#WSY`}Su0&C;2F$Ze(q>F(@Wm^Gw!)(j;dk9Ad{STaxn)IV9FZhm*n+U} zi;4y*3v%A`_c7a__DJ8D1b@dl0Std3F||4Wtvi)fCcBRh!X9$1x!_VzUh>*S5s!oq z;qd{J_r79EL2wIeiGAqFstWtkfIJpjVh%zFo*=55B9Zq~y0=^iqHWfQl@O!Ak;(o*m!pZqe9 z%U2oDOhR)BvW8&F70L;2TpkzIutIvNQaTjjs5V#8mV4!NQ}zN=i`i@WI1z0eN-iCS z;vL-Wxc^Vc_qK<5RPh(}*8dLT{~GzE{w2o$2kMFaEl&q zP{V=>&3kW7tWaK-Exy{~`v4J0U#OZBk{a9{&)&QG18L@6=bsZ1zC_d{{pKZ-Ey>I> z;8H0t4bwyQqgu4hmO`3|4K{R*5>qnQ&gOfdy?z`XD%e5+pTDzUt3`k^u~SaL&XMe= z9*h#kT(*Q9jO#w2Hd|Mr-%DV8i_1{J1MU~XJ3!WUplhXDYBpJH><0OU`**nIvPIof z|N8@I=wA)sf45SAvx||f?Z5uB$kz1qL3Ky_{%RPdP5iN-D2!p5scq}buuC00C@jom zhfGKm3|f?Z0iQ|K$Z~!`8{nmAS1r+fp6r#YDOS8V*;K&Gs7Lc&f^$RC66O|)28oh`NHy&vq zJh+hAw8+ybTB0@VhWN^0iiTnLsCWbS_y`^gs!LX!Lw{yE``!UVzrV24tP8o;I6-65 z1MUiHw^{bB15tmrVT*7-#sj6cs~z`wk52YQJ*TG{SE;KTm#Hf#a~|<(|ImHH17nNM z`Ub{+J3dMD!)mzC8b(2tZtokKW5pAwHa?NFiso~# z1*iaNh4lQ4TS)|@G)H4dZV@l*Vd;Rw;-;odDhW2&lJ%m@jz+Panv7LQm~2Js6rOW3 z0_&2cW^b^MYW3)@o;neZ<{B4c#m48dAl$GCc=$>ErDe|?y@z`$uq3xd(%aAsX)D%l z>y*SQ%My`yDP*zof|3@_w#cjaW_YW4BdA;#Glg1RQcJGY*CJ9`H{@|D+*e~*457kd z73p<%fB^PV!Ybw@)Dr%(ZJbX}xmCStCYv#K3O32ej{$9IzM^I{6FJ8!(=azt7RWf4 z7ib0UOPqN40X!wOnFOoddd8`!_IN~9O)#HRTyjfc#&MCZ zZAMzOVB=;qwt8gV?{Y2?b=iSZG~RF~uyx18K)IDFLl})G1v@$(s{O4@RJ%OTJyF+Cpcx4jmy|F3euCnMK!P2WTDu5j z{{gD$=M*pH!GGzL%P)V2*ROm>!$Y=z|D`!_yY6e7SU$~a5q8?hZGgaYqaiLnkK%?0 zs#oI%;zOxF@g*@(V4p!$7dS1rOr6GVs6uYCTt2h)eB4?(&w8{#o)s#%gN@BBosRUe z)@P@8_Zm89pr~)b>e{tbPC~&_MR--iB{=)y;INU5#)@Gix-YpgP<-c2Ms{9zuCX|3 z!p(?VaXww&(w&uBHzoT%!A2=3HAP>SDxcljrego7rY|%hxy3XlODWffO_%g|l+7Y_ zqV(xbu)s4lV=l7M;f>vJl{`6qBm>#ZeMA}kXb97Z)?R97EkoI?x6Lp0yu1Z>PS?2{ z0QQ(8D)|lc9CO3B~e(pQM&5(1y&y=e>C^X$`)_&XuaI!IgDTVqt31wX#n+@!a_A0ZQkA zCJ2@M_4Gb5MfCrm5UPggeyh)8 zO9?`B0J#rkoCx(R0I!ko_2?iO@|oRf1;3r+i)w-2&j?=;NVIdPFsB)`|IC0zk6r9c zRrkfxWsiJ(#8QndNJj@{@WP2Ackr|r1VxV{7S&rSU(^)-M8gV>@UzOLXu9K<{6e{T zXJ6b92r$!|lwjhmgqkdswY&}c)KW4A)-ac%sU;2^fvq7gfUW4Bw$b!i@duy1CAxSn z(pyh$^Z=&O-q<{bZUP+$U}=*#M9uVc>CQVgDs4swy5&8RAHZ~$)hrTF4W zPsSa~qYv_0mJnF89RnnJTH`3}w4?~epFl=D(35$ zWa07ON$`OMBOHgCmfO(9RFc<)?$x)N}Jd2A(<*Ll7+4jrRt9w zwGxExUXd9VB#I|DwfxvJ;HZ8Q{37^wDhaZ%O!oO(HpcqfLH%#a#!~;Jl7F5>EX_=8 z{()l2NqPz>La3qJR;_v+wlK>GsHl;uRA8%j`A|yH@k5r%55S9{*Cp%uw6t`qc1!*T za2OeqtQj7sAp#Q~=5Fs&aCR9v>5V+s&RdNvo&H~6FJOjvaj--2sYYBvMq;55%z8^o z|BJDA4vzfow#DO#ZQHh;Oq_{r+qP{R9ox2TOgwQiv7Ow!zjN+A@BN;0tA2lUb#+zO z(^b89eV)D7UVE+h{mcNc6&GtpOqDn_?VAQ)Vob$hlFwW%xh>D#wml{t&Ofmm_d_+; zKDxzdr}`n2Rw`DtyIjrG)eD0vut$}dJAZ0AohZ+ZQdWXn_Z@dI_y=7t3q8x#pDI-K z2VVc&EGq445Rq-j0=U=Zx`oBaBjsefY;%)Co>J3v4l8V(T8H?49_@;K6q#r~Wwppc z4XW0(4k}cP=5ex>-Xt3oATZ~bBWKv)aw|I|Lx=9C1s~&b77idz({&q3T(Y(KbWO?+ zmcZ6?WeUsGk6>km*~234YC+2e6Zxdl~<_g2J|IE`GH%n<%PRv-50; zH{tnVts*S5*_RxFT9eM0z-pksIb^drUq4>QSww=u;UFCv2AhOuXE*V4z?MM`|ABOC4P;OfhS(M{1|c%QZ=!%rQTDFx`+}?Kdx$&FU?Y<$x;j7z=(;Lyz+?EE>ov!8vvMtSzG!nMie zsBa9t8as#2nH}n8xzN%W%U$#MHNXmDUVr@GX{?(=yI=4vks|V)!-W5jHsU|h_&+kY zS_8^kd3jlYqOoiI`ZqBVY!(UfnAGny!FowZWY_@YR0z!nG7m{{)4OS$q&YDyw6vC$ zm4!$h>*|!2LbMbxS+VM6&DIrL*X4DeMO!@#EzMVfr)e4Tagn~AQHIU8?e61TuhcKD zr!F4(kEebk(Wdk-?4oXM(rJwanS>Jc%<>R(siF+>+5*CqJLecP_we33iTFTXr6W^G z7M?LPC-qFHK;E!fxCP)`8rkxZyFk{EV;G-|kwf4b$c1k0atD?85+|4V%YATWMG|?K zLyLrws36p%Qz6{}>7b>)$pe>mR+=IWuGrX{3ZPZXF3plvuv5Huax86}KX*lbPVr}L z{C#lDjdDeHr~?l|)Vp_}T|%$qF&q#U;ClHEPVuS+Jg~NjC1RP=17=aQKGOcJ6B3mp z8?4*-fAD~}sX*=E6!}^u8)+m2j<&FSW%pYr_d|p_{28DZ#Cz0@NF=gC-o$MY?8Ca8 zr5Y8DSR^*urS~rhpX^05r30Ik#2>*dIOGxRm0#0YX@YQ%Mg5b6dXlS!4{7O_kdaW8PFSdj1=ryI-=5$fiieGK{LZ+SX(1b=MNL!q#lN zv98?fqqTUH8r8C7v(cx#BQ5P9W>- zmW93;eH6T`vuJ~rqtIBg%A6>q>gnWb3X!r0wh_q;211+Om&?nvYzL1hhtjB zK_7G3!n7PL>d!kj){HQE zE8(%J%dWLh1_k%gVXTZt zEdT09XSKAx27Ncaq|(vzL3gm83q>6CAw<$fTnMU05*xAe&rDfCiu`u^1)CD<>sx0i z*hr^N_TeN89G(nunZoLBf^81#pmM}>JgD@Nn1l*lN#a=B=9pN%tmvYFjFIoKe_(GF z-26x{(KXdfsQL7Uv6UtDuYwV`;8V3w>oT_I<`Ccz3QqK9tYT5ZQzbop{=I=!pMOCb zCU68`n?^DT%^&m>A%+-~#lvF!7`L7a{z<3JqIlk1$<||_J}vW1U9Y&eX<}l8##6i( zZcTT@2`9(Mecptm@{3A_Y(X`w9K0EwtPq~O!16bq{7c0f7#(3wn-^)h zxV&M~iiF!{-6A@>o;$RzQ5A50kxXYj!tcgme=Qjrbje~;5X2xryU;vH|6bE(8z^<7 zQ>BG7_c*JG8~K7Oe68i#0~C$v?-t@~@r3t2inUnLT(c=URpA9kA8uq9PKU(Ps(LVH zqgcqW>Gm?6oV#AldDPKVRcEyQIdTT`Qa1j~vS{<;SwyTdr&3*t?J)y=M7q*CzucZ&B0M=joT zBbj@*SY;o2^_h*>R0e({!QHF0=)0hOj^B^d*m>SnRrwq>MolNSgl^~r8GR#mDWGYEIJA8B<|{{j?-7p zVnV$zancW3&JVDtVpIlI|5djKq0(w$KxEFzEiiL=h5Jw~4Le23@s(mYyXWL9SX6Ot zmb)sZaly_P%BeX_9 zw&{yBef8tFm+%=--m*J|o~+Xg3N+$IH)t)=fqD+|fEk4AAZ&!wcN5=mi~Vvo^i`}> z#_3ahR}Ju)(Px7kev#JGcSwPXJ2id9%Qd2A#Uc@t8~egZ8;iC{e! z%=CGJOD1}j!HW_sgbi_8suYnn4#Ou}%9u)dXd3huFIb!ytlX>Denx@pCS-Nj$`VO&j@(z!kKSP0hE4;YIP#w9ta=3DO$7f*x zc9M4&NK%IrVmZAe=r@skWD`AEWH=g+r|*13Ss$+{c_R!b?>?UaGXlw*8qDmY#xlR= z<0XFbs2t?8i^G~m?b|!Hal^ZjRjt<@a? z%({Gn14b4-a|#uY^=@iiKH+k?~~wTj5K1A&hU z2^9-HTC)7zpoWK|$JXaBL6C z#qSNYtY>65T@Zs&-0cHeu|RX(Pxz6vTITdzJdYippF zC-EB+n4}#lM7`2Ry~SO>FxhKboIAF#Z{1wqxaCb{#yEFhLuX;Rx(Lz%T`Xo1+a2M}7D+@wol2)OJs$TwtRNJ={( zD@#zTUEE}#Fz#&(EoD|SV#bayvr&E0vzmb%H?o~46|FAcx?r4$N z&67W3mdip-T1RIxwSm_&(%U|+WvtGBj*}t69XVd&ebn>KOuL(7Y8cV?THd-(+9>G7*Nt%T zcH;`p={`SOjaf7hNd(=37Lz3-51;58JffzIPgGs_7xIOsB5p2t&@v1mKS$2D$*GQ6 zM(IR*j4{nri7NMK9xlDy-hJW6sW|ZiDRaFiayj%;(%51DN!ZCCCXz+0Vm#};70nOx zJ#yA0P3p^1DED;jGdPbQWo0WATN=&2(QybbVdhd=Vq*liDk`c7iZ?*AKEYC#SY&2g z&Q(Ci)MJ{mEat$ZdSwTjf6h~roanYh2?9j$CF@4hjj_f35kTKuGHvIs9}Re@iKMxS-OI*`0S z6s)fOtz}O$T?PLFVSeOjSO26$@u`e<>k(OSP!&YstH3ANh>)mzmKGNOwOawq-MPXe zy4xbeUAl6tamnx))-`Gi2uV5>9n(73yS)Ukma4*7fI8PaEwa)dWHs6QA6>$}7?(L8 ztN8M}?{Tf!Zu22J5?2@95&rQ|F7=FK-hihT-vDp!5JCcWrVogEnp;CHenAZ)+E+K5 z$Cffk5sNwD_?4+ymgcHR(5xgt20Z8M`2*;MzOM#>yhk{r3x=EyM226wb&!+j`W<%* zSc&|`8!>dn9D@!pYow~(DsY_naSx7(Z4i>cu#hA5=;IuI88}7f%)bRkuY2B;+9Uep zpXcvFWkJ!mQai63BgNXG26$5kyhZ2&*3Q_tk)Ii4M>@p~_~q_cE!|^A;_MHB;7s#9 zKzMzK{lIxotjc};k67^Xsl-gS!^*m*m6kn|sbdun`O?dUkJ{0cmI0-_2y=lTAfn*Y zKg*A-2sJq)CCJgY0LF-VQvl&6HIXZyxo2#!O&6fOhbHXC?%1cMc6y^*dOS{f$=137Ds1m01qs`>iUQ49JijsaQ( zksqV9@&?il$|4Ua%4!O15>Zy&%gBY&wgqB>XA3!EldQ%1CRSM(pp#k~-pkcCg4LAT zXE=puHbgsw)!xtc@P4r~Z}nTF=D2~j(6D%gTBw$(`Fc=OOQ0kiW$_RDd=hcO0t97h zb86S5r=>(@VGy1&#S$Kg_H@7G^;8Ue)X5Y+IWUi`o;mpvoV)`fcVk4FpcT|;EG!;? zHG^zrVVZOm>1KFaHlaogcWj(v!S)O(Aa|Vo?S|P z5|6b{qkH(USa*Z7-y_Uvty_Z1|B{rTS^qmEMLEYUSk03_Fg&!O3BMo{b^*`3SHvl0 zhnLTe^_vVIdcSHe)SQE}r~2dq)VZJ!aSKR?RS<(9lzkYo&dQ?mubnWmgMM37Nudwo z3Vz@R{=m2gENUE3V4NbIzAA$H1z0pagz94-PTJyX{b$yndsdKptmlKQKaaHj@3=ED zc7L?p@%ui|RegVYutK$64q4pe9+5sv34QUpo)u{1ci?)_7gXQd{PL>b0l(LI#rJmN zGuO+%GO`xneFOOr4EU(Wg}_%bhzUf;d@TU+V*2#}!2OLwg~%D;1FAu=Un>OgjPb3S z7l(riiCwgghC=Lm5hWGf5NdGp#01xQ59`HJcLXbUR3&n%P(+W2q$h2Qd z*6+-QXJ*&Kvk9ht0f0*rO_|FMBALen{j7T1l%=Q>gf#kma zQlg#I9+HB+z*5BMxdesMND`_W;q5|FaEURFk|~&{@qY32N$G$2B=&Po{=!)x5b!#n zxLzblkq{yj05#O7(GRuT39(06FJlalyv<#K4m}+vs>9@q-&31@1(QBv82{}Zkns~K ze{eHC_RDX0#^A*JQTwF`a=IkE6Ze@j#-8Q`tTT?k9`^ZhA~3eCZJ-Jr{~7Cx;H4A3 zcZ+Zj{mzFZbVvQ6U~n>$U2ZotGsERZ@}VKrgGh0xM;Jzt29%TX6_&CWzg+YYMozrM z`nutuS)_0dCM8UVaKRj804J4i%z2BA_8A4OJRQ$N(P9Mfn-gF;4#q788C@9XR0O3< zsoS4wIoyt046d+LnSCJOy@B@Uz*#GGd#+Ln1ek5Dv>(ZtD@tgZlPnZZJGBLr^JK+!$$?A_fA3LOrkoDRH&l7 zcMcD$Hsjko3`-{bn)jPL6E9Ds{WskMrivsUu5apD z?grQO@W7i5+%X&E&p|RBaEZ(sGLR@~(y^BI@lDMot^Ll?!`90KT!JXUhYS`ZgX3jnu@Ja^seA*M5R@f`=`ynQV4rc$uT1mvE?@tz)TN<=&H1%Z?5yjxcpO+6y_R z6EPuPKM5uxKpmZfT(WKjRRNHs@ib)F5WAP7QCADvmCSD#hPz$V10wiD&{NXyEwx5S z6NE`3z!IS^$s7m}PCwQutVQ#~w+V z=+~->DI*bR2j0^@dMr9`p>q^Ny~NrAVxrJtX2DUveic5vM%#N*XO|?YAWwNI$Q)_) zvE|L(L1jP@F%gOGtnlXtIv2&1i8q<)Xfz8O3G^Ea~e*HJsQgBxWL(yuLY+jqUK zRE~`-zklrGog(X}$9@ZVUw!8*=l`6mzYLtsg`AvBYz(cxmAhr^j0~(rzXdiOEeu_p zE$sf2(w(BPAvO5DlaN&uQ$4@p-b?fRs}d7&2UQ4Fh?1Hzu*YVjcndqJLw0#q@fR4u zJCJ}>_7-|QbvOfylj+e^_L`5Ep9gqd>XI3-O?Wp z-gt*P29f$Tx(mtS`0d05nHH=gm~Po_^OxxUwV294BDKT>PHVlC5bndncxGR!n(OOm znsNt@Q&N{TLrmsoKFw0&_M9$&+C24`sIXGWgQaz=kY;S{?w`z^Q0JXXBKFLj0w0U6P*+jPKyZHX9F#b0D1$&(- zrm8PJd?+SrVf^JlfTM^qGDK&-p2Kdfg?f>^%>1n8bu&byH(huaocL>l@f%c*QkX2i znl}VZ4R1en4S&Bcqw?$=Zi7ohqB$Jw9x`aM#>pHc0x z0$!q7iFu zZ`tryM70qBI6JWWTF9EjgG@>6SRzsd}3h+4D8d~@CR07P$LJ}MFsYi-*O%XVvD@yT|rJ+Mk zDllJ7$n0V&A!0flbOf)HE6P_afPWZmbhpliqJuw=-h+r;WGk|ntkWN(8tKlYpq5Ow z(@%s>IN8nHRaYb*^d;M(D$zGCv5C|uqmsDjwy4g=Lz>*OhO3z=)VD}C<65;`89Ye} zSCxrv#ILzIpEx1KdLPlM&%Cctf@FqTKvNPXC&`*H9=l=D3r!GLM?UV zOxa(8ZsB`&+76S-_xuj?G#wXBfDY@Z_tMpXJS7^mp z@YX&u0jYw2A+Z+bD#6sgVK5ZgdPSJV3>{K^4~%HV?rn~4D)*2H!67Y>0aOmzup`{D zzDp3c9yEbGCY$U<8biJ_gB*`jluz1ShUd!QUIQJ$*1;MXCMApJ^m*Fiv88RZ zFopLViw}{$Tyhh_{MLGIE2~sZ)t0VvoW%=8qKZ>h=adTe3QM$&$PO2lfqH@brt!9j ziePM8$!CgE9iz6B<6_wyTQj?qYa;eC^{x_0wuwV~W+^fZmFco-o%wsKSnjXFEx02V zF5C2t)T6Gw$Kf^_c;Ei3G~uC8SM-xyycmXyC2hAVi-IfXqhu$$-C=*|X?R0~hu z8`J6TdgflslhrmDZq1f?GXF7*ALeMmOEpRDg(s*H`4>_NAr`2uqF;k;JQ+8>A|_6ZNsNLECC%NNEb1Y1dP zbIEmNpK)#XagtL4R6BC{C5T(+=yA-(Z|Ap}U-AfZM#gwVpus3(gPn}Q$CExObJ5AC z)ff9Yk?wZ}dZ-^)?cbb9Fw#EjqQ8jxF4G3=L?Ra zg_)0QDMV1y^A^>HRI$x?Op@t;oj&H@1xt4SZ9(kifQ zb59B*`M99Td7@aZ3UWvj1rD0sE)d=BsBuW*KwkCds7ay(7*01_+L}b~7)VHI>F_!{ zyxg-&nCO?v#KOUec0{OOKy+sjWA;8rTE|Lv6I9H?CI?H(mUm8VXGwU$49LGpz&{nQp2}dinE1@lZ1iox6{ghN&v^GZv9J${7WaXj)<0S4g_uiJ&JCZ zr8-hsu`U%N;+9N^@&Q0^kVPB3)wY(rr}p7{p0qFHb3NUUHJb672+wRZs`gd1UjKPX z4o6zljKKA+Kkj?H>Ew63o%QjyBk&1!P22;MkD>sM0=z_s-G{mTixJCT9@_|*(p^bz zJ8?ZZ&;pzV+7#6Mn`_U-)k8Pjg?a;|Oe^us^PoPY$Va~yi8|?+&=y$f+lABT<*pZr zP}D{~Pq1Qyni+@|aP;ixO~mbEW9#c0OU#YbDZIaw=_&$K%Ep2f%hO^&P67hApZe`x zv8b`Mz@?M_7-)b!lkQKk)JXXUuT|B8kJlvqRmRpxtQDgvrHMXC1B$M@Y%Me!BSx3P z#2Eawl$HleZhhTS6Txm>lN_+I`>eV$&v9fOg)%zVn3O5mI*lAl>QcHuW6!Kixmq`X zBCZ*Ck6OYtDiK!N47>jxI&O2a9x7M|i^IagRr-fmrmikEQGgw%J7bO|)*$2FW95O4 zeBs>KR)izRG1gRVL;F*sr8A}aRHO0gc$$j&ds8CIO1=Gwq1%_~E)CWNn9pCtBE}+`Jelk4{>S)M)`Ll=!~gnn1yq^EX(+y*ik@3Ou0qU`IgYi3*doM+5&dU!cho$pZ zn%lhKeZkS72P?Cf68<#kll_6OAO26bIbueZx**j6o;I0cS^XiL`y+>{cD}gd%lux} z)3N>MaE24WBZ}s0ApfdM;5J_Ny}rfUyxfkC``Awo2#sgLnGPewK};dORuT?@I6(5~ z?kE)Qh$L&fwJXzK){iYx!l5$Tt|^D~MkGZPA}(o6f7w~O2G6Vvzdo*a;iXzk$B66$ zwF#;wM7A+(;uFG4+UAY(2`*3XXx|V$K8AYu#ECJYSl@S=uZW$ksfC$~qrrbQj4??z-)uz0QL}>k^?fPnJTPw% zGz)~?B4}u0CzOf@l^um}HZzbaIwPmb<)< zi_3@E9lc)Qe2_`*Z^HH;1CXOceL=CHpHS{HySy3T%<^NrWQ}G0i4e1xm_K3(+~oi$ zoHl9wzb?Z4j#90DtURtjtgvi7uw8DzHYmtPb;?%8vb9n@bszT=1qr)V_>R%s!92_` zfnHQPANx z<#hIjIMm#*(v*!OXtF+w8kLu`o?VZ5k7{`vw{Yc^qYclpUGIM_PBN1+c{#Vxv&E*@ zxg=W2W~JuV{IuRYw3>LSI1)a!thID@R=bU+cU@DbR^_SXY`MC7HOsCN z!dO4OKV7(E_Z8T#8MA1H`99?Z!r0)qKW_#|29X3#Jb+5+>qUidbeP1NJ@)(qi2S-X zao|f0_tl(O+$R|Qwd$H{_ig|~I1fbp_$NkI!0E;Y z6JrnU{1Ra6^on{9gUUB0mwzP3S%B#h0fjo>JvV~#+X0P~JV=IG=yHG$O+p5O3NUgG zEQ}z6BTp^Fie)Sg<){Z&I8NwPR(=mO4joTLHkJ>|Tnk23E(Bo`FSbPc05lF2-+)X? z6vV3*m~IBHTy*^E!<0nA(tCOJW2G4DsH7)BxLV8kICn5lu6@U*R`w)o9;Ro$i8=Q^V%uH8n3q=+Yf;SFRZu z!+F&PKcH#8cG?aSK_Tl@K9P#8o+jry@gdexz&d(Q=47<7nw@e@FFfIRNL9^)1i@;A z28+$Z#rjv-wj#heI|<&J_DiJ*s}xd-f!{J8jfqOHE`TiHHZVIA8CjkNQ_u;Ery^^t zl1I75&u^`1_q)crO+JT4rx|z2ToSC>)Or@-D zy3S>jW*sNIZR-EBsfyaJ+Jq4BQE4?SePtD2+jY8*%FsSLZ9MY>+wk?}}}AFAw)vr{ml)8LUG-y9>^t!{~|sgpxYc0Gnkg`&~R z-pilJZjr@y5$>B=VMdZ73svct%##v%wdX~9fz6i3Q-zOKJ9wso+h?VME7}SjL=!NUG{J?M&i!>ma`eoEa@IX`5G>B1(7;%}M*%-# zfhJ(W{y;>MRz!Ic8=S}VaBKqh;~7KdnGEHxcL$kA-6E~=!hrN*zw9N+_=odt<$_H_8dbo;0=42wcAETPCVGUr~v(`Uai zb{=D!Qc!dOEU6v)2eHSZq%5iqK?B(JlCq%T6av$Cb4Rko6onlG&?CqaX7Y_C_cOC3 zYZ;_oI(}=>_07}Oep&Ws7x7-R)cc8zfe!SYxJYP``pi$FDS)4Fvw5HH=FiU6xfVqIM!hJ;Rx8c0cB7~aPtNH(Nmm5Vh{ibAoU#J6 zImRCr?(iyu_4W_6AWo3*vxTPUw@vPwy@E0`(>1Qi=%>5eSIrp^`` zK*Y?fK_6F1W>-7UsB)RPC4>>Ps9)f+^MqM}8AUm@tZ->j%&h1M8s*s!LX5&WxQcAh z8mciQej@RPm?660%>{_D+7er>%zX_{s|$Z+;G7_sfNfBgY(zLB4Ey}J9F>zX#K0f6 z?dVNIeEh?EIShmP6>M+d|0wMM85Sa4diw1hrg|ITJ}JDg@o8y>(rF9mXk5M z2@D|NA)-7>wD&wF;S_$KS=eE84`BGw3g0?6wGxu8ys4rwI?9U=*^VF22t3%mbGeOh z`!O-OpF7#Vceu~F`${bW0nYVU9ecmk31V{tF%iv&5hWofC>I~cqAt@u6|R+|HLMMX zVxuSlMFOK_EQ86#E8&KwxIr8S9tj_goWtLv4f@!&h8;Ov41{J~496vp9vX=(LK#j! zAwi*21RAV-LD>9Cw3bV_9X(X3)Kr0-UaB*7Y>t82EQ%!)(&(XuAYtTsYy-dz+w=$ir)VJpe!_$ z6SGpX^i(af3{o=VlFPC);|J8#(=_8#vdxDe|Cok+ANhYwbE*FO`Su2m1~w+&9<_9~ z-|tTU_ACGN`~CNW5WYYBn^B#SwZ(t4%3aPp z;o)|L6Rk569KGxFLUPx@!6OOa+5OjQLK5w&nAmwxkC5rZ|m&HT8G%GVZxB_@ME z>>{rnXUqyiJrT(8GMj_ap#yN_!9-lO5e8mR3cJiK3NE{_UM&=*vIU`YkiL$1%kf+1 z4=jk@7EEj`u(jy$HnzE33ZVW_J4bj}K;vT?T91YlO(|Y0FU4r+VdbmQ97%(J5 zkK*Bed8+C}FcZ@HIgdCMioV%A<*4pw_n}l*{Cr4}a(lq|injK#O?$tyvyE`S%(1`H z_wwRvk#13ElkZvij2MFGOj`fhy?nC^8`Zyo%yVcUAfEr8x&J#A{|moUBAV_^f$hpaUuyQeY3da^ zS9iRgf87YBwfe}>BO+T&Fl%rfpZh#+AM?Dq-k$Bq`vG6G_b4z%Kbd&v>qFjow*mBl z-OylnqOpLg}or7_VNwRg2za3VBK6FUfFX{|TD z`Wt0Vm2H$vdlRWYQJqDmM?JUbVqL*ZQY|5&sY*?!&%P8qhA~5+Af<{MaGo(dl&C5t zE%t!J0 zh6jqANt4ABdPxSTrVV}fLsRQal*)l&_*rFq(Ez}ClEH6LHv{J#v?+H-BZ2)Wy{K@9 z+ovXHq~DiDvm>O~r$LJo!cOuwL+Oa--6;UFE2q@g3N8Qkw5E>ytz^(&($!O47+i~$ zKM+tkAd-RbmP{s_rh+ugTD;lriL~`Xwkad#;_aM?nQ7L_muEFI}U_4$phjvYgleK~`Fo`;GiC07&Hq1F<%p;9Q;tv5b?*QnR%8DYJH3P>Svmv47Y>*LPZJy8_{9H`g6kQpyZU{oJ`m%&p~D=K#KpfoJ@ zn-3cqmHsdtN!f?~w+(t+I`*7GQA#EQC^lUA9(i6=i1PqSAc|ha91I%X&nXzjYaM{8$s&wEx@aVkQ6M{E2 zfzId#&r(XwUNtPcq4Ngze^+XaJA1EK-%&C9j>^9(secqe{}z>hR5CFNveMsVA)m#S zk)_%SidkY-XmMWlVnQ(mNJ>)ooszQ#vaK;!rPmGKXV7am^_F!Lz>;~{VrIO$;!#30XRhE1QqO_~#+Ux;B_D{Nk=grn z8Y0oR^4RqtcYM)7a%@B(XdbZCOqnX#fD{BQTeLvRHd(irHKq=4*jq34`6@VAQR8WG z^%)@5CXnD_T#f%@-l${>y$tfb>2LPmc{~5A82|16mH)R?&r#KKLs7xpN-D`=&Cm^R zvMA6#Ahr<3X>Q7|-qfTY)}32HkAz$_mibYV!I)u>bmjK`qwBe(>za^0Kt*HnFbSdO z1>+ryKCNxmm^)*$XfiDOF2|{-v3KKB?&!(S_Y=Ht@|ir^hLd978xuI&N{k>?(*f8H z=ClxVJK_%_z1TH0eUwm2J+2To7FK4o+n_na)&#VLn1m;!+CX+~WC+qg1?PA~KdOlC zW)C@pw75_xoe=w7i|r9KGIvQ$+3K?L{7TGHwrQM{dCp=Z*D}3kX7E-@sZnup!BImw z*T#a=+WcTwL78exTgBn|iNE3#EsOorO z*kt)gDzHiPt07fmisA2LWN?AymkdqTgr?=loT7z@d`wnlr6oN}@o|&JX!yPzC*Y8d zu6kWlTzE1)ckyBn+0Y^HMN+GA$wUO_LN6W>mxCo!0?oiQvT`z$jbSEu&{UHRU0E8# z%B^wOc@S!yhMT49Y)ww(Xta^8pmPCe@eI5C*ed96)AX9<>))nKx0(sci8gwob_1}4 z0DIL&vsJ1_s%<@y%U*-eX z5rN&(zef-5G~?@r79oZGW1d!WaTqQn0F6RIOa9tJ=0(kdd{d1{<*tHT#cCvl*i>YY zH+L7jq8xZNcTUBqj(S)ztTU!TM!RQ}In*n&Gn<>(60G7}4%WQL!o>hbJqNDSGwl#H z`4k+twp0cj%PsS+NKaxslAEu9!#U3xT1|_KB6`h=PI0SW`P9GTa7caD1}vKEglV8# zjKZR`pluCW19c2fM&ZG)c3T3Um;ir3y(tSCJ7Agl6|b524dy5El{^EQBG?E61H0XY z`bqg!;zhGhyMFl&(o=JWEJ8n~z)xI}A@C0d2hQGvw7nGv)?POU@(kS1m=%`|+^ika zXl8zjS?xqW$WlO?Ewa;vF~XbybHBor$f<%I&*t$F5fynwZlTGj|IjZtVfGa7l&tK} zW>I<69w(cZLu)QIVG|M2xzW@S+70NinQzk&Y0+3WT*cC)rx~04O-^<{JohU_&HL5XdUKW!uFy|i$FB|EMu0eUyW;gsf`XfIc!Z0V zeK&*hPL}f_cX=@iv>K%S5kL;cl_$v?n(Q9f_cChk8Lq$glT|=e+T*8O4H2n<=NGmn z+2*h+v;kBvF>}&0RDS>)B{1!_*XuE8A$Y=G8w^qGMtfudDBsD5>T5SB;Qo}fSkkiV ze^K^M(UthkwrD!&*tTsu>Dacdj_q`~V%r_twr$(Ct&_dKeeXE?fA&4&yASJWJ*}~- zel=@W)tusynfC_YqH4ll>4Eg`Xjs5F7Tj>tTLz<0N3)X<1px_d2yUY>X~y>>93*$) z5PuNMQLf9Bu?AAGO~a_|J2akO1M*@VYN^VxvP0F$2>;Zb9;d5Yfd8P%oFCCoZE$ z4#N$^J8rxYjUE_6{T%Y>MmWfHgScpuGv59#4u6fpTF%~KB^Ae`t1TD_^Ud#DhL+Dm zbY^VAM#MrAmFj{3-BpVSWph2b_Y6gCnCAombVa|1S@DU)2r9W<> zT5L8BB^er3zxKt1v(y&OYk!^aoQisqU zH(g@_o)D~BufUXcPt!Ydom)e|aW{XiMnes2z&rE?og>7|G+tp7&^;q?Qz5S5^yd$i z8lWr4g5nctBHtigX%0%XzIAB8U|T6&JsC4&^hZBw^*aIcuNO47de?|pGXJ4t}BB`L^d8tD`H`i zqrP8?#J@8T#;{^B!KO6J=@OWKhAerih(phML`(Rg7N1XWf1TN>=Z3Do{l_!d~DND&)O)D>ta20}@Lt77qSnVsA7>)uZAaT9bsB>u&aUQl+7GiY2|dAEg@%Al3i316y;&IhQL^8fw_nwS>f60M_-m+!5)S_6EPM7Y)(Nq^8gL7(3 zOiot`6Wy6%vw~a_H?1hLVzIT^i1;HedHgW9-P#)}Y6vF%C=P70X0Tk^z9Te@kPILI z_(gk!k+0%CG)%!WnBjjw*kAKs_lf#=5HXC00s-}oM-Q1aXYLj)(1d!_a7 z*Gg4Fe6F$*ujVjI|79Z5+Pr`us%zW@ln++2l+0hsngv<{mJ%?OfSo_3HJXOCys{Ug z00*YR-(fv<=&%Q!j%b-_ppA$JsTm^_L4x`$k{VpfLI(FMCap%LFAyq;#ns5bR7V+x zO!o;c5y~DyBPqdVQX)8G^G&jWkBy2|oWTw>)?5u}SAsI$RjT#)lTV&Rf8;>u*qXnb z8F%Xb=7#$m)83z%`E;49)t3fHInhtc#kx4wSLLms!*~Z$V?bTyUGiS&m>1P(952(H zuHdv=;o*{;5#X-uAyon`hP}d#U{uDlV?W?_5UjJvf%11hKwe&(&9_~{W)*y1nR5f_ z!N(R74nNK`y8>B!0Bt_Vr!;nc3W>~RiKtGSBkNlsR#-t^&;$W#)f9tTlZz>n*+Fjz z3zXZ;jf(sTM(oDzJt4FJS*8c&;PLTW(IQDFs_5QPy+7yhi1syPCarvqrHFcf&yTy)^O<1EBx;Ir`5W{TIM>{8w&PB>ro4;YD<5LF^TjTb0!zAP|QijA+1Vg>{Afv^% zmrkc4o6rvBI;Q8rj4*=AZacy*n8B{&G3VJc)so4$XUoie0)vr;qzPZVbb<#Fc=j+8CGBWe$n|3K& z_@%?{l|TzKSlUEO{U{{%Fz_pVDxs7i9H#bnbCw7@4DR=}r_qV!Zo~CvD4ZI*+j3kO zW6_=|S`)(*gM0Z;;}nj`73OigF4p6_NPZQ-Od~e$c_);;4-7sR>+2u$6m$Gf%T{aq zle>e3(*Rt(TPD}03n5)!Ca8Pu!V}m6v0o1;5<1h$*|7z|^(3$Y&;KHKTT}hV056wuF0Xo@mK-52~r=6^SI1NC%c~CC?n>yX6wPTgiWYVz!Sx^atLby9YNn1Rk{g?|pJaxD4|9cUf|V1_I*w zzxK)hRh9%zOl=*$?XUjly5z8?jPMy%vEN)f%T*|WO|bp5NWv@B(K3D6LMl!-6dQg0 zXNE&O>Oyf%K@`ngCvbGPR>HRg5!1IV$_}m@3dWB7x3t&KFyOJn9pxRXCAzFr&%37wXG;z^xaO$ekR=LJG ztIHpY8F5xBP{mtQidqNRoz= z@){+N3(VO5bD+VrmS^YjG@+JO{EOIW)9=F4v_$Ed8rZtHvjpiEp{r^c4F6Ic#ChlC zJX^DtSK+v(YdCW)^EFcs=XP7S>Y!4=xgmv>{S$~@h=xW-G4FF9?I@zYN$e5oF9g$# zb!eVU#J+NjLyX;yb)%SY)xJdvGhsnE*JEkuOVo^k5PyS=o#vq!KD46UTW_%R=Y&0G zFj6bV{`Y6)YoKgqnir2&+sl+i6foAn-**Zd1{_;Zb7Ki=u394C5J{l^H@XN`_6XTKY%X1AgQM6KycJ+= zYO=&t#5oSKB^pYhNdzPgH~aEGW2=ec1O#s-KG z71}LOg@4UEFtp3GY1PBemXpNs6UK-ax*)#$J^pC_me;Z$Je(OqLoh|ZrW*mAMBFn< zHttjwC&fkVfMnQeen8`Rvy^$pNRFVaiEN4Pih*Y3@jo!T0nsClN)pdrr9AYLcZxZ| zJ5Wlj+4q~($hbtuY zVQ7hl>4-+@6g1i`1a)rvtp-;b0>^`Dloy(#{z~ytgv=j4q^Kl}wD>K_Y!l~ zp(_&7sh`vfO(1*MO!B%<6E_bx1)&s+Ae`O)a|X=J9y~XDa@UB`m)`tSG4AUhoM=5& znWoHlA-(z@3n0=l{E)R-p8sB9XkV zZ#D8wietfHL?J5X0%&fGg@MH~(rNS2`GHS4xTo7L$>TPme+Is~!|79=^}QbPF>m%J zFMkGzSndiPO|E~hrhCeo@&Ea{M(ieIgRWMf)E}qeTxT8Q#g-!Lu*x$v8W^M^>?-g= zwMJ$dThI|~M06rG$Sv@C@tWR>_YgaG&!BAbkGggVQa#KdtDB)lMLNVLN|51C@F^y8 zCRvMB^{GO@j=cHfmy}_pCGbP%xb{pNN>? z?7tBz$1^zVaP|uaatYaIN+#xEN4jBzwZ|YI_)p(4CUAz1ZEbDk>J~Y|63SZaak~#0 zoYKruYsWHoOlC1(MhTnsdUOwQfz5p6-D0}4;DO$B;7#M{3lSE^jnTT;ns`>!G%i*F?@pR1JO{QTuD0U+~SlZxcc8~>IB{)@8p`P&+nDxNj`*gh|u?yrv$phpQcW)Us)bi`kT%qLj(fi{dWRZ%Es2!=3mI~UxiW0$-v3vUl?#g{p6eF zMEUAqo5-L0Ar(s{VlR9g=j7+lt!gP!UN2ICMokAZ5(Agd>})#gkA2w|5+<%-CuEP# zqgcM}u@3(QIC^Gx<2dbLj?cFSws_f3e%f4jeR?4M^M3cx1f+Qr6ydQ>n)kz1s##2w zk}UyQc+Z5G-d-1}{WzjkLXgS-2P7auWSJ%pSnD|Uivj5u!xk0 z_^-N9r9o;(rFDt~q1PvE#iJZ_f>J3gcP$)SOqhE~pD2|$=GvpL^d!r z6u=sp-CrMoF7;)}Zd7XO4XihC4ji?>V&(t^?@3Q&t9Mx=qex6C9d%{FE6dvU6%d94 zIE;hJ1J)cCqjv?F``7I*6bc#X)JW2b4f$L^>j{*$R`%5VHFi*+Q$2;nyieduE}qdS{L8y8F08yLs?w}{>8>$3236T-VMh@B zq-nujsb_1aUv_7g#)*rf9h%sFj*^mIcImRV*k~Vmw;%;YH(&ylYpy!&UjUVqqtfG` zox3esju?`unJJA_zKXRJP)rA3nXc$m^{S&-p|v|-0x9LHJm;XIww7C#R$?00l&Yyj z=e}gKUOpsImwW?N)+E(awoF@HyP^EhL+GlNB#k?R<2>95hz!h9sF@U20DHSB3~WMa zk90+858r@-+vWwkawJ)8ougd(i#1m3GLN{iSTylYz$brAsP%=&m$mQQrH$g%3-^VR zE%B`Vi&m8f3T~&myTEK28BDWCVzfWir1I?03;pX))|kY5ClO^+bae z*7E?g=3g7EiisYOrE+lA)2?Ln6q2*HLNpZEWMB|O-JI_oaHZB%CvYB(%=tU= zE*OY%QY58fW#RG5=gm0NR#iMB=EuNF@)%oZJ}nmm=tsJ?eGjia{e{yuU0l3{d^D@)kVDt=1PE)&tf_hHC%0MB znL|CRCPC}SeuVTdf>-QV70`0(EHizc21s^sU>y%hW0t!0&y<7}Wi-wGy>m%(-jsDj zP?mF|>p_K>liZ6ZP(w5(|9Ga%>tLgb$|doDDfkdW>Z z`)>V2XC?NJT26mL^@ zf+IKr27TfM!UbZ@?zRddC7#6ss1sw%CXJ4FWC+t3lHZupzM77m^=9 z&(a?-LxIq}*nvv)y?27lZ{j zifdl9hyJudyP2LpU$-kXctshbJDKS{WfulP5Dk~xU4Le4c#h^(YjJit4#R8_khheS z|8(>2ibaHES4+J|DBM7I#QF5u-*EdN{n=Kt@4Zt?@Tv{JZA{`4 zU#kYOv{#A&gGPwT+$Ud}AXlK3K7hYzo$(fBSFjrP{QQ zeaKg--L&jh$9N}`pu{Bs>?eDFPaWY4|9|foN%}i;3%;@4{dc+iw>m}{3rELqH21G! z`8@;w-zsJ1H(N3%|1B@#ioLOjib)j`EiJqPQVSbPSPVHCj6t5J&(NcWzBrzCiDt{4 zdlPAUKldz%6x5II1H_+jv)(xVL+a;P+-1hv_pM>gMRr%04@k;DTokASSKKhU1Qms| zrWh3a!b(J3n0>-tipg{a?UaKsP7?+|@A+1WPDiQIW1Sf@qDU~M_P65_s}7(gjTn0X zucyEm)o;f8UyshMy&>^SC3I|C6jR*R_GFwGranWZe*I>K+0k}pBuET&M~ z;Odo*ZcT?ZpduHyrf8E%IBFtv;JQ!N_m>!sV6ly$_1D{(&nO~w)G~Y`7sD3#hQk%^ zp}ucDF_$!6DAz*PM8yE(&~;%|=+h(Rn-=1Wykas_-@d&z#=S}rDf`4w(rVlcF&lF! z=1)M3YVz7orwk^BXhslJ8jR);sh^knJW(Qmm(QdSgIAIdlN4Te5KJisifjr?eB{FjAX1a0AB>d?qY4Wx>BZ8&}5K0fA+d{l8 z?^s&l8#j7pR&ijD?0b%;lL9l$P_mi2^*_OL+b}4kuLR$GAf85sOo02?Y#90}CCDiS zZ%rbCw>=H~CBO=C_JVV=xgDe%b4FaEFtuS7Q1##y686r%F6I)s-~2(}PWK|Z8M+Gu zl$y~5@#0Ka%$M<&Cv%L`a8X^@tY&T7<0|(6dNT=EsRe0%kp1Qyq!^43VAKYnr*A5~ zsI%lK1ewqO;0TpLrT9v}!@vJK{QoVa_+N4FYT#h?Y8rS1S&-G+m$FNMP?(8N`MZP zels(*?kK{{^g9DOzkuZXJ2;SrOQsp9T$hwRB1(phw1c7`!Q!by?Q#YsSM#I12RhU{$Q+{xj83axHcftEc$mNJ8_T7A-BQc*k(sZ+~NsO~xAA zxnbb%dam_fZlHvW7fKXrB~F&jS<4FD2FqY?VG?ix*r~MDXCE^WQ|W|WM;gsIA4lQP zJ2hAK@CF*3*VqPr2eeg6GzWFlICi8S>nO>5HvWzyZTE)hlkdC_>pBej*>o0EOHR|) z$?};&I4+_?wvL*g#PJ9)!bc#9BJu1(*RdNEn>#Oxta(VWeM40ola<0aOe2kSS~{^P zDJBd}0L-P#O-CzX*%+$#v;(x%<*SPgAje=F{Zh-@ucd2DA(yC|N_|ocs*|-!H%wEw z@Q!>siv2W;C^^j^59OAX03&}&D*W4EjCvfi(ygcL#~t8XGa#|NPO+*M@Y-)ctFA@I z-p7npT1#5zOLo>7q?aZpCZ=iecn3QYklP;gF0bq@>oyBq94f6C=;Csw3PkZ|5q=(c zfs`aw?II0e(h=|7o&T+hq&m$; zBrE09Twxd9BJ2P+QPN}*OdZ-JZV7%av@OM7v!!NL8R;%WFq*?{9T3{ct@2EKgc8h) zMxoM$SaF#p<`65BwIDfmXG6+OiK0e)`I=!A3E`+K@61f}0e z!2a*FOaDrOe>U`q%K!QN`&=&0C~)CaL3R4VY(NDt{Xz(Xpqru5=r#uQN1L$Je1*dkdqQ*=lofQaN%lO!<5z9ZlHgxt|`THd>2 zsWfU$9=p;yLyJyM^t zS2w9w?Bpto`@H^xJpZDKR1@~^30Il6oFGfk5%g6w*C+VM)+%R@gfIwNprOV5{F^M2 zO?n3DEzpT+EoSV-%OdvZvNF+pDd-ZVZ&d8 zKeIyrrfPN=EcFRCPEDCVflX#3-)Ik_HCkL(ejmY8vzcf-MTA{oHk!R2*36`O68$7J zf}zJC+bbQk--9Xm!u#lgLvx8TXx2J258E5^*IZ(FXMpq$2LUUvhWQPs((z1+2{Op% z?J}9k5^N=z;7ja~zi8a_-exIqWUBJwohe#4QJ`|FF*$C{lM18z^#hX6!5B8KAkLUX ziP=oti-gpV(BsLD{0(3*dw}4JxK23Y7M{BeFPucw!sHpY&l%Ws4pSm`+~V7;bZ%Dx zeI)MK=4vC&5#;2MT7fS?^ch9?2;%<8Jlu-IB&N~gg8t;6S-#C@!NU{`p7M8@2iGc& zg|JPg%@gCoCQ&s6JvDU&`X2S<57f(k8nJ1wvBu{8r?;q3_kpZZ${?|( z+^)UvR33sjSd)aT!UPkA;ylO6{aE3MQa{g%Mcf$1KONcjO@&g5zPHWtzM1rYC{_K> zgQNcs<{&X{OA=cEWw5JGqpr0O>x*Tfak2PE9?FuWtz^DDNI}rwAaT0(bdo-<+SJ6A z&}S%boGMWIS0L}=S>|-#kRX;e^sUsotry(MjE|3_9duvfc|nwF#NHuM-w7ZU!5ei8 z6Mkf>2)WunY2eU@C-Uj-A zG(z0Tz2YoBk>zCz_9-)4a>T46$(~kF+Y{#sA9MWH%5z#zNoz)sdXq7ZR_+`RZ%0(q zC7&GyS_|BGHNFl8Xa%@>iWh%Gr?=J5<(!OEjauj5jyrA-QXBjn0OAhJJ9+v=!LK`` z@g(`^*84Q4jcDL`OA&ZV60djgwG`|bcD*i50O}Q{9_noRg|~?dj%VtKOnyRs$Uzqg z191aWoR^rDX#@iSq0n z?9Sg$WSRPqSeI<}&n1T3!6%Wj@5iw5`*`Btni~G=&;J+4`7g#OQTa>u`{4ZZ(c@s$ zK0y;ySOGD-UTjREKbru{QaS>HjN<2)R%Nn-TZiQ(Twe4p@-saNa3~p{?^V9Nixz@a zykPv~<@lu6-Ng9i$Lrk(xi2Tri3q=RW`BJYOPC;S0Yly%77c727Yj-d1vF!Fuk{Xh z)lMbA69y7*5ufET>P*gXQrxsW+ zz)*MbHZv*eJPEXYE<6g6_M7N%#%mR{#awV3i^PafNv(zyI)&bH?F}2s8_rR(6%!V4SOWlup`TKAb@ee>!9JKPM=&8g#BeYRH9FpFybxBXQI2|g}FGJfJ+ zY-*2hB?o{TVL;Wt_ek;AP5PBqfDR4@Z->_182W z{P@Mc27j6jE*9xG{R$>6_;i=y{qf(c`5w9fa*`rEzX6t!KJ(p1H|>J1pC-2zqWENF zmm=Z5B4u{cY2XYl(PfrInB*~WGWik3@1oRhiMOS|D;acnf-Bs(QCm#wR;@Vf!hOPJ zgjhDCfDj$HcyVLJ=AaTbQ{@vIv14LWWF$=i-BDoC11}V;2V8A`S>_x)vIq44-VB-v z*w-d}$G+Ql?En8j!~ZkCpQ$|cA0|+rrY>tiCeWxkRGPoarxlGU2?7%k#F693RHT24 z-?JsiXlT2PTqZqNb&sSc>$d;O4V@|b6VKSWQb~bUaWn1Cf0+K%`Q&Wc<>mQ>*iEGB zbZ;aYOotBZ{vH3y<0A*L0QVM|#rf*LIsGx(O*-7)r@yyBIzJnBFSKBUSl1e|8lxU* zzFL+YDVVkIuzFWeJ8AbgN&w(4-7zbiaMn{5!JQXu)SELk*CNL+Fro|2v|YO)1l15t zs(0^&EB6DPMyaqvY>=KL>)tEpsn;N5Q#yJj<9}ImL((SqErWN3Q=;tBO~ExTCs9hB z2E$7eN#5wX4<3m^5pdjm#5o>s#eS_Q^P)tm$@SawTqF*1dj_i#)3};JslbLKHXl_N z)Fxzf>FN)EK&Rz&*|6&%Hs-^f{V|+_vL1S;-1K-l$5xiC@}%uDuwHYhmsV?YcOUlk zOYkG5v2+`+UWqpn0aaaqrD3lYdh0*!L`3FAsNKu=Q!vJu?Yc8n|CoYyDo_`r0mPoo z8>XCo$W4>l(==h?2~PoRR*kEe)&IH{1sM41mO#-36`02m#nTX{r*r`Q5rZ2-sE|nA zhnn5T#s#v`52T5|?GNS`%HgS2;R(*|^egNPDzzH_z^W)-Q98~$#YAe)cEZ%vge965AS_am#DK#pjPRr-!^za8>`kksCAUj(Xr*1NW5~e zpypt_eJpD&4_bl_y?G%>^L}=>xAaV>KR6;^aBytqpiHe%!j;&MzI_>Sx7O%F%D*8s zSN}cS^<{iiK)=Ji`FpO#^zY!_|D)qeRNAtgmH)m;qC|mq^j(|hL`7uBz+ULUj37gj zksdbnU+LSVo35riSX_4z{UX=%n&}7s0{WuZYoSfwAP`8aKN9P@%e=~1`~1ASL-z%# zw>DO&ixr}c9%4InGc*_y42bdEk)ZdG7-mTu0bD@_vGAr*NcFoMW;@r?@LUhRI zCUJgHb`O?M3!w)|CPu~ej%fddw20lod?Ufp8Dmt0PbnA0J%KE^2~AIcnKP()025V> zG>noSM3$5Btmc$GZoyP^v1@Poz0FD(6YSTH@aD0}BXva?LphAiSz9f&Y(aDAzBnUh z?d2m``~{z;{}kZJ>a^wYI?ry(V9hIoh;|EFc0*-#*`$T0DRQ1;WsqInG;YPS+I4{g zJGpKk%%Sdc5xBa$Q^_I~(F97eqDO7AN3EN0u)PNBAb+n+ zWBTxQx^;O9o0`=g+Zrt_{lP!sgWZHW?8bLYS$;1a@&7w9rD9|Ge;Gb?sEjFoF9-6v z#!2)t{DMHZ2@0W*fCx;62d#;jouz`R5Y(t{BT=$N4yr^^o$ON8d{PQ=!O zX17^CrdM~7D-;ZrC!||<+FEOxI_WI3CA<35va%4v>gc zEX-@h8esj=a4szW7x{0g$hwoWRQG$yK{@3mqd-jYiVofJE!Wok1* znV7Gm&Ssq#hFuvj1sRyHg(6PFA5U*Q8Rx>-blOs=lb`qa{zFy&n4xY;sd$fE+<3EI z##W$P9M{B3c3Si9gw^jlPU-JqD~Cye;wr=XkV7BSv#6}DrsXWFJ3eUNrc%7{=^sP> zrp)BWKA9<}^R9g!0q7yWlh;gr_TEOD|#BmGq<@IV;ueg+D2}cjpp+dPf&Q(36sFU&K8}hA85U61faW&{ zlB`9HUl-WWCG|<1XANN3JVAkRYvr5U4q6;!G*MTdSUt*Mi=z_y3B1A9j-@aK{lNvx zK%p23>M&=KTCgR!Ee8c?DAO2_R?B zkaqr6^BSP!8dHXxj%N1l+V$_%vzHjqvu7p@%Nl6;>y*S}M!B=pz=aqUV#`;h%M0rU zHfcog>kv3UZAEB*g7Er@t6CF8kHDmKTjO@rejA^ULqn!`LwrEwOVmHx^;g|5PHm#B zZ+jjWgjJ!043F+&#_;D*mz%Q60=L9Ove|$gU&~As5^uz@2-BfQ!bW)Khn}G+Wyjw- z19qI#oB(RSNydn0t~;tAmK!P-d{b-@@E5|cdgOS#!>%#Rj6ynkMvaW@37E>@hJP^8 z2zk8VXx|>#R^JCcWdBCy{0nPmYFOxN55#^-rlqobe0#L6)bi?E?SPymF*a5oDDeSd zO0gx?#KMoOd&G(2O@*W)HgX6y_aa6iMCl^~`{@UR`nMQE`>n_{_aY5nA}vqU8mt8H z`oa=g0SyiLd~BxAj2~l$zRSDHxvDs;I4>+M$W`HbJ|g&P+$!U7-PHX4RAcR0szJ*( ze-417=bO2q{492SWrqDK+L3#ChUHtz*@MP)e^%@>_&#Yk^1|tv@j4%3T)diEX zATx4K*hcO`sY$jk#jN5WD<=C3nvuVsRh||qDHnc~;Kf59zr0;c7VkVSUPD%NnnJC_ zl3F^#f_rDu8l}l8qcAz0FFa)EAt32IUy_JLIhU_J^l~FRH&6-ivSpG2PRqzDdMWft>Zc(c)#tb%wgmWN%>IOPm zZi-noqS!^Ftb81pRcQi`X#UhWK70hy4tGW1mz|+vI8c*h@ zfFGJtW3r>qV>1Z0r|L>7I3un^gcep$AAWfZHRvB|E*kktY$qQP_$YG60C@X~tTQjB3%@`uz!qxtxF+LE!+=nrS^07hn` zEgAp!h|r03h7B!$#OZW#ACD+M;-5J!W+{h|6I;5cNnE(Y863%1(oH}_FTW})8zYb$7czP zg~Szk1+_NTm6SJ0MS_|oSz%e(S~P-&SFp;!k?uFayytV$8HPwuyELSXOs^27XvK-D zOx-Dl!P|28DK6iX>p#Yb%3`A&CG0X2S43FjN%IB}q(!hC$fG}yl1y9W&W&I@KTg6@ zK^kpH8=yFuP+vI^+59|3%Zqnb5lTDAykf z9S#X`3N(X^SpdMyWQGOQRjhiwlj!0W-yD<3aEj^&X%=?`6lCy~?`&WSWt z?U~EKFcCG_RJ(Qp7j=$I%H8t)Z@6VjA#>1f@EYiS8MRHZphp zMA_5`znM=pzUpBPO)pXGYpQ6gkine{6u_o!P@Q+NKJ}k!_X7u|qfpAyIJb$_#3@wJ z<1SE2Edkfk9C!0t%}8Yio09^F`YGzpaJHGk*-ffsn85@)%4@`;Fv^8q(-Wk7r=Q8p zT&hD`5(f?M{gfzGbbwh8(}G#|#fDuk7v1W)5H9wkorE0ZZjL0Q1=NRGY>zwgfm81DdoaVwNH;or{{eSyybt)m<=zXoA^RALYG-2t zouH|L*BLvmm9cdMmn+KGopyR@4*=&0&4g|FLoreZOhRmh=)R0bg~ zT2(8V_q7~42-zvb)+y959OAv!V$u(O3)%Es0M@CRFmG{5sovIq4%8Ahjk#*5w{+)+ zMWQoJI_r$HxL5km1#6(e@{lK3Udc~n0@g`g$s?VrnQJ$!oPnb?IHh-1qA`Rz$)Ai< z6w$-MJW-gKNvOhL+XMbE7&mFt`x1KY>k4(!KbbpZ`>`K@1J<(#vVbjx@Z@(6Q}MF# zMnbr-f55(cTa^q4+#)=s+ThMaV~E`B8V=|W_fZWDwiso8tNMTNse)RNBGi=gVwgg% zbOg8>mbRN%7^Um-7oj4=6`$|(K7!+t^90a{$18Z>}<#!bm%ZEFQ{X(yBZMc>lCz0f1I2w9Sq zuGh<9<=AO&g6BZte6hn>Qmvv;Rt)*cJfTr2=~EnGD8P$v3R|&1RCl&7)b+`=QGapi zPbLg_pxm`+HZurtFZ;wZ=`Vk*do~$wB zxoW&=j0OTbQ=Q%S8XJ%~qoa3Ea|au5o}_(P;=!y-AjFrERh%8la!z6Fn@lR?^E~H12D?8#ht=1F;7@o4$Q8GDj;sSC%Jfn01xgL&%F2 zwG1|5ikb^qHv&9hT8w83+yv&BQXOQyMVJSBL(Ky~p)gU3#%|blG?IR9rP^zUbs7rOA0X52Ao=GRt@C&zlyjNLv-} z9?*x{y(`509qhCV*B47f2hLrGl^<@SuRGR!KwHei?!CM10Tq*YDIoBNyRuO*>3FU? zHjipIE#B~y3FSfOsMfj~F9PNr*H?0oHyYB^G(YyNh{SxcE(Y-`x5jFMKb~HO*m+R% zrq|ic4fzJ#USpTm;X7K+E%xsT_3VHKe?*uc4-FsILUH;kL>_okY(w`VU*8+l>o>Jm ziU#?2^`>arnsl#)*R&nf_%>A+qwl%o{l(u)M?DK1^mf260_oteV3#E_>6Y4!_hhVD zM8AI6MM2V*^_M^sQ0dmHu11fy^kOqXqzpr?K$`}BKWG`=Es(9&S@K@)ZjA{lj3ea7_MBP zk(|hBFRjHVMN!sNUkrB;(cTP)T97M$0Dtc&UXSec<+q?y>5=)}S~{Z@ua;1xt@=T5 zI7{`Z=z_X*no8s>mY;>BvEXK%b`a6(DTS6t&b!vf_z#HM{Uoy_5fiB(zpkF{})ruka$iX*~pq1ZxD?q68dIo zIZSVls9kFGsTwvr4{T_LidcWtt$u{kJlW7moRaH6+A5hW&;;2O#$oKyEN8kx`LmG)Wfq4ykh+q{I3|RfVpkR&QH_x;t41Uw z`P+tft^E2B$domKT@|nNW`EHwyj>&}K;eDpe z1bNOh=fvIfk`&B61+S8ND<(KC%>y&?>opCnY*r5M+!UrWKxv0_QvTlJc>X#AaI^xo zaRXL}t5Ej_Z$y*|w*$6D+A?Lw-CO-$itm^{2Ct82-<0IW)0KMNvJHgBrdsIR0v~=H z?n6^}l{D``Me90`^o|q!olsF?UX3YSq^6Vu>Ijm>>PaZI8G@<^NGw{Cx&%|PwYrfw zR!gX_%AR=L3BFsf8LxI|K^J}deh0ZdV?$3r--FEX`#INxsOG6_=!v)DI>0q|BxT)z z-G6kzA01M?rba+G_mwNMQD1mbVbNTWmBi*{s_v_Ft9m2Avg!^78(QFu&n6mbRJ2bA zv!b;%yo{g*9l2)>tsZJOOp}U~8VUH`}$ z8p_}t*XIOehezolNa-a2x0BS})Y9}&*TPgua{Ewn-=wVrmJUeU39EKx+%w%=ixQWK zDLpwaNJs65#6o7Ln7~~X+p_o2BR1g~VCfxLzxA{HlWAI6^H;`juI=&r1jQrUv_q0Z z1Ja-tjdktrrP>GOC*#p?*xfQU5MqjMsBe!9lh(u8)w$e@Z|>aUHI5o;MGw*|Myiz3 z-f0;pHg~Q#%*Kx8MxH%AluVXjG2C$)WL-K63@Q`#y9_k_+}eR(x4~dp7oV-ek0H>I zgy8p#i4GN{>#v=pFYUQT(g&b$OeTy-X_#FDgNF8XyfGY6R!>inYn8IR2RDa&O!(6< znXs{W!bkP|s_YI*Yx%4stI`=ZO45IK6rBs`g7sP40ic}GZ58s?Mc$&i`kq_tfci>N zIHrC0H+Qpam1bNa=(`SRKjixBTtm&e`j9porEci!zdlg1RI0Jw#b(_Tb@RQK1Zxr_ z%7SUeH6=TrXt3J@js`4iDD0=IoHhK~I7^W8^Rcp~Yaf>2wVe|Hh1bUpX9ATD#moByY57-f2Ef1TP^lBi&p5_s7WGG9|0T}dlfxOx zXvScJO1Cnq`c`~{Dp;{;l<-KkCDE+pmexJkd}zCgE{eF=)K``-qC~IT6GcRog_)!X z?fK^F8UDz$(zFUrwuR$qro5>qqn>+Z%<5>;_*3pZ8QM|yv9CAtrAx;($>4l^_$_-L z*&?(77!-=zvnCVW&kUcZMb6;2!83si518Y%R*A3JZ8Is|kUCMu`!vxDgaWjs7^0j( ziTaS4HhQ)ldR=r)_7vYFUr%THE}cPF{0H45FJ5MQW^+W>P+eEX2kLp3zzFe*-pFVA zdDZRybv?H|>`9f$AKVjFWJ=wegO7hOOIYCtd?Vj{EYLT*^gl35|HQ`R=ti+ADm{jyQE7K@kdjuqJhWVSks>b^ zxha88-h3s;%3_5b1TqFCPTxVjvuB5U>v=HyZ$?JSk+&I%)M7KE*wOg<)1-Iy)8-K! z^XpIt|0ibmk9RtMmlUd7#Ap3Q!q9N4atQy)TmrhrFhfx1DAN`^vq@Q_SRl|V z#lU<~n67$mT)NvHh`%als+G-)x1`Y%4Bp*6Un5Ri9h=_Db zA-AdP!f>f0m@~>7X#uBM?diI@)Egjuz@jXKvm zJo+==juc9_<;CqeRaU9_Mz@;3e=E4=6TK+c`|uu#pIqhSyNm`G(X)&)B`8q0RBv#> z`gGlw(Q=1Xmf55VHj%C#^1lpc>LY8kfA@|rlC1EA<1#`iuyNO z(=;irt{_&K=i4)^x%;U(Xv<)+o=dczC5H3W~+e|f~{*ucxj@{Yi-cw^MqYr3fN zF5D+~!wd$#al?UfMnz(@K#wn`_5na@rRr8XqN@&M&FGEC@`+OEv}sI1hw>Up0qAWf zL#e4~&oM;TVfjRE+10B_gFlLEP9?Q-dARr3xi6nQqnw>k-S;~b z;!0s2VS4}W8b&pGuK=7im+t(`nz@FnT#VD|!)eQNp-W6)@>aA+j~K*H{$G`y2|QHY z|Hmy+CR@#jWY4~)lr1qBJB_RfHJFfP<}pK5(#ZZGSqcpyS&}01LnTWk5fzmXMGHkJ zTP6L^B+uj;lmB_W<~4=${+v0>z31M!-_O@o-O9GyW)j_mjx}!0@br_LE-7SIuPP84 z;5=O(U*g_um0tyG|61N@d9lEuOeiRd+#NY^{nd5;-CVlw&Ap7J?qwM^?E29wvS}2d zbzar4Fz&RSR(-|s!Z6+za&Z zY#D<5q_JUktIzvL0)yq_kLWG6DO{ri=?c!y!f(Dk%G{8)k`Gym%j#!OgXVDD3;$&v@qy#ISJfp=Vm>pls@9-mapVQChAHHd-x+OGx)(*Yr zC1qDUTZ6mM(b_hi!TuFF2k#8uI2;kD70AQ&di$L*4P*Y-@p`jdm%_c3f)XhYD^6M8&#Y$ZpzQMcR|6nsH>b=*R_Von!$BTRj7yGCXokoAQ z&ANvx0-Epw`QIEPgI(^cS2f(Y85yV@ygI{ewyv5Frng)e}KCZF7JbR(&W618_dcEh(#+^zZFY;o<815<5sOHQdeax9_!PyM&;{P zkBa5xymca0#)c#tke@3KNEM8a_mT&1gm;p&&JlMGH(cL(b)BckgMQ^9&vRwj!~3@l zY?L5}=Jzr080OGKb|y`ee(+`flQg|!lo6>=H)X4`$Gz~hLmu2a%kYW_Uu8x09Pa0J zKZ`E$BKJ=2GPj_3l*TEcZ*uYRr<*J^#5pILTT;k_cgto1ZL-%slyc16J~OH-(RgDA z%;EjEnoUkZ&acS{Q8`{i6T5^nywgqQI5bDIymoa7CSZG|WWVk>GM9)zy*bNih|QIm z%0+(Nnc*a_xo;$=!HQYaapLms>J1ToyjtFByY`C2H1wT#178#4+|{H0BBqtCdd$L% z_3Hc60j@{t9~MjM@LBalR&6@>B;9?r<7J~F+WXyYu*y3?px*=8MAK@EA+jRX8{CG?GI-< z54?Dc9CAh>QTAvyOEm0^+x;r2BWX|{3$Y7)L5l*qVE*y0`7J>l2wCmW zL1?|a`pJ-l{fb_N;R(Z9UMiSj6pQjOvQ^%DvhIJF!+Th7jO2~1f1N+(-TyCFYQZYw z4)>7caf^Ki_KJ^Zx2JUb z&$3zJy!*+rCV4%jqwyuNY3j1ZEiltS0xTzd+=itTb;IPYpaf?8Y+RSdVdpacB(bVQ zC(JupLfFp8y43%PMj2}T|VS@%LVp>hv4Y!RPMF?pp8U_$xCJ)S zQx!69>bphNTIb9yn*_yfj{N%bY)t{L1cs8<8|!f$;UQ*}IN=2<6lA;x^(`8t?;+ST zh)z4qeYYgZkIy{$4x28O-pugO&gauRh3;lti9)9Pvw+^)0!h~%m&8Q!AKX%urEMnl z?yEz?g#ODn$UM`+Q#$Q!6|zsq_`dLO5YK-6bJM6ya>}H+vnW^h?o$z;V&wvuM$dR& zeEq;uUUh$XR`TWeC$$c&Jjau2it3#%J-y}Qm>nW*s?En?R&6w@sDXMEr#8~$=b(gk zwDC3)NtAP;M2BW_lL^5ShpK$D%@|BnD{=!Tq)o(5@z3i7Z){} zGr}Exom_qDO{kAVkZ*MbLNHE666Kina#D{&>Jy%~w7yX$oj;cYCd^p9zy z8*+wgSEcj$4{WxKmCF(5o7U4jqwEvO&dm1H#7z}%VXAbW&W24v-tS6N3}qrm1OnE)fUkoE8yMMn9S$?IswS88tQWm4#Oid#ckgr6 zRtHm!mfNl-`d>O*1~d7%;~n+{Rph6BBy^95zqI{K((E!iFQ+h*C3EsbxNo_aRm5gj zKYug($r*Q#W9`p%Bf{bi6;IY0v`pB^^qu)gbg9QHQ7 zWBj(a1YSu)~2RK8Pi#C>{DMlrqFb9e_RehEHyI{n?e3vL_}L>kYJC z_ly$$)zFi*SFyNrnOt(B*7E$??s67EO%DgoZL2XNk8iVx~X_)o++4oaK1M|ou73vA0K^503j@uuVmLcHH4ya-kOIDfM%5%(E z+Xpt~#7y2!KB&)PoyCA+$~DXqxPxxALy!g-O?<9+9KTk4Pgq4AIdUkl`1<1#j^cJg zgU3`0hkHj_jxV>`Y~%LAZl^3o0}`Sm@iw7kwff{M%VwtN)|~!p{AsfA6vB5UolF~d zHWS%*uBDt<9y!9v2Xe|au&1j&iR1HXCdyCjxSgG*L{wmTD4(NQ=mFjpa~xooc6kju z`~+d{j7$h-;HAB04H!Zscu^hZffL#9!p$)9>sRI|Yovm)g@F>ZnosF2EgkU3ln0bR zTA}|+E(tt)!SG)-bEJi_0m{l+(cAz^pi}`9=~n?y&;2eG;d9{M6nj>BHGn(KA2n|O zt}$=FPq!j`p&kQ8>cirSzkU0c08%8{^Qyqi-w2LoO8)^E7;;I1;HQ6B$u0nNaX2CY zSmfi)F`m94zL8>#zu;8|{aBui@RzRKBlP1&mfFxEC@%cjl?NBs`cr^nm){>;$g?rhKr$AO&6qV_Wbn^}5tfFBry^e1`%du2~o zs$~dN;S_#%iwwA_QvmMjh%Qo?0?rR~6liyN5Xmej8(*V9ym*T`xAhHih-v$7U}8=dfXi2i*aAB!xM(Xekg*ix@r|ymDw*{*s0?dlVys2e)z62u1 z+k3esbJE=-P5S$&KdFp+2H7_2e=}OKDrf( z9-207?6$@f4m4B+9E*e((Y89!q?zH|mz_vM>kp*HGXldO0Hg#!EtFhRuOm$u8e~a9 z5(roy7m$Kh+zjW6@zw{&20u?1f2uP&boD}$#Zy)4o&T;vyBoqFiF2t;*g=|1=)PxB z8eM3Mp=l_obbc?I^xyLz?4Y1YDWPa+nm;O<$Cn;@ane616`J9OO2r=rZr{I_Kizyc zP#^^WCdIEp*()rRT+*YZK>V@^Zs=ht32x>Kwe zab)@ZEffz;VM4{XA6e421^h~`ji5r%)B{wZu#hD}f3$y@L0JV9f3g{-RK!A?vBUA}${YF(vO4)@`6f1 z-A|}e#LN{)(eXloDnX4Vs7eH|<@{r#LodP@Nz--$Dg_Par%DCpu2>2jUnqy~|J?eZ zBG4FVsz_A+ibdwv>mLp>P!(t}E>$JGaK$R~;fb{O3($y1ssQQo|5M;^JqC?7qe|hg zu0ZOqeFcp?qVn&Qu7FQJ4hcFi&|nR!*j)MF#b}QO^lN%5)4p*D^H+B){n8%VPUzi! zDihoGcP71a6!ab`l^hK&*dYrVYzJ0)#}xVrp!e;lI!+x+bfCN0KXwUAPU9@#l7@0& QuEJmfE|#`Dqx|px0L@K;Y5)KL diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d3ca171f..1af9e093 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists 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 From 385805eba0d2d5462bd8cbedbf06981b2ecf61fe Mon Sep 17 00:00:00 2001 From: Erik Eelde Date: Sun, 3 Dec 2023 22:43:27 +0100 Subject: [PATCH 13/15] Instrumentation test scaffolding --- gradle/libs.versions.toml | 1 + toggles-app/build.gradle.kts | 10 ++++++-- .../toggles/ExampleInstrumentationTest.kt | 17 +++++++++++++ .../toggles/ExampleInstrumentationTest2.kt | 24 +++++++++++++++++++ 4 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 toggles-app/src/androidTest/java/se/eelde/toggles/ExampleInstrumentationTest.kt create mode 100644 toggles-app/src/androidTest/java/se/eelde/toggles/ExampleInstrumentationTest2.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e4cd6026..3e4cdd61 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -59,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" } diff --git a/toggles-app/build.gradle.kts b/toggles-app/build.gradle.kts index daaa3a88..a1165b56 100644 --- a/toggles-app/build.gradle.kts +++ b/toggles-app/build.gradle.kts @@ -77,6 +77,8 @@ android { manifestPlaceholders["togglesPermission"] = togglesPermission buildConfigField("String", "CONFIG_AUTHORITY", "\"$togglesAuthority\"") + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } packaging { resources { @@ -168,8 +170,6 @@ 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) @@ -182,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() + } + } +} From ae036c4c1e1ec6bc076dd109254b50b6c8130765 Mon Sep 17 00:00:00 2001 From: Erik Eelde Date: Mon, 4 Dec 2023 00:29:05 +0100 Subject: [PATCH 14/15] Overflow for action + scope view readded --- .../configurations/ConfigurationsEntry.kt | 101 ++++++--- .../java/se/eelde/toggles/MainActivity.kt | 10 + .../eelde/toggles/dialogs/scope/ScopeView.kt | 196 ++++++++++++++---- .../toggles/dialogs/scope/ScopeViewModel.kt | 20 +- toggles-app/src/main/res/values/strings.xml | 3 +- 5 files changed, 257 insertions(+), 73 deletions(-) diff --git a/modules/configurations/src/main/java/se/eelde/toggles/configurations/ConfigurationsEntry.kt b/modules/configurations/src/main/java/se/eelde/toggles/configurations/ConfigurationsEntry.kt index a2837dcb..b1e5eef6 100644 --- a/modules/configurations/src/main/java/se/eelde/toggles/configurations/ConfigurationsEntry.kt +++ b/modules/configurations/src/main/java/se/eelde/toggles/configurations/ConfigurationsEntry.kt @@ -9,9 +9,13 @@ 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 @@ -20,6 +24,10 @@ 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 @@ -35,6 +43,7 @@ fun NavGraphBuilder.configurationsNavigations( navigateToIntegerConfiguration: (scopeId: Long, configurationId: Long) -> Unit, navigateToStringConfiguration: (scopeId: Long, configurationId: Long) -> Unit, navigateToEnumConfiguration: (scopeId: Long, configurationId: Long) -> Unit, + navigateToScopeView: (Long) -> Unit, back: () -> Unit, ) { composable( @@ -86,40 +95,70 @@ fun NavGraphBuilder.configurationsNavigations( } }, actions = { - IconButton(onClick = { - viewModel.restartApplication(uiState.value.application!!) - }) { - Icon( - imageVector = Icons.Outlined.Cyclone, - contentDescription = null + 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 + ) + } ) - } - if (!searching) { - IconButton(onClick = { - launcher.launch( - Intent( - Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - Uri.fromParts( - "package", - uiState.value.application!!.packageName, - null + + DropdownMenuItem( + text = { Text("Appinfo") }, + 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 - ) - } + }, + 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 + ) } } ) diff --git a/toggles-app/src/main/java/se/eelde/toggles/MainActivity.kt b/toggles-app/src/main/java/se/eelde/toggles/MainActivity.kt index fd7215aa..0c794b15 100644 --- a/toggles-app/src/main/java/se/eelde/toggles/MainActivity.kt +++ b/toggles-app/src/main/java/se/eelde/toggles/MainActivity.kt @@ -25,6 +25,7 @@ import se.eelde.toggles.applications.applicationNavigations import se.eelde.toggles.booleanconfiguration.BooleanValueView import se.eelde.toggles.composetheme.TogglesTheme import se.eelde.toggles.configurations.configurationsNavigations +import se.eelde.toggles.dialogs.scope.ScopeValueView import se.eelde.toggles.enumconfiguration.EnumValueView import se.eelde.toggles.help.HelpView import se.eelde.toggles.integerconfiguration.IntegerValueView @@ -63,6 +64,9 @@ fun Navigation( }, navigateToEnumConfiguration = { scopeId: Long, configurationId: Long -> navController.navigate("configuration/$configurationId/$scopeId/enum") + }, + navigateToScopeView = { applicationId: Long -> + navController.navigate("scopes/$applicationId") } ) { navController.popBackStack() } composable( @@ -74,6 +78,12 @@ fun Navigation( ) { BooleanValueView { navController.popBackStack() } } + composable( + "scopes/{applicationId}", + arguments = listOf(navArgument("applicationId") { type = NavType.LongType }) + ) { + ScopeValueView { navController.popBackStack() } + } composable( "configuration/{configurationId}/{scopeId}/integer", arguments = listOf( 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 c334ebb5..823de7bd 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,65 +1,187 @@ 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 { - item { - ListItem( - modifier = Modifier.clickable { - scope.launch { - viewModel.selectScope(it) - navController.popBackStack() - } +@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/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 From 6784b0cb37c5a0842c6945d63e9152ea051e4d32 Mon Sep 17 00:00:00 2001 From: Erik Eelde Date: Mon, 4 Dec 2023 01:17:41 +0100 Subject: [PATCH 15/15] Tidy up value views --- .../booleanconfiguration/BooleanValueView.kt | 5 ++-- .../configurations/ConfigurationsEntry.kt | 2 +- modules/enumconfiguration/build.gradle.kts | 1 + .../enumconfiguration/EnumValueView.kt | 24 ++++++++++++++----- .../enumconfiguration/EnumValueViewModel.kt | 22 +++++++++++++---- .../integerconfiguration/IntegerValueView.kt | 17 +++++++++---- .../stringconfiguration/StringValueView.kt | 4 ++-- .../eelde/toggles/dialogs/scope/ScopeView.kt | 19 +++++++++------ 8 files changed, 68 insertions(+), 26 deletions(-) diff --git a/modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration/BooleanValueView.kt b/modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration/BooleanValueView.kt index 11fd972b..a94a76e9 100644 --- a/modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration/BooleanValueView.kt +++ b/modules/booleanconfiguration/src/main/java/se/eelde/toggles/booleanconfiguration/BooleanValueView.kt @@ -26,6 +26,7 @@ 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( @@ -37,7 +38,7 @@ fun BooleanValueView( Scaffold( topBar = { TopAppBar( - title = { Text("") }, + title = { Text("Boolean configuration") }, navigationIcon = { IconButton(onClick = { back() }) { @@ -73,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/modules/configurations/src/main/java/se/eelde/toggles/configurations/ConfigurationsEntry.kt b/modules/configurations/src/main/java/se/eelde/toggles/configurations/ConfigurationsEntry.kt index b1e5eef6..7665b9a7 100644 --- a/modules/configurations/src/main/java/se/eelde/toggles/configurations/ConfigurationsEntry.kt +++ b/modules/configurations/src/main/java/se/eelde/toggles/configurations/ConfigurationsEntry.kt @@ -36,7 +36,7 @@ import androidx.navigation.NavType import androidx.navigation.compose.composable import androidx.navigation.navArgument -@Suppress("LongMethod") +@Suppress("LongMethod", "LongParameterList") @OptIn(ExperimentalMaterial3Api::class) fun NavGraphBuilder.configurationsNavigations( navigateToBooleanConfiguration: (scopeId: Long, configurationId: Long) -> Unit, diff --git a/modules/enumconfiguration/build.gradle.kts b/modules/enumconfiguration/build.gradle.kts index 133f4f90..112937a3 100644 --- a/modules/enumconfiguration/build.gradle.kts +++ b/modules/enumconfiguration/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { 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) diff --git a/modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration/EnumValueView.kt b/modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration/EnumValueView.kt index 30300dad..04df1c05 100644 --- a/modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration/EnumValueView.kt +++ b/modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration/EnumValueView.kt @@ -1,14 +1,15 @@ package se.eelde.toggles.enumconfiguration -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.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 @@ -39,7 +40,7 @@ fun EnumValueView( Scaffold( topBar = { TopAppBar( - title = { Text("") }, + title = { Text("Enum configuration") }, navigationIcon = { IconButton(onClick = { back() }) { @@ -72,7 +73,7 @@ internal fun EnumValueView( ) { val scope = rememberCoroutineScope() - Surface(modifier = modifier) { + Surface(modifier = modifier.padding(16.dp)) { Column { Text( modifier = Modifier.padding(8.dp), @@ -82,14 +83,25 @@ internal fun EnumValueView( LazyColumn { state.configurationValues.forEach { wrenchPredefinedConfigurationValue -> item { + val selected = + wrenchPredefinedConfigurationValue.value == state.selectedConfigurationValue?.value ListItem( - modifier = Modifier.clickable { + modifier = Modifier.selectable( + selected = selected + ) { scope.launch { setEnumValue(wrenchPredefinedConfigurationValue.value.toString()) - popBackStack() } }, - headlineContent = { Text(text = wrenchPredefinedConfigurationValue.value.toString()) } + headlineContent = { Text(text = wrenchPredefinedConfigurationValue.value.toString()) }, + leadingContent = { + if (selected) { + Icon( + imageVector = Icons.Filled.Link, + contentDescription = null + ) + } + } ) } } diff --git a/modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration/EnumValueViewModel.kt b/modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration/EnumValueViewModel.kt index 0be8c600..186403a9 100644 --- a/modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration/EnumValueViewModel.kt +++ b/modules/enumconfiguration/src/main/java/se/eelde/toggles/enumconfiguration/EnumValueViewModel.kt @@ -24,19 +24,23 @@ import javax.inject.Inject 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 @@ -74,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)) } } } @@ -84,18 +90,26 @@ 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) + } } } diff --git a/modules/integerconfiguration/src/main/java/se/eelde/toggles/integerconfiguration/IntegerValueView.kt b/modules/integerconfiguration/src/main/java/se/eelde/toggles/integerconfiguration/IntegerValueView.kt index c40524f0..aca18cba 100644 --- a/modules/integerconfiguration/src/main/java/se/eelde/toggles/integerconfiguration/IntegerValueView.kt +++ b/modules/integerconfiguration/src/main/java/se/eelde/toggles/integerconfiguration/IntegerValueView.kt @@ -33,7 +33,12 @@ import kotlinx.coroutines.launch @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 = {}, @@ -52,7 +57,7 @@ fun IntegerValueView( Scaffold( topBar = { TopAppBar( - title = { Text("") }, + title = { Text("Integer configuration") }, navigationIcon = { IconButton(onClick = { back() }) { @@ -88,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), @@ -100,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/modules/stringconfiguration/src/main/java/se/eelde/toggles/stringconfiguration/StringValueView.kt b/modules/stringconfiguration/src/main/java/se/eelde/toggles/stringconfiguration/StringValueView.kt index 79bb5b27..1fc72214 100644 --- a/modules/stringconfiguration/src/main/java/se/eelde/toggles/stringconfiguration/StringValueView.kt +++ b/modules/stringconfiguration/src/main/java/se/eelde/toggles/stringconfiguration/StringValueView.kt @@ -53,7 +53,7 @@ fun StringValueView( Scaffold( topBar = { TopAppBar( - title = { Text("") }, + title = { Text("String configuration") }, navigationIcon = { IconButton(onClick = { back() }) { @@ -89,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/scope/ScopeView.kt b/toggles-app/src/main/java/se/eelde/toggles/dialogs/scope/ScopeView.kt index 823de7bd..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 @@ -66,6 +66,7 @@ fun ScopeValueView( } } +@Suppress("LongMethod") @Composable internal fun ScopeValueView( viewState: ViewState, @@ -108,16 +109,17 @@ internal fun ScopeValueView( val showDeleteScopeView = rememberSaveable { mutableStateOf(false) } Row { - Button(modifier = Modifier.padding(16.dp), + Button( + modifier = Modifier.padding(16.dp), onClick = { showAddScopeView.value = true } ) { Text("Add") } - OutlinedButton(modifier = Modifier.padding(16.dp), + OutlinedButton( + modifier = Modifier.padding(16.dp), enabled = viewState.scopes.size > 1, onClick = { showDeleteScopeView.value = true } - ) - { + ) { Text("Delete") } } @@ -128,7 +130,8 @@ internal fun ScopeValueView( createScope(it) showAddScopeView.value = false }, - dismiss = { showAddScopeView.value = false }) + dismiss = { showAddScopeView.value = false } + ) } if (showDeleteScopeView.value) { DeleteScopeView( @@ -159,8 +162,10 @@ fun AddScopeView( style = MaterialTheme.typography.titleMedium, text = stringResource(id = R.string.scope_add_information) ) - TextField(value = scopeName.value, - onValueChange = { scopeName.value = it }) + TextField( + value = scopeName.value, + onValueChange = { scopeName.value = it } + ) Button(onClick = { addScope(scopeName.value) }) { Text("Add") }