diff --git a/.github/workflows/manual-release-core.yml b/.github/workflows/manual-release-core.yml new file mode 100644 index 00000000..02569e98 --- /dev/null +++ b/.github/workflows/manual-release-core.yml @@ -0,0 +1,44 @@ +name: Manual release workflow + +on: + workflow_dispatch: + +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - name: Check out code + uses: actions/checkout@v2 + + - name: + run: | + echo "${{secrets.SECRING_GPG_B64}}" > ~/.gradle/secring.gpg.b64 + base64 -d ~/.gradle/secring.gpg.b64 > ~/.gradle/secring.gpg + touch ~/.gradle/gradle.properties + echo "signing.keyId=${{secrets.SIGNING_KEYID}}" >> ~/.gradle/gradle.properties + echo "signing.password=${{secrets.SIGNING_PASSWORD}}" >> ~/.gradle/gradle.properties + echo "signing.secretKeyRingFile=~/.gradle/secring.gpg" >> ~/.gradle/gradle.properties + echo "mavenCentralRepositoryUsername=${{secrets.MAVEN_CENTRAL_REPOSITORY_USERNAME}}" >> ~/.gradle/gradle.properties + echo "mavenCentralRepositoryPassword=${{secrets.MAVEN_CENTRAL_REPOSITORY_PASSWORD}}" >> ~/.gradle/gradle.properties + + - name: Check out java + uses: actions/setup-java@v1 + with: + java-version: 11 + + - name: Run Detekt + run: ./gradlew :toggles-core:detekt + + - name: Run Android Lint + run: ./gradlew :toggles-core:lint + + - name: Run Android Unit test + run: ./gradlew :toggles-core:testDebugUnitTest + + - name: Publish core library + run: ./gradlew :toggles-core:publish --no-daemon --no-parallel diff --git a/.github/workflows/manual-release-flow.yml b/.github/workflows/manual-release-flow.yml new file mode 100644 index 00000000..4014130f --- /dev/null +++ b/.github/workflows/manual-release-flow.yml @@ -0,0 +1,44 @@ +name: Manual release workflow + +on: + workflow_dispatch: + +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - name: Check out code + uses: actions/checkout@v2 + + - name: + run: | + echo "${{secrets.SECRING_GPG_B64}}" > ~/.gradle/secring.gpg.b64 + base64 -d ~/.gradle/secring.gpg.b64 > ~/.gradle/secring.gpg + touch ~/.gradle/gradle.properties + echo "signing.keyId=${{secrets.SIGNING_KEYID}}" >> ~/.gradle/gradle.properties + echo "signing.password=${{secrets.SIGNING_PASSWORD}}" >> ~/.gradle/gradle.properties + echo "signing.secretKeyRingFile=~/.gradle/secring.gpg" >> ~/.gradle/gradle.properties + echo "mavenCentralRepositoryUsername=${{secrets.MAVEN_CENTRAL_REPOSITORY_USERNAME}}" >> ~/.gradle/gradle.properties + echo "mavenCentralRepositoryPassword=${{secrets.MAVEN_CENTRAL_REPOSITORY_PASSWORD}}" >> ~/.gradle/gradle.properties + + - name: Check out java + uses: actions/setup-java@v1 + with: + java-version: 11 + + - name: Run Detekt + run: ./gradlew :toggles-flow:detekt + + - name: Run Android Lint + run: ./gradlew :toggles-flow:lint + + - name: Run Android Unit test + run: ./gradlew :toggles-flow:testDebugUnitTest + + - name: Publish flow library + run: ./gradlew :toggles-flow:publish --no-daemon --no-parallel diff --git a/.github/workflows/manual-release-prefs.yml b/.github/workflows/manual-release-prefs.yml new file mode 100644 index 00000000..22e42d54 --- /dev/null +++ b/.github/workflows/manual-release-prefs.yml @@ -0,0 +1,44 @@ +name: Manual release workflow + +on: + workflow_dispatch: + +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - name: Check out code + uses: actions/checkout@v2 + + - name: + run: | + echo "${{secrets.SECRING_GPG_B64}}" > ~/.gradle/secring.gpg.b64 + base64 -d ~/.gradle/secring.gpg.b64 > ~/.gradle/secring.gpg + touch ~/.gradle/gradle.properties + echo "signing.keyId=${{secrets.SIGNING_KEYID}}" >> ~/.gradle/gradle.properties + echo "signing.password=${{secrets.SIGNING_PASSWORD}}" >> ~/.gradle/gradle.properties + echo "signing.secretKeyRingFile=~/.gradle/secring.gpg" >> ~/.gradle/gradle.properties + echo "mavenCentralRepositoryUsername=${{secrets.MAVEN_CENTRAL_REPOSITORY_USERNAME}}" >> ~/.gradle/gradle.properties + echo "mavenCentralRepositoryPassword=${{secrets.MAVEN_CENTRAL_REPOSITORY_PASSWORD}}" >> ~/.gradle/gradle.properties + + - name: Check out java + uses: actions/setup-java@v1 + with: + java-version: 11 + + - name: Run Detekt + run: ./gradlew :toggles-prefs:detekt + + - name: Run Android Lint + run: ./gradlew :toggles-prefs:lint + + - name: Run Android Unit test + run: ./gradlew :toggles-prefs:testDebugUnitTest + + - name: Publish core library + run: ./gradlew :toggles-prefs:publish --no-daemon --no-parallel diff --git a/.github/workflows/manual-release.yml b/.github/workflows/manual-release.yml index 24b071f2..146b9ccf 100644 --- a/.github/workflows/manual-release.yml +++ b/.github/workflows/manual-release.yml @@ -29,7 +29,7 @@ jobs: - name: Check out java uses: actions/setup-java@v1 with: - java-version: 1.8 + java-version: 11 - name: Run Detekt run: ./gradlew detekt diff --git a/.github/workflows/post-merge.yml b/.github/workflows/post-merge.yml index 69cfedc3..d7ac2801 100644 --- a/.github/workflows/post-merge.yml +++ b/.github/workflows/post-merge.yml @@ -19,7 +19,7 @@ jobs: - name: Check out java uses: actions/setup-java@v1 with: - java-version: 1.8 + java-version: 11 - name: Build apks run: ./gradlew :toggles-app:packageDebug :toggles-sample:packageDebug @@ -29,3 +29,4 @@ jobs: with: name: Apks path: '**/build/outputs/apk/*' + retention-days: 14 diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 878ff05f..18383c1d 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -19,7 +19,7 @@ jobs: - name: Check out java uses: actions/setup-java@v1 with: - java-version: 1.8 + java-version: 11 - name: Run Detekt run: ./gradlew detekt @@ -35,4 +35,5 @@ jobs: if: failure() with: name: Reports - path: '**/build/reports/*' \ No newline at end of file + path: '**/build/reports/*' + retention-days: 2 diff --git a/README.md b/README.md index 586ac161..118a3c93 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,10 @@ Stores settings / toggles behind a content provider. This is a development tools meant to facilitate feature switching in an external app so that configurations will be retained across clear data / uninstalls. -2 premade libraries to talk to the toggles application. "Prefs" and "Coroutines": +2 premade libraries to talk to the toggles application. "Prefs" and "Flow": -## Toggles-coroutines library -Exposes switches from toggles using a kotlin stream. +## Toggles-flow library +Exposes switches from toggles using a kotlin flow. ## Toggles-prefs library One-shot fetch of a toggle. Similar API as androids SharedPreferences. diff --git a/build.gradle.kts b/build.gradle.kts index 296058b9..27742a9b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,6 +2,9 @@ import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { + + val composeVersion by extra("1.0.0-beta02") + repositories { google() mavenCentral() @@ -10,29 +13,33 @@ buildscript { } dependencies { - classpath("com.android.tools:r8:2.1.75") - classpath("com.android.tools.build:gradle:4.1.1") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.21") - classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.3.2") + classpath("com.android.tools.build:gradle:7.0.0-alpha10") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.31") + classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.3.4") classpath("com.google.gms:oss-licenses:0.9.2") - classpath("com.google.dagger:hilt-android-gradle-plugin:2.30.1-alpha") - // https://github.com/Triple-T/gradle-play-publisher/issues/864 - classpath("com.github.triplet.gradle:play-publisher:3.0.0") - classpath("com.google.gms:google-services:4.3.4") + classpath("com.google.dagger:hilt-android-gradle-plugin:2.33-beta") + classpath("com.google.gms:google-services:4.3.5") + classpath("com.vanniktech:gradle-maven-publish-plugin:0.14.2") + classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.4.30") } } -dependencies { - detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.15.0") -} - plugins { - id("com.github.ben-manes.versions") version "0.36.0" + id("com.github.ben-manes.versions") version "0.38.0" id("se.eelde.build-optimizations") version "0.2.0" - id("io.gitlab.arturbosch.detekt") version "1.14.2" + id("io.gitlab.arturbosch.detekt") version "1.16.0" } allprojects { + apply(plugin = "io.gitlab.arturbosch.detekt") + + dependencies { + detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.16.0") + } + detekt { + autoCorrect = true + } + repositories { google() mavenCentral() diff --git a/gradle.properties b/gradle.properties index 4c054e00..8ef65895 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,15 +6,15 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2g +org.gradle.jvmargs=-Xmx2g -Xms500m -Dfile.encoding=UTF-8 + # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -VERSION_NAME=0.3 -VERSION_CODE=5 GROUP=se.eelde.toggles POM_DESCRIPTION=Toggles +POM_INCEPTION_YEAR=2018 POM_URL=https://github.com/erikeelde/toggles POM_SCM_URL=https://github.com/erikeelde/toggles POM_SCM_CONNECTION=scm:git@github.com:erikeelde/toggles.git @@ -24,6 +24,9 @@ POM_LICENCE_URL=https://raw.githubusercontent.com/erikeelde/toggles/master/LICEN POM_LICENCE_DIST=repo POM_DEVELOPER_ID=erikeelde, warting POM_DEVELOPER_NAME=Erik Eelde, Stefan Wärting + +RELEASE_REPOSITORY_URL=https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ +SNAPSHOT_REPOSITORY_URL=https://s01.oss.sonatype.org/content/repositories/snapshots/ # Enable rudimentary R class namespacing where each library only contains # references to the resources it declares instead of declarations plus all # transitive dependency references. @@ -31,3 +34,10 @@ POM_DEVELOPER_NAME=Erik Eelde, Stefan Wärting android.nonTransitiveRClass=false android.useAndroidX=true 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 diff --git a/gradle/gradle-mvn-push.gradle b/gradle/gradle-mvn-push.gradle deleted file mode 100644 index 58aaa11b..00000000 --- a/gradle/gradle-mvn-push.gradle +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2013 Chris Banes - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -apply plugin: 'maven' -apply plugin: 'signing' - -def isReleaseBuild() { - return VERSION_NAME.contains("SNAPSHOT") == false -} - -def getReleaseRepositoryUrl() { - return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL - : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" -} - -def getSnapshotRepositoryUrl() { - return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL - : "https://oss.sonatype.org/content/repositories/snapshots/" -} - -def getRepositoryUsername() { - return hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : "" -} - -def getRepositoryPassword() { - return hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : "" -} - -afterEvaluate { project -> - uploadArchives { - repositories { - mavenDeployer { - beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } - - pom.groupId = GROUP - pom.artifactId = POM_ARTIFACT_ID - pom.version = VERSION_NAME - - repository(url: getReleaseRepositoryUrl()) { - authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) - } - snapshotRepository(url: getSnapshotRepositoryUrl()) { - authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) - } - - pom.project { - name POM_NAME - packaging POM_PACKAGING - description POM_DESCRIPTION - url POM_URL - - scm { - url POM_SCM_URL - connection POM_SCM_CONNECTION - developerConnection POM_SCM_DEV_CONNECTION - } - - licenses { - license { - name POM_LICENCE_NAME - url POM_LICENCE_URL - distribution POM_LICENCE_DIST - } - } - - developers { - developer { - id POM_DEVELOPER_ID - name POM_DEVELOPER_NAME - } - } - } - } - } - } - - signing { - required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") } - sign configurations.archives - } - - task androidJavadocs(type: Javadoc) { - source = android.sourceSets.main.java.srcDirs - classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) - failOnError false - } - - task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { - classifier = 'javadoc' - from androidJavadocs.destinationDir - } - - task androidSourcesJar(type: Jar) { - classifier = 'sources' - from android.sourceSets.main.java.sourceFiles - } - - artifacts { - archives androidSourcesJar - archives androidJavadocsJar - } -} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5fbf4944..78549f88 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-6.7.1-all.zip +distributionUrl=https://services.gradle.org/distributions/gradle-6.8.3-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew.bat b/gradlew.bat index 107acd32..ac1b06f9 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,89 +1,89 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="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 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="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 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/lint.xml b/lint.xml index a9fd4310..8fedf799 100644 --- a/lint.xml +++ b/lint.xml @@ -3,6 +3,8 @@ + + diff --git a/settings.gradle.kts b/settings.gradle.kts index 9fd5ac45..43113e23 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,7 +1,9 @@ +rootProject.name = "Toggles" + include( ":toggles-app", ":toggles-core", - "toggles-coroutines", + ":toggles-flow", ":toggles-prefs", ":toggles-sample" ) diff --git a/toggles-app/build.gradle.kts b/toggles-app/build.gradle.kts index 61095587..a41ea17f 100644 --- a/toggles-app/build.gradle.kts +++ b/toggles-app/build.gradle.kts @@ -10,19 +10,10 @@ plugins { id("androidx.navigation.safeargs.kotlin") id("com.google.gms.oss.licenses.plugin") id("dagger.hilt.android.plugin") - id("io.gitlab.arturbosch.detekt") - id("com.github.triplet.play") + id("com.github.triplet.play") version "3.3.0-agp4.2" id("kotlin-android") } -dependencies { - detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.15.0") -} - -detekt { - autoCorrect = true -} - kapt { javacOptions { // Increase the max count of errors from annotation processors. @@ -31,10 +22,6 @@ kapt { } } -hilt { - enableTransformForLocalTests = true -} - play { serviceAccountCredentials.set(file("../service_account.json")) defaultToAppBundles.set(true) @@ -60,6 +47,12 @@ android { buildFeatures { viewBinding = true + compose = true + } + + composeOptions { + val composeVersion: String by rootProject.extra + kotlinCompilerExtensionVersion = composeVersion } testOptions { @@ -70,10 +63,10 @@ android { defaultConfig { applicationId = "se.eelde.toggles" - minSdk = 16 + minSdk = 21 targetSdk = 30 - versionCode = 4 - versionName = "1.01.00" + versionCode = 5 + versionName = "1.01.01" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true @@ -96,6 +89,10 @@ android { } kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() + freeCompilerArgs = listOfNotNull( + "-Xopt-in=kotlin.RequiresOptIn" + ) + useIR = true } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 @@ -110,12 +107,11 @@ android { getByName("debug") { isMinifyEnabled = false proguardFiles(getDefaultProguardFile("proguard-android.txt")) - versionNameSuffix = " debug" // applicationIdSuffix = ".debug" } } - lintOptions { + lint { baselineFile = file("lint-baseline.xml") isCheckReleaseBuilds = true isAbortOnError = true @@ -138,62 +134,80 @@ kapt { } dependencies { + implementation(project(":toggles-flow")) + val composeVersion: String by rootProject.extra + + implementation("androidx.ui:ui-tooling:1.0.0-alpha07") + implementation("androidx.compose.runtime:runtime:$composeVersion") + implementation("androidx.compose.ui:ui:$composeVersion") + implementation("androidx.compose.foundation:foundation-layout:$composeVersion") + implementation("androidx.compose.material:material:$composeVersion") + implementation("androidx.compose.material:material-icons-extended:$composeVersion") + implementation("androidx.compose.foundation:foundation:$composeVersion") + implementation("androidx.compose.animation:animation:$composeVersion") + implementation("androidx.compose.ui:ui-tooling:$composeVersion") + implementation("androidx.compose.runtime:runtime-livedata:$composeVersion") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha03") + implementation("androidx.navigation:navigation-compose:1.0.0-alpha09") + implementation("androidx.legacy:legacy-support-v4:1.0.0") - testImplementation("junit:junit:4.13.1") + testImplementation("junit:junit:4.13.2") testImplementation("androidx.test:core-ktx:1.3.0") testImplementation("androidx.test.ext:truth:1.3.0") testImplementation("androidx.test:rules:1.3.0") testImplementation("androidx.test:runner:1.3.0") testImplementation("androidx.test.ext:junit:1.1.2") - testImplementation("androidx.room:room-testing:2.3.0-alpha04") - testImplementation("org.robolectric:robolectric:4.4") + testImplementation("androidx.room:room-testing:2.3.0-beta03") + testImplementation("org.robolectric:robolectric:4.5.1") testImplementation("androidx.test.espresso:espresso-core:3.3.0") testImplementation("androidx.arch.core:core-testing:2.1.0") - testImplementation("androidx.work:work-testing:2.4.0") + testImplementation("androidx.work:work-testing:2.5.0") - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-rc01") - implementation(platform("com.google.firebase:firebase-bom:26.1.1")) + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.0") + implementation(platform("com.google.firebase:firebase-bom:26.7.0")) - implementation("com.google.dagger:hilt-android:2.30.1-alpha") - kapt("com.google.dagger:hilt-android-compiler:2.30.1-alpha") - implementation("androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02") - kapt("androidx.hilt:hilt-compiler:1.0.0-alpha02") - implementation("androidx.hilt:hilt-work:1.0.0-alpha02") + implementation("com.google.dagger:hilt-android:2.33-beta") + kapt("com.google.dagger:hilt-android-compiler:2.33-beta") + implementation("androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03") + kapt("androidx.hilt:hilt-compiler:1.0.0-beta01") + implementation("androidx.hilt:hilt-work:1.0.0-beta01") - testImplementation("com.google.dagger:hilt-android-testing:2.30.1-alpha") - kaptTest("com.google.dagger:hilt-android-compiler:2.30.1-alpha") + testImplementation("com.google.dagger:hilt-android-testing:2.33-beta") + kaptTest("com.google.dagger:hilt-android-compiler:2.33-beta") - implementation("androidx.lifecycle:lifecycle-common-java8:2.3.0-rc01") - kapt("androidx.room:room-compiler:2.3.0-alpha04") + implementation("androidx.lifecycle:lifecycle-common-java8:2.3.0") + kapt("androidx.room:room-compiler:2.3.0-beta03") - implementation("com.google.dagger:dagger:2.30.1") - kapt("com.google.dagger:dagger-compiler:2.30.1") + implementation("com.google.dagger:dagger:2.33") + kapt("com.google.dagger:dagger-compiler:2.33") - implementation("androidx.appcompat:appcompat:1.3.0-alpha02") - implementation("androidx.recyclerview:recyclerview:1.2.0-beta01") - implementation("com.google.android.material:material:1.3.0-beta01") + implementation("androidx.appcompat:appcompat:1.3.0-beta01") + implementation("androidx.recyclerview:recyclerview:1.2.0-beta02") + implementation("com.google.android.material:material:1.3.0") implementation("androidx.constraintlayout:constraintlayout:2.0.4") implementation("androidx.cardview:cardview:1.0.0") implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-rc01") - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-rc01") - implementation("androidx.lifecycle:lifecycle-livedata-core-ktx:2.3.0-rc01") - implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-rc01") - implementation("androidx.room:room-runtime:2.3.0-alpha04") - implementation("androidx.room:room-ktx:2.3.0-alpha04") - implementation("androidx.paging:paging-runtime-ktx:3.0.0-alpha11") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.0") + implementation("androidx.lifecycle:lifecycle-livedata-core-ktx:2.3.0") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.3.0") + implementation("androidx.room:room-runtime:2.3.0-beta03") + implementation("androidx.room:room-ktx:2.3.0-beta03") + implementation("androidx.paging:paging-runtime-ktx:3.0.0-beta02") - implementation("androidx.navigation:navigation-fragment-ktx:2.3.2") - implementation("androidx.navigation:navigation-ui-ktx:2.3.2") + implementation("androidx.navigation:navigation-fragment-ktx:2.3.4") + implementation("androidx.navigation:navigation-ui-ktx:2.3.4") implementation("com.izettle.wrench:wrench-core:0.3") implementation(project(":toggles-core")) implementation(project(":toggles-prefs")) + implementation(project(":toggles-flow")) + + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3") - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.4.21") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2") + implementation("androidx.core:core-ktx:1.3.2") + implementation("androidx.work:work-runtime-ktx:2.5.0") - implementation("androidx.core:core-ktx:1.5.0-alpha05") - implementation("androidx.work:work-runtime-ktx:2.4.0") + debugImplementation("com.squareup.leakcanary:leakcanary-android:2.6") } diff --git a/toggles-app/lint-baseline.xml b/toggles-app/lint-baseline.xml index 5b2ec11c..fde2bd79 100644 --- a/toggles-app/lint-baseline.xml +++ b/toggles-app/lint-baseline.xml @@ -1,4 +1,3 @@ - - + diff --git a/toggles-app/src/main/AndroidManifest.xml b/toggles-app/src/main/AndroidManifest.xml index dd42a4b7..dbe79f4b 100644 --- a/toggles-app/src/main/AndroidManifest.xml +++ b/toggles-app/src/main/AndroidManifest.xml @@ -36,7 +36,8 @@ android:allowEmbedded="true" android:documentLaunchMode="always" android:resizeableActivity="true" - tools:targetApi="n" > + tools:targetApi="n" + android:exported="true"> @@ -47,7 +48,8 @@ - + diff --git a/toggles-app/src/main/java/com/izettle/wrench/MainActivity.kt b/toggles-app/src/main/java/com/izettle/wrench/MainActivity.kt index ed95bda5..a6bcaa0d 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/MainActivity.kt +++ b/toggles-app/src/main/java/com/izettle/wrench/MainActivity.kt @@ -2,11 +2,14 @@ package com.izettle.wrench import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.NavigationUI import androidx.navigation.ui.setupActionBarWithNavController import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import se.eelde.toggles.R import se.eelde.toggles.databinding.ActivityMainBinding import se.eelde.toggles.notification.NotificationWorker @@ -23,14 +26,18 @@ class MainActivity : AppCompatActivity() { setContentView(binding.root) // navController = findNavController(R.id.nav_host_fragment) // https://issuetracker.google.com/issues/142847973 - navController = (supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment).navController + navController = + (supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment).navController setupActionBarWithNavController(navController, binding.drawerLayout) NavigationUI.setupWithNavController(binding.navView, navController) - NotificationWorker.scheduleNotification(this) + lifecycleScope.launch(Dispatchers.IO) { + NotificationWorker.scheduleNotification(applicationContext) + } } - override fun onSupportNavigateUp(): Boolean = NavigationUI.navigateUp(navController, binding.drawerLayout) + override fun onSupportNavigateUp(): Boolean = + NavigationUI.navigateUp(navController, binding.drawerLayout) } diff --git a/toggles-app/src/main/java/com/izettle/wrench/applicationlist/ApplicationViewHolder.kt b/toggles-app/src/main/java/com/izettle/wrench/applicationlist/ApplicationViewHolder.kt index 502e17f5..316d4f10 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/applicationlist/ApplicationViewHolder.kt +++ b/toggles-app/src/main/java/com/izettle/wrench/applicationlist/ApplicationViewHolder.kt @@ -29,7 +29,8 @@ internal class ApplicationViewHolder(val binding: ApplicationListItemBinding) : val applicationId = application.id Navigation.findNavController(v).navigate( ApplicationsFragmentDirections.actionApplicationsFragmentToConfigurationsFragment( - applicationId + applicationId, + application.applicationLabel ) ) } diff --git a/toggles-app/src/main/java/com/izettle/wrench/applicationlist/ApplicationViewModel.kt b/toggles-app/src/main/java/com/izettle/wrench/applicationlist/ApplicationViewModel.kt index 33378ff6..ac2d1be9 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/applicationlist/ApplicationViewModel.kt +++ b/toggles-app/src/main/java/com/izettle/wrench/applicationlist/ApplicationViewModel.kt @@ -1,6 +1,5 @@ package com.izettle.wrench.applicationlist -import androidx.hilt.lifecycle.ViewModelInject import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.Pager @@ -9,10 +8,12 @@ import androidx.paging.PagingData import androidx.paging.cachedIn import com.izettle.wrench.database.WrenchApplication import com.izettle.wrench.database.WrenchApplicationDao +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow +import javax.inject.Inject -internal class ApplicationViewModel -@ViewModelInject constructor(applicationDao: WrenchApplicationDao) : ViewModel() { +@HiltViewModel +internal class ApplicationViewModel @Inject constructor(applicationDao: WrenchApplicationDao) : ViewModel() { internal val applications: Flow> = Pager(PagingConfig(pageSize = 20)) { applicationDao.getApplications() diff --git a/toggles-app/src/main/java/com/izettle/wrench/applicationlist/ApplicationsFragment.kt b/toggles-app/src/main/java/com/izettle/wrench/applicationlist/ApplicationsFragment.kt index f8eb2f92..ef04fe9f 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/applicationlist/ApplicationsFragment.kt +++ b/toggles-app/src/main/java/com/izettle/wrench/applicationlist/ApplicationsFragment.kt @@ -13,11 +13,13 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.launch import se.eelde.toggles.databinding.FragmentApplicationsBinding +import se.eelde.toggles.viewLifecycle @AndroidEntryPoint internal class ApplicationsFragment : Fragment() { - private lateinit var binding: FragmentApplicationsBinding + private var binding: FragmentApplicationsBinding by viewLifecycle() + private val model by viewModels() override fun onCreateView( diff --git a/toggles-app/src/main/java/com/izettle/wrench/configurationlist/ConfigurationViewModel.kt b/toggles-app/src/main/java/com/izettle/wrench/configurationlist/ConfigurationViewModel.kt index 304f41fc..2a24f26a 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/configurationlist/ConfigurationViewModel.kt +++ b/toggles-app/src/main/java/com/izettle/wrench/configurationlist/ConfigurationViewModel.kt @@ -1,7 +1,6 @@ package com.izettle.wrench.configurationlist import android.text.TextUtils -import androidx.hilt.lifecycle.ViewModelInject import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData @@ -14,9 +13,12 @@ import com.izettle.wrench.database.WrenchConfigurationDao import com.izettle.wrench.database.WrenchConfigurationWithValues import com.izettle.wrench.database.WrenchScope import com.izettle.wrench.database.WrenchScopeDao +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch +import javax.inject.Inject -class ConfigurationViewModel @ViewModelInject internal constructor( +@HiltViewModel +class ConfigurationViewModel @Inject internal constructor( private val applicationDao: WrenchApplicationDao, configurationDao: WrenchConfigurationDao, private val scopeDao: WrenchScopeDao diff --git a/toggles-app/src/main/java/com/izettle/wrench/configurationlist/ConfigurationsFragment.kt b/toggles-app/src/main/java/com/izettle/wrench/configurationlist/ConfigurationsFragment.kt index b083e821..b1f3eb90 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/configurationlist/ConfigurationsFragment.kt +++ b/toggles-app/src/main/java/com/izettle/wrench/configurationlist/ConfigurationsFragment.kt @@ -1,5 +1,6 @@ package com.izettle.wrench.configurationlist +import android.annotation.SuppressLint import android.app.ActivityManager import android.content.Context import android.content.Intent @@ -18,25 +19,33 @@ import androidx.appcompat.widget.SearchView import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.Observer +import androidx.lifecycle.lifecycleScope import androidx.navigation.Navigation import androidx.navigation.findNavController import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.snackbar.Snackbar -import com.izettle.wrench.core.Bolt import com.izettle.wrench.database.WrenchApplication import com.izettle.wrench.database.WrenchConfigurationWithValues import com.izettle.wrench.dialogs.scope.ScopeFragment import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import se.eelde.toggles.R +import se.eelde.toggles.core.Toggle +import se.eelde.toggles.flow.toggleFlow import se.eelde.toggles.databinding.FragmentConfigurationsBinding +import se.eelde.toggles.viewLifecycle @AndroidEntryPoint class ConfigurationsFragment : Fragment(), SearchView.OnQueryTextListener, ConfigurationRecyclerViewAdapter.Listener { - private lateinit var binding: FragmentConfigurationsBinding + private var binding: FragmentConfigurationsBinding by viewLifecycle() + private var currentFilter: CharSequence? = null private var searchView: SearchView? = null @@ -77,6 +86,7 @@ class ConfigurationsFragment : FragmentConfigurationsBinding.inflate(layoutInflater, container, false) .also { binding = it }.root + @SuppressLint("NotifyDataSetChanged") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -241,6 +251,8 @@ class ConfigurationsFragment : } } + @Suppress("LongMethod") + @OptIn(ExperimentalCoroutinesApi::class) override fun configurationClicked(v: View, configuration: WrenchConfigurationWithValues) { if (model.selectedScopeLiveData.value == null) { Snackbar.make(binding.animator, "No selected scope found", Snackbar.LENGTH_LONG).show() @@ -249,17 +261,42 @@ class ConfigurationsFragment : val selectedScopeId = model.selectedScopeLiveData.value!!.id - if (TextUtils.equals(String::class.java.name, configuration.type) || TextUtils.equals(Bolt.TYPE.STRING, configuration.type) + if (TextUtils.equals( + String::class.java.name, + configuration.type + ) || TextUtils.equals(Toggle.TYPE.STRING, configuration.type) ) { - v.findNavController().navigate( - ConfigurationsFragmentDirections.actionConfigurationsFragmentToStringValueFragment( - configuration.id, - selectedScopeId - ) - ) + viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { + toggleFlow( + requireContext(), + "Use autocomplete in String configurations", + false + ).first { autoCompleteEnabled -> + if (autoCompleteEnabled) { + launch(Dispatchers.Main) { + v.findNavController().navigate( + ConfigurationsFragmentDirections.actionConfigurationsFragmentToAutoCompleteStringValueFragment( + configuration.id, + selectedScopeId + ) + ) + } + } else { + launch(Dispatchers.Main) { + v.findNavController().navigate( + ConfigurationsFragmentDirections.actionConfigurationsFragmentToStringValueFragment( + configuration.id, + selectedScopeId + ) + ) + } + } + autoCompleteEnabled + } + } } else if (TextUtils.equals(Int::class.java.name, configuration.type) || TextUtils.equals( - Bolt.TYPE.INTEGER, + Toggle.TYPE.INTEGER, configuration.type ) ) { @@ -273,9 +310,8 @@ class ConfigurationsFragment : } else if (TextUtils.equals( Boolean::class.java.name, configuration.type - ) || TextUtils.equals(Bolt.TYPE.BOOLEAN, configuration.type) + ) || TextUtils.equals(Toggle.TYPE.BOOLEAN, configuration.type) ) { - v.findNavController().navigate( ConfigurationsFragmentDirections.actionConfigurationsFragmentToBooleanValueFragment( configuration.id, @@ -283,7 +319,7 @@ class ConfigurationsFragment : ) ) } else if (TextUtils.equals(Enum::class.java.name, configuration.type) || TextUtils.equals( - Bolt.TYPE.ENUM, + Toggle.TYPE.ENUM, configuration.type ) ) { diff --git a/toggles-app/src/main/java/com/izettle/wrench/database/WrenchConfigurationDao.kt b/toggles-app/src/main/java/com/izettle/wrench/database/WrenchConfigurationDao.kt index ea1ae6a5..40d08024 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/database/WrenchConfigurationDao.kt +++ b/toggles-app/src/main/java/com/izettle/wrench/database/WrenchConfigurationDao.kt @@ -23,7 +23,7 @@ interface WrenchConfigurationDao { " INNER JOIN " + ConfigurationValueTable.TABLE_NAME + " ON configuration.id = configurationValue.configurationId " + " WHERE configuration.id = (:configurationId) AND configurationValue.scope = (:scopeId)" ) - fun getBolt(configurationId: Long, scopeId: Long): Cursor + fun getToggle(configurationId: Long, scopeId: Long): Cursor @Query( "SELECT configuration.id, " + @@ -34,7 +34,7 @@ interface WrenchConfigurationDao { " INNER JOIN " + ConfigurationValueTable.TABLE_NAME + " ON configuration.id = configurationValue.configurationId " + " WHERE configuration.configurationKey = (:configurationKey) AND configurationValue.scope = (:scopeId)" ) - fun getBolt(configurationKey: String, scopeId: Long): Cursor + fun getToggle(configurationKey: String, scopeId: Long): Cursor @Query( "SELECT * " + diff --git a/toggles-app/src/main/java/com/izettle/wrench/database/tables/ConfigurationTable.kt b/toggles-app/src/main/java/com/izettle/wrench/database/tables/ConfigurationTable.kt index c18ec2f2..2f9cea27 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/database/tables/ConfigurationTable.kt +++ b/toggles-app/src/main/java/com/izettle/wrench/database/tables/ConfigurationTable.kt @@ -1,13 +1,13 @@ package com.izettle.wrench.database.tables -import com.izettle.wrench.core.ColumnNames +import se.eelde.toggles.core.ColumnNames interface ConfigurationTable { companion object { const val TABLE_NAME = "configuration" - const val COL_ID = ColumnNames.Bolt.COL_ID + const val COL_ID = ColumnNames.Toggle.COL_ID const val COL_APP_ID = "applicationId" - const val COL_KEY = ColumnNames.Bolt.COL_KEY - const val COL_TYPE = ColumnNames.Bolt.COL_TYPE + const val COL_KEY = ColumnNames.Toggle.COL_KEY + const val COL_TYPE = ColumnNames.Toggle.COL_TYPE } } diff --git a/toggles-app/src/main/java/com/izettle/wrench/database/tables/ConfigurationValueTable.kt b/toggles-app/src/main/java/com/izettle/wrench/database/tables/ConfigurationValueTable.kt index 7ae1e4cd..0ac3c06c 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/database/tables/ConfigurationValueTable.kt +++ b/toggles-app/src/main/java/com/izettle/wrench/database/tables/ConfigurationValueTable.kt @@ -1,13 +1,13 @@ package com.izettle.wrench.database.tables -import com.izettle.wrench.core.ColumnNames +import se.eelde.toggles.core.ColumnNames interface ConfigurationValueTable { companion object { const val TABLE_NAME = "configurationValue" - const val COL_ID = ColumnNames.Bolt.COL_ID + const val COL_ID = ColumnNames.Toggle.COL_ID const val COL_CONFIG_ID = "configurationId" - const val COL_VALUE = ColumnNames.Bolt.COL_VALUE + const val COL_VALUE = ColumnNames.Toggle.COL_VALUE const val COL_SCOPE = "scope" } } diff --git a/toggles-app/src/main/java/com/izettle/wrench/database/tables/PredefinedConfigurationValueTable.kt b/toggles-app/src/main/java/com/izettle/wrench/database/tables/PredefinedConfigurationValueTable.kt index 46b86ec5..5bb367a0 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/database/tables/PredefinedConfigurationValueTable.kt +++ b/toggles-app/src/main/java/com/izettle/wrench/database/tables/PredefinedConfigurationValueTable.kt @@ -1,13 +1,13 @@ package com.izettle.wrench.database.tables -import com.izettle.wrench.core.ColumnNames +import se.eelde.toggles.core.ColumnNames interface PredefinedConfigurationValueTable { companion object { const val TABLE_NAME = "predefinedConfigurationValue" - const val COL_ID = ColumnNames.Nut.COL_ID - const val COL_CONFIG_ID = ColumnNames.Nut.COL_CONFIG_ID - const val COL_VALUE = ColumnNames.Nut.COL_VALUE + const val COL_ID = ColumnNames.ToggleValue.COL_ID + const val COL_CONFIG_ID = ColumnNames.ToggleValue.COL_CONFIG_ID + const val COL_VALUE = ColumnNames.ToggleValue.COL_VALUE } } diff --git a/toggles-app/src/main/java/com/izettle/wrench/dialogs/autocompletestringvalue/AutoCompleteStringValueFragment.kt b/toggles-app/src/main/java/com/izettle/wrench/dialogs/autocompletestringvalue/AutoCompleteStringValueFragment.kt new file mode 100644 index 00000000..861e16ed --- /dev/null +++ b/toggles-app/src/main/java/com/izettle/wrench/dialogs/autocompletestringvalue/AutoCompleteStringValueFragment.kt @@ -0,0 +1,71 @@ +package com.izettle.wrench.dialogs.autocompletestringvalue + +import android.app.Dialog +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.viewModels +import dagger.hilt.android.AndroidEntryPoint +import se.eelde.toggles.databinding.FragmentAutocompleteStringValueBinding +import se.eelde.toggles.viewLifecycle + +@AndroidEntryPoint +class AutoCompleteStringValueFragment : DialogFragment() { + + private var binding: FragmentAutocompleteStringValueBinding by viewLifecycle() + private val viewModel by viewModels() + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + this.binding = FragmentAutocompleteStringValueBinding.inflate(layoutInflater) + + viewModel.viewState.observe( + this, + { viewState -> + if (viewState != null) { + val invisible = (this.binding.container.visibility == View.INVISIBLE) + if (binding.container.visibility == View.INVISIBLE && viewState.title != null) { + binding.container.visibility = View.VISIBLE + } + binding.title.text = viewState.title + + if (invisible) { + binding.value.jumpDrawablesToCurrentState() + } + + if (viewState.saving || viewState.reverting) { + binding.value.isEnabled = false + binding.save.isEnabled = false + binding.revert.isEnabled = false + } + } + } + ) + + viewModel.viewEffects.observe( + this, + { viewEffect -> + if (viewEffect != null) { + viewEffect.getContentIfNotHandled()?.let { contentIfNotHandled -> + when (contentIfNotHandled) { + ViewEffect.Dismiss -> dismiss() + is ViewEffect.ValueChanged -> binding.value.setText(contentIfNotHandled.value) + } + } + } + } + ) + + binding.revert.setOnClickListener { + viewModel.revertClick() + } + + binding.save.setOnClickListener { + viewModel.saveClick(binding.value.text.toString()) + } + + return AlertDialog.Builder(requireActivity()) + .setView(binding.root) + .create() + } +} diff --git a/toggles-app/src/main/java/com/izettle/wrench/dialogs/autocompletestringvalue/AutoCompleteStringValueFragmentViewModel.kt b/toggles-app/src/main/java/com/izettle/wrench/dialogs/autocompletestringvalue/AutoCompleteStringValueFragmentViewModel.kt new file mode 100644 index 00000000..31cd1819 --- /dev/null +++ b/toggles-app/src/main/java/com/izettle/wrench/dialogs/autocompletestringvalue/AutoCompleteStringValueFragmentViewModel.kt @@ -0,0 +1,162 @@ +package com.izettle.wrench.dialogs.autocompletestringvalue + +import android.app.Application +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope +import com.izettle.wrench.Event +import com.izettle.wrench.database.WrenchConfigurationDao +import com.izettle.wrench.database.WrenchConfigurationValue +import com.izettle.wrench.database.WrenchConfigurationValueDao +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.launch +import se.eelde.toggles.core.TogglesProviderContract +import se.eelde.toggles.provider.notifyUpdate +import java.util.Date +import javax.inject.Inject + +@HiltViewModel +class FragmentStringValueViewModel @Inject internal constructor( + private val savedStateHandle: SavedStateHandle, + private val application: Application, + private val configurationDao: WrenchConfigurationDao, + private val configurationValueDao: WrenchConfigurationValueDao +) : ViewModel() { + + private val intentChannel = Channel(Channel.UNLIMITED) + + private val _state = MutableStateFlow(reduce(ViewState(), PartialViewState.Empty)) + + private val state: StateFlow + get() = _state + + private val configurationId: Long = savedStateHandle.get("configurationId")!! + private val scopeId: Long = savedStateHandle.get("scopeId")!! + + private var selectedConfigurationValue: WrenchConfigurationValue? = null + + internal val viewState = state.asLiveData() + + internal val viewEffects = MutableLiveData>() + + init { + viewModelScope.launch { + intentChannel.consumeAsFlow().collect { viewAction -> + when (viewAction) { + is ViewAction.SaveAction -> { + _state.value = reduce(viewState.value!!, PartialViewState.Saving) + updateConfigurationValue(viewAction.value).join() + viewEffects.value = Event(ViewEffect.Dismiss) + } + ViewAction.RevertAction -> { + _state.value = reduce(viewState.value!!, PartialViewState.Reverting) + deleteConfigurationValue() + viewEffects.value = Event(ViewEffect.Dismiss) + } + } + } + } + + viewModelScope.launch { + configurationDao.getConfiguration(configurationId).collect { + _state.value = reduce(viewState.value!!, PartialViewState.NewConfiguration(it.key)) + } + } + viewModelScope.launch { + configurationValueDao.getConfigurationValue(configurationId, scopeId).collect { + if (it != null) { + selectedConfigurationValue = it + viewEffects.value = Event(ViewEffect.ValueChanged(it.value!!)) + } + } + } + } + + private fun reduce(previousState: ViewState, partialViewState: PartialViewState): ViewState { + return when (partialViewState) { + 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) + } + } + } + + internal fun saveClick(value: String) { + intentChannel.offer(ViewAction.SaveAction(value)) + } + + internal fun revertClick() { + intentChannel.offer(ViewAction.RevertAction) + } + + private suspend fun updateConfigurationValue(value: String): Job = coroutineScope { + viewModelScope.launch(Dispatchers.IO) { + if (selectedConfigurationValue != null) { + configurationValueDao.updateConfigurationValue(configurationId, scopeId, value) + } else { + val wrenchConfigurationValue = + WrenchConfigurationValue(0, configurationId, value, scopeId) + wrenchConfigurationValue.id = configurationValueDao.insert(wrenchConfigurationValue) + } + configurationDao.touch(configurationId, Date()) + + application.contentResolver.notifyUpdate(TogglesProviderContract.toggleUri(configurationId)) + } + } + + private suspend fun deleteConfigurationValue(): Job = coroutineScope { + viewModelScope.launch(Dispatchers.IO) { + selectedConfigurationValue?.let { + configurationValueDao.delete(it) + + application.contentResolver.notifyUpdate( + TogglesProviderContract.toggleUri( + configurationId + ) + ) + } + } + } +} + +internal sealed class ViewAction { + data class SaveAction(val value: String) : ViewAction() + object RevertAction : ViewAction() +} + +internal sealed class ViewEffect { + object Dismiss : ViewEffect() + data class ValueChanged(val value: String) : ViewEffect() +} + +internal data class ViewState( + val title: String? = null, + val saving: Boolean = false, + val reverting: Boolean = false +) + +private sealed class PartialViewState { + object Empty : PartialViewState() + data class NewConfiguration(val title: String?) : PartialViewState() + + object Saving : PartialViewState() + object Reverting : PartialViewState() +} diff --git a/toggles-app/src/main/java/com/izettle/wrench/dialogs/booleanvalue/BooleanValueFragment.kt b/toggles-app/src/main/java/com/izettle/wrench/dialogs/booleanvalue/BooleanValueFragment.kt index 3b1229e0..1b7aadfa 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/dialogs/booleanvalue/BooleanValueFragment.kt +++ b/toggles-app/src/main/java/com/izettle/wrench/dialogs/booleanvalue/BooleanValueFragment.kt @@ -7,10 +7,8 @@ import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import androidx.fragment.app.viewModels import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.ExperimentalCoroutinesApi import se.eelde.toggles.databinding.FragmentBooleanValueBinding -@ExperimentalCoroutinesApi @AndroidEntryPoint class BooleanValueFragment : DialogFragment() { @@ -70,13 +68,4 @@ class BooleanValueFragment : DialogFragment() { .setView(binding.root) .create() } - - companion object { - - fun newInstance(args: BooleanValueFragmentArgs): BooleanValueFragment { - val fragment = BooleanValueFragment() - fragment.arguments = args.toBundle() - return fragment - } - } } diff --git a/toggles-app/src/main/java/com/izettle/wrench/dialogs/booleanvalue/FragmentBooleanValueViewModel.kt b/toggles-app/src/main/java/com/izettle/wrench/dialogs/booleanvalue/FragmentBooleanValueViewModel.kt index 53e354af..698275ee 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/dialogs/booleanvalue/FragmentBooleanValueViewModel.kt +++ b/toggles-app/src/main/java/com/izettle/wrench/dialogs/booleanvalue/FragmentBooleanValueViewModel.kt @@ -1,7 +1,6 @@ package com.izettle.wrench.dialogs.booleanvalue + import android.app.Application -import androidx.hilt.Assisted -import androidx.hilt.lifecycle.ViewModelInject import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel @@ -11,8 +10,8 @@ import com.izettle.wrench.Event import com.izettle.wrench.database.WrenchConfigurationDao import com.izettle.wrench.database.WrenchConfigurationValue import com.izettle.wrench.database.WrenchConfigurationValueDao +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.coroutineScope @@ -24,11 +23,11 @@ import kotlinx.coroutines.launch import se.eelde.toggles.core.TogglesProviderContract import se.eelde.toggles.provider.notifyUpdate import java.util.Date +import javax.inject.Inject -@ExperimentalCoroutinesApi -class FragmentBooleanValueViewModel -@ViewModelInject internal constructor( - @Assisted private val savedStateHandle: SavedStateHandle, +@HiltViewModel +class FragmentBooleanValueViewModel @Inject internal constructor( + private val savedStateHandle: SavedStateHandle, private val application: Application, private val configurationDao: WrenchConfigurationDao, private val configurationValueDao: WrenchConfigurationValueDao @@ -113,12 +112,13 @@ class FragmentBooleanValueViewModel if (selectedConfigurationValue != null) { configurationValueDao.updateConfigurationValue(configurationId, scopeId, value) } else { - val wrenchConfigurationValue = WrenchConfigurationValue(0, configurationId, value, scopeId) + val wrenchConfigurationValue = + WrenchConfigurationValue(0, configurationId, value, scopeId) wrenchConfigurationValue.id = configurationValueDao.insert(wrenchConfigurationValue) } configurationDao.touch(configurationId, Date()) - application.contentResolver.notifyUpdate(TogglesProviderContract.boltUri(configurationId)) + application.contentResolver.notifyUpdate(TogglesProviderContract.toggleUri(configurationId)) } } @@ -127,7 +127,11 @@ class FragmentBooleanValueViewModel selectedConfigurationValue?.let { configurationValueDao.delete(it) - application.contentResolver.notifyUpdate(TogglesProviderContract.boltUri(configurationId)) + application.contentResolver.notifyUpdate( + TogglesProviderContract.toggleUri( + configurationId + ) + ) } } } diff --git a/toggles-app/src/main/java/com/izettle/wrench/dialogs/enumvalue/EnumValueFragment.kt b/toggles-app/src/main/java/com/izettle/wrench/dialogs/enumvalue/EnumValueFragment.kt index 4538201f..6637038f 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/dialogs/enumvalue/EnumValueFragment.kt +++ b/toggles-app/src/main/java/com/izettle/wrench/dialogs/enumvalue/EnumValueFragment.kt @@ -11,10 +11,8 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.izettle.wrench.database.WrenchPredefinedConfigurationValue import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.ExperimentalCoroutinesApi import se.eelde.toggles.databinding.FragmentEnumValueBinding -@ExperimentalCoroutinesApi @AndroidEntryPoint class EnumValueFragment : DialogFragment(), PredefinedValueRecyclerViewAdapter.Listener { @@ -82,13 +80,4 @@ class EnumValueFragment : DialogFragment(), PredefinedValueRecyclerViewAdapter.L viewModel.saveClick(item.value!!) dismiss() } - - companion object { - - fun newInstance(args: EnumValueFragmentArgs): EnumValueFragment { - val fragment = EnumValueFragment() - fragment.arguments = args.toBundle() - return fragment - } - } } diff --git a/toggles-app/src/main/java/com/izettle/wrench/dialogs/enumvalue/FragmentEnumValueViewModel.kt b/toggles-app/src/main/java/com/izettle/wrench/dialogs/enumvalue/FragmentEnumValueViewModel.kt index b9d590fa..326ba704 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/dialogs/enumvalue/FragmentEnumValueViewModel.kt +++ b/toggles-app/src/main/java/com/izettle/wrench/dialogs/enumvalue/FragmentEnumValueViewModel.kt @@ -1,8 +1,6 @@ package com.izettle.wrench.dialogs.enumvalue import android.app.Application -import androidx.hilt.Assisted -import androidx.hilt.lifecycle.ViewModelInject import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle @@ -15,8 +13,8 @@ import com.izettle.wrench.database.WrenchConfigurationValue import com.izettle.wrench.database.WrenchConfigurationValueDao import com.izettle.wrench.database.WrenchPredefinedConfigurationValue import com.izettle.wrench.database.WrenchPredefinedConfigurationValueDao +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.coroutineScope @@ -29,10 +27,11 @@ import se.eelde.toggles.core.TogglesProviderContract import se.eelde.toggles.provider.notifyInsert import se.eelde.toggles.provider.notifyUpdate import java.util.Date +import javax.inject.Inject -@ExperimentalCoroutinesApi -class FragmentEnumValueViewModel @ViewModelInject internal constructor( - @Assisted private val savedStateHandle: SavedStateHandle, +@HiltViewModel +class FragmentEnumValueViewModel @Inject internal constructor( + private val savedStateHandle: SavedStateHandle, private val application: Application, private val configurationDao: WrenchConfigurationDao, private val configurationValueDao: WrenchConfigurationValueDao, @@ -67,14 +66,22 @@ class FragmentEnumValueViewModel @ViewModelInject internal constructor( is ViewAction.SaveAction -> { _state.value = reduce(viewState.value!!, PartialViewState.Saving) updateConfigurationValue(viewAction.value).join() - application.contentResolver.notifyUpdate(TogglesProviderContract.boltUri(configurationId)) + application.contentResolver.notifyUpdate( + TogglesProviderContract.toggleUri( + configurationId + ) + ) viewEffects.value = Event(ViewEffect.Dismiss) } ViewAction.RevertAction -> { _state.value = reduce(viewState.value!!, PartialViewState.Reverting) deleteConfigurationValue().join() - application.contentResolver.notifyInsert(TogglesProviderContract.boltUri(configurationId)) + application.contentResolver.notifyInsert( + TogglesProviderContract.toggleUri( + configurationId + ) + ) viewEffects.value = Event(ViewEffect.Dismiss) } @@ -126,12 +133,13 @@ class FragmentEnumValueViewModel @ViewModelInject internal constructor( if (selectedConfigurationValue != null) { configurationValueDao.updateConfigurationValue(configurationId, scopeId, value) } else { - val wrenchConfigurationValue = WrenchConfigurationValue(0, configurationId, value, scopeId) + val wrenchConfigurationValue = + WrenchConfigurationValue(0, configurationId, value, scopeId) wrenchConfigurationValue.id = configurationValueDao.insert(wrenchConfigurationValue) } configurationDao.touch(configurationId, Date()) - application.contentResolver.notifyUpdate(TogglesProviderContract.boltUri(configurationId)) + application.contentResolver.notifyUpdate(TogglesProviderContract.toggleUri(configurationId)) } } @@ -139,7 +147,7 @@ class FragmentEnumValueViewModel @ViewModelInject internal constructor( viewModelScope.launch(Dispatchers.IO) { configurationValueDao.delete(selectedConfigurationValue!!) - application.contentResolver.notifyUpdate(TogglesProviderContract.boltUri(configurationId)) + application.contentResolver.notifyUpdate(TogglesProviderContract.toggleUri(configurationId)) } } } diff --git a/toggles-app/src/main/java/com/izettle/wrench/dialogs/enumvalue/PredefinedValueRecyclerViewAdapter.kt b/toggles-app/src/main/java/com/izettle/wrench/dialogs/enumvalue/PredefinedValueRecyclerViewAdapter.kt index 6528e685..dba956b0 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/dialogs/enumvalue/PredefinedValueRecyclerViewAdapter.kt +++ b/toggles-app/src/main/java/com/izettle/wrench/dialogs/enumvalue/PredefinedValueRecyclerViewAdapter.kt @@ -36,7 +36,8 @@ class PredefinedValueRecyclerViewAdapter internal constructor( fun onClick(view: View, item: WrenchPredefinedConfigurationValue) } - inner class ViewHolder(val binding: SimpleListItemBinding) : RecyclerView.ViewHolder(binding.root) + inner class ViewHolder(val binding: SimpleListItemBinding) : + RecyclerView.ViewHolder(binding.root) companion object { private val DIFF_CALLBACK = @@ -44,16 +45,12 @@ class PredefinedValueRecyclerViewAdapter internal constructor( override fun areItemsTheSame( oldApplication: WrenchPredefinedConfigurationValue, newApplication: WrenchPredefinedConfigurationValue - ): Boolean { - return oldApplication.id == newApplication.id - } + ) = oldApplication.id == newApplication.id override fun areContentsTheSame( oldApplication: WrenchPredefinedConfigurationValue, newApplication: WrenchPredefinedConfigurationValue - ): Boolean { - return oldApplication == newApplication - } + ) = oldApplication == newApplication } } } diff --git a/toggles-app/src/main/java/com/izettle/wrench/dialogs/integervalue/FragmentIntegerValueViewModel.kt b/toggles-app/src/main/java/com/izettle/wrench/dialogs/integervalue/FragmentIntegerValueViewModel.kt index 93bab0eb..4b9a9bde 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/dialogs/integervalue/FragmentIntegerValueViewModel.kt +++ b/toggles-app/src/main/java/com/izettle/wrench/dialogs/integervalue/FragmentIntegerValueViewModel.kt @@ -1,8 +1,6 @@ package com.izettle.wrench.dialogs.integervalue import android.app.Application -import androidx.hilt.Assisted -import androidx.hilt.lifecycle.ViewModelInject import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel @@ -12,8 +10,8 @@ import com.izettle.wrench.Event import com.izettle.wrench.database.WrenchConfigurationDao import com.izettle.wrench.database.WrenchConfigurationValue import com.izettle.wrench.database.WrenchConfigurationValueDao +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.coroutineScope @@ -25,11 +23,11 @@ import kotlinx.coroutines.launch import se.eelde.toggles.core.TogglesProviderContract import se.eelde.toggles.provider.notifyUpdate import java.util.Date +import javax.inject.Inject -@ExperimentalCoroutinesApi -class FragmentIntegerValueViewModel -@ViewModelInject internal constructor( - @Assisted private val savedStateHandle: SavedStateHandle, +@HiltViewModel +class FragmentIntegerValueViewModel @Inject internal constructor( + private val savedStateHandle: SavedStateHandle, private val application: Application, private val configurationDao: WrenchConfigurationDao, private val configurationValueDao: WrenchConfigurationValueDao @@ -114,12 +112,13 @@ class FragmentIntegerValueViewModel if (selectedConfigurationValue != null) { configurationValueDao.updateConfigurationValue(configurationId, scopeId, value) } else { - val wrenchConfigurationValue = WrenchConfigurationValue(0, configurationId, value, scopeId) + val wrenchConfigurationValue = + WrenchConfigurationValue(0, configurationId, value, scopeId) wrenchConfigurationValue.id = configurationValueDao.insert(wrenchConfigurationValue) } configurationDao.touch(configurationId, Date()) - application.contentResolver.notifyUpdate(TogglesProviderContract.boltUri(configurationId)) + application.contentResolver.notifyUpdate(TogglesProviderContract.toggleUri(configurationId)) } } @@ -128,7 +127,11 @@ class FragmentIntegerValueViewModel selectedConfigurationValue?.let { configurationValueDao.delete(it) - application.contentResolver.notifyUpdate(TogglesProviderContract.boltUri(configurationId)) + application.contentResolver.notifyUpdate( + TogglesProviderContract.toggleUri( + configurationId + ) + ) } } } diff --git a/toggles-app/src/main/java/com/izettle/wrench/dialogs/integervalue/IntegerValueFragment.kt b/toggles-app/src/main/java/com/izettle/wrench/dialogs/integervalue/IntegerValueFragment.kt index 5126a673..d2a43212 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/dialogs/integervalue/IntegerValueFragment.kt +++ b/toggles-app/src/main/java/com/izettle/wrench/dialogs/integervalue/IntegerValueFragment.kt @@ -7,10 +7,8 @@ import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import androidx.fragment.app.viewModels import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.ExperimentalCoroutinesApi import se.eelde.toggles.databinding.FragmentIntegerValueBinding -@ExperimentalCoroutinesApi @AndroidEntryPoint class IntegerValueFragment : DialogFragment() { @@ -69,13 +67,4 @@ class IntegerValueFragment : DialogFragment() { .setView(binding.root) .create() } - - companion object { - - fun newInstance(args: IntegerValueFragmentArgs): IntegerValueFragment { - val fragment = IntegerValueFragment() - fragment.arguments = args.toBundle() - return fragment - } - } } diff --git a/toggles-app/src/main/java/com/izettle/wrench/dialogs/scope/ScopeFragmentViewModel.kt b/toggles-app/src/main/java/com/izettle/wrench/dialogs/scope/ScopeFragmentViewModel.kt index 06832655..5b42bbed 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/dialogs/scope/ScopeFragmentViewModel.kt +++ b/toggles-app/src/main/java/com/izettle/wrench/dialogs/scope/ScopeFragmentViewModel.kt @@ -1,17 +1,21 @@ package com.izettle.wrench.dialogs.scope import android.database.sqlite.SQLiteException -import androidx.hilt.lifecycle.ViewModelInject import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.izettle.wrench.database.WrenchScope import com.izettle.wrench.database.WrenchScopeDao +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import java.util.Date +import javax.inject.Inject + +@HiltViewModel +class ScopeFragmentViewModel @Inject internal constructor( + private val scopeDao: WrenchScopeDao +) : ViewModel() { -class ScopeFragmentViewModel -@ViewModelInject internal constructor(private val scopeDao: WrenchScopeDao) : ViewModel() { private var applicationId: Long = 0 internal val selectedScopeLiveData: LiveData by lazy { diff --git a/toggles-app/src/main/java/com/izettle/wrench/dialogs/stringvalue/StringValueFragment.kt b/toggles-app/src/main/java/com/izettle/wrench/dialogs/stringvalue/StringValueFragment.kt index 4ec5c121..54cf21b7 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/dialogs/stringvalue/StringValueFragment.kt +++ b/toggles-app/src/main/java/com/izettle/wrench/dialogs/stringvalue/StringValueFragment.kt @@ -7,10 +7,8 @@ import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import androidx.fragment.app.viewModels import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.ExperimentalCoroutinesApi import se.eelde.toggles.databinding.FragmentStringValueBinding -@ExperimentalCoroutinesApi @AndroidEntryPoint class StringValueFragment : DialogFragment() { @@ -69,13 +67,4 @@ class StringValueFragment : DialogFragment() { .setView(binding.root) .create() } - - companion object { - - fun newInstance(args: StringValueFragmentArgs): StringValueFragment { - val fragment = StringValueFragment() - fragment.arguments = args.toBundle() - return fragment - } - } } diff --git a/toggles-app/src/main/java/com/izettle/wrench/dialogs/stringvalue/FragmentStringValueViewModel.kt b/toggles-app/src/main/java/com/izettle/wrench/dialogs/stringvalue/StringValueFragmentViewModel.kt similarity index 90% rename from toggles-app/src/main/java/com/izettle/wrench/dialogs/stringvalue/FragmentStringValueViewModel.kt rename to toggles-app/src/main/java/com/izettle/wrench/dialogs/stringvalue/StringValueFragmentViewModel.kt index 509824e3..e7d228c7 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/dialogs/stringvalue/FragmentStringValueViewModel.kt +++ b/toggles-app/src/main/java/com/izettle/wrench/dialogs/stringvalue/StringValueFragmentViewModel.kt @@ -1,8 +1,6 @@ package com.izettle.wrench.dialogs.stringvalue import android.app.Application -import androidx.hilt.Assisted -import androidx.hilt.lifecycle.ViewModelInject import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel @@ -12,8 +10,8 @@ import com.izettle.wrench.Event import com.izettle.wrench.database.WrenchConfigurationDao import com.izettle.wrench.database.WrenchConfigurationValue import com.izettle.wrench.database.WrenchConfigurationValueDao +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.coroutineScope @@ -25,11 +23,12 @@ import kotlinx.coroutines.launch import se.eelde.toggles.core.TogglesProviderContract import se.eelde.toggles.provider.notifyUpdate import java.util.Date +import javax.inject.Inject -@ExperimentalCoroutinesApi +@HiltViewModel class FragmentStringValueViewModel -@ViewModelInject internal constructor( - @Assisted private val savedStateHandle: SavedStateHandle, +@Inject internal constructor( + private val savedStateHandle: SavedStateHandle, private val application: Application, private val configurationDao: WrenchConfigurationDao, private val configurationValueDao: WrenchConfigurationValueDao @@ -114,12 +113,13 @@ class FragmentStringValueViewModel if (selectedConfigurationValue != null) { configurationValueDao.updateConfigurationValue(configurationId, scopeId, value) } else { - val wrenchConfigurationValue = WrenchConfigurationValue(0, configurationId, value, scopeId) + val wrenchConfigurationValue = + WrenchConfigurationValue(0, configurationId, value, scopeId) wrenchConfigurationValue.id = configurationValueDao.insert(wrenchConfigurationValue) } configurationDao.touch(configurationId, Date()) - application.contentResolver.notifyUpdate(TogglesProviderContract.boltUri(configurationId)) + application.contentResolver.notifyUpdate(TogglesProviderContract.toggleUri(configurationId)) } } @@ -128,7 +128,11 @@ class FragmentStringValueViewModel selectedConfigurationValue?.let { configurationValueDao.delete(it) - application.contentResolver.notifyUpdate(TogglesProviderContract.boltUri(configurationId)) + application.contentResolver.notifyUpdate( + TogglesProviderContract.toggleUri( + configurationId + ) + ) } } } diff --git a/toggles-app/src/main/java/com/izettle/wrench/help/HelpFragment.kt b/toggles-app/src/main/java/com/izettle/wrench/help/HelpFragment.kt index 9c78efe2..14a132bc 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/help/HelpFragment.kt +++ b/toggles-app/src/main/java/com/izettle/wrench/help/HelpFragment.kt @@ -4,12 +4,21 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import se.eelde.toggles.R +import se.eelde.toggles.help.HelpView class HelpFragment : Fragment() { - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_help, container, false) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_help, container, false).apply { + findViewById(R.id.compose).setContent { + HelpView() + } + } } } diff --git a/toggles-app/src/main/java/com/izettle/wrench/oss/detail/LicenceMetadataLiveData.kt b/toggles-app/src/main/java/com/izettle/wrench/oss/detail/LicenceMetadataLiveData.kt index cae41544..c6f71094 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/oss/detail/LicenceMetadataLiveData.kt +++ b/toggles-app/src/main/java/com/izettle/wrench/oss/detail/LicenceMetadataLiveData.kt @@ -5,7 +5,7 @@ import androidx.lifecycle.LiveData import com.izettle.wrench.oss.LicenceMetadata import com.izettle.wrench.oss.list.OssLoading -class LicenceMetadataLiveData(val context: Context, val licenceMetadata: LicenceMetadata) : LiveData() { +class LicenceMetadataLiveData(val context: Context, private val licenceMetadata: LicenceMetadata) : LiveData() { init { run { diff --git a/toggles-app/src/main/java/com/izettle/wrench/oss/detail/OssDetailFragment.kt b/toggles-app/src/main/java/com/izettle/wrench/oss/detail/OssDetailFragment.kt index 991d5f67..2377281f 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/oss/detail/OssDetailFragment.kt +++ b/toggles-app/src/main/java/com/izettle/wrench/oss/detail/OssDetailFragment.kt @@ -1,11 +1,12 @@ package com.izettle.wrench.oss.detail -import android.app.Dialog import android.os.Bundle import android.text.util.Linkify -import androidx.appcompat.app.AlertDialog +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup import androidx.core.text.util.LinkifyCompat -import androidx.fragment.app.DialogFragment +import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.fragment.navArgs import com.izettle.wrench.oss.LicenceMetadata @@ -13,35 +14,29 @@ import dagger.hilt.android.AndroidEntryPoint import se.eelde.toggles.databinding.FragmentOssDetailBinding @AndroidEntryPoint -class OssDetailFragment : DialogFragment() { +class OssDetailFragment : Fragment() { private lateinit var binding: FragmentOssDetailBinding private val viewModel by viewModels() private val args: OssDetailFragmentArgs by navArgs() - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - - val root = FragmentOssDetailBinding.inflate(layoutInflater).also { - binding = it - }.root + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = FragmentOssDetailBinding.inflate(layoutInflater).also { + binding = it + }.root + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val licenceMetadata = LicenceMetadata(args.dependency, args.skip.toLong(), args.length) - viewModel.getThirdPartyMetadata(licenceMetadata).observe( - this, - { - binding.text.text = it - LinkifyCompat.addLinks(binding.text, Linkify.WEB_URLS) - } - ) - - return AlertDialog.Builder(requireActivity()) - .setTitle(licenceMetadata.dependency) - .setView(root) - .setPositiveButton("dismiss") { _, _ -> - dismiss() - } - .create() + binding.title.text = licenceMetadata.dependency + + viewModel.getThirdPartyMetadata(licenceMetadata).observe(viewLifecycleOwner) { + binding.text.text = it + LinkifyCompat.addLinks(binding.text, Linkify.WEB_URLS) + } } } diff --git a/toggles-app/src/main/java/com/izettle/wrench/oss/detail/OssDetailViewModel.kt b/toggles-app/src/main/java/com/izettle/wrench/oss/detail/OssDetailViewModel.kt index 3f932a71..107bffa1 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/oss/detail/OssDetailViewModel.kt +++ b/toggles-app/src/main/java/com/izettle/wrench/oss/detail/OssDetailViewModel.kt @@ -1,12 +1,14 @@ package com.izettle.wrench.oss.detail import android.app.Application -import androidx.hilt.lifecycle.ViewModelInject import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import com.izettle.wrench.oss.LicenceMetadata +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject -class OssDetailViewModel @ViewModelInject internal constructor(val application: Application) : ViewModel() { +@HiltViewModel +class OssDetailViewModel @Inject internal constructor(val application: Application) : ViewModel() { fun getThirdPartyMetadata(licenceMetadata: LicenceMetadata): LiveData { return LicenceMetadataLiveData(application, licenceMetadata) diff --git a/toggles-app/src/main/java/com/izettle/wrench/oss/list/OssListViewModel.kt b/toggles-app/src/main/java/com/izettle/wrench/oss/list/OssListViewModel.kt index 6708d954..efafeb95 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/oss/list/OssListViewModel.kt +++ b/toggles-app/src/main/java/com/izettle/wrench/oss/list/OssListViewModel.kt @@ -1,12 +1,14 @@ package com.izettle.wrench.oss.list import android.app.Application -import androidx.hilt.lifecycle.ViewModelInject import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import com.izettle.wrench.oss.LicenceMetadata +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject -class OssListViewModel @ViewModelInject internal constructor(val application: Application) : ViewModel() { +@HiltViewModel +class OssListViewModel @Inject internal constructor(val application: Application) : ViewModel() { fun getThirdPartyMetadata(): LiveData> { return ThirdPartyLicenceMetadataLiveData(application) } diff --git a/toggles-app/src/main/java/com/izettle/wrench/provider/WrenchProvider.kt b/toggles-app/src/main/java/com/izettle/wrench/provider/WrenchProvider.kt index fdcc6a51..22a1eb44 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/provider/WrenchProvider.kt +++ b/toggles-app/src/main/java/com/izettle/wrench/provider/WrenchProvider.kt @@ -28,13 +28,14 @@ import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.GlobalScope 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.notification.configurationRequested +import se.eelde.toggles.notification.ChangedHelper import se.eelde.toggles.provider.IPackageManagerWrapper import se.eelde.toggles.provider.notifyInsert import se.eelde.toggles.provider.notifyUpdate @@ -74,6 +75,10 @@ class WrenchProvider : ContentProvider() { applicationEntryPoint.providesWrenchPreferences() } + private val changedHelper: ChangedHelper by lazy { + applicationEntryPoint.providerChangedHelper() + } + private val applicationEntryPoint: WrenchProviderEntryPoint by lazy { EntryPointAccessors.fromApplication(context!!, WrenchProviderEntryPoint::class.java) } @@ -89,6 +94,7 @@ class WrenchProvider : ContentProvider() { fun provideTogglesNotificationDao(): TogglesNotificationDao fun providePackageManagerWrapper(): IPackageManagerWrapper fun providesWrenchPreferences(): ITogglesPreferences + fun providerChangedHelper(): ChangedHelper } @Synchronized @@ -137,7 +143,7 @@ class WrenchProvider : ContentProvider() { when (uriMatcher.match(uri)) { CURRENT_CONFIGURATION_ID -> { val scope = getSelectedScope(context, scopeDao, callingApplication.id) - cursor = configurationDao.getBolt( + cursor = configurationDao.getToggle( java.lang.Long.valueOf(uri.lastPathSegment!!), scope!!.id ) @@ -146,7 +152,7 @@ class WrenchProvider : ContentProvider() { cursor.close() val defaultScope = getDefaultScope(context, scopeDao, callingApplication.id) - cursor = configurationDao.getBolt( + cursor = configurationDao.getToggle( java.lang.Long.valueOf(uri.lastPathSegment!!), defaultScope!!.id ) @@ -156,37 +162,37 @@ class WrenchProvider : ContentProvider() { val bolt = Bolt.fromCursor(cursor) cursor.moveToPrevious() context?.apply { - configurationRequested( - this, - configurationDao, - togglesNotificationDao, + changedHelper.configurationRequested( callingApplication, - bolt + bolt.id, + bolt.key, + bolt.value, + GlobalScope, ) } } } CURRENT_CONFIGURATION_KEY -> { val scope = getSelectedScope(context, scopeDao, callingApplication.id) - cursor = configurationDao.getBolt(uri.lastPathSegment!!, scope!!.id) + cursor = configurationDao.getToggle(uri.lastPathSegment!!, scope!!.id) if (cursor.count == 0) { cursor.close() val defaultScope = getDefaultScope(context, scopeDao, callingApplication.id) - cursor = configurationDao.getBolt(uri.lastPathSegment!!, defaultScope!!.id) + cursor = configurationDao.getToggle(uri.lastPathSegment!!, defaultScope!!.id) } if (cursor.moveToFirst()) { val bolt = Bolt.fromCursor(cursor) cursor.moveToPrevious() context?.apply { - configurationRequested( - this, - configurationDao, - togglesNotificationDao, + changedHelper.configurationRequested( callingApplication, - bolt + bolt.id, + bolt.key, + bolt.value, + GlobalScope, ) } } diff --git a/toggles-app/src/main/java/se/eelde/toggles/ViewLifecycle.kt b/toggles-app/src/main/java/se/eelde/toggles/ViewLifecycle.kt new file mode 100644 index 00000000..a79615e4 --- /dev/null +++ b/toggles-app/src/main/java/se/eelde/toggles/ViewLifecycle.kt @@ -0,0 +1,61 @@ +package se.eelde.toggles + +import androidx.fragment.app.Fragment +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.Observer +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +/* +* Based on https://gist.github.com/jamiesanson/d1a3ed0910cd605e928572ce245bafc4 +* +* Refactored to use the preferred `DefaultLifecycleObserver` which does not rely on the annotation processor. +* See https://developer.android.com/reference/kotlin/androidx/lifecycle/Lifecycle#init +* +* Usage: +* `private var binding: TheViewBinding by viewLifecycle()` +*/ + +/** + * An extension to bind and unbind a value based on the view lifecycle of a Fragment. + * The binding will be unbound in onDestroyView. + * + * @throws IllegalStateException If the getter is invoked before the binding is set, + * or after onDestroyView an exception is thrown. + */ +fun Fragment.viewLifecycle(): ReadWriteProperty = + object : ReadWriteProperty, DefaultLifecycleObserver { + + private var binding: T? = null + + init { + // Observe the view lifecycle of the Fragment. + // The view lifecycle owner is null before onCreateView and after onDestroyView. + // The observer is automatically removed after the onDestroy event. + this@viewLifecycle + .viewLifecycleOwnerLiveData + .observe(this@viewLifecycle, Observer { owner: LifecycleOwner? -> + owner?.lifecycle?.addObserver(this) + }) + } + + override fun onDestroy(owner: LifecycleOwner) { + binding = null + } + + override fun getValue( + thisRef: Fragment, + property: KProperty<*> + ): T { + return this.binding ?: error("Called before onCreateView or after onDestroyView.") + } + + override fun setValue( + thisRef: Fragment, + property: KProperty<*>, + value: T + ) { + this.binding = value + } + } diff --git a/toggles-app/src/main/java/com/izettle/wrench/di/ApplicationModule.kt b/toggles-app/src/main/java/se/eelde/toggles/di/ApplicationModule.kt similarity index 97% rename from toggles-app/src/main/java/com/izettle/wrench/di/ApplicationModule.kt rename to toggles-app/src/main/java/se/eelde/toggles/di/ApplicationModule.kt index 443369d5..f1403886 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/di/ApplicationModule.kt +++ b/toggles-app/src/main/java/se/eelde/toggles/di/ApplicationModule.kt @@ -1,4 +1,4 @@ -package com.izettle.wrench.di +package se.eelde.toggles.di import android.content.Context import com.izettle.wrench.preferences.ITogglesPreferences diff --git a/toggles-app/src/main/java/com/izettle/wrench/di/DatabaseModule.kt b/toggles-app/src/main/java/se/eelde/toggles/di/DatabaseModule.kt similarity index 98% rename from toggles-app/src/main/java/com/izettle/wrench/di/DatabaseModule.kt rename to toggles-app/src/main/java/se/eelde/toggles/di/DatabaseModule.kt index 0fa9230e..7afec340 100644 --- a/toggles-app/src/main/java/com/izettle/wrench/di/DatabaseModule.kt +++ b/toggles-app/src/main/java/se/eelde/toggles/di/DatabaseModule.kt @@ -1,4 +1,4 @@ -package com.izettle.wrench.di +package se.eelde.toggles.di import android.content.Context import androidx.room.Room diff --git a/toggles-app/src/main/java/se/eelde/toggles/help/HelpView.kt b/toggles-app/src/main/java/se/eelde/toggles/help/HelpView.kt new file mode 100644 index 00000000..667db7d0 --- /dev/null +++ b/toggles-app/src/main/java/se/eelde/toggles/help/HelpView.kt @@ -0,0 +1,10 @@ +package se.eelde.toggles.help + +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +@Composable +fun HelpView() { + Text(text = "Implementation", color = Color.White) +} diff --git a/toggles-app/src/main/java/se/eelde/toggles/notification/BubbleCompatNotificationHelper.kt b/toggles-app/src/main/java/se/eelde/toggles/notification/BubbleCompatNotificationHelper.kt index 902edcf9..90d0adcf 100644 --- a/toggles-app/src/main/java/se/eelde/toggles/notification/BubbleCompatNotificationHelper.kt +++ b/toggles-app/src/main/java/se/eelde/toggles/notification/BubbleCompatNotificationHelper.kt @@ -1,5 +1,6 @@ package se.eelde.toggles.notification +import android.annotation.SuppressLint import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager @@ -22,6 +23,11 @@ import se.eelde.toggles.R import se.eelde.toggles.bubble.BubbleActivity import se.eelde.toggles.core.TogglesProviderContract +// Since we're not targeting android S we need a local version of this to make a lint check happy +// https://developer.android.com/about/versions/12/behavior-changes-12#pending-intent-mutability +// https://developer.android.com/reference/android/app/PendingIntent#FLAG_MUTABLE +const val MUTABILITY_FLAG_FROM_ANDROID_31 = 0x02000000 + @RequiresApi(Build.VERSION_CODES.R) class BubbleCompatNotificationHelper(private val context: Context) { companion object { @@ -57,6 +63,7 @@ class BubbleCompatNotificationHelper(private val context: Context) { // updateShortcuts(null) } + @SuppressLint("WrongConstant") @Suppress("LongMethod") @WorkerThread fun showNotification( @@ -64,11 +71,13 @@ class BubbleCompatNotificationHelper(private val context: Context) { togglesNotifications: List, fromUser: Boolean ) { - val applicationIcon = context.packageManager.getApplicationIcon(wrenchApplication.packageName) + val applicationIcon = + context.packageManager.getApplicationIcon(wrenchApplication.packageName) val icon = IconCompat.createWithAdaptiveBitmap(applicationIcon.toBitmap()) val user = Person.Builder().setName(context.getString(R.string.sender_you)).build() - val person = Person.Builder().setName(wrenchApplication.applicationLabel).setIcon(icon).build() + val person = + Person.Builder().setName(wrenchApplication.applicationLabel).setIcon(icon).build() val contentUri = TogglesProviderContract.applicationUri(wrenchApplication.id) val pendingIntent = PendingIntent.getActivity( @@ -78,7 +87,7 @@ class BubbleCompatNotificationHelper(private val context: Context) { Intent(context, BubbleActivity::class.java) .setAction(Intent.ACTION_VIEW) .setData(contentUri), - PendingIntent.FLAG_UPDATE_CURRENT + PendingIntent.FLAG_UPDATE_CURRENT or MUTABILITY_FLAG_FROM_ANDROID_31 ) val builder = NotificationCompat.Builder(context, CHANNEL_NEW_MESSAGES) @@ -119,7 +128,7 @@ class BubbleCompatNotificationHelper(private val context: Context) { Intent(context, BubbleActivity::class.java) .setAction(Intent.ACTION_VIEW) .setData(contentUri), - PendingIntent.FLAG_UPDATE_CURRENT + PendingIntent.FLAG_UPDATE_CURRENT or MUTABILITY_FLAG_FROM_ANDROID_31 ) ) // Direct Reply @@ -132,7 +141,7 @@ class BubbleCompatNotificationHelper(private val context: Context) { context, REQUEST_CONTENT, Intent(context, BubbleActivity::class.java).setData(contentUri), - PendingIntent.FLAG_UPDATE_CURRENT + PendingIntent.FLAG_UPDATE_CURRENT or MUTABILITY_FLAG_FROM_ANDROID_31 ) ) .addRemoteInput( diff --git a/toggles-app/src/main/java/se/eelde/toggles/notification/ChangedHelper.kt b/toggles-app/src/main/java/se/eelde/toggles/notification/ChangedHelper.kt index c8dbbbe3..ed78beaf 100644 --- a/toggles-app/src/main/java/se/eelde/toggles/notification/ChangedHelper.kt +++ b/toggles-app/src/main/java/se/eelde/toggles/notification/ChangedHelper.kt @@ -1,33 +1,67 @@ package se.eelde.toggles.notification import android.content.Context -import com.izettle.wrench.core.Bolt import com.izettle.wrench.database.TogglesNotification import com.izettle.wrench.database.TogglesNotificationDao import com.izettle.wrench.database.WrenchApplication import com.izettle.wrench.database.WrenchConfigurationDao +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import se.eelde.toggles.core.Toggle +import se.eelde.toggles.flow.toggleFlow import java.util.Date +import javax.inject.Inject -fun configurationRequested( - context: Context, - configurationDao: WrenchConfigurationDao, - togglesNotificationDao: TogglesNotificationDao, - application: WrenchApplication, - bolt: Bolt, +class ChangedHelper @Inject constructor( + @ApplicationContext private val context: Context, + private val configurationDao: WrenchConfigurationDao, + private val togglesNotificationDao: TogglesNotificationDao, ) { - configurationDao.getWrenchConfigurationById(application.id, bolt.id) - ?.let { configuration -> - togglesNotificationDao.insert( - TogglesNotification( - applicationId = application.id, - applicationPackageName = application.packageName, - configurationId = configuration.id, - configurationKey = bolt.key, - configurationValue = bolt.value!!, - added = Date(), - ) - ) - } + fun configurationRequested( + application: WrenchApplication, + toggle: Toggle, + scope: CoroutineScope + ) = configurationRequested( + application, + toggle.id, + toggle.key, + toggle.value, + scope, + ) + + @OptIn(ExperimentalCoroutinesApi::class) + fun configurationRequested( + application: WrenchApplication, + toggleId: Long, + toggleKey: String, + toggleValue: String?, + scope: CoroutineScope + ) { + scope.launch(Dispatchers.IO) { + val notificationsEnabled = + toggleFlow(context, "Enable notifications", false).first() - NotificationWorker.scheduleNotification(context) + if (notificationsEnabled) { + configurationDao.getWrenchConfigurationById(application.id, toggleId) + ?.let { configuration -> + togglesNotificationDao.insert( + TogglesNotification( + applicationId = application.id, + applicationPackageName = application.packageName, + configurationId = configuration.id, + configurationKey = toggleKey, + configurationValue = toggleValue!!, + added = Date(), + ) + ) + } + + NotificationWorker.scheduleNotification(context) + } + } + } } diff --git a/toggles-app/src/main/java/se/eelde/toggles/notification/NotificationWorker.kt b/toggles-app/src/main/java/se/eelde/toggles/notification/NotificationWorker.kt index 814e6821..1fc5e15d 100644 --- a/toggles-app/src/main/java/se/eelde/toggles/notification/NotificationWorker.kt +++ b/toggles-app/src/main/java/se/eelde/toggles/notification/NotificationWorker.kt @@ -13,8 +13,7 @@ import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.toBitmap -import androidx.hilt.Assisted -import androidx.hilt.work.WorkerInject +import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequest @@ -24,21 +23,30 @@ import androidx.work.WorkerParameters import com.izettle.wrench.MainActivity import com.izettle.wrench.database.WrenchApplication import com.izettle.wrench.database.WrenchDatabase +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first import se.eelde.toggles.core.TogglesProviderContract +import se.eelde.toggles.flow.toggleFlow import java.util.concurrent.TimeUnit -class NotificationWorker @WorkerInject constructor( +@HiltWorker +class NotificationWorker @AssistedInject constructor( @Assisted appContext: Context, @Assisted workerParams: WorkerParameters, val wrenchDatabase: WrenchDatabase, -) : - CoroutineWorker(appContext, workerParams) { +) : CoroutineWorker(appContext, workerParams) { companion object { - fun scheduleNotification(context: Context) { + private const val DEFAULT_NOTIFICATION_DEBOUNCE = 1000 + + @OptIn(ExperimentalCoroutinesApi::class) + suspend fun scheduleNotification(context: Context) { + val notificationDebounce = toggleFlow(context, "Toggle request debounce timeout (milliseconds)", DEFAULT_NOTIFICATION_DEBOUNCE).first() val notificationWorker: OneTimeWorkRequest = OneTimeWorkRequestBuilder() - .setInitialDelay(2, TimeUnit.SECONDS) + .setInitialDelay(notificationDebounce.toLong(), TimeUnit.MILLISECONDS) .build() WorkManager 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 c3511039..cb70c471 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 @@ -6,9 +6,9 @@ import android.content.ContentValues import android.content.Context import android.content.pm.PackageManager import android.database.Cursor +import android.database.sqlite.SQLiteConstraintException import android.net.Uri import android.os.Binder -import com.izettle.wrench.core.Bolt import com.izettle.wrench.database.TogglesNotificationDao import com.izettle.wrench.database.WrenchApplication import com.izettle.wrench.database.WrenchApplicationDao @@ -25,14 +25,16 @@ import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.GlobalScope 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.core.TogglesProviderContract -import se.eelde.toggles.notification.configurationRequested +import se.eelde.toggles.notification.ChangedHelper import java.util.Date class TogglesProvider : ContentProvider() { @@ -69,6 +71,10 @@ class TogglesProvider : ContentProvider() { applicationEntryPoint.providesWrenchPreferences() } + private val changedHelper: ChangedHelper by lazy { + applicationEntryPoint.providerChangedHelper() + } + private val applicationEntryPoint: TogglesProviderEntryPoint by lazy { EntryPointAccessors.fromApplication(context!!, TogglesProviderEntryPoint::class.java) } @@ -84,9 +90,9 @@ class TogglesProvider : ContentProvider() { fun provideTogglesNotificationDao(): TogglesNotificationDao fun providePackageManagerWrapper(): IPackageManagerWrapper fun providesWrenchPreferences(): ITogglesPreferences + fun providerChangedHelper(): ChangedHelper } - @Synchronized private fun getCallingApplication(applicationDao: WrenchApplicationDao): WrenchApplication { var wrenchApplication: WrenchApplication? = applicationDao.loadByPackageName(packageManagerWrapper.callingApplicationPackageName!!) @@ -112,7 +118,7 @@ class TogglesProvider : ContentProvider() { override fun onCreate() = true - @Suppress("LongMethod") + @Suppress("LongMethod", "NestedBlockDepth") override fun query( uri: Uri, projection: Array?, @@ -131,7 +137,7 @@ class TogglesProvider : ContentProvider() { when (uriMatcher.match(uri)) { CURRENT_CONFIGURATION_ID -> { val scope = getSelectedScope(context, scopeDao, callingApplication.id) - cursor = configurationDao.getBolt( + cursor = configurationDao.getToggle( java.lang.Long.valueOf(uri.lastPathSegment!!), scope!!.id ) @@ -140,48 +146,48 @@ class TogglesProvider : ContentProvider() { cursor.close() val defaultScope = getDefaultScope(context, scopeDao, callingApplication.id) - cursor = configurationDao.getBolt( + cursor = configurationDao.getToggle( java.lang.Long.valueOf(uri.lastPathSegment!!), defaultScope!!.id ) } if (cursor.moveToFirst()) { - val bolt = Bolt.fromCursor(cursor) + val toggle = Toggle.fromCursor(cursor) cursor.moveToPrevious() - context?.apply { - configurationRequested( - this, - configurationDao, - togglesNotificationDao, - callingApplication, - bolt - ) + if (!isTogglesApplication(callingApplication)) { + context?.apply { + changedHelper.configurationRequested( + callingApplication, + toggle, + GlobalScope + ) + } } } } CURRENT_CONFIGURATION_KEY -> { val scope = getSelectedScope(context, scopeDao, callingApplication.id) - cursor = configurationDao.getBolt(uri.lastPathSegment!!, scope!!.id) + cursor = configurationDao.getToggle(uri.lastPathSegment!!, scope!!.id) if (cursor.count == 0) { cursor.close() val defaultScope = getDefaultScope(context, scopeDao, callingApplication.id) - cursor = configurationDao.getBolt(uri.lastPathSegment!!, defaultScope!!.id) + cursor = configurationDao.getToggle(uri.lastPathSegment!!, defaultScope!!.id) } if (cursor.moveToFirst()) { - val bolt = Bolt.fromCursor(cursor) + val toggle = Toggle.fromCursor(cursor) cursor.moveToPrevious() - context?.apply { - configurationRequested( - this, - configurationDao, - togglesNotificationDao, - callingApplication, - bolt - ) + if (!isTogglesApplication(callingApplication)) { + context?.apply { + changedHelper.configurationRequested( + callingApplication, + toggle, + GlobalScope + ) + } } } } @@ -212,14 +218,14 @@ class TogglesProvider : ContentProvider() { val insertId: Long when (uriMatcher.match(uri)) { CURRENT_CONFIGURATIONS -> { - val bolt = Bolt.fromContentValues(values!!) + val toggle = Toggle.fromContentValues(values!!) var wrenchConfiguration: WrenchConfiguration? = - configurationDao.getWrenchConfiguration(callingApplication.id, bolt.key) + configurationDao.getWrenchConfiguration(callingApplication.id, toggle.key) if (wrenchConfiguration == null) { wrenchConfiguration = - WrenchConfiguration(0, callingApplication.id, bolt.key, bolt.type) + WrenchConfiguration(0, callingApplication.id, toggle.key, toggle.type) wrenchConfiguration.id = configurationDao.insert(wrenchConfiguration) } @@ -229,15 +235,19 @@ class TogglesProvider : ContentProvider() { val wrenchConfigurationValue = WrenchConfigurationValue( 0, wrenchConfiguration.id, - bolt.value, + toggle.value, defaultScope!!.id ) wrenchConfigurationValue.configurationId = wrenchConfiguration.id - wrenchConfigurationValue.value = bolt.value + wrenchConfigurationValue.value = toggle.value wrenchConfigurationValue.scope = defaultScope.id - wrenchConfigurationValue.id = - configurationValueDao.insertSync(wrenchConfigurationValue) + try { + wrenchConfigurationValue.id = + configurationValueDao.insertSync(wrenchConfigurationValue) + } catch (e: SQLiteConstraintException) { + // this happens when the app is initially launched because many of many calls into assertValidApiVersion() + } insertId = wrenchConfiguration.id } @@ -281,18 +291,18 @@ class TogglesProvider : ContentProvider() { val updatedRows: Int when (uriMatcher.match(uri)) { CURRENT_CONFIGURATION_ID -> { - val bolt = Bolt.fromContentValues(values!!) + val toggle = Toggle.fromContentValues(values!!) val scope = getSelectedScope(context, scopeDao, callingApplication.id) updatedRows = configurationValueDao.updateConfigurationValueSync( java.lang.Long.parseLong(uri.lastPathSegment!!), scope!!.id, - bolt.value!! + toggle.value!! ) if (updatedRows == 0) { val wrenchConfigurationValue = WrenchConfigurationValue( 0, java.lang.Long.parseLong(uri.lastPathSegment!!), - bolt.value, + toggle.value, scope.id ) configurationValueDao.insertSync(wrenchConfigurationValue) @@ -401,6 +411,7 @@ class TogglesProvider : ContentProvider() { return scope } + @Synchronized private fun assertValidApiVersion(togglesPreferences: ITogglesPreferences, uri: Uri) { var l: Long = 0 val strictApiVersion = try { diff --git a/toggles-app/src/main/res/layout/application_list_item.xml b/toggles-app/src/main/res/layout/application_list_item.xml index d525faad..e868b1c7 100644 --- a/toggles-app/src/main/res/layout/application_list_item.xml +++ b/toggles-app/src/main/res/layout/application_list_item.xml @@ -14,7 +14,6 @@ android:layout_width="48dp" android:layout_height="48dp" android:layout_marginStart="8dp" - android:layout_marginLeft="8dp" android:layout_marginTop="8dp" android:layout_marginBottom="8dp" android:contentDescription="@string/application_icon" @@ -30,7 +29,6 @@ android:layout_height="21dp" android:layout_margin="@dimen/text_margin" android:layout_marginStart="8dp" - android:layout_marginLeft="8dp" android:layout_marginTop="8dp" android:layout_marginBottom="8dp" android:textAppearance="?attr/textAppearanceListItem" diff --git a/toggles-app/src/main/res/layout/fragment_autocomplete_string_value.xml b/toggles-app/src/main/res/layout/fragment_autocomplete_string_value.xml new file mode 100644 index 00000000..4632ca8f --- /dev/null +++ b/toggles-app/src/main/res/layout/fragment_autocomplete_string_value.xml @@ -0,0 +1,69 @@ + + + + + + + + + + +