From 60d32beb6d61f389db605c67025de1857586664c Mon Sep 17 00:00:00 2001 From: Angel Sanadinov Date: Thu, 5 Nov 2026 19:18:52 +0100 Subject: [PATCH 1/2] fix: update dependencies and fix issues --- .github/workflows/default.yml | 39 ++- .gitignore | 2 + build.gradle.kts | 30 +- client-android/build.gradle.kts | 143 ++++----- .../1.json | 58 ++++ .../1.json | 46 +++ .../2.json | 76 +++++ .../test/client/activities/ActivityModels.kt | 9 +- .../NotificationViewModelSpec.kt | 19 +- .../schedules/TaskScheduleEntityDaoSpec.kt | 10 + .../schedules/TaskScheduleRepositorySpec.kt | 12 + .../schedules/TaskScheduleViewModelSpec.kt | 11 +- .../persistence/tasks/TaskEntityDaoSpec.kt | 12 +- .../persistence/tasks/TaskRepositorySpec.kt | 17 +- .../persistence/tasks/TaskViewModelSpec.kt | 21 +- client-android/src/main/AndroidManifest.xml | 26 +- .../client/EventuallyApplication.kt | 7 + .../client/activities/MainActivity.kt | 68 +++++ .../client/activities/NewTaskActivity.kt | 16 +- .../client/activities/StatsActivity.kt | 20 +- .../client/activities/TaskDetailsActivity.kt | 77 ++--- .../activities/fragments/TaskListFragment.kt | 7 +- .../client/activities/helpers/TaskDetails.kt | 3 +- .../receivers/ExactAlarmPermissionReceiver.kt | 23 ++ .../activities/views/MaterialDayPicker.kt | 70 +++++ .../eventually/client/di/RepositoryModule.kt | 31 ++ .../client/persistence/Converters.kt | 1 + .../NotificationEntityDatabase.kt | 2 +- .../notifications/NotificationViewModel.kt | 60 +--- .../schedules/TaskScheduleEntityDao.kt | 3 + .../schedules/TaskScheduleEntityDatabase.kt | 2 +- .../schedules/TaskScheduleRepository.kt | 2 + .../schedules/TaskScheduleViewModel.kt | 41 +-- .../client/persistence/tasks/TaskEntityDao.kt | 5 +- .../persistence/tasks/TaskEntityDatabase.kt | 2 +- .../persistence/tasks/TaskRepository.kt | 4 +- .../client/persistence/tasks/TaskViewModel.kt | 35 +-- .../scheduling/AlarmManagerExtensions.kt | 21 +- .../NotificationManagerExtensions.kt | 11 +- .../client/scheduling/NotificationQueue.kt | 20 +- .../client/scheduling/SchedulerService.kt | 102 ++++--- .../main/res/color/day_chip_background.xml | 5 + .../src/main/res/color/day_chip_text.xml | 5 + .../res/layout/input_schedule_repeating.xml | 2 +- .../src/main/res/layout/view_day_chip.xml | 6 + .../src/main/res/values/strings.xml | 2 + client-android/src/main/res/values/styles.xml | 21 +- .../helpers/DateTimeExtensionsSpec.kt | 18 +- .../ExactAlarmPermissionReceiverSpec.kt | 58 ++++ .../activities/views/MaterialDayPickerSpec.kt | 86 ++++++ .../test/client/persistence/ConvertersSpec.kt | 20 ++ .../scheduling/AlarmManagerExtensionsSpec.kt | 55 ++++ .../NotificationManagerExtensionsSpec.kt | 49 ++- .../scheduling/NotificationQueueSpec.kt | 32 +- .../client/scheduling/SchedulerServiceSpec.kt | 94 ++++-- core/build.gradle.kts | 42 ++- .../main/kotlin/eventually/core/model/Task.kt | 6 + .../core/scheduling/SchedulerOps.kt | 20 +- .../eventually/test/core/model/TaskSpec.kt | 12 + .../test/core/scheduling/SchedulerOpsSpec.kt | 62 ++++ gradle.properties | 13 +- gradle/libs.versions.toml | 90 ++++++ gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 48749 bytes gradle/wrapper/gradle-wrapper.properties | 7 +- gradlew | 283 +++++++++++------- gradlew.bat | 93 +++--- settings.gradle.kts | 22 ++ 67 files changed, 1655 insertions(+), 612 deletions(-) create mode 100644 client-android/schemas/eventually.client.persistence.notifications.NotificationEntityDatabase/1.json create mode 100644 client-android/schemas/eventually.client.persistence.schedules.TaskScheduleEntityDatabase/1.json create mode 100644 client-android/schemas/eventually.client.persistence.tasks.TaskEntityDatabase/2.json create mode 100644 client-android/src/main/java/eventually/client/EventuallyApplication.kt create mode 100644 client-android/src/main/java/eventually/client/activities/receivers/ExactAlarmPermissionReceiver.kt create mode 100644 client-android/src/main/java/eventually/client/activities/views/MaterialDayPicker.kt create mode 100644 client-android/src/main/java/eventually/client/di/RepositoryModule.kt create mode 100644 client-android/src/main/res/color/day_chip_background.xml create mode 100644 client-android/src/main/res/color/day_chip_text.xml create mode 100644 client-android/src/main/res/layout/view_day_chip.xml create mode 100644 client-android/src/test/java/eventually/test/client/activities/receivers/ExactAlarmPermissionReceiverSpec.kt create mode 100644 client-android/src/test/java/eventually/test/client/activities/views/MaterialDayPickerSpec.kt create mode 100644 gradle/libs.versions.toml diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index 405d776..7f6c1fd 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -7,37 +7,34 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Setup JDK 11 - uses: actions/setup-java@v3 + - name: Setup JDK 21 + uses: actions/setup-java@77dd5b2ce225ea11469928ac317850245a74ae78 # v6.2.0 with: - java-version: 11 - distribution: adopt + java-version: 21 + distribution: temurin - - name: Run QA - run: | - mkdir ${{ github.workspace }}/target - ./gradlew core:qa + - name: Setup Gradle + uses: gradle/actions/setup-gradle@8da46b539cd8fe6ab27ca75128919a18d757604c # v6.4.1 - - name: Push Coverage Result - uses: codecov/codecov-action@v3 - with: - flags: kotlin + - name: Run QA + run: ./gradlew core:qa android: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Setup JDK 11 - uses: actions/setup-java@v3 + - name: Setup JDK 21 + uses: actions/setup-java@77dd5b2ce225ea11469928ac317850245a74ae78 # v6.2.0 with: - java-version: 11 - distribution: adopt + java-version: 21 + distribution: temurin + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@8da46b539cd8fe6ab27ca75128919a18d757604c # v6.4.1 - name: Run QA - run: | - mkdir ${{ github.workspace }}/target - ./gradlew client-android:qa + run: ./gradlew client-android:qa diff --git a/.gitignore b/.gitignore index 005fc48..4d4cdb6 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ libraries/tools/kotlin-test-js-runner/lib/ local.properties *.hprof client-android/release +.local +.kotlin diff --git a/build.gradle.kts b/build.gradle.kts index c8d5e4f..c3a41d9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,36 +1,18 @@ import org.gradle.api.tasks.testing.logging.TestExceptionFormat -buildscript { - repositories { - jcenter() - google() - } - - dependencies { - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21") - classpath("com.android.tools.build:gradle:7.4.1") - } -} - plugins { - id("io.gitlab.arturbosch.detekt") version "1.22.0" + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.jvm) apply false + alias(libs.plugins.ksp) apply false + alias(libs.plugins.hilt) apply false + alias(libs.plugins.detekt) jacoco } -allprojects { - repositories { - jcenter() - mavenCentral() - google() - } -} - subprojects { version = "1.1.1-SNAPSHOT" -} -allprojects { - tasks.withType { + tasks.withType().configureEach { testLogging { exceptionFormat = TestExceptionFormat.FULL showCauses = true diff --git a/client-android/build.gradle.kts b/client-android/build.gradle.kts index 02bc723..9144e49 100644 --- a/client-android/build.gradle.kts +++ b/client-android/build.gradle.kts @@ -1,67 +1,17 @@ plugins { - id("com.android.application") - id("kotlin-android") - kotlin("kapt") -} - -dependencies { - implementation(project(":core")) - - implementation("com.google.code.gson:gson:2.10.1") - implementation("org.jetbrains.kotlin:kotlin-stdlib:1.7.21") - implementation("androidx.appcompat:appcompat:1.6.1") - implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0") - implementation("androidx.constraintlayout:constraintlayout:2.1.4") - implementation("androidx.navigation:navigation-fragment-ktx:2.5.3") - implementation("androidx.navigation:navigation-ui-ktx:2.5.3") - implementation("androidx.legacy:legacy-support-v4:1.0.0") - implementation("androidx.recyclerview:recyclerview:1.2.1") - implementation("androidx.preference:preference-ktx:1.2.0") - implementation("com.google.android.material:material:1.8.0") - implementation("androidx.room:room-runtime:2.5.0") - implementation("androidx.room:room-ktx:2.5.0") - implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1") - implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1") - implementation("androidx.work:work-runtime-ktx:2.8.0") - implementation("ru.cleverpumpkin:crunchycalendar:2.3.0") - implementation("ca.antonious:materialdaypicker:0.7.4") - implementation("io.noties.markwon:core:4.6.2") - implementation("io.noties.markwon:ext-strikethrough:4.6.2") - implementation("io.noties.markwon:ext-tables:4.6.2") - implementation("io.noties.markwon:ext-tasklist:4.6.2") - implementation("com.github.skydoves:colorpickerview:2.2.4") - - kapt("androidx.room:room-compiler:2.5.0") - - api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") - api("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4") - - testImplementation("io.mockk:mockk:1.13.3") - testImplementation("org.robolectric:robolectric:4.9.2") - testImplementation("junit:junit:4.13.2") - testImplementation("androidx.test:core:1.5.0") - - androidTestImplementation("androidx.test:runner:1.5.2") - androidTestImplementation("androidx.test:rules:1.5.0") - androidTestImplementation("androidx.test.ext:junit:1.1.5") - androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") - androidTestImplementation("androidx.room:room-testing:2.5.0") - androidTestImplementation("androidx.arch.core:core-testing:2.1.0") - - debugImplementation("androidx.fragment:fragment-testing:1.5.5") { - exclude(group = "androidx.test", module = "monitor") - } + alias(libs.plugins.android.application) + alias(libs.plugins.ksp) + alias(libs.plugins.hilt) } android { - compileSdk = 33 + namespace = "eventually.client" + compileSdk = 35 defaultConfig { - minSdk = 26 - targetSdk = 33 - applicationId = "eventually.client" + minSdk = 26 + targetSdk = 35 versionCode = 3 versionName = "1.1.1-SNAPSHOT" @@ -69,6 +19,10 @@ android { } buildTypes { + getByName("debug") { + applicationIdSuffix = ".debug" + versionNameSuffix = "-debug" + } getByName("release") { isMinifyEnabled = true proguardFiles( @@ -81,28 +35,77 @@ android { buildFeatures { dataBinding = true viewBinding = true + buildConfig = true } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = "1.8" - } - packagingOptions { - resources { - excludes += setOf("META-INF/atomicfu.kotlin_module") - } + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } testOptions { unitTests.isIncludeAndroidResources = true } - namespace = "eventually.client" +} + +kotlin { + jvmToolchain(21) +} + +ksp { + arg("room.schemaLocation", "$projectDir/schemas") +} + +dependencies { + implementation(project(":core")) + + implementation(libs.gson) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.coordinatorlayout) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.navigation.fragment.ktx) + implementation(libs.androidx.navigation.ui.ktx) + implementation(libs.androidx.recyclerview) + implementation(libs.androidx.preference.ktx) + implementation(libs.androidx.material) + implementation(libs.androidx.room.runtime) + implementation(libs.androidx.room.ktx) + implementation(libs.androidx.lifecycle.common.java8) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.lifecycle.livedata.ktx) + implementation(libs.androidx.lifecycle.service) + implementation(libs.crunchycalendar) + implementation(libs.markwon.core) + implementation(libs.markwon.ext.strikethrough) + implementation(libs.markwon.ext.tables) + implementation(libs.markwon.ext.tasklist) + implementation(libs.colorpickerview) + + implementation(libs.hilt.android) + ksp(libs.hilt.compiler) + + ksp(libs.androidx.room.compiler) + + api(libs.kotlinx.coroutines.core) + api(libs.kotlinx.coroutines.android) + + testImplementation(libs.mockk) + testImplementation(libs.robolectric) + testImplementation(libs.junit4) + testImplementation(libs.androidx.test.core) + + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.androidx.test.rules) + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.androidx.test.espresso.core) + androidTestImplementation(libs.androidx.room.testing) + androidTestImplementation(libs.androidx.arch.core.testing) + + debugImplementation(libs.androidx.fragment.testing) { + exclude(group = "androidx.test", module = "monitor") + } } tasks.register("qa") { - dependsOn("check") + dependsOn("check", "compileDebugAndroidTestKotlin") } diff --git a/client-android/schemas/eventually.client.persistence.notifications.NotificationEntityDatabase/1.json b/client-android/schemas/eventually.client.persistence.notifications.NotificationEntityDatabase/1.json new file mode 100644 index 0000000..b17f00c --- /dev/null +++ b/client-android/schemas/eventually.client.persistence.notifications.NotificationEntityDatabase/1.json @@ -0,0 +1,58 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "7240a1045dc9200ce86f4d4ed2417cc7", + "entities": [ + { + "tableName": "notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `task` INTEGER NOT NULL, `instance` TEXT NOT NULL, `type` TEXT NOT NULL, `hash` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "task", + "columnName": "task", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7240a1045dc9200ce86f4d4ed2417cc7')" + ] + } +} \ No newline at end of file diff --git a/client-android/schemas/eventually.client.persistence.schedules.TaskScheduleEntityDatabase/1.json b/client-android/schemas/eventually.client.persistence.schedules.TaskScheduleEntityDatabase/1.json new file mode 100644 index 0000000..b4f6c4f --- /dev/null +++ b/client-android/schemas/eventually.client.persistence.schedules.TaskScheduleEntityDatabase/1.json @@ -0,0 +1,46 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "9bf3e237a764c15a4311e47bd9dedef7", + "entities": [ + { + "tableName": "schedules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`task` INTEGER NOT NULL, `instances` TEXT NOT NULL, `dismissed` TEXT NOT NULL, PRIMARY KEY(`task`))", + "fields": [ + { + "fieldPath": "task", + "columnName": "task", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "instances", + "columnName": "instances", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dismissed", + "columnName": "dismissed", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "task" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9bf3e237a764c15a4311e47bd9dedef7')" + ] + } +} \ No newline at end of file diff --git a/client-android/schemas/eventually.client.persistence.tasks.TaskEntityDatabase/2.json b/client-android/schemas/eventually.client.persistence.tasks.TaskEntityDatabase/2.json new file mode 100644 index 0000000..0a7e7a4 --- /dev/null +++ b/client-android/schemas/eventually.client.persistence.tasks.TaskEntityDatabase/2.json @@ -0,0 +1,76 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "6aeb5644984f1ed79143f32f24d65f91", + "entities": [ + { + "tableName": "tasks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `description` TEXT NOT NULL, `goal` TEXT NOT NULL, `schedule` TEXT NOT NULL, `contextSwitch` INTEGER NOT NULL, `isActive` INTEGER NOT NULL, `color` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "goal", + "columnName": "goal", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schedule", + "columnName": "schedule", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contextSwitch", + "columnName": "contextSwitch", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6aeb5644984f1ed79143f32f24d65f91')" + ] + } +} \ No newline at end of file diff --git a/client-android/src/androidTest/java/eventually/test/client/activities/ActivityModels.kt b/client-android/src/androidTest/java/eventually/test/client/activities/ActivityModels.kt index cb69ea9..ee913f5 100644 --- a/client-android/src/androidTest/java/eventually/test/client/activities/ActivityModels.kt +++ b/client-android/src/androidTest/java/eventually/test/client/activities/ActivityModels.kt @@ -4,10 +4,13 @@ import android.app.Application import androidx.room.Room import androidx.test.core.app.ApplicationProvider import eventually.client.persistence.notifications.NotificationEntityDatabase +import eventually.client.persistence.notifications.NotificationRepository import eventually.client.persistence.notifications.NotificationViewModel import eventually.client.persistence.schedules.TaskScheduleEntityDatabase +import eventually.client.persistence.schedules.TaskScheduleRepository import eventually.client.persistence.schedules.TaskScheduleViewModel import eventually.client.persistence.tasks.TaskEntityDatabase +import eventually.client.persistence.tasks.TaskRepository import eventually.client.persistence.tasks.TaskViewModel object ActivityModels { @@ -42,7 +45,11 @@ object ActivityModels { tasks.clearAllTables() schedules.clearAllTables() notifications.clearAllTables() - f(TaskViewModel(application), TaskScheduleViewModel(application), NotificationViewModel(application)) + f( + TaskViewModel(TaskRepository(application)), + TaskScheduleViewModel(TaskScheduleRepository(application)), + NotificationViewModel(NotificationRepository(application)) + ) } finally { tasks.clearAllTables() schedules.clearAllTables() diff --git a/client-android/src/androidTest/java/eventually/test/client/persistence/notifications/NotificationViewModelSpec.kt b/client-android/src/androidTest/java/eventually/test/client/persistence/notifications/NotificationViewModelSpec.kt index d3f0040..d98880e 100644 --- a/client-android/src/androidTest/java/eventually/test/client/persistence/notifications/NotificationViewModelSpec.kt +++ b/client-android/src/androidTest/java/eventually/test/client/persistence/notifications/NotificationViewModelSpec.kt @@ -6,6 +6,7 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import eventually.client.persistence.notifications.NotificationEntity import eventually.client.persistence.notifications.NotificationEntityDatabase +import eventually.client.persistence.notifications.NotificationRepository import eventually.client.persistence.notifications.NotificationViewModel import eventually.core.model.Task import eventually.core.model.Task.Schedule.Repeating.Interval.Companion.toInterval @@ -31,7 +32,7 @@ class NotificationViewModelSpec { withModel { model -> assertThat(model.notifications.await(), equalTo(emptyList())) - runBlocking { model.put(task, instance, entity.type).await() } + runBlocking { model.put(task, instance, entity.type) } eventually { assertThat( @@ -47,7 +48,7 @@ class NotificationViewModelSpec { withModel { model -> assertThat(model.notifications.await(), equalTo(emptyList())) - runBlocking { model.put(entity).await() } + runBlocking { model.put(entity) } eventually { assertThat( @@ -64,15 +65,15 @@ class NotificationViewModelSpec { assertThat(model.notifications.await(), equalTo(emptyList())) runBlocking { - model.put(task, instance, entity.type).await() + model.put(task, instance, entity.type) assertThat( - model.get(entity.task, entity.instance, entity.type).await()?.copy(id = entity.id), + model.get(entity.task, entity.instance, entity.type)?.copy(id = entity.id), equalTo(entity) ) assertThat( - model.get(entity.task, entity.instance, "execution").await(), + model.get(entity.task, entity.instance, "execution"), equalTo(null) ) } @@ -84,7 +85,7 @@ class NotificationViewModelSpec { withModel { model -> assertThat(model.notifications.await(), equalTo(emptyList())) - val notificationId = runBlocking { model.put(task, instance, entity.type).await() } + val notificationId = runBlocking { model.put(task, instance, entity.type) } eventually { assertThat( @@ -119,8 +120,8 @@ class NotificationViewModelSpec { withModel { model -> assertThat(model.notifications.await(), equalTo(emptyList())) - val contextNotificationId = runBlocking { model.put(task, instance, "context").await() } - val executionNotificationId = runBlocking { model.put(task, instance, "execution").await() } + val contextNotificationId = runBlocking { model.put(task, instance, "context") } + val executionNotificationId = runBlocking { model.put(task, instance, "execution") } eventually { assertThat( @@ -184,7 +185,7 @@ class NotificationViewModelSpec { private fun withModel(f: (model: NotificationViewModel) -> Unit) { val application = ApplicationProvider.getApplicationContext() - val model = NotificationViewModel(application) + val model = NotificationViewModel(NotificationRepository(application)) val db = NotificationEntityDatabase.getInstance(application) try { diff --git a/client-android/src/androidTest/java/eventually/test/client/persistence/schedules/TaskScheduleEntityDaoSpec.kt b/client-android/src/androidTest/java/eventually/test/client/persistence/schedules/TaskScheduleEntityDaoSpec.kt index 986ad55..fc4901d 100644 --- a/client-android/src/androidTest/java/eventually/test/client/persistence/schedules/TaskScheduleEntityDaoSpec.kt +++ b/client-android/src/androidTest/java/eventually/test/client/persistence/schedules/TaskScheduleEntityDaoSpec.kt @@ -59,6 +59,16 @@ class TaskScheduleEntityDaoSpec { } } + @Test + fun retrieveAllScheduleEntities() { + withDao { dao -> + assertThat(runBlocking { dao.getAll() }, equalTo(emptyList())) + + runBlocking { dao.put(entity) } + assertThat(runBlocking { dao.getAll() }, equalTo(listOf(entity))) + } + } + private val instant = Instant.now().truncatedTo(ChronoUnit.SECONDS) private val instance1 = TaskInstance(instant = instant) private val instance2 = TaskInstance(instant = instant.plusSeconds(42)).copy(postponed = Duration.ofMinutes(5)) diff --git a/client-android/src/androidTest/java/eventually/test/client/persistence/schedules/TaskScheduleRepositorySpec.kt b/client-android/src/androidTest/java/eventually/test/client/persistence/schedules/TaskScheduleRepositorySpec.kt index ca6e151..fe9680f 100644 --- a/client-android/src/androidTest/java/eventually/test/client/persistence/schedules/TaskScheduleRepositorySpec.kt +++ b/client-android/src/androidTest/java/eventually/test/client/persistence/schedules/TaskScheduleRepositorySpec.kt @@ -75,6 +75,16 @@ class TaskScheduleRepositorySpec { assertThat(repo.schedules.await(), equalTo(emptyList())) } + @Test + fun retrieveAllSchedules() { + val repo = createRepo() + + assertThat(runBlocking { repo.all() }, equalTo(emptyList())) + + runBlocking { repo.put(schedule) } + assertThat(runBlocking { repo.all() }, equalTo(listOf(entity))) + } + private val instant = Instant.now().truncatedTo(ChronoUnit.SECONDS) private val instance1 = TaskInstance(instant = instant) private val instance2 = TaskInstance(instant = instant.plusSeconds(42)).copy(postponed = Duration.ofMinutes(5)) @@ -130,6 +140,8 @@ class TaskScheduleRepositorySpec { override fun get(): LiveData> = data + override suspend fun getAll(): List = entities.values.toList() + override suspend fun put(entity: TaskScheduleEntity) { entities[entity.task] = entity data.value = entities.toList().map { it.second } diff --git a/client-android/src/androidTest/java/eventually/test/client/persistence/schedules/TaskScheduleViewModelSpec.kt b/client-android/src/androidTest/java/eventually/test/client/persistence/schedules/TaskScheduleViewModelSpec.kt index 683fd30..6e18f8b 100644 --- a/client-android/src/androidTest/java/eventually/test/client/persistence/schedules/TaskScheduleViewModelSpec.kt +++ b/client-android/src/androidTest/java/eventually/test/client/persistence/schedules/TaskScheduleViewModelSpec.kt @@ -6,6 +6,7 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import eventually.client.persistence.schedules.TaskScheduleEntity import eventually.client.persistence.schedules.TaskScheduleEntityDatabase +import eventually.client.persistence.schedules.TaskScheduleRepository import eventually.client.persistence.schedules.TaskScheduleViewModel import eventually.core.model.Task import eventually.core.model.Task.Schedule.Repeating.Interval.Companion.toInterval @@ -33,7 +34,7 @@ class TaskScheduleViewModelSpec { withModel { model -> assertThat(model.schedules.await(), equalTo(emptyList())) - runBlocking { model.put(schedule).await() } + runBlocking { model.put(schedule) } eventually { assertThat(model.schedules.await(), equalTo(listOf(entity))) @@ -46,7 +47,7 @@ class TaskScheduleViewModelSpec { withModel { model -> assertThat(model.schedules.await(), equalTo(emptyList())) - runBlocking { model.put(entity).await() } + runBlocking { model.put(entity) } eventually { assertThat(model.schedules.await(), equalTo(listOf(entity))) @@ -59,7 +60,7 @@ class TaskScheduleViewModelSpec { withModel { model -> assertThat(model.schedules.await(), equalTo(emptyList())) - runBlocking { model.put(schedule).await() } + runBlocking { model.put(schedule) } eventually { assertThat(model.schedules.await(), equalTo(listOf(entity))) @@ -79,7 +80,7 @@ class TaskScheduleViewModelSpec { withModel { model -> assertThat(model.schedules.await(), equalTo(emptyList())) - runBlocking { model.put(schedule).await() } + runBlocking { model.put(schedule) } eventually { assertThat(model.schedules.await(), equalTo(listOf(entity))) @@ -143,7 +144,7 @@ class TaskScheduleViewModelSpec { private fun withModel(f: (model: TaskScheduleViewModel) -> Unit) { val application = ApplicationProvider.getApplicationContext() - val model = TaskScheduleViewModel(application) + val model = TaskScheduleViewModel(TaskScheduleRepository(application)) val db = TaskScheduleEntityDatabase.getInstance(application) try { diff --git a/client-android/src/androidTest/java/eventually/test/client/persistence/tasks/TaskEntityDaoSpec.kt b/client-android/src/androidTest/java/eventually/test/client/persistence/tasks/TaskEntityDaoSpec.kt index dbb46f2..59ae2ba 100644 --- a/client-android/src/androidTest/java/eventually/test/client/persistence/tasks/TaskEntityDaoSpec.kt +++ b/client-android/src/androidTest/java/eventually/test/client/persistence/tasks/TaskEntityDaoSpec.kt @@ -75,7 +75,17 @@ class TaskEntityDaoSpec { } assertThat(dao.get().await().size, equalTo(5)) - assertThat(dao.goals().await().sorted(), equalTo(listOf("goal-1", "goal-2", "goal-3"))) + assertThat(runBlocking { dao.getGoals() }.sorted(), equalTo(listOf("goal-1", "goal-2", "goal-3"))) + } + } + + @Test + fun retrieveAllTaskEntities() { + withDao { dao -> + assertThat(runBlocking { dao.getAll() }, equalTo(emptyList())) + + val taskId = runBlocking { dao.put(entity) } + assertThat(runBlocking { dao.getAll() }, equalTo(listOf(entity.copy(id = taskId.toInt())))) } } diff --git a/client-android/src/androidTest/java/eventually/test/client/persistence/tasks/TaskRepositorySpec.kt b/client-android/src/androidTest/java/eventually/test/client/persistence/tasks/TaskRepositorySpec.kt index e361022..9c9ddc3 100644 --- a/client-android/src/androidTest/java/eventually/test/client/persistence/tasks/TaskRepositorySpec.kt +++ b/client-android/src/androidTest/java/eventually/test/client/persistence/tasks/TaskRepositorySpec.kt @@ -3,7 +3,6 @@ package eventually.test.client.persistence.tasks import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.map import androidx.test.ext.junit.runners.AndroidJUnit4 import eventually.client.persistence.tasks.TaskEntity import eventually.client.persistence.tasks.TaskEntityDao @@ -78,7 +77,17 @@ class TaskRepositorySpec { } assertThat(repo.tasks.await().size, equalTo(5)) - assertThat(repo.goals.await().sorted(), equalTo(listOf("goal-1", "goal-2", "goal-3"))) + assertThat(runBlocking { repo.goals() }.sorted(), equalTo(listOf("goal-1", "goal-2", "goal-3"))) + } + + @Test + fun retrieveAllTasks() { + val repo = createRepo() + + assertThat(runBlocking { repo.all() }, equalTo(emptyList())) + + val taskId = runBlocking { repo.put(task) } + assertThat(runBlocking { repo.all() }, equalTo(listOf(task.copy(id = taskId.toInt())))) } @get:Rule @@ -105,7 +114,9 @@ class TaskRepositorySpec { override fun get(): LiveData> = data - override fun goals(): LiveData> = data.map { it.map { t -> t.goal }.distinct() } + override suspend fun getAll(): List = entities.values.toList() + + override suspend fun getGoals(): List = entities.values.map { it.goal }.distinct() override suspend fun put(entity: TaskEntity): Long { val id = entities[entity.id]?.id ?: (entities.size + 1) diff --git a/client-android/src/androidTest/java/eventually/test/client/persistence/tasks/TaskViewModelSpec.kt b/client-android/src/androidTest/java/eventually/test/client/persistence/tasks/TaskViewModelSpec.kt index 20aa4b5..106393b 100644 --- a/client-android/src/androidTest/java/eventually/test/client/persistence/tasks/TaskViewModelSpec.kt +++ b/client-android/src/androidTest/java/eventually/test/client/persistence/tasks/TaskViewModelSpec.kt @@ -5,6 +5,7 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import eventually.client.persistence.tasks.TaskEntityDatabase +import eventually.client.persistence.tasks.TaskRepository import eventually.client.persistence.tasks.TaskViewModel import eventually.core.model.Task import eventually.core.model.Task.Schedule.Repeating.Interval.Companion.toInterval @@ -28,7 +29,7 @@ class TaskViewModelSpec { withModel { model -> assertThat(model.tasks.await(), equalTo(emptyList())) - val taskId = runBlocking { model.put(task).await() } + val taskId = runBlocking { model.put(task) } eventually { assertThat(model.tasks.await(), equalTo(listOf(task.copy(id = taskId.toInt())))) @@ -41,7 +42,7 @@ class TaskViewModelSpec { withModel { model -> assertThat(model.tasks.await(), equalTo(emptyList())) - val taskId = runBlocking { model.put(task).await() } + val taskId = runBlocking { model.put(task) } eventually { assertThat(model.tasks.await(), equalTo(listOf(task.copy(id = taskId.toInt())))) @@ -61,7 +62,7 @@ class TaskViewModelSpec { withModel { model -> assertThat(model.tasks.await(), equalTo(emptyList())) - val taskId = runBlocking { model.put(task).await() } + val taskId = runBlocking { model.put(task) } eventually { assertThat(model.tasks.await(), equalTo(listOf(task.copy(id = taskId.toInt())))) @@ -81,15 +82,15 @@ class TaskViewModelSpec { assertThat(model.tasks.await(), equalTo(emptyList())) runBlocking { - model.put(task.copy(goal = "goal-1")).await() - model.put(task.copy(goal = "goal-2")).await() - model.put(task.copy(goal = "goal-3")).await() - model.put(task.copy(goal = "goal-2")).await() - model.put(task.copy(goal = "goal-3")).await() + model.put(task.copy(goal = "goal-1")) + model.put(task.copy(goal = "goal-2")) + model.put(task.copy(goal = "goal-3")) + model.put(task.copy(goal = "goal-2")) + model.put(task.copy(goal = "goal-3")) } assertThat(model.tasks.await().size, equalTo(5)) - assertThat(model.goals.await().sorted(), equalTo(listOf("goal-1", "goal-2", "goal-3"))) + assertThat(runBlocking { model.goals() }.sorted(), equalTo(listOf("goal-1", "goal-2", "goal-3"))) } } @@ -112,7 +113,7 @@ class TaskViewModelSpec { private fun withModel(f: (model: TaskViewModel) -> Unit) { val application = ApplicationProvider.getApplicationContext() - val model = TaskViewModel(application) + val model = TaskViewModel(TaskRepository(application)) val db = TaskEntityDatabase.getInstance(application) try { diff --git a/client-android/src/main/AndroidManifest.xml b/client-android/src/main/AndroidManifest.xml index 8ee6f0a..85db6c5 100644 --- a/client-android/src/main/AndroidManifest.xml +++ b/client-android/src/main/AndroidManifest.xml @@ -3,9 +3,15 @@ - + + + + + android:exported="false" + android:foregroundServiceType="specialUse"> + + + + + + + + @@ -61,6 +81,7 @@ @@ -71,6 +92,7 @@ diff --git a/client-android/src/main/java/eventually/client/EventuallyApplication.kt b/client-android/src/main/java/eventually/client/EventuallyApplication.kt new file mode 100644 index 0000000..f732fdb --- /dev/null +++ b/client-android/src/main/java/eventually/client/EventuallyApplication.kt @@ -0,0 +1,7 @@ +package eventually.client + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class EventuallyApplication : Application() diff --git a/client-android/src/main/java/eventually/client/activities/MainActivity.kt b/client-android/src/main/java/eventually/client/activities/MainActivity.kt index 4adcf92..b34cd9c 100644 --- a/client-android/src/main/java/eventually/client/activities/MainActivity.kt +++ b/client-android/src/main/java/eventually/client/activities/MainActivity.kt @@ -1,11 +1,18 @@ package eventually.client.activities +import android.Manifest +import android.app.AlarmManager import android.app.NotificationManager import android.app.SearchManager import android.content.Context import android.content.Intent import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build import android.os.Bundle +import android.provider.Settings as AndroidSettings +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.SearchView import androidx.core.content.ContextCompat @@ -15,18 +22,22 @@ import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.appbar.MaterialToolbar +import com.google.android.material.snackbar.Snackbar import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator +import dagger.hilt.android.AndroidEntryPoint import eventually.client.R import eventually.client.activities.fragments.TaskCalendarFragment import eventually.client.activities.fragments.TaskListFragment import eventually.client.activities.fragments.TaskListItemAdapter import eventually.client.activities.fragments.TaskSummaryFragment +import eventually.client.scheduling.AlarmManagerExtensions.canUseExactAlarms import eventually.client.scheduling.NotificationManagerExtensions.createInstanceNotificationChannels import eventually.client.scheduling.SchedulerService import eventually.client.settings.Settings import eventually.client.settings.Settings.getStatsEnabled +@AndroidEntryPoint class MainActivity : AppCompatActivity(), SharedPreferences.OnSharedPreferenceChangeListener { private lateinit var pager: ViewPager2 @@ -34,8 +45,15 @@ class MainActivity : AppCompatActivity(), SharedPreferences.OnSharedPreferenceCh private lateinit var topAppBar: MaterialToolbar + private var exactAlarmPromptShown: Boolean = false + + private val notificationPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) {} + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + exactAlarmPromptShown = savedInstanceState?.getBoolean(StateExactAlarmPromptShown) ?: false setContentView(R.layout.activity_main) pager = findViewById(R.id.pager) @@ -147,10 +165,22 @@ class MainActivity : AppCompatActivity(), SharedPreferences.OnSharedPreferenceCh notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.createInstanceNotificationChannels(applicationContext) + requestNotificationPermissionIfNeeded() + startForegroundService(Intent(this, SchedulerService::class.java)) evaluate() } + override fun onResume() { + super.onResume() + promptForExactAlarmsIfNeeded() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putBoolean(StateExactAlarmPromptShown, exactAlarmPromptShown) + } + override fun onSharedPreferenceChanged(updated: SharedPreferences, key: String?) { if (key == Settings.Keys.StatsEnabled) { topAppBar.menu.findItem(R.id.stats).isVisible = updated.getStatsEnabled() @@ -164,6 +194,39 @@ class MainActivity : AppCompatActivity(), SharedPreferences.OnSharedPreferenceCh startService(intent) } + private fun requestNotificationPermissionIfNeeded() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return + + val granted = ContextCompat.checkSelfPermission( + this, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + + if (!granted) { + notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + } + + private fun promptForExactAlarmsIfNeeded() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return + if (exactAlarmPromptShown) return + + val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager + if (alarmManager.canUseExactAlarms()) return + + exactAlarmPromptShown = true + + Snackbar + .make(pager, R.string.exact_alarm_prompt_message, Snackbar.LENGTH_LONG) + .setAction(R.string.exact_alarm_prompt_action) { + val intent = Intent(AndroidSettings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM).apply { + data = Uri.fromParts("package", packageName, null) + } + runCatching { startActivity(intent) } + } + .show() + } + private sealed class Position { object Summary : Position() object List : Position() @@ -179,4 +242,9 @@ class MainActivity : AppCompatActivity(), SharedPreferences.OnSharedPreferenceCh } } } + + companion object { + private const val StateExactAlarmPromptShown: String = + "eventually.client.activities.MainActivity.state_exact_alarm_prompt_shown" + } } diff --git a/client-android/src/main/java/eventually/client/activities/NewTaskActivity.kt b/client-android/src/main/java/eventually/client/activities/NewTaskActivity.kt index bfa424c..1f7f5e5 100644 --- a/client-android/src/main/java/eventually/client/activities/NewTaskActivity.kt +++ b/client-android/src/main/java/eventually/client/activities/NewTaskActivity.kt @@ -3,23 +3,27 @@ package eventually.client.activities import android.os.Bundle import android.widget.Button import android.widget.Toast +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil -import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import com.google.android.material.appbar.MaterialToolbar +import dagger.hilt.android.AndroidEntryPoint import eventually.client.R import eventually.client.activities.helpers.TaskDetails.initTaskDetails import eventually.client.activities.helpers.TaskManagement import eventually.client.databinding.ActivityNewTaskBinding import eventually.client.persistence.tasks.TaskViewModel +import kotlinx.coroutines.launch import java.time.Instant +@AndroidEntryPoint class NewTaskActivity : AppCompatActivity() { + private val taskViewModel: TaskViewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val taskViewModel = ViewModelProvider(this).get(TaskViewModel::class.java) - val date = when (val date = intent.extras?.getLong(ExtraDate)) { null -> null else -> when { @@ -28,10 +32,10 @@ class NewTaskActivity : AppCompatActivity() { } } - taskViewModel.goals.observe(this) { goals -> - taskViewModel.goals.removeObservers(this) + lifecycleScope.launch { + val goals = taskViewModel.goals() - val binding: ActivityNewTaskBinding = DataBindingUtil.setContentView(this, R.layout.activity_new_task) + val binding: ActivityNewTaskBinding = DataBindingUtil.setContentView(this@NewTaskActivity, R.layout.activity_new_task) findViewById(R.id.topAppBar).setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() diff --git a/client-android/src/main/java/eventually/client/activities/StatsActivity.kt b/client-android/src/main/java/eventually/client/activities/StatsActivity.kt index 34dfd6f..e93388c 100644 --- a/client-android/src/main/java/eventually/client/activities/StatsActivity.kt +++ b/client-android/src/main/java/eventually/client/activities/StatsActivity.kt @@ -15,11 +15,12 @@ import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.Toast +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil import androidx.fragment.app.DialogFragment -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope +import dagger.hilt.android.AndroidEntryPoint import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.textfield.TextInputLayout import eventually.client.R @@ -38,10 +39,11 @@ import kotlinx.coroutines.launch import java.time.ZoneId import java.time.format.DateTimeFormatter +@AndroidEntryPoint class StatsActivity : AppCompatActivity() { - private lateinit var taskViewModel: TaskViewModel - private lateinit var taskScheduleViewModel: TaskScheduleViewModel - private lateinit var notificationViewModel: NotificationViewModel + private val taskViewModel: TaskViewModel by viewModels() + private val taskScheduleViewModel: TaskScheduleViewModel by viewModels() + private val notificationViewModel: NotificationViewModel by viewModels() private lateinit var binding: ActivityStatsBinding @@ -117,10 +119,6 @@ class StatsActivity : AppCompatActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.activity_stats) - taskViewModel = ViewModelProvider(this)[TaskViewModel::class.java] - taskScheduleViewModel = ViewModelProvider(this)[TaskScheduleViewModel::class.java] - notificationViewModel = ViewModelProvider(this)[NotificationViewModel::class.java] - binding = DataBindingUtil.setContentView(this, R.layout.activity_stats) taskViewModel.tasks.observe(this) { tasks -> @@ -224,9 +222,9 @@ class StatsActivity : AppCompatActivity() { private fun importData(data: DataExport) { lifecycleScope.launch(Dispatchers.IO) { - data.tasks.forEach { taskViewModel.put(it).join() } - data.schedules.forEach { taskScheduleViewModel.put(it).join() } - data.notifications.forEach { notificationViewModel.put(it).join() } + data.tasks.forEach { taskViewModel.put(it) } + data.schedules.forEach { taskScheduleViewModel.put(it) } + data.notifications.forEach { notificationViewModel.put(it) } val intent = Intent(this@StatsActivity, SchedulerService::class.java) intent.action = SchedulerService.ActionReload diff --git a/client-android/src/main/java/eventually/client/activities/TaskDetailsActivity.kt b/client-android/src/main/java/eventually/client/activities/TaskDetailsActivity.kt index ec68d5b..53b37ab 100644 --- a/client-android/src/main/java/eventually/client/activities/TaskDetailsActivity.kt +++ b/client-android/src/main/java/eventually/client/activities/TaskDetailsActivity.kt @@ -12,13 +12,15 @@ import android.text.style.StyleSpan import android.view.View import android.widget.Button import android.widget.Toast +import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isVisible +import androidx.activity.viewModels import androidx.databinding.DataBindingUtil import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.ViewModelProvider import androidx.preference.PreferenceManager +import dagger.hilt.android.AndroidEntryPoint import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.floatingactionbutton.FloatingActionButton @@ -40,18 +42,25 @@ import eventually.core.model.Task import eventually.core.model.TaskSchedule import java.time.Instant +@AndroidEntryPoint class TaskDetailsActivity : AppCompatActivity() { private lateinit var service: SchedulerService private var serviceConnected: Boolean = false - private lateinit var taskViewModel: TaskViewModel + private val taskViewModel: TaskViewModel by viewModels() private lateinit var topAppBar: MaterialToolbar private lateinit var binding: ActivityTaskDetailsBinding private lateinit var deleteButton: FloatingActionButton private lateinit var notificationManager: NotificationManager + private val exitEditingOnBack = object : OnBackPressedCallback(enabled = false) { + override fun handleOnBackPressed() { + disableEditing() + } + } + private val serviceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName?, service: IBinder?) { val binder = service as SchedulerService.SchedulerBinder @@ -80,19 +89,11 @@ class TaskDetailsActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - taskViewModel = ViewModelProvider(this).get(TaskViewModel::class.java) binding = DataBindingUtil.setContentView(this, R.layout.activity_task_details) deleteButton = findViewById(R.id.button_delete) notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - } - @Deprecated("Deprecated") - override fun onBackPressed() { - if (isEditingEnabled()) { - disableEditing() - } else { - onBackPressedDispatcher.onBackPressed() - } + onBackPressedDispatcher.addCallback(this, exitEditingOnBack) } private fun updateView() { @@ -132,11 +133,7 @@ class TaskDetailsActivity : AppCompatActivity() { } topAppBar.setNavigationOnClickListener { - if (isEditingEnabled()) { - disableEditing() - } else { - onBackPressedDispatcher.onBackPressed() - } + onBackPressedDispatcher.onBackPressed() } deleteButton.setOnClickListener { @@ -209,28 +206,30 @@ class TaskDetailsActivity : AppCompatActivity() { handlers = handlers ) - initTaskDetails( - binding = binding.details, - task = task, - default = Instant.now(), - goals = tasks.map { it.goal }, - operation = getString(R.string.existing_task_update_action) - ).let { fields -> - task?.let { - topAppBar.subtitle = task.name - - val button = findViewById