From 6e19eb943d271094e919d06a10614f2eb5f4bfb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Mon, 29 Mar 2021 17:37:26 +0200 Subject: [PATCH 001/869] Add deploy to AppGallery github actions config (#1259) --- .github/workflows/test.yml | 38 ++++++++++++++++++++++++++++++++++++++ app/build.gradle | 12 ++++++++++++ build.gradle | 1 + 3 files changed, 51 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a81b333f3..c26a50bca 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -69,6 +69,44 @@ jobs: PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }} run: ./gradlew publishPlayRelease -PenableFirebase --stacktrace; + deploy-app-gallery: + name: Deploy to AppGallery + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: app-gallery + needs: [ unit-tests ] + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + with: + java-version: 11 + - uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }} + - name: Decrypt keys + env: + ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }} + SERVICES_ENCRYPT_KEY: ${{ secrets.SERVICES_ENCRYPT_KEY }} + run: | + gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/agconnect-services.json.gpg + gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/key.p12.gpg + gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg + - name: Build HMS version + env: + PLAY_KEY_ALIAS: ${{ secrets.PLAY_KEY_ALIAS }} + PLAY_KEY_PASSWORD: ${{ secrets.PLAY_KEY_PASSWORD }} + PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }} + run: ./gradlew assembleHmsRelease --stacktrace + - name: Upload APK to AppGallery + env: + AGC_CLIENT_ID: ${{ secrets.AGC_CLIENT_ID }} + AGC_CLIENT_SECRET: ${{ secrets.AGC_CLIENT_SECRET }} + run: ./gradlew publishHuaweiAppGalleryHmsRelease --stacktrace; + deploy-appcenter: name: Deploy to App Center runs-on: ubuntu-latest diff --git a/app/build.gradle b/app/build.gradle index 80496ff1e..a9e9ed76c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,6 +4,7 @@ apply plugin: 'kotlin-kapt' apply plugin: 'dagger.hilt.android.plugin' apply plugin: 'com.google.firebase.crashlytics' apply plugin: 'com.github.triplet.play' +apply plugin: 'ru.cian.huawei-publish' apply plugin: 'com.mikepenz.aboutlibraries.plugin' apply plugin: 'com.google.gms.google-services' apply plugin: 'com.huawei.agconnect' @@ -140,6 +141,17 @@ play { updatePriority = 3 } +huaweiPublish { + instances { + hmsRelease { + clientId = System.getenv("AGC_CLIENT_ID") + clientSecret = System.getenv("AGC_CLIENT_SECRET") + buildFormat = "apk" + deployType = "draft" + } + } +} + ext { work_manager = "2.5.0" work_hilt = "1.0.0-beta01" diff --git a/build.gradle b/build.gradle index 21ae47e7e..222ea1779 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,7 @@ buildscript { classpath 'com.huawei.agconnect:agcp:1.5.1.200' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.1' classpath "com.github.triplet.gradle:play-publisher:2.8.0" + classpath "ru.cian:huawei-publish-gradle-plugin:1.2.2" classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.1.1" classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0" classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries" From fada13e2d31396e04ea0682297bf22adf47b6657 Mon Sep 17 00:00:00 2001 From: Mateusz Idziejczak Date: Mon, 29 Mar 2021 18:39:08 +0200 Subject: [PATCH 002/869] Update issue templates (#1257) --- .../bug_report.md} | 9 +++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 +++++++++++++++++++ 2 files changed, 29 insertions(+) rename .github/{ISSUE_TEMPLATE.md => ISSUE_TEMPLATE/bug_report.md} (61%) create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE/bug_report.md similarity index 61% rename from .github/ISSUE_TEMPLATE.md rename to .github/ISSUE_TEMPLATE/bug_report.md index 27d57f599..237721cb0 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,3 +1,12 @@ +--- +name: Bug report +about: Utwórz raport błędu, aby pomóc nam ulepszyć Wulkanowego +title: '' +labels: '' +assignees: '' + +--- + ## Co powinno się dziać diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..6194a41e9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Zaproponuj nowy pomysł dla Wulkanowego +title: '' +labels: '' +assignees: '' + +--- + +** Czy Twoja prośba o funkcję jest związana z problemem? Proszę opisz.** +Jasny i zwięzły opis problemu. Np. Zawsze jestem sfrustrowany, gdy [...] + +** Opisz żądane rozwiązanie ** +Jasny i zwięzły opis tego, co chcesz, aby się wydarzyło. + +** Opisz alternatywy, które rozważałeś ** +Jasny i zwięzły opis wszelkich rozważanych alternatywnych rozwiązań lub funkcji. + +** Dodatkowy kontekst ** +Dodaj inny kontekst lub zrzuty ekranu dotyczące żądania funkcji tutaj. From 8a5ca8c91fc024d3b9a258630911be63a2cbf1f7 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 30 Mar 2021 10:48:12 +0000 Subject: [PATCH 003/869] Bump firebase-bom from 26.7.0 to 26.8.0 (#1263) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index a9e9ed76c..acb7f1125 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -218,7 +218,7 @@ dependencies { implementation "io.github.wulkanowy:AppKillerManager:3.0.0" implementation 'me.xdrop:fuzzywuzzy:1.3.1' - playImplementation platform('com.google.firebase:firebase-bom:26.7.0') + playImplementation platform('com.google.firebase:firebase-bom:26.8.0') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' From f2130998eca6b1c4423468966ee74cc55fb4afbb Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 30 Mar 2021 10:49:40 +0000 Subject: [PATCH 004/869] Bump firebase-crashlytics-gradle from 2.5.1 to 2.5.2 (#1264) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 222ea1779..9f74cc3d3 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.5' classpath 'com.huawei.agconnect:agcp:1.5.1.200' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.1' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.2' classpath "com.github.triplet.gradle:play-publisher:2.8.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.2.2" classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.1.1" From 3071e1958480b251ce27484c9659151daa88d0af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Studzi=C5=84ski?= <48914870+studzinskik@users.noreply.github.com> Date: Tue, 30 Mar 2021 13:59:36 +0200 Subject: [PATCH 005/869] Implement a toggleable setting to count an arithmetic average of grades when all weights are equal to zero (#1186) --- .../repositories/PreferencesRepository.kt | 6 ++ .../ui/modules/grade/GradeAverageProvider.kt | 12 ++-- .../github/wulkanowy/utils/GradeExtension.kt | 9 ++- .../main/res/values/preferences_defaults.xml | 1 + app/src/main/res/values/preferences_keys.xml | 1 + app/src/main/res/values/strings.xml | 1 + .../res/xml/scheme_preferences_advanced.xml | 6 ++ .../modules/grade/GradeAverageProviderTest.kt | 70 ++++++++++++++++++- .../wulkanowy/utils/GradeExtensionTest.kt | 2 +- 9 files changed, 97 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index 5bd1f3c12..375dd62e7 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -146,6 +146,12 @@ class PreferencesRepository @Inject constructor( R.bool.pref_default_subjects_without_grades ) + val isOptionalArithmeticAverage: Boolean + get() = getBoolean( + R.string.pref_key_optional_arithmetic_average, + R.bool.pref_default_optional_arithmetic_average + ) + var isKitkatDialogDisabled: Boolean get() = sharedPref.getBoolean("kitkat_dialog_disabled", false) set(value) = sharedPref.edit { putBoolean("kitkat_dialog_disabled", value) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt index 7e9b56b94..67472fa34 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt @@ -34,6 +34,8 @@ class GradeAverageProvider @Inject constructor( private val minusModifier get() = preferencesRepository.gradeMinusModifier + private val isOptionalArithmeticAverage get() = preferencesRepository.isOptionalArithmeticAverage + fun getGradesDetailsWithAverage(student: Student, semesterId: Int, forceRefresh: Boolean) = flowWithResourceIn { val semesters = semesterRepository.getSemesters(student) @@ -130,7 +132,7 @@ class GradeAverageProvider @Inject constructor( val updatedFirstSemesterGrades = firstSemesterSubject?.grades?.updateModifiers(student).orEmpty() - (updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage() + (updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage(isOptionalArithmeticAverage) } else { secondSemesterSubject.average } @@ -146,9 +148,9 @@ class GradeAverageProvider @Inject constructor( return if (!isAnyVulcanAverage || gradeAverageForceCalc) { val secondSemesterAverage = - secondSemesterSubject.grades.updateModifiers(student).calcAverage() + secondSemesterSubject.grades.updateModifiers(student).calcAverage(isOptionalArithmeticAverage) val firstSemesterAverage = firstSemesterSubject?.grades?.updateModifiers(student) - ?.calcAverage() ?: secondSemesterAverage + ?.calcAverage(isOptionalArithmeticAverage) ?: secondSemesterAverage (secondSemesterAverage + firstSemesterAverage) / divider } else { @@ -179,7 +181,7 @@ class GradeAverageProvider @Inject constructor( GradeSubject( subject = summary.subject, average = if (!isAnyAverage || gradeAverageForceCalc) { - grades.updateModifiers(student).calcAverage() + grades.updateModifiers(student).calcAverage(isOptionalArithmeticAverage) } else summary.average, points = summary.pointsSum, summary = summary, @@ -211,7 +213,7 @@ class GradeAverageProvider @Inject constructor( proposedPoints = "", finalPoints = "", pointsSum = "", - average = if (calcAverage) details.updateModifiers(student).calcAverage() else .0 + average = if (calcAverage) details.updateModifiers(student).calcAverage(isOptionalArithmeticAverage) else .0 ) } } diff --git a/app/src/main/java/io/github/wulkanowy/utils/GradeExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/GradeExtension.kt index 6facb5ef6..820e7f435 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/GradeExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/GradeExtension.kt @@ -3,14 +3,17 @@ package io.github.wulkanowy.utils import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.GradeSummary +import io.github.wulkanowy.sdk.scrapper.grades.* -fun List.calcAverage(): Double { +fun List.calcAverage(isOptionalArithmeticAverage: Boolean): Double { + val isArithmeticAverage = isOptionalArithmeticAverage && !any { it.weightValue != .0 } var counter = 0.0 var denominator = 0.0 forEach { - counter += (it.value + it.modifier) * it.weightValue - denominator += it.weightValue + val weight = if (isArithmeticAverage && isGradeValid(it.entry)) 1.0 else it.weightValue + counter += (it.value + it.modifier) * weight + denominator += weight } return if (denominator != 0.0) counter / denominator else 0.0 } diff --git a/app/src/main/res/values/preferences_defaults.xml b/app/src/main/res/values/preferences_defaults.xml index fb82e0ed6..a3aa62f81 100644 --- a/app/src/main/res/values/preferences_defaults.xml +++ b/app/src/main/res/values/preferences_defaults.xml @@ -24,4 +24,5 @@ false false false + false diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index 0cfa485e3..1d00dcd6b 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -26,4 +26,5 @@ timetable_show_timers homework_fullscreen subjects_without_grades + optional_arithmetic_average diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c4109d2b8..1b7c9e427 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -507,6 +507,7 @@ Value of the plus Value of the minus Reply with message history + Show arithmetic average when no weights provided Advanced Appearance & Behavior diff --git a/app/src/main/res/xml/scheme_preferences_advanced.xml b/app/src/main/res/xml/scheme_preferences_advanced.xml index 1d7e9b832..461037871 100644 --- a/app/src/main/res/xml/scheme_preferences_advanced.xml +++ b/app/src/main/res/xml/scheme_preferences_advanced.xml @@ -25,6 +25,12 @@ app:key="@string/pref_key_grade_average_force_calc" app:singleLineTitle="false" app:title="@string/pref_view_grade_average_force_calc" /> + () to emptyList() } @@ -227,6 +271,7 @@ class GradeAverageProviderTest { @Test fun `force calc current semester average with default modifiers in scraper mode`() { every { preferencesRepository.gradeAverageForceCalc } returns true + every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flowWithResource { secondGradeWithModifier to secondSummariesWithModifier } @@ -241,6 +286,7 @@ class GradeAverageProviderTest { val student = student.copy(loginMode = Sdk.Mode.SCRAPPER.name) every { preferencesRepository.gradeAverageForceCalc } returns true + every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeMinusModifier } returns .33 every { preferencesRepository.gradePlusModifier } returns .33 @@ -258,6 +304,7 @@ class GradeAverageProviderTest { val student = student.copy(loginMode = Sdk.Mode.API.name) every { preferencesRepository.gradeAverageForceCalc } returns true + every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeMinusModifier } returns .33 // useless in this mode every { preferencesRepository.gradePlusModifier } returns .33 @@ -275,6 +322,7 @@ class GradeAverageProviderTest { val student = student.copy(loginMode = Sdk.Mode.HYBRID.name) every { preferencesRepository.gradeAverageForceCalc } returns true + every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeMinusModifier } returns .33 // useless in this mode every { preferencesRepository.gradePlusModifier } returns .33 @@ -290,6 +338,7 @@ class GradeAverageProviderTest { @Test fun `calc current semester average`() { every { preferencesRepository.gradeAverageForceCalc } returns false + every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flowWithResource { secondGrades to secondSummaries } @@ -303,6 +352,7 @@ class GradeAverageProviderTest { @Test fun `force calc current semester average`() { every { preferencesRepository.gradeAverageForceCalc } returns true + every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ONE_SEMESTER coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flowWithResource { secondGrades to secondSummaries } @@ -316,6 +366,7 @@ class GradeAverageProviderTest { @Test fun `force calc full year average when current is first`() { every { preferencesRepository.gradeAverageForceCalc } returns true + every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flowWithResource { firstGrades to firstSummaries } @@ -329,6 +380,7 @@ class GradeAverageProviderTest { @Test fun `calc full year average when current is first with load from cache sequence`() { every { preferencesRepository.gradeAverageForceCalc } returns true + every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flow { @@ -361,6 +413,7 @@ class GradeAverageProviderTest { fun `calc both semesters average`() { every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS every { preferencesRepository.gradeAverageForceCalc } returns false + every { preferencesRepository.isOptionalArithmeticAverage } returns false coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flowWithResource { firstGrades to listOf( getSummary(22, "Matematyka", 3.0), @@ -384,6 +437,7 @@ class GradeAverageProviderTest { @Test fun `calc both semesters average when current is second with load from cache sequence`() { every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS + every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageForceCalc } returns false coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flow { emit(Resource.loading()) @@ -431,6 +485,7 @@ class GradeAverageProviderTest { @Test fun `force calc full year average`() { every { preferencesRepository.gradeAverageForceCalc } returns true + every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flowWithResource { firstGrades to firstSummaries } coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flowWithResource { @@ -450,6 +505,7 @@ class GradeAverageProviderTest { @Test fun `calc full year average when current is second with load from cache sequence`() { every { preferencesRepository.gradeAverageForceCalc } returns true + every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flow { @@ -492,6 +548,7 @@ class GradeAverageProviderTest { @Test fun `calc both semesters average when no summaries`() { every { preferencesRepository.gradeAverageForceCalc } returns true + every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flowWithResource { firstGrades to emptyList() } @@ -507,6 +564,7 @@ class GradeAverageProviderTest { @Test fun `force calc full year average when no summaries`() { every { preferencesRepository.gradeAverageForceCalc } returns true + every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flowWithResource { firstGrades to emptyList() } @@ -522,6 +580,7 @@ class GradeAverageProviderTest { @Test fun `calc both semesters average when missing summaries in both semesters`() { every { preferencesRepository.gradeAverageForceCalc } returns false + every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flowWithResource { @@ -545,6 +604,7 @@ class GradeAverageProviderTest { @Test fun `calc both semesters average when missing summary in second semester`() { every { preferencesRepository.gradeAverageForceCalc } returns false + every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flowWithResource { firstGrades to firstSummaries } @@ -560,6 +620,7 @@ class GradeAverageProviderTest { @Test fun `calc both semesters average when missing summary in first semester`() { every { preferencesRepository.gradeAverageForceCalc } returns false + every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flowWithResource { firstGrades to firstSummaries.dropLast(1) } @@ -575,6 +636,7 @@ class GradeAverageProviderTest { @Test fun `force calc full year average when missing summary in first semester`() { every { preferencesRepository.gradeAverageForceCalc } returns true + every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flowWithResource { firstGrades to firstSummaries.dropLast(1) } @@ -591,6 +653,7 @@ class GradeAverageProviderTest { fun `force calc both semesters average with different average from all grades and from two semesters`() { every { preferencesRepository.gradeAverageForceCalc } returns true every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.BOTH_SEMESTERS + every { preferencesRepository.isOptionalArithmeticAverage } returns false coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flowWithResource { listOf( @@ -620,6 +683,7 @@ class GradeAverageProviderTest { @Test fun `force calc full year average with different average from all grades and from two semesters`() { every { preferencesRepository.gradeAverageForceCalc } returns true + every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeAverageMode } returns GradeAverageMode.ALL_YEAR coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flowWithResource { @@ -652,6 +716,7 @@ class GradeAverageProviderTest { val student = student.copy(loginMode = Sdk.Mode.SCRAPPER.name) every { preferencesRepository.gradeAverageForceCalc } returns true + every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeMinusModifier } returns .33 every { preferencesRepository.gradePlusModifier } returns .5 @@ -688,6 +753,7 @@ class GradeAverageProviderTest { val student = student.copy(loginMode = Sdk.Mode.SCRAPPER.name) every { preferencesRepository.gradeAverageForceCalc } returns true + every { preferencesRepository.isOptionalArithmeticAverage } returns false every { preferencesRepository.gradeMinusModifier } returns .33 every { preferencesRepository.gradePlusModifier } returns .5 @@ -719,7 +785,7 @@ class GradeAverageProviderTest { assertEquals(5.5555, items.single { it.subject == "Fizyka" }.average, .0001) // (from details): 5.72727272 + 4,8 → .average() } - private fun getGrade(semesterId: Int, subject: String, value: Double, modifier: Double = 0.0, weight: Double = 1.0): Grade { + private fun getGrade(semesterId: Int, subject: String, value: Double, modifier: Double = 0.0, weight: Double = 1.0, entry: String = ""): Grade { return Grade( studentId = 101, semesterId = semesterId, @@ -731,7 +797,7 @@ class GradeAverageProviderTest { date = now(), weight = "", gradeSymbol = "", - entry = "", + entry = entry, description = "", comment = "", color = "" diff --git a/app/src/test/java/io/github/wulkanowy/utils/GradeExtensionTest.kt b/app/src/test/java/io/github/wulkanowy/utils/GradeExtensionTest.kt index 958b9169b..32b1602e9 100644 --- a/app/src/test/java/io/github/wulkanowy/utils/GradeExtensionTest.kt +++ b/app/src/test/java/io/github/wulkanowy/utils/GradeExtensionTest.kt @@ -28,7 +28,7 @@ class GradeExtensionTest { createGrade(4.0, 1.0, 0.0), createGrade(1.0, 9.0, 0.5), createGrade(0.0, .0, 0.0) - ).calcAverage(), 0.005) + ).calcAverage(false), 0.005) } @Test From 0bdd33ef4a54e34e2ef2372456de4915139e088f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sat, 3 Apr 2021 11:46:36 +0200 Subject: [PATCH 006/869] Migrate to material components bottom navigation (#1244) --- app/build.gradle | 1 - .../wulkanowy/ui/modules/main/MainActivity.kt | 43 +++++++------------ .../ui/widgets/SwipeDisabledViewPager.kt | 19 ++++++++ app/src/main/res/layout/activity_login.xml | 6 +-- app/src/main/res/layout/activity_main.xml | 9 ++-- app/src/main/res/values/dimens.xml | 5 --- app/src/main/res/values/styles.xml | 4 ++ app/src/main/res/xml/identificators.xml | 9 ---- 8 files changed, 47 insertions(+), 49 deletions(-) create mode 100644 app/src/main/java/io/github/wulkanowy/ui/widgets/SwipeDisabledViewPager.kt delete mode 100644 app/src/main/res/values/dimens.xml delete mode 100644 app/src/main/res/xml/identificators.xml diff --git a/app/build.gradle b/app/build.gradle index e5e51b106..c29a25c90 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -202,7 +202,6 @@ dependencies { implementation "androidx.hilt:hilt-work:$work_hilt" kapt "androidx.hilt:hilt-compiler:$work_hilt" - implementation "com.aurelhubert:ahbottomnavigation:2.3.4" implementation "com.ncapdevi:frag-nav:3.3.0" implementation "com.github.YarikSOffice:lingver:1.3.0" diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt index 37c6c6e75..741cfb180 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt @@ -26,8 +26,6 @@ import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat -import com.aurelhubert.ahbottomnavigation.AHBottomNavigation.TitleState.ALWAYS_SHOW -import com.aurelhubert.ahbottomnavigation.AHBottomNavigationItem import com.google.android.material.elevation.ElevationOverlayProvider import com.ncapdevi.fragnav.FragNavController import com.ncapdevi.fragnav.FragNavController.Companion.HIDE @@ -220,32 +218,21 @@ class MainActivity : BaseActivity(), MainVie } with(binding.mainBottomNav) { - addItems( - listOf( - AHBottomNavigationItem(R.string.grade_title, R.drawable.ic_main_grade, 0), - AHBottomNavigationItem( - R.string.attendance_title, - R.drawable.ic_main_attendance, - 0 - ), - AHBottomNavigationItem(R.string.exam_title, R.drawable.ic_main_exam, 0), - AHBottomNavigationItem( - R.string.timetable_title, - R.drawable.ic_main_timetable, - 0 - ), - AHBottomNavigationItem(R.string.more_title, R.drawable.ic_main_more, 0) - ) - ) - accentColor = getThemeAttrColor(R.attr.colorPrimary) - inactiveColor = getThemeAttrColor(R.attr.colorOnSurface, 153) - defaultBackgroundColor = - overlayProvider.compositeOverlayWithThemeSurfaceColorIfNeeded(dpToPx(8f)) - titleState = ALWAYS_SHOW - currentItem = startMenuIndex - isBehaviorTranslationEnabled = false - setTitleTextSizeInSp(10f, 10f) - setOnTabSelectedListener(presenter::onTabSelected) + with(menu) { + add(Menu.NONE, 0, Menu.NONE, R.string.grade_title) + .setIcon(R.drawable.ic_main_grade) + add(Menu.NONE, 1, Menu.NONE, R.string.attendance_title) + .setIcon(R.drawable.ic_main_attendance) + add(Menu.NONE, 2, Menu.NONE, R.string.exam_title) + .setIcon(R.drawable.ic_main_exam) + add(Menu.NONE, 3, Menu.NONE, R.string.timetable_title) + .setIcon(R.drawable.ic_main_timetable) + add(Menu.NONE, 4, Menu.NONE, R.string.more_title) + .setIcon(R.drawable.ic_main_more) + } + selectedItemId = startMenuIndex + setOnNavigationItemSelectedListener { presenter.onTabSelected(it.itemId, false) } + setOnNavigationItemReselectedListener { presenter.onTabSelected(it.itemId, true) } } with(navController) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/widgets/SwipeDisabledViewPager.kt b/app/src/main/java/io/github/wulkanowy/ui/widgets/SwipeDisabledViewPager.kt new file mode 100644 index 000000000..eb5cae4f3 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/widgets/SwipeDisabledViewPager.kt @@ -0,0 +1,19 @@ +package io.github.wulkanowy.ui.widgets + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import androidx.viewpager.widget.ViewPager + +class SwipeDisabledViewPager : ViewPager { + + constructor(context: Context) : super(context) + + constructor(context: Context, attr: AttributeSet) : super(context, attr) + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(ev: MotionEvent) = false + + override fun onInterceptTouchEvent(ev: MotionEvent) = false +} diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 3841b25cd..e55ea8b95 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -6,11 +6,11 @@ + android:layout_height="wrap_content" + android:background="@android:color/transparent" /> - diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 2ea0a4d39..91283abee 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -17,11 +17,14 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="?actionBarSize" - android:layout_marginBottom="@dimen/bottom_navigation_height" /> + android:layout_marginBottom="56dp" /> - + android:layout_gravity="bottom" + app:itemTextAppearanceActive="@style/WulkanowyTheme.TextAppearanceBottomNavigation" + app:itemTextAppearanceInactive="@style/WulkanowyTheme.TextAppearanceBottomNavigation" + app:labelVisibilityMode="labeled" /> diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml deleted file mode 100644 index a4bf80ed6..000000000 --- a/app/src/main/res/values/dimens.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 8dp - 8dp - diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 297a4f288..08867b79e 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -34,6 +34,10 @@ ?android:textColorPrimary + + - - - \ No newline at end of file diff --git a/app/src/main/res/values/preferences_values.xml b/app/src/main/res/values/preferences_values.xml index 44f54c170..2d2eedb4c 100644 --- a/app/src/main/res/values/preferences_values.xml +++ b/app/src/main/res/values/preferences_values.xml @@ -109,15 +109,4 @@ both_semesters all_year - - - Don\'t show - Show all - Show smaller - - - no - yes - small - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1b7c9e427..3d54c16a0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -40,7 +40,6 @@ Hybrid Token PIN - API key Symbol Sign in Password too short @@ -66,7 +65,6 @@ Email Discord Send email - Describe details of problem: Zgłoszenie: Problemy z logowaniem Informacje o aplikacji:\n\nUrządzenie: %1$s\nWersja SDK: %2$s\nWersja aplikacji: %3$s\nDodatkowe informacje: %4$s\nOstatni błąd: %5$s\n\nOpis problemu: Make sure you select the correct UONET+ register variation! @@ -91,13 +89,10 @@ Weight Weight: %s Comment - No new grades Number of new ratings: %1$d Average: %1$.2f Points: %s No average - Predicted: %1$s - Final: %1$s Total points Final grade Predicted grade @@ -188,10 +183,6 @@ Unknown Number of lesson No entries - - %1$d absence - %1$d absences - Absence reason (optional) Send Absence excuse request sent successfully! @@ -200,7 +191,6 @@ - Attendance Total @@ -216,7 +206,6 @@ Trash (no subject) No messages - An error occurred while downloading message content From: To: Date: %s @@ -357,8 +346,6 @@ Student logout Student account Parent account - Mobile API mode - Hybrid mode Edit data Accounts manager Select student @@ -479,7 +466,6 @@ Mark current lesson Show groups next to subjects Show chart list in class grades - Show whole class lessons Show subjects without grades Grades color scheme Subjects sorting @@ -532,7 +518,6 @@ - New entries in register New grades Lucky number New messages diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 08867b79e..8fb07e1a7 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -11,6 +11,7 @@ @color/colorError @color/colorDivider @color/colorSwipeRefresh + @android:color/darker_gray ?android:textColorPrimary @style/PreferenceThemeOverlay false @@ -24,6 +25,8 @@ + + + + From ec6d18968f8d7c996314c8a1f1c3db7dcba5b4c4 Mon Sep 17 00:00:00 2001 From: Mateusz Idziejczak Date: Sat, 7 Aug 2021 10:27:51 +0200 Subject: [PATCH 130/869] Fix filter search bug (#1427) * Fix filter search bug * refractor --- .../wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt index cf1811822..93c7408f6 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt @@ -185,7 +185,9 @@ class MessageTabPresenter @Inject constructor( .debounce(250) .map { query -> lastSearchQuery = query - getFilteredData(query) + val isOnlyUnread = view?.onlyUnread == true + val isOnlyWithAttachments = view?.onlyWithAttachments == true + getFilteredData(query, isOnlyUnread, isOnlyWithAttachments) } .catch { Timber.e(it) } .collect { From 2a913461551c57919bb0a797067d106a13777fcf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Aug 2021 13:53:54 +0000 Subject: [PATCH 131/869] Bump activity-ktx from 1.3.0 to 1.3.1 (#1434) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 0b279ca43..dd6f3e5a5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -172,7 +172,7 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1" implementation "androidx.core:core-ktx:1.6.0" - implementation "androidx.activity:activity-ktx:1.3.0" + implementation "androidx.activity:activity-ktx:1.3.1" implementation "androidx.appcompat:appcompat:1.3.1" implementation "androidx.appcompat:appcompat-resources:1.3.1" implementation "androidx.fragment:fragment-ktx:1.3.6" From 7c94837af01d56a737b66235454740e00d7af509 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Aug 2021 13:54:31 +0000 Subject: [PATCH 132/869] Bump coil from 1.3.1 to 1.3.2 (#1433) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index dd6f3e5a5..9783c14dc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -214,7 +214,7 @@ dependencies { implementation "at.favre.lib:slf4j-timber:1.0.1" implementation 'com.github.bastienpaulfr:Treessence:1.0.4' implementation "com.mikepenz:aboutlibraries-core:$about_libraries" - implementation "io.coil-kt:coil:1.3.1" + implementation "io.coil-kt:coil:1.3.2" implementation "io.github.wulkanowy:AppKillerManager:3.0.0" implementation 'me.xdrop:fuzzywuzzy:1.3.1' implementation 'com.fredporciuncula:flow-preferences:1.5.0' From 4ae3f7b0163357f48057eeba26e84a38523f5e86 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Aug 2021 13:54:48 +0000 Subject: [PATCH 133/869] Bump google-services from 4.3.8 to 4.3.9 (#1432) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 8f70f8111..23c0276ff 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ buildscript { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.android.tools.build:gradle:7.0.0' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" - classpath 'com.google.gms:google-services:4.3.8' + classpath 'com.google.gms:google-services:4.3.9' classpath 'com.huawei.agconnect:agcp:1.6.0.300' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1' classpath "com.github.triplet.gradle:play-publisher:2.8.0" From 72ef5f428ea17a63d6fc3ba161a55cfcd09c0aa3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Aug 2021 13:55:03 +0000 Subject: [PATCH 134/869] Bump firebase-bom from 28.3.0 to 28.3.1 (#1431) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 9783c14dc..05d1a1508 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -219,7 +219,7 @@ dependencies { implementation 'me.xdrop:fuzzywuzzy:1.3.1' implementation 'com.fredporciuncula:flow-preferences:1.5.0' - playImplementation platform('com.google.firebase:firebase-bom:28.3.0') + playImplementation platform('com.google.firebase:firebase-bom:28.3.1') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' From 626169de115072abb1612a04b3544a03796de43f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Tue, 10 Aug 2021 11:55:51 +0200 Subject: [PATCH 135/869] Add drag and drop to dashboard tiles (#1415) --- .../repositories/PreferencesRepository.kt | 30 +++++++++- .../ui/modules/dashboard/DashboardAdapter.kt | 57 +++++++++++++------ .../ui/modules/dashboard/DashboardFragment.kt | 9 +++ .../dashboard/DashboardItemMoveCallback.kt | 55 ++++++++++++++++++ .../modules/dashboard/DashboardPresenter.kt | 18 +++++- .../item_dashboard_horizontal_group.xml | 5 +- .../res/layout/subitem_dashboard_grades.xml | 13 ++++- app/src/main/res/values/strings.xml | 2 +- 8 files changed, 162 insertions(+), 27 deletions(-) create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index 5b97b65da..1e6cd5115 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -2,9 +2,12 @@ package io.github.wulkanowy.data.repositories import android.content.Context import android.content.SharedPreferences -import com.squareup.moshi.Moshi +import androidx.core.content.edit import com.fredporciuncula.flow.preferences.FlowSharedPreferences import com.fredporciuncula.flow.preferences.Preference +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.ui.modules.dashboard.DashboardItem @@ -21,8 +24,13 @@ import javax.inject.Singleton class PreferencesRepository @Inject constructor( private val sharedPref: SharedPreferences, private val flowSharedPref: FlowSharedPreferences, - @ApplicationContext val context: Context + @ApplicationContext val context: Context, + moshi: Moshi ) { + @OptIn(ExperimentalStdlibApi::class) + private val dashboardItemsPositionAdapter: JsonAdapter> = + moshi.adapter() + val startMenuIndex: Int get() = getString(R.string.pref_key_start_menu, R.string.pref_default_startup).toInt() @@ -160,6 +168,19 @@ class PreferencesRepository @Inject constructor( R.bool.pref_default_optional_arithmetic_average ) + var dashboardItemsPosition: Map? + get() { + val json = sharedPref.getString(PREF_KEY_DASHBOARD_ITEMS_POSITION, null) ?: return null + + return dashboardItemsPositionAdapter.fromJson(json) + } + set(value) = sharedPref.edit { + putString( + PREF_KEY_DASHBOARD_ITEMS_POSITION, + dashboardItemsPositionAdapter.toJson(value) + ) + } + val selectedDashboardTilesFlow: Flow> get() = selectedDashboardTilesPreference.asFlow() .map { set -> @@ -199,4 +220,9 @@ class PreferencesRepository @Inject constructor( private fun getBoolean(id: String, default: Int) = sharedPref.getBoolean(id, context.resources.getBoolean(default)) + + private companion object { + + private const val PREF_KEY_DASHBOARD_ITEMS_POSITION = "dashboard_items_position" + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt index 9f3d546e5..fa081ce7f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt @@ -12,7 +12,6 @@ import androidx.core.view.updateLayoutParams import androidx.core.view.updateMarginsRelative import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Timetable @@ -38,10 +37,9 @@ import java.util.Timer import javax.inject.Inject import kotlin.concurrent.timer -class DashboardAdapter @Inject constructor() : - ListAdapter(DashboardAdapterDiffCallback()) { +class DashboardAdapter @Inject constructor() : RecyclerView.Adapter() { - var lessonsTimer: Timer? = null + private var lessonsTimer: Timer? = null var onAccountTileClickListener: () -> Unit = {} @@ -63,7 +61,23 @@ class DashboardAdapter @Inject constructor() : var onConferencesTileClickListener: () -> Unit = {} - override fun getItemViewType(position: Int) = getItem(position).type.ordinal + val items = mutableListOf() + + fun submitList(newItems: List) { + val diffResult = + DiffUtil.calculateDiff(DiffCallback(newItems, items.toMutableList())) + + with(items) { + clear() + addAll(newItems) + } + + diffResult.dispatchUpdatesTo(this) + } + + override fun getItemCount() = items.size + + override fun getItemViewType(position: Int) = items[position].type.ordinal override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val inflater = LayoutInflater.from(parent.context) @@ -119,7 +133,7 @@ class DashboardAdapter @Inject constructor() : } private fun bindAccountViewHolder(accountViewHolder: AccountViewHolder, position: Int) { - val item = getItem(position) as DashboardItem.Account + val item = items[position] as DashboardItem.Account val student = item.student val isLoading = item.isLoading @@ -147,7 +161,7 @@ class DashboardAdapter @Inject constructor() : horizontalGroupViewHolder: HorizontalGroupViewHolder, position: Int ) { - val item = getItem(position) as DashboardItem.HorizontalGroup + val item = items[position] as DashboardItem.HorizontalGroup val unreadMessagesCount = item.unreadMessagesCount val attendancePercentage = item.attendancePercentage val luckyNumber = item.luckyNumber @@ -221,7 +235,7 @@ class DashboardAdapter @Inject constructor() : } private fun bindGradesViewHolder(gradesViewHolder: GradesViewHolder, position: Int) { - val item = getItem(position) as DashboardItem.Grades + val item = items[position] as DashboardItem.Grades val subjectWithGrades = item.subjectWithGrades.orEmpty() val gradeTheme = item.gradeTheme val error = item.error @@ -250,7 +264,7 @@ class DashboardAdapter @Inject constructor() : } private fun bindLessonsViewHolder(lessonsViewHolder: LessonsViewHolder, position: Int) { - val item = getItem(position) as DashboardItem.Lessons + val item = items[position] as DashboardItem.Lessons val timetableFull = item.lessons val binding = lessonsViewHolder.binding @@ -519,7 +533,7 @@ class DashboardAdapter @Inject constructor() : } private fun bindHomeworkViewHolder(homeworkViewHolder: HomeworkViewHolder, position: Int) { - val item = getItem(position) as DashboardItem.Homework + val item = items[position] as DashboardItem.Homework val homeworkList = item.homework.orEmpty() val error = item.error val isLoading = item.isLoading @@ -557,7 +571,7 @@ class DashboardAdapter @Inject constructor() : announcementsViewHolder: AnnouncementsViewHolder, position: Int ) { - val item = getItem(position) as DashboardItem.Announcements + val item = items[position] as DashboardItem.Announcements val schoolAnnouncementList = item.announcement.orEmpty() val error = item.error val isLoading = item.isLoading @@ -594,7 +608,7 @@ class DashboardAdapter @Inject constructor() : } private fun bindExamsViewHolder(examsViewHolder: ExamsViewHolder, position: Int) { - val item = getItem(position) as DashboardItem.Exams + val item = items[position] as DashboardItem.Exams val exams = item.exams.orEmpty() val error = item.error val isLoading = item.isLoading @@ -630,7 +644,7 @@ class DashboardAdapter @Inject constructor() : conferencesViewHolder: ConferencesViewHolder, position: Int ) { - val item = getItem(position) as DashboardItem.Conferences + val item = items[position] as DashboardItem.Conferences val conferences = item.conferences.orEmpty() val error = item.error val isLoading = item.isLoading @@ -703,13 +717,20 @@ class DashboardAdapter @Inject constructor() : val adapter by lazy { DashboardConferencesAdapter() } } - class DashboardAdapterDiffCallback : DiffUtil.ItemCallback() { + private class DiffCallback( + private val newList: List, + private val oldList: List + ) : DiffUtil.Callback() { - override fun areItemsTheSame(oldItem: DashboardItem, newItem: DashboardItem) = - oldItem.type == newItem.type + override fun getNewListSize() = newList.size - override fun areContentsTheSame(oldItem: DashboardItem, newItem: DashboardItem) = - oldItem == newItem + override fun getOldListSize() = oldList.size + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = + newList[newItemPosition] == oldList[oldItemPosition] + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = + newList[newItemPosition].type == oldList[oldItemPosition].type } private companion object { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt index 283f57451..3392280bc 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt @@ -8,6 +8,7 @@ import android.view.View import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import androidx.recyclerview.widget.DefaultItemAnimator +import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -68,6 +69,12 @@ class DashboardFragment : BaseFragment(R.layout.fragme override fun initView() { val mainActivity = requireActivity() as MainActivity + val itemTouchHelper = ItemTouchHelper( + DashboardItemMoveCallback( + dashboardAdapter, + presenter::onDragAndDropEnd + ) + ) dashboardAdapter.apply { onAccountTileClickListener = { mainActivity.pushView(AccountFragment.newInstance()) } @@ -104,6 +111,8 @@ class DashboardFragment : BaseFragment(R.layout.fragme adapter = dashboardAdapter (itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false } + + itemTouchHelper.attachToRecyclerView(dashboardRecycler) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt new file mode 100644 index 000000000..cf4097a4a --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt @@ -0,0 +1,55 @@ +package io.github.wulkanowy.ui.modules.dashboard + +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import java.util.Collections + +class DashboardItemMoveCallback( + private val dashboardAdapter: DashboardAdapter, + private var onUserInteractionEndListener: (List) -> Unit = {} +) : ItemTouchHelper.Callback() { + + override fun isLongPressDragEnabled() = true + + override fun isItemViewSwipeEnabled() = false + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + //Not implemented + } + + override fun getMovementFlags( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ): Int { + val dragFlags = if (viewHolder.bindingAdapterPosition != 0) { + ItemTouchHelper.UP or ItemTouchHelper.DOWN + } else 0 + + return makeMovementFlags(dragFlags, 0) + } + + override fun canDropOver( + recyclerView: RecyclerView, + current: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ) = target.bindingAdapterPosition != 0 + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + val list = dashboardAdapter.items.toMutableList() + + Collections.swap(list, viewHolder.bindingAdapterPosition, target.bindingAdapterPosition) + + dashboardAdapter.submitList(list) + return true + } + + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + super.clearView(recyclerView, viewHolder) + + onUserInteractionEndListener(dashboardAdapter.items.toList()) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt index 12374859d..0e24f0a14 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt @@ -68,6 +68,16 @@ class DashboardPresenter @Inject constructor( .launch("dashboard_pref") } + fun onDragAndDropEnd(list: List) { + dashboardItemLoadedList.clear() + dashboardItemLoadedList.addAll(list) + + val positionList = + list.mapIndexed { index, dashboardItem -> Pair(dashboardItem.type, index) }.toMap() + + preferencesRepository.dashboardItemsPosition = positionList + } + fun loadData(forceRefresh: Boolean = false, tilesToLoad: Set) { val oldDashboardDataToLoad = dashboardTilesToLoad @@ -622,6 +632,7 @@ class DashboardPresenter @Inject constructor( private fun updateData(dashboardItem: DashboardItem, forceRefresh: Boolean) { val isForceRefreshError = forceRefresh && dashboardItem.error != null + val dashboardItemsPosition = preferencesRepository.dashboardItemsPosition with(dashboardItemLoadedList) { removeAll { it.type == dashboardItem.type && !isForceRefreshError } @@ -636,7 +647,12 @@ class DashboardPresenter @Inject constructor( } } - dashboardItemLoadedList.sortBy { tile -> dashboardItemsToLoad.single { it == tile.type }.ordinal } + dashboardItemLoadedList.sortBy { tile -> + dashboardItemsPosition?.getOrDefault( + tile.type, + tile.type.ordinal + 100 + ) ?: tile.type.ordinal + } val isItemsLoaded = dashboardItemsToLoad.all { type -> dashboardItemLoadedList.any { it.type == type } } diff --git a/app/src/main/res/layout/item_dashboard_horizontal_group.xml b/app/src/main/res/layout/item_dashboard_horizontal_group.xml index a8532e6fb..bbd8f3517 100644 --- a/app/src/main/res/layout/item_dashboard_horizontal_group.xml +++ b/app/src/main/res/layout/item_dashboard_horizontal_group.xml @@ -4,8 +4,9 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginHorizontal="12dp" - android:layout_marginVertical="2dp"> + android:paddingHorizontal="12dp" + android:layout_marginVertical="2dp" + android:clipToPadding="false"> + app:layout_constraintTop_toTopOf="parent"> + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c6323668a..c1b3a3ee4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -501,7 +501,7 @@ until %1$s No upcoming lessons - An error occurred while loading the lesson + An error occurred while loading the lessons Homework No homework to do From 9c819835ca8debbe5890add702148630c2ecef02 Mon Sep 17 00:00:00 2001 From: Mateusz Idziejczak Date: Sun, 15 Aug 2021 15:59:32 +0200 Subject: [PATCH 136/869] Add last sync date in sync settings (#1436) --- .../data/repositories/PreferencesRepository.kt | 15 +++++++++++++++ .../github/wulkanowy/services/sync/SyncWorker.kt | 3 +++ .../ui/modules/settings/sync/SyncFragment.kt | 6 ++++++ .../ui/modules/settings/sync/SyncPresenter.kt | 15 ++++++++++++++- .../ui/modules/settings/sync/SyncView.kt | 2 ++ app/src/main/res/values/preferences_defaults.xml | 1 + app/src/main/res/values/preferences_keys.xml | 1 + app/src/main/res/values/strings.xml | 1 + 8 files changed, 43 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index 1e6cd5115..e725c42a0 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -13,9 +13,12 @@ import io.github.wulkanowy.R import io.github.wulkanowy.ui.modules.dashboard.DashboardItem import io.github.wulkanowy.ui.modules.grade.GradeAverageMode import io.github.wulkanowy.ui.modules.grade.GradeSortingMode +import io.github.wulkanowy.utils.toLocalDateTime +import io.github.wulkanowy.utils.toTimestamp import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import java.time.LocalDateTime import javax.inject.Inject import javax.inject.Singleton @@ -168,6 +171,13 @@ class PreferencesRepository @Inject constructor( R.bool.pref_default_optional_arithmetic_average ) + var lasSyncDate: LocalDateTime + get() = getLong( + R.string.pref_key_last_sync_date, + R.string.pref_default_last_sync_date + ).toLocalDateTime() + set(value) = sharedPref.edit().putLong("last_sync_date", value.toTimestamp()).apply() + var dashboardItemsPosition: Map? get() { val json = sharedPref.getString(PREF_KEY_DASHBOARD_ITEMS_POSITION, null) ?: return null @@ -211,6 +221,11 @@ class PreferencesRepository @Inject constructor( return flowSharedPref.getStringSet(prefKey, defaultSet) } + private fun getLong(id: Int, default: Int) = getLong(context.getString(id), default) + + private fun getLong(id: String, default: Int) = + sharedPref.getLong(id, context.resources.getString(default).toLong()) + private fun getString(id: Int, default: Int) = getString(context.getString(id), default) private fun getString(id: String, default: Int) = diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/SyncWorker.kt b/app/src/main/java/io/github/wulkanowy/services/sync/SyncWorker.kt index 49d61a41d..ea1f79cbf 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/SyncWorker.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/SyncWorker.kt @@ -22,6 +22,8 @@ import io.github.wulkanowy.services.sync.works.Work import io.github.wulkanowy.utils.getCompatColor import kotlinx.coroutines.coroutineScope import timber.log.Timber +import java.time.LocalDateTime +import java.time.ZoneId import kotlin.random.Random @HiltWorker @@ -48,6 +50,7 @@ class SyncWorker @AssistedInject constructor( Timber.i("${work::class.java.simpleName} is starting") work.doWork(student, semester) Timber.i("${work::class.java.simpleName} result: Success") + preferencesRepository.lasSyncDate = LocalDateTime.now(ZoneId.systemDefault()) null } catch (e: Throwable) { Timber.w("${work::class.java.simpleName} result: An exception ${e.message} occurred") diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt index 207fe2ff6..342988092 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt @@ -39,6 +39,12 @@ class SyncFragment : PreferenceFragmentCompat(), } } + override fun setLastSyncDate(lastSyncDate: String) { + findPreference(getString(R.string.pref_key_services_force_sync))?.run { + summary = getString(R.string.pref_services_last_full_sync_date, lastSyncDate) + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) presenter.onAttachView(this) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncPresenter.kt index 36b8d792c..63e86a475 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncPresenter.kt @@ -8,6 +8,7 @@ import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.isHolidays +import io.github.wulkanowy.utils.toFormattedString import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.onEach import timber.log.Timber @@ -27,6 +28,7 @@ class SyncPresenter @Inject constructor( Timber.i("Settings sync view was initialized") view.setServicesSuspended(preferencesRepository.serviceEnableKey, now().isHolidays) view.initView() + setSyncDateInView() } fun onSharedPreferenceChanged(key: String) { @@ -63,10 +65,21 @@ class SyncPresenter @Inject constructor( } else -> Timber.d("Sync now state: ${workInfo.state}") } - if (workInfo.state.isFinished) setSyncInProgress(false) + if (workInfo.state.isFinished) { + setSyncInProgress(false) + setSyncDateInView() + } }.catch { Timber.e(it, "Sync now failed") }.launch("sync") } } + + private fun setSyncDateInView() { + val lastSyncDate = preferencesRepository.lasSyncDate + + if (lastSyncDate.year == 1970) return + + view?.setLastSyncDate(lastSyncDate.toFormattedString("dd.MM.yyyy HH:mm:ss")) + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncView.kt index 9da473ba7..6ffe156f2 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncView.kt @@ -10,6 +10,8 @@ interface SyncView : BaseView { fun initView() + fun setLastSyncDate(lastSyncDate: String) + fun setServicesSuspended(serviceEnablesKey: String, isHolidays: Boolean) fun setSyncInProgress(inProgress: Boolean) diff --git a/app/src/main/res/values/preferences_defaults.xml b/app/src/main/res/values/preferences_defaults.xml index 5721763f1..1ba9359cd 100644 --- a/app/src/main/res/values/preferences_defaults.xml +++ b/app/src/main/res/values/preferences_defaults.xml @@ -25,6 +25,7 @@ false false false + 0 LUCKY_NUMBER MESSAGES diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index 876e43337..aa60bb7e6 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -31,4 +31,5 @@ optional_arithmetic_average message_send_is_draft message_send_recipients + last_sync_date diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c1b3a3ee4..0b25f4a5d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -615,6 +615,7 @@ Synced! Sync failed Sync in progress + Last full sync: %s Value of the plus Value of the minus From 428e40d7fe6f9b8b67a078868bbc98bd9040d753 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Aug 2021 13:32:34 +0000 Subject: [PATCH 137/869] Bump hianalytics from 6.1.0.300 to 6.1.1.300 (#1441) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 05d1a1508..fae7d6e6a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -225,7 +225,7 @@ dependencies { playImplementation 'com.google.firebase:firebase-crashlytics:' playImplementation 'com.google.android.play:core-ktx:1.8.1' - hmsImplementation 'com.huawei.hms:hianalytics:6.1.0.300' + hmsImplementation 'com.huawei.hms:hianalytics:6.1.1.300' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.0.300' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 9c5d2fbf8462b5f9c927154818e037b6abdb7835 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Aug 2021 13:32:48 +0000 Subject: [PATCH 138/869] Bump google-services from 4.3.9 to 4.3.10 (#1439) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 23c0276ff..fecbb019f 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ buildscript { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.android.tools.build:gradle:7.0.0' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" - classpath 'com.google.gms:google-services:4.3.9' + classpath 'com.google.gms:google-services:4.3.10' classpath 'com.huawei.agconnect:agcp:1.6.0.300' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1' classpath "com.github.triplet.gradle:play-publisher:2.8.0" From d3b3939d2649ff7d1e9d779d31f6ecdd9b3f495d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Aug 2021 13:44:06 +0000 Subject: [PATCH 139/869] Bump timber from 4.7.1 to 5.0.1 (#1440) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index fae7d6e6a..5e1ae9abc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -210,7 +210,7 @@ dependencies { implementation "com.squareup.moshi:moshi-adapters:$moshi" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi" - implementation "com.jakewharton.timber:timber:4.7.1" + implementation "com.jakewharton.timber:timber:5.0.1" implementation "at.favre.lib:slf4j-timber:1.0.1" implementation 'com.github.bastienpaulfr:Treessence:1.0.4' implementation "com.mikepenz:aboutlibraries-core:$about_libraries" From eb94e06d54c88a6f1069df4f06e5f6ef5560fe87 Mon Sep 17 00:00:00 2001 From: Mateusz Idziejczak Date: Sun, 22 Aug 2021 16:33:12 +0200 Subject: [PATCH 140/869] Fix buggy timers in timetable (#1428) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rafał Borcz --- .../ui/modules/timetable/TimetableAdapter.kt | 201 +++++++++++++----- .../ui/modules/timetable/TimetableFragment.kt | 20 +- 2 files changed, 152 insertions(+), 69 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt index f049f828e..87b3362db 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt @@ -7,6 +7,7 @@ import android.view.View.VISIBLE import android.view.ViewGroup import android.widget.TextView import androidx.core.view.ViewCompat +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Timetable @@ -26,31 +27,58 @@ import kotlin.concurrent.timer class TimetableAdapter @Inject constructor() : RecyclerView.Adapter() { - private enum class ViewType(val id: Int) { - ITEM_NORMAL(1), - ITEM_SMALL(2) + private enum class ViewType { + ITEM_NORMAL, + ITEM_SMALL } - var items = mutableListOf() - set(value) { - field = value - resetTimers() - } - var onClickListener: (Timetable) -> Unit = {} - var showWholeClassPlan: String = "no" + private var showWholeClassPlan: String = "no" - var showGroupsInPlan: Boolean = false + private var showGroupsInPlan: Boolean = false - var showTimers: Boolean = false + private var showTimers: Boolean = false - private val timers = mutableMapOf() + private val timers = mutableMapOf() - fun resetTimers() { - Timber.d("Timetable timers (${timers.size}) reset") + private val items = mutableListOf() + + fun submitList( + newTimetable: List, + showWholeClassPlan: String = this.showWholeClassPlan, + showGroupsInPlan: Boolean = this.showGroupsInPlan, + showTimers: Boolean = this.showTimers + ) { + val isFlagsDifferent = this.showWholeClassPlan != showWholeClassPlan + || this.showGroupsInPlan != showGroupsInPlan + || this.showTimers != showTimers + + val diffResult = DiffUtil.calculateDiff( + TimetableAdapterDiffCallback( + oldList = items.toMutableList(), + newList = newTimetable, + isFlagsDifferent = isFlagsDifferent + ) + ) + + this.showGroupsInPlan = showGroupsInPlan + this.showTimers = showTimers + this.showWholeClassPlan = showWholeClassPlan + + items.clear() + items.addAll(newTimetable) + + diffResult.dispatchUpdatesTo(this) + } + + fun clearTimers() { + Timber.d("Timetable timers (${timers.size}) cleared") with(timers) { - forEach { (_, timer) -> timer.cancel() } + forEach { (_, timer) -> + timer?.cancel() + timer?.purge() + } clear() } } @@ -58,16 +86,20 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter ViewType.ITEM_SMALL.id - else -> ViewType.ITEM_NORMAL.id + !items[position].isStudentPlan && showWholeClassPlan == "small" -> ViewType.ITEM_SMALL.ordinal + else -> ViewType.ITEM_NORMAL.ordinal } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val inflater = LayoutInflater.from(parent.context) return when (viewType) { - ViewType.ITEM_NORMAL.id -> ItemViewHolder(ItemTimetableBinding.inflate(inflater, parent, false)) - ViewType.ITEM_SMALL.id -> SmallItemViewHolder(ItemTimetableSmallBinding.inflate(inflater, parent, false)) + ViewType.ITEM_NORMAL.ordinal -> ItemViewHolder( + ItemTimetableBinding.inflate(inflater, parent, false) + ) + ViewType.ITEM_SMALL.ordinal -> SmallItemViewHolder( + ItemTimetableSmallBinding.inflate(inflater, parent, false) + ) else -> throw IllegalStateException() } } @@ -111,6 +143,12 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter i < position && !item.isStudentPlan }.size)?.let { - if (!it.canceled && it.isStudentPlan) it.end - else null - } + return items.filter { it.isStudentPlan } + .getOrNull(position - 1 - items.filterIndexed { i, item -> i < position && !item.isStudentPlan }.size) + ?.let { + if (!it.canceled && it.isStudentPlan) it.end + else null + } } private fun updateTimeLeft(binding: ItemTimetableBinding, lesson: Timetable, position: Int) { @@ -148,11 +188,18 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter, + private val newList: List, + private val isFlagsDifferent: Boolean + ) : DiffUtil.Callback() { + + override fun getOldListSize() = oldList.size + + override fun getNewListSize() = newList.size + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = + oldList[oldItemPosition].id == newList[newItemPosition].id + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = + oldList[oldItemPosition] == newList[newItemPosition] && !isFlagsDifferent + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt index 2bc7aa595..a374e166c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt @@ -49,7 +49,7 @@ class TimetableFragment : BaseFragment(R.layout.fragme override val titleStringId get() = R.string.timetable_title - override val isViewEmpty get() = timetableAdapter.items.isEmpty() + override val isViewEmpty get() = timetableAdapter.itemCount > 0 override val currentStackSize get() = (activity as? MainActivity)?.currentStackSize @@ -109,20 +109,16 @@ class TimetableFragment : BaseFragment(R.layout.fragme showGroupsInPlanType: Boolean, showTimetableTimers: Boolean ) { - with(timetableAdapter) { - items = data.toMutableList() - showTimers = showTimetableTimers + timetableAdapter.submitList( + newTimetable = data.toMutableList(), + showGroupsInPlan = showGroupsInPlanType, + showTimers = showTimetableTimers, showWholeClassPlan = showWholeClassPlanType - showGroupsInPlan = showGroupsInPlanType - notifyDataSetChanged() - } + ) } override fun clearData() { - with(timetableAdapter) { - items = mutableListOf() - notifyDataSetChanged() - } + timetableAdapter.submitList(listOf()) } override fun updateNavigationDay(date: String) { @@ -226,7 +222,7 @@ class TimetableFragment : BaseFragment(R.layout.fragme } override fun onDestroyView() { - timetableAdapter.resetTimers() + timetableAdapter.clearTimers() presenter.onDetachView() super.onDestroyView() } From 02b87c8c6a7b419a552abff2d63bfb7f0b840637 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Aug 2021 13:42:20 +0000 Subject: [PATCH 141/869] Bump firebase-bom from 28.3.1 to 28.4.0 (#1446) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 5e1ae9abc..1338bd8df 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -219,7 +219,7 @@ dependencies { implementation 'me.xdrop:fuzzywuzzy:1.3.1' implementation 'com.fredporciuncula:flow-preferences:1.5.0' - playImplementation platform('com.google.firebase:firebase-bom:28.3.1') + playImplementation platform('com.google.firebase:firebase-bom:28.4.0') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' From 1cfabe43a5e837b46d8041137f1f1246512a6660 Mon Sep 17 00:00:00 2001 From: Mateusz Idziejczak Date: Mon, 23 Aug 2021 15:48:48 +0200 Subject: [PATCH 142/869] Add captions for averages from how many items have been counted (#1437) --- .../grade/summary/GradeSummaryAdapter.kt | 38 ++++++++++++++----- .../scrollable_header_grade_summary.xml | 20 ++++++++++ app/src/main/res/values/strings.xml | 1 + 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt index 4de6044bf..9a888ddcc 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt @@ -5,6 +5,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.databinding.ItemGradeSummaryBinding @@ -35,8 +36,12 @@ class GradeSummaryAdapter @Inject constructor( val inflater = LayoutInflater.from(parent.context) return when (viewType) { - ViewType.HEADER.id -> HeaderViewHolder(ScrollableHeaderGradeSummaryBinding.inflate(inflater, parent, false)) - ViewType.ITEM.id -> ItemViewHolder(ItemGradeSummaryBinding.inflate(inflater, parent, false)) + ViewType.HEADER.id -> HeaderViewHolder( + ScrollableHeaderGradeSummaryBinding.inflate(inflater, parent, false) + ) + ViewType.ITEM.id -> ItemViewHolder( + ItemGradeSummaryBinding.inflate(inflater, parent, false) + ) else -> throw IllegalStateException() } } @@ -51,13 +56,27 @@ class GradeSummaryAdapter @Inject constructor( private fun bindHeaderViewHolder(binding: ScrollableHeaderGradeSummaryBinding) { if (items.isEmpty()) return + val context = binding.root.context + val finalItemsCount = items.count { it.finalGrade.matches("[0-6][+-]?".toRegex()) } + val calculatedItemsCount = items.count { value -> value.average != 0.0 } + val finalAverage = items.calcAverage( + preferencesRepository.gradePlusModifier, + preferencesRepository.gradeMinusModifier + ) + val calculatedAverage = items.filter { value -> value.average != 0.0 } + .map { values -> values.average } + .reversed() // fix average precision + .average() + with(binding) { - gradeSummaryScrollableHeaderFinal.text = formatAverage(items.calcAverage(preferencesRepository.gradePlusModifier, preferencesRepository.gradeMinusModifier)) - gradeSummaryScrollableHeaderCalculated.text = formatAverage(items - .filter { value -> value.average != 0.0 } - .map { values -> values.average } - .reversed() // fix average precision - .average() + gradeSummaryScrollableHeaderFinal.text = formatAverage(finalAverage) + gradeSummaryScrollableHeaderCalculated.text = formatAverage(calculatedAverage) + gradeSummaryScrollableHeaderFinalSubjectCount.text = + context.getString(R.string.grade_summary_from_subjects, finalItemsCount, items.size) + gradeSummaryScrollableHeaderCalculatedSubjectCount.text = context.getString( + R.string.grade_summary_from_subjects, + calculatedItemsCount, + items.size ) } } @@ -71,7 +90,8 @@ class GradeSummaryAdapter @Inject constructor( gradeSummaryItemPredicted.text = "${item.predictedGrade} ${item.proposedPoints}".trim() gradeSummaryItemFinal.text = "${item.finalGrade} ${item.finalPoints}".trim() - gradeSummaryItemPointsContainer.visibility = if (item.pointsSum.isBlank()) View.GONE else View.VISIBLE + gradeSummaryItemPointsContainer.visibility = + if (item.pointsSum.isBlank()) View.GONE else View.VISIBLE } } diff --git a/app/src/main/res/layout/scrollable_header_grade_summary.xml b/app/src/main/res/layout/scrollable_header_grade_summary.xml index 2eea20f42..29657ba1a 100644 --- a/app/src/main/res/layout/scrollable_header_grade_summary.xml +++ b/app/src/main/res/layout/scrollable_header_grade_summary.xml @@ -20,6 +20,7 @@ android:layout_height="wrap_content" android:gravity="center" android:minLines="2" + android:textStyle="bold" android:text="@string/grade_summary_calculated_average" android:textSize="16sp" /> @@ -30,6 +31,15 @@ android:gravity="center_horizontal" android:textSize="21sp" tools:text="6,00" /> + + @@ -53,5 +64,14 @@ android:gravity="center_horizontal" android:textSize="21sp" tools:text="6,00" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0b25f4a5d..8b3f3b8e8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -101,6 +101,7 @@ Predicted grade Calculated average Final average + From %d of %d subjects Summary Class Mark as read From 076948a680b1e576cbdf1386b498dc763c165fbe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Aug 2021 13:53:04 +0000 Subject: [PATCH 143/869] Bump gradle from 7.0.0 to 7.0.1 (#1445) --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 59536 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index fecbb019f..d169291d6 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ buildscript { } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.android.tools.build:gradle:7.0.0' + classpath 'com.android.tools.build:gradle:7.0.1' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.10' classpath 'com.huawei.agconnect:agcp:1.6.0.300' diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..7454180f2ae8848c63b8b4dea2cb829da983f2fa 100644 GIT binary patch delta 18435 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 delta 17845 zcmbPmk@@gB<_&e6yi1vtJgr$67;dmLFgQ*Y6q1{~l2fDpZm@Ky$p5=JX56RQf;=>) zY-`wN%Hp*uY|E0hxs4ea8dC!V)>=(}VwSV}&7(BtztX=R1jqkc^RkEI>dbGx z>A%E(d|hO0wfJ$f{Nv+2#(mG@4!JLHxNJCcPWAUW)#u)q+kJa>&|dyN^9P=LMuNw= zzh)fd)MQ(FVup&}wTJanIxkmjWaOMPan@$FbBQY$4@($MF#Ry8QzNc#&dkPNGfpIi zbbV$!#uH;;a896bPf_F#9@)zC`pZw->02u8ee$`L_2Kf*r)DIs(B^cXQmG)(msehJ zBf3bof4bhUxSQfrz3#1JITX6CTD)dsM)lQG$0Q$$w@Mpqka;+NORTlU+4=&zo!pJb zo8HV!;^41TeRONe_SnLOR`%L|JnddjSZ4WNbf@vr+GxA$QywOqTW3(h*R8$h6^pfN z+J+*%wa$F&Ls%^5-0Ho)@PCKfQqL(_+nf$GXQgKJ?M@9$%J6)){*&vxGS>FPqw$|JrXM0b(_|@XC(1k;FFwe9CpXC3_Ib;4o zpVF;u;R`13spJj}YMZ4je{jaMXa0hdz4ayEFVbT==@q<&?~uYz-q>&Vg|3xPx%<84 z3-hlJ%6<>jkLtP3;mmEmt1!)$$I*1h#oPwUxDPu-j` z^VOR;$J#R`&)B}S)N#9aL|QD|-_AU#*eSl%b4zmj`$dW^FGHM`Yu$6ZyxQ2+!C1E><4yUi|Ww z_39g&yPolzMac(V{C98R)#KS2g*MACI30V^pAk%lw9ZwJB>b>vS(VfWdCbQ%C)j-B% zrTTZtF)iHu6{|QIgPELoLX%f5oAsDSH157ov+WXf(Y9S3t2B#u_L?4zm~j14N}>Cg zmN{{gc&EPScH6ykUD`=;(=Th^o}9CDUBns)=jkXd33J|1Z^09e3c>y_GTXAa_s7IP{uW4XY~Z^c!b4|cYst|`dU zSJ|@I`GA)5ga7+9ot;0-%B^KVkYMV%v4%q>;DKeWE($&b2uFWO4z_@#1r=b+EK z^_!Ac^c;IOJzSznZ`Sq})!L~?BU*fxt~uePv9jcRz~SS2ESl=)DgXJXxb~6m#TVUQ ztNnNRyNR$h`EOq-KdZxwGs`+#OZw>MmA9XnU9DVrXG+Nrkq4~Ku}{sE5??JWy*a~# zXQs!Ez57C|rc3!-CYc`5k2o#4xQVYy{Pn?l(G4G~S=zL>a8FuN=)SaGeB1XWH!iqj zPfh*PllEbC0^@}yvq@!kPwHDA=x^CPIp*8*z>kdI$~z{6&2$eZwoE>d`T}8=4u}cgO{krHotJhdJr10*lj=zp~SYI9Mb$4=9 zR>^J6+i-dJ#R3kECl6;$_j{07$9=y$Y|_~y2b_7<2|bqME4F-bf2G{R&SmweSxSZB zWZh$a28IZQ$$UcU^=qRzE2NK?*7dV&^e(7$6jFWBk}500e_>MTq-i-57pZvcahWhh zGjdY+fk`c_Ne_=YiOjX=dwZhutwvdndf}X9d9#=0&9<00hhvKCZLj_lYww!96#VXS z`-?~a7GMACdnax9&m*Sb;I!rP`d=+KXPW2VtNz?qpB7&A*gZU6d691YpEd1am)}>u zkf>cPvv<48=iiHdGPnM_;GFkM+3?4<2fvc1{yXAa?{z=$<#~}`#r&a{;wxV0zuYbN zi|OywjDJxZ{`O}4yXL(6Wi{8|xlZ-(7h3&d=ht2MzWT-KFa1fs=DU18ul2XPJ#I;T z=!^SDKIdNivpeIS;Mw|$7v5jy%l*=?|CN97kH2Nee}|>=RWIC478|TQBX{eFYO2b= zD*~Yfn!9bjxatMpe4E%B>hYk+Q8WC;;)2%pZ5OwPT>J3E>te{t7~OfMPC;uA#mzX| z?Xo$-#M>oUeb&-Nk!n)&51kf`TlmR~|LyaroR;vFdu}pc z_<8F;RZTUoc=38q;M1yBbIzdVQ@vW|nqMSaXWPm=5SyAWXk@0n$Ms=!=)F~+%%*N% zy~p|K+(XA=ovK%#%GJ8R>{H&vo2zI|(0`&#brntMW*A1~Hbyxn!o{q_tk>oXOXu08%XLC#xJtK2Ab3;(Rh>M{r>L`_30WVs~#()Oz-MAxU28pym{yP7&9zP1>U@SBP(NZ zRrbv*t~M*f&H61zrR%e2)cl=W{arLQq3HOTi-8;aMPrwl-ZIL1IdR5Cqr{h;Es0x8 zPI^YHO1QJQU)o3ROB;{2r0HWH)6U9&8G35Z-QG2)e9ygDapm6OD{=c9SU>MIpT8-y zIG>kOPfQQG{?w1fW^ELP3h z_NMWaVwF}*H_HnfaWkPLq3-NWR?UUCH$1B1;STaV ze9Jn^BC?lbzF%)o#}y6BdwfSfUa7Cxuh_F@gKu+QB(3@5A5Ybl{SwV#Ieq_k-4eOC{sUtxzm`nMj|qp`i_ZiuyIRat zzd3W==WtO0r^$uU_TdS|UL7l)zufwoVq#-%Y$nS4r$)U~nwvPyqPl!09m6O;dI z&*b0N&Q(NQyVUke$uf%PY?d3Z_2v_=%-$FZE5^+`o-V0un2~+*Ok=#UO7XPtlRRsk z*1WN;*QmL>M7z1xsaD4Pl$+FJUH?hquF>iXk+ZftDb-A$<8Wfvj4Vd} zhao=Odk@Z0zb4in_Hf(M=Z}P>ud=lG%@8YltSg{?x-qk`Hjq=!QQGK{U|~r}3LD>n zYv=r)X5<;qY%YKy(PrUA_Lq*ma*L+8 z@dtOU%w?<>iTBag+a-)1;c`VtT`q;=faOU9cN~hGrC?mapzBtziaZQs;tek9KZBOJlhff z*mIx1$$RIIzuikLj4!>a-)?MVT4%pkGiR6l)Oqo)QuPeNe;P9EH1>R2@WNJQR=csb z9m{>ahNgz8k;=adwHy-fJz4bklED7lU&6bln*4E6k6nI!=Bbo_nKkp|6Rqk?^6ULK zJa4Es&@3w{NLX-Q-|%WY$Ev!|YZ8BcwU{&48ZviiO;cT5u+)BPumslvw^pmmUH6_l?66L5anrvX+G~GC{^F0oIvzQ( z*?bNuIv4a9qaGjsx!B`OvyK+M`X7$ZX*nOiTW|X!E#}?KX=-=+=GA-%{lVEg z|4IMh`ex?ev&*yoh}g#-m>#=p1&5Mx{ZH1fF%Rxk=oDB<^|O3?xh>CMjYr1)!y22* z*}?(|=|0)VuO6P2Vl{DozU!Y^U;JI(Ze)EV=KJgMvbX-rggAw>lykx_LvQ5S*cY6BenMx5M)k(kb&v0fy!*Ym z?^eWxnXZx#BcW%w1ZUr@1GpyISRl(zdd1z6^C- zPTN%POP_kxzr8!L-q&&0$(3FYHh-uoi>Y{cD&S~!xryW)Tg%V-KWmxVi>B#lIknYs zhDC(FN$S~a{!d~jx8}{y%=yJ-S~m^ZG7l|tEq*tzosaWos&Lp9w;;_AjsaD-$`30w z-TqM2Z~Ab?Op*7ANk)7Z&%S@@_Fm>sZt;(~+&`WEteR`>BPHgyOs3w)qN4+}rc zP&^Ri_}t2-g{-IKqZtCB`|e+IKmL^0`s2Jw<#wt+>Z8@xlr3)f z5>e!5mvXm6CwYyFo#2(4{hmqD%gneITr{75wlcJFvdcP)N7wdyD{SU!N#49ZvRovx zxaQuh8qLpfcc$IV;5*lG?EI{Bf%^Tiu5$`&9-5rm`--!DtN*P>zoTO}%66US(0*+# z=H)n1bo2YdcY>FtuD`Vm6TSI|ck=GuyhT?_dS>`&smtG-;`CofaOwpX-Q~q5Go|mh zfBgOF*!$x@{_Xk5Zcw)%;f?6}JU&uaTaOv@vz@Kxs$7m(p6kg?(;;*4SRZGu8YhDZ2 z$J7ev!y;2nOyZVZYI}D2N!Ev*dcl#0O?I|R&tB%S(??6DdEVV+~ z_Nr42=Q{nrxn@)SrOi*K%`b`G*&cmbYW;;pCc1g0{+A~`%v>ei_GoGFmNmzp`%PYd z_3DKe#l9Dde$=_HepFl;{%eZK+D|*P%nw~KJ$Lzi@zR&eu5G$iRppu+dNkv0ng7qe zu1~U4_ZiKfwQP3#_UY4Y{Evx7O<(qP{xVhli)`R^j`|&e3PI3NBmmi+DI=(5`)NyE5 zNW|RrxM8P~W^WqEk!jsvBI#n%Zcr_9#X3{MwBB5Hxxrmmu0H(ee9)d*Wp<#>las73P;K)%-$Z* zRlof*%X_A#jz5)Rm77}YrrGtHa&5WqfBs_nghb94KR6TIEWgZKdVbOK#&0U$zSx8& z=sDWA#;Jb$#QQ7rmt@Kf>k{KyzDXhzLvPl&oImOQ#pq|mA?uf)zZ8dXN#BT!K6d*B z>#xd+m0h!6h`$h@ICqi9K2K5OfUuPpvRccuzp%E}?^|)9o2%Ayp}uxWP{bKMhjTnv z^-mm~^!nK@Ns09$Pnmz5P3UKx{KRLYQ?xQy>>S2zS45Hv{xz_k)_S_O#^OoH`%$s@Za7Nzo_k4f8J-Xf(e_Z74?i;4QzuWh+OD%u#z%=q%T|#U+ z<0l)IQ-`;R-B7D%ieW#Pz47&V#Z;Dh<62S1lL-@MM}%+K-@y)dQBa^vFV^@HP@4a_9tX~)8Nfj7;Nqfb9!JxZMvZYRBmt1qTe7mZ6 zhUSK={M=>Cf7ASXZ7l_EPiEd|YtqW1dGOWT5?7@!lRvR&{$vcTSJ>2D{O=CSvgt3g zj?I^84l+;EJCIwOX?5xDlehlCC#(a0Y4I-0T(SIQbx6ossvx z{PCL6{L|rWCzB5^?Y+1AQVA2!@%?V<`bkgkCM}KIaq0P^q&sW%B-dSLvK3q}#Im=) zPvOU3ix&9<#*Y`XRkQ{8X`QN1l>8>`u%&F_l)eQ4zXBzaHG0_t#P}B(wCsP^#6GPt zc~)Fw3j^2t&@V9ybQWKKA*E!&CZC?c|LDz6slP&1e;H=SyZJvk(K=_nzQ_JYd@LP5 zMSq3f(6G7Vp=uluuXS01^-F0W^YXL_w(Dd#Sv-$Udi`bBp`oU(; z*WB0HEGe!R@hAIJ-9pAmb+*$rMUU7oQOkVq>9fx<->R%m+RG(r-iN+JhxFdh*kY!& z=Ig!YZC9V#D$Awbn^Cc2?_<+dJO-cci%ON%KMWL;a+}rCcGvEU$b4&)HD+yX*>`+1 zraZeK+7&;i*uPS;EUvZn@{WwXP2P7`uV}hn;1M&q+IoiJiJP;f*$?HPIFZ3|B>&Xj z%0iz%)9%dXKmOnRiS5yTtzT+YsS`HOt&_Gjx2Si|`WGw1)p9ys^Fh>~#k;$9UbRp; z@}D(=*MRx^AN|d7Dkr%(XC|AOFXv=n;GJxsZ&$ChQ21?zU9y9EV2dVOcbX!nXMutP zlTty$(=8tC<=d0;`zKl0`l%jWa;x*Qu(fQE-zHIoy%u}Fzxr~|S?Ksl``ybgPi1|1 z`uoD2bItSrg+JdbDOdUDp*BNUCo9*yg-OQqRyrNs{NzK9<(;(;EVZ;$b~2q0{b0j- z{`+Irx;+6x_2*Vs2*$lR?)|4?Wm9#?6|ZAaF%!D=gFhDSaXP3SdZy&$`k;?lceE4V zPd=ReUjIpw*DU<16k!K&zGOxJwM>1XU!_E`Nz^lY?LhRwG}=8$=K<)>#z8sv~S6y z(?7bJ)~~wJx1RINs~fj3iDlYvTED#Z{ls(c=Du~`RsOxK^t}*=i{FdoUzhEeCzJ7N zfVCL~UA` zl&mAU*_?YytMoVSA5mxPyjFJ}nRIMj$)g#GUtb-6)E087=her*yF@2{*14F@e=LZ} zTWOi;1(SrAe+w7C>{-gsD;XBtZfyJDT-mFQD!rdne`p&R*Q~gb;=U`7S780Q$=_RQoQRW`6pt=`g9Yy>?~7Z zPTQOMy?5V!5LjW9rRFc}krxrYXW{dhQ)WWDix+)jh&sMa(Alv4jl|>UikBu{p1-~S z&E+814J8w^<8SpHoqg=(B|q_O`KfPBgtu%~s?X!&@ko7M<*N8drzqZc+NHW{9m{ej z|LNZIgI}b`bNlxBy;AEex%QoV@Z#^^qj>^5G-D(t9Gcm)_LW!B)VP2n_X1?j-CE=H z(`Z?%_l-Y=X-lu@P8ak%oOf&c_U`PaqG#d))#p@m|K^;N`_%GDD0lizsq4Wj%^v?S zGoJevYoMgQ`piRza{%;6s&Ws zKQfJe=w`9{*lz_!*kDYXHRqpJvgoY z;r*if$z1;yKP>zczEDB&h=}Wy%tPv@L<1Lcy8f8yZ*JlEs<`aG}WAD688+JI~7ir0Q#C_L@cvZy}C z^Z1u3f~(@^tcnku(0=#0pG07^aIouEk?_SGnX^`iGP+*LOtQMP#>Cw2!K@jL*|(V zFV-rpc6o}{8J!na=dV>Vcf5;PIlH4>(`d!=`lvOHC5Jq239p=Xc%G+}u9dNQuxbgFC(zl4+8o7(SwjJlrnd=gI&N-3w=7j(g z@fq(_-R@amD(hY&(O6)}QoEtx(K~PH3I#rqa;NPAYcw+#PR@GX$|nAG#-)Uc`MU!3DIA8Q_3dvjiPoa*(7#ofY}mwXMUZ`T^gQsY^C4YQs3nl%}$%VcAfolAA?w*=KT63kF_&u*R7n9emLUg zKE3wWazEn=-@DelnzEp8y-@zQuh$}ec8HveRV^0x-Cp5tCL;EkgL_7YeDm_X(>FEc*~w#XWa`aU1(_vSl*Oio#ir9qju&+OYb$7kx2NBe?< z;$Mn?o4Zl&$x6e!8!vX-CB8jZx>9xW<0susUFq}h#EEb1Yq}nO#KeDDd&OQ(`J5a5 zvlahOXZfJm!BOUvAf6r^aI~`gUd-c~@_T#QKHFA)&iwFsws~pa-X$g3h`_hvD=};`2ZVn_ciPFQh?{p;uLu#%40jI@i?m|8@5lFaw_D&vbDpI) z#lNIJuow6;HQp^XK61gT<>$pOR2j{f<@nWNr`x3Eo}U^6uN0*(*l81?_x&1cZLg$H z{yK?+J!z^dCPW!_%}kj&_23(xCdYMWGQZWc|2^$)5bpQPG+a;m;GZvQv%G{W6@YrvIXd&j=HPbilT)$>xI!kn0;qg$b6;Bm& zM0ZG>Ty@o~=UX>l5pVtEbXl{D(Xu@CGZ)_$Vq^caD)AJ{ibPrei#GEX7#!Z&d8;(w zydPWSt9vI7TrIetIp^V;KI`+3WVXIv5}N)|rS5FlGAT z5RGCpY)|^J=xkTye9>UruD34h7p_QrqtSQnq|J_3I&%`Vyw@%N&%c?+vX7JH2G1mw z$@84t>hC(~)+sEi@s8Gt=9(pNRKl}I%~jvP_d{Mp+olbFPvousr<}jzg3F?dN4)j_ z1RgWq>>cgJYvOzU%+0yqE#KdqdDp(a{tr{YA!X*mC#eyB0U9ShI4sk)>MDvco#5bk zT&g+n>yLffab{UfiCb!p+%Gy*|w zsjC;aN&OB!y{-EXHI0N6I7rNq;%-W$I_B_kIIs%<`R* zYYy=qHAtD<%u#99U{Km8u%q(?oAV*P)Ih1@EB<)Y{S=AXSJ3rhI-i=e>VahoH%hA9 z&~dvt$LSo4kxvDDMOA^)_u+`q8Z|H2~m62)7OOE(_R z)?&H8xSnU?6XR)e2TekGE5c56PkejBGyeQz+c^?xs_jRLXY5x{Gycn;v!Q~qoB7Da z)t&Fl%l2sh>pPM9Xo`@2soC!EGkU`8zvqM+wr{yr`zuAT#q5&H z3=Dd@3=9gB1+8Q!ce@JKCsycmwix8_c_?Tnx-%tB7EqC3Qq|$T^2+^w4RP^+$-0Zcx?~3oPy;WDY)c61MeKSw0RC4_I z{iF5wz1`pM6rcZO|9oEc^Y;Hc-%AMUKHSfqssG$NSmCVXC7+4Q3S?%jyd+Y8^7$5p zsYM=-U&=k2ZzHip;rxV+GcTRt`jlfAJgZ>B^AC3vWFKr^GIM!h;e=%$Y8J*XDD-Ub zZY~j@o*5&zDu3Pu-(?siJVgAsMbuLUB?{tm4$>pI#cNc=puClAiN? zAGCT`r&g3sxOX~x$>%u?>!&{7=~%~pon=RTnAB zy;Ae#mxM;}&F#IEGijOUkDO^MqnZBry}Y@qaPzU-OnYn(ST9LVpOE!4_R3P&NS5I*P&#q9=A^|o3r($?jD9CS2MMlle|%4&v@>+~j(nTF z=c<=hu0QskJE&*&lFI!8bqWh~l!dCQD66SXzx zR{5RI=${p8JMH4#+^EprECFNYx1IAUOcJ*kpDk0)iI^@oJ#-iE?$vBc$D$S=s$6%w zMR%vk43oQWa=VvJn&^GD{_DgQk(aNfsZE~Uvtwb(>Y~R1^@lhUp2rJ!y1&`#^Io%( zd+)ud$&hBLQgPd|BCZ&ZCzzA@kWN>imF%SoMf$4fchk%GI| zX9-{6%vjf5GRsqXkBux-^PxAZct5_I(mT~hS?%JrTkP+pw;eR$dn3y^V_EOSRbie= zeR^Q(_EZJR9nPoh7})NP7n6W?pzP6;0K(v$;VO4FOvY_cBB znwYdccJ0UG?N@!;*9&QDo{Nn$3fnkYtMA0Mq`u1!SQel4lMG~?T6ogsRF-k5Lyvj& z?z3jMbHbGyS2TOBkSaSKyzZ5qBe#e8TRG2F7nLKHSYDE>H-9sw^{DvVmg^GP3>u!7 z*S%-`mSgxp>~fduB+pKUBM&n20!;ijSm@q<-*77DewNf*KO^5;Z#$>VJ-Rxos`gNq zy{p?$|UOM@^ zHGg++GoP&0eQe!3he`GeKdLic-R4pr@3$-|D&ole{a)za{27c&sEd;)^G9(i>$rnae{aG$IP&9 z@wwY}&5+Frlf2j2>BG7&;6zDPkKFZ_{IIK9lh<8;skJL=xpwCKW9BdZXx>}@dHGMbUVCME_L6;_`PY7Kt})I! zvE8BmcT(av%enavqZc}@YwP&_jMsFkMor1SUihW#?Dx`@)=G9ho3LmRiNH=c9<8Q1&%a^-~m zN4I=@BGvtx$9qOvDgW*>OwWJ+oZ&Zr`R&rW2Kg&LZyRWqzn!p2{nE0cYKw~x%}nnk z9@zP|xa;xJk75nCr`D^beQDj@<9)VZb>^|d+XP=vQ&PAiym7af*zv0`4|KJ@)jRB# z+$O%`+CT0?cf**3-|jQ`@?!E~$nT{)PO*3zRw8kIKM)Ag_aZ0|5F zs0?|2T;k#H7KXZ2)v3otif-RTVmEw{V!^ZFm@ zx%H>(_d9#OiV}SJCH>#tbM*r5|0e#Qvhl-=O}@{st9<<%dB>)Bi?CV$is#FoE_--K zapV8^BeL~ZU&?>+pZotrPco&O8<^ z(@Mc-m^Yv8idu0pFE@drWGpCyMrTHcGEmGGvPdmg~7TzGpZoYdJ-yzN&ZZTgho-&EZ z@Uu>3`)HQ251uqO7y*4^7e@_K^qzHQUpDsCln;^(bJohns--oEVk zFm1}I(x87QbXKmjKYqKD_waUyR#o105}zL1T+J-k%sD+P$=Z_Zyv9mt@A-QzR#|K; z%X`9TJ#nIQx613}yJmiC|M6{AUNQNyMB47-Q8z;WRd3srZl34=4xRKfU3=2SBz3mX`^C$i9P7UMI^|DcQRdcr zJG)M?vm9R`=(+Cnm2DfAU*vO{Tl%TmS;w$m^3WB@k1jG>Gp|fc%j!Jc+S%2%BqHj_ z+0rW?-cGod?2>au_6AQ#&a;$Hi{`MHeiAF4_4{edn(|{G0)9>Nu3{G2+WSf?`)sCS z<(bwQOHTOCynT4iEZ*I-ym*8!b!vRJEs8Fv3O_wJbXh>q@*BZiSI&e+zi7EGQuKAw z3zZ^;yo$_ukczKC9l#d)>K$qOy#e5l{3As*At5ub9(#Rcp4XTGfPWyeI2t znigfoT)!aDnD+izpK!u1$Gc8yvOGKEPPcj$?KS=6loK?4;+-iQ&iq;-*&Hpv{rGX( z`nmsP-p_e?Z+`i?QIiHpM6@b6@FdtdbN7fhWhpFx4S)yGxVnlP3(92 z!nmtvhTidTx!MU_PtGyEXDwn}I4}H?-Pb)`>yCZqxe(_p@A})YCYFyc&gS`4-U~Z6 zIdW|i={djLoz27EN%+*M{t01Wg)M(lr@Ci6O5A$r_(eZ;`&&C_NG{VX5lfljzImlY z{^yrbJyR@gUJ8HlRQjq`Z?v&Wc;%-nvrjyAd|z5(Tjerqv4{%q`K?d?nq&&kI_aDG z{esJ$C*jr3_v*A(d^`ELC(Hhqddr_?lch^_p78(FI;G9NV#EApb{?+feUmneSWRmz zW=&bUL~@J3B>kYGyn?XPr*=-uTj1}gB)@ji-!GX3oR^Gt34Z&UkSAH&me*FFX1w5e z`pe=4v%HO8itYLNGVIK^q-VJ^nji02%zwXkAA{RBaUW~1g}%)udzaQUq?uHPReV;z zyq)RlKBxS9YfUyAJ$b!+ruLrJd(mzGq69xxaxHwarrCM_syEATvU{%ocG~;Ie+O6l zxsh*WmlT|4IJ@#&W!Mkyzl!fS-p&lP<87_q)B10zyoFcs>a7#5`KMRrFaD@_|B35g zll=4(>aj)zg%r#a)`?|e(`$ko9mZ2 zm$0YgE&BXI`oW{4ud-jxE>Zu%88LmL8Oyf1-fvkUOD@Jr-90OH*ZGUu*|_x=CmyKR zkiRe0`qSd<^%d`YXBixt`qyMa+cSZRUoQWQDlS|Psnpj^sJC(m=JML}Jvg&_`J$g1 zQ)QQavNjBAJX5UQrsd9G`EF@zB})g_tp1>@-yOGPh)m?s*%y3LVc!h@#~12Ys^U3& zzbigi$0o3T;Z+G+ezChe)iEssyTV?sRylKPjq|}NS8oU2FyoKWTU7nh?#3INh&qAA zalgclm8M2Y?v_tZ+U;5Y#L_RZx6jw!?{mUl!L&*SD~q(Biqk~guZG(Bn5K#{Z&`9H zT2pb?7quv!Eqb>jH^c;UO*7wKadG3KokzoO-eJ1Zb?E9o$Ece^pLj3a z`)Ru;9#ej??7#)ZU7icmH0Px=8*e>uVyjll-yeLJwlAE|e?|MHb;aZ_!PD!vI?LKF z30ktpbH}rl+uyI+V7F$H#(!>}V$(lzwsT5r=5;;nZQjBa7_OU{`IRxM`S>igjJ;ce z=2Ygi3iq_`yPes`@<>RXDQi36t2+^>nVG8{osEAo;r4szihtD4dTp) zRx3p5-ZN)tk$kSVtVWbgN@M!P?=J;4>KRUM>dN!6wrQC8=-Hk%#dGKVzNpMKeR4X7 zr_X7YV}`wT4fbuFY1|iVZ!$hj@?75k=zsE;9h-9xG}Nj7a=u}9&eq}H;pUJlL2)1Y z7+2jq;@cc@`_caSucB;NUqATTRP{qasg=9DI>ie$W7{%r1v{-j5_zU-OH4wtdV zZ48!KTK+Y-yX1`!f9uBZjOH!UGWTN(9PRhICv>>m$KLQ4mu)j^<=C(~V|UfP29-w# z3dDU~tL4|&a{RvAKlihQh8zpae&4GNk-7<6g$;Ib)JNQ8&W~}kKk!HW;LAXk7s($^ zPJZysqTvtAw_f?g_zJEAe+|?Axm0eEF1Xuy_XTH<=LgZ|f0}Eqxo&nSmAn??75y?Z zpnBuIpI?H1N!AqeX3SqWe{sIO&2Jvt)d&6sh%xi8+~DTO&GXl(aj(FNDn0IRRa`7> z8}bw$C;z#c8M3^u zXSz->I#tb66MAy;z4pt{{zC$j-iJ+FZ4lkz{#@<+tw_-ue9xz7I@E`qiDGi$n-IS3 z+Uyh0ElXP26@LhMOy})32|xPXd&8g85kEwaTwDF&sQ$#X`qs7YM5CPQPq-x*u82E2 zL!9Yc@a-w=#m|~QUH5978fm{~$$bs`4kqCfpYvy}xR_sgZFa&W_5I>Yy<3)V*x_>N zYx5P=qmD-$KONn4_{~IFwWjGB%=Kl4>S>SIS9nZWJ)xiF%IoEMEr%{QmvBBhktJzp z=5Ug6Q=o3^o=a1?ezLy^>RdUm+2mm7x_J}69XPnq|Ky^*mm~x77czX>I9>0+xqL^f z?wqeYhtz|7-W}H4qMv<0t)cgYzQkW?-(~+D*VV}$`^nz(-QTMH-}HNq|CyH8H(wV} zt&iBjE66AO#ZG0;=^M`B8>WAISgAYj`bK}rYV9{h{r6Ze{}&Y7&9M9G{1>JbHAY!I z_Q|@QHP_R7Yi<9U?!6q#FUj)d`oq%;xs??D^R3)-N|Y`0|FV-(d;2V`{X(UVwoM4z zDl_La$1c;V<^Ua;%E`x`@*aG>Pp6YBP*SStdClVbw4}~6cAI3A{xGW*d=tEx#k%h3 z&l^R|zE4-YNZYpN!#y8iZLQF~*UJlDm@*WbZ3;gBA#R0N1mA@D$Aj%&n@m<;7&X)9 zvxR;7dHr)yizL47bBwI~Z4!=UQ%m3xC)*aS5_NmW1x8!`G)|14A>g*5J zKF(bD+R$5P?bQ0I$@%;|;n(W69^p}Sx}n(EEGua8%m0?7l#Fhwds+HinS`dxa~akt zg@xFiJpXP@Y5Rf1-jkJY3TM8t-+D66;q=UyxqcCKp=WlUFB6(%zk2HZi(EB-KJ4X| zf4}hZ6X6Gina25zr7F5?32AeZZQ5fkEtjuym=?@1XU--y25ak7SKo$at;^4&YHSi7 zKRajhJ@wwLx${pKs&6`hZ+PN^L(*m*F&QsMCfhqk2U;aZ+YC)}Gl(dV1QwBo48hIjDKe^q@h| zw22m3_d{ChtsYOEU{q(h?$d`k+A9n7U#f&}I%<%Xr5d$7W$D}#7D*rY7JIBnIuWhw zsC)Zq#MS0qkG2M|ly{lWc4Aex*?DXEFJ9i@Z2PtgymhfE@xjmLT)gw^d-Ucj?&}u_ zHeTMbcGV*8Kl@apXFXSJcI9U-uwC+SS45J=pLjV=LoFw{tIhR~y_`dY*{qkx7d~B| z8L`yg=i>RO4_n!~YIHmH@Vfoll)}y#8_@1sr}Cd?+Wi*?PUV*+*h{TS&)|#n+~&9? zn@9PGKx)`7lS9c3onI=_l=fa&bmMqMzOO^v^`M{X5&ZKHUshXr#$o;K3(_YHK5F0m z=eO^FYfj>&8=5_nt}nNmA+H=elOGf}h<#&Qo|jaZvokPEs z=8CXG>K{Bg=O?uW+K3-pu4>bN-s8l>%{q5QK3dMR*H7fo|L|c^hknP8B#HP*1tRf$ zkG0qQ36Kf**$t{rqy~%dJn3z8t!BbmyBdw;o+`O8eH- zaN-O*|lNNKmm=`~zx}+*=W!3^x{gNB5ny7oOj@Zn*TRAUtxb&e9dj zwl2!L$<$`6>V4LFSz7+mO}qcA{Y}U_{p?lFZW~Pp1^M6Q3;pt+UFEg6Q#@_zC?=Mx zTCcl0=SS0o$1gHKPYZ)SMq0@kt_wIKpYlgg zRC4`8f1`gr*IB>pd~>R0=Bz7Sl6S+pectkh&2X5Sd6h*bCyGk=} z!EM1>E~SV2)&(DQGp#+madF(q9^-GhMYCJ>Kk${j{bO~{{*AY9=ji>)%j15L`);n$ zy+g9Ag!gM+s-IiBg1=APB>2ev)IZI_rj7MMH;Y>5bOwpKnQFg!alPa*+sC)L7RJ+e zuX^^@HoCiR<@}2N?=$$0yUphQnh?x9rzCs5{39YuXKJ{$*NTwznA>|8}v9W zP1@Q{Ci~S{-G;C|yItO%dHh1(hVT0poBg{l-@mKmmDyEvGN5BueTHWEmDFJ2f+b(i z=DYaZ+pBix|K*)~)=ethKcR9_P0@dbx3_;Cx?{iRop?uY(SL4BY1X4^I}+Y}QrRu0 z_2Zh;#-8n$~1kjoab%pKO%HFNhq`4 z=l6{K$*L2d&Hm^j`qq?X)5jFvgExOfENZ(j^MKir(0|%7&d1G`T$QrQR1Lo<`f^#K zf`hZDt?8F}obO)9-B>JrbMb4hlgAcbFymXt$HMMp`X%($Jsu^OWS&0(?-nx1yEQGE za767`%z3@4Ij`s4U2-Evq2KC*&foG`0aXtaoP+AE=jvYG{>sEPt?P^YVh$Eg_hUSs zikFtWpCQlP9mY7z|43)T@7yopg)7<)Z|04+`IRj9*DILIf6~QZy}diwgO`^dTB@`| zNI12sJn`Dn>&N1?<{wyH(&yjj{%%)qqmHD^9N$S>cW*b`BBc;?*i`1FZQN1Ysc+2s z*15T9gdcymqyB`u=F7lC*)8vFdMCe7*`t@Pe>$5zhrhjCqkNouZAA9TrN%TyMMp@7v1jBU2#D-_}76;y;Tgd|Cu*)rD+tXA#czAd&7Q_C<6n7FaraF zJOcwmdQoCZPO5HlPUhq}<#IevwY1J^pFHEc^(hm>1SvjMR26scNi#7sOkQ|TX0y*V zB_>wzffAF8Z)C}!?D}RvfF+Gwe3J`xL?;*AY1Qx1SRhVpdON(it=Hx;h{mCe=W?=2Mmi&|JZVF6hu@c~cDP7WNXAYL!>%=ix-->;5`yBxp(CUmD zCI$vcHUVYSt1SDJ8j8edFwRU$@A}O zF-^;uY?voA`T1R2ris~;4fC`n2i((QdQ&pF=AJ*3PwC{d_d=PbmQGf=Z_RY1baLYT zSVsTJ3HPNY-@Na~#85kV;Yyjwd+Nm}$2`ztnpOwqX1@@e{Qo}R Date: Mon, 23 Aug 2021 18:02:21 +0200 Subject: [PATCH 144/869] New timetable widget design (#1384) --- app/build.gradle | 2 +- .../background_widget_header_timetable.xml | 7 +++++++ ...ackground_widget_header_timetable_dark.xml | 7 +++++++ .../background_widget_item_timetable.xml | 5 +++++ .../background_widget_item_timetable_dark.xml | 5 +++++ .../drawable/background_widget_timetable.xml | 5 +++++ .../background_widget_timetable_dark.xml | 5 +++++ .../drawable/img_timetable_widget_preview.png | Bin 5284 -> 25538 bytes .../main/res/layout/item_widget_timetable.xml | 7 +------ .../res/layout/item_widget_timetable_dark.xml | 7 +------ .../layout/item_widget_timetable_small.xml | 2 +- .../item_widget_timetable_small_dark.xml | 2 +- app/src/main/res/layout/widget_timetable.xml | 13 ++++++++++--- .../main/res/layout/widget_timetable_dark.xml | 13 ++++++++++--- 14 files changed, 59 insertions(+), 21 deletions(-) create mode 100644 app/src/main/res/drawable/background_widget_header_timetable.xml create mode 100644 app/src/main/res/drawable/background_widget_header_timetable_dark.xml create mode 100644 app/src/main/res/drawable/background_widget_item_timetable.xml create mode 100644 app/src/main/res/drawable/background_widget_item_timetable_dark.xml create mode 100644 app/src/main/res/drawable/background_widget_timetable.xml create mode 100644 app/src/main/res/drawable/background_widget_timetable_dark.xml mode change 100644 => 100755 app/src/main/res/drawable/img_timetable_widget_preview.png diff --git a/app/build.gradle b/app/build.gradle index 1338bd8df..409e6c4d1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -164,7 +164,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:496dc01d15" + implementation "io.github.wulkanowy:sdk:b991d0c" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' diff --git a/app/src/main/res/drawable/background_widget_header_timetable.xml b/app/src/main/res/drawable/background_widget_header_timetable.xml new file mode 100644 index 000000000..98eec700d --- /dev/null +++ b/app/src/main/res/drawable/background_widget_header_timetable.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_widget_header_timetable_dark.xml b/app/src/main/res/drawable/background_widget_header_timetable_dark.xml new file mode 100644 index 000000000..616a91279 --- /dev/null +++ b/app/src/main/res/drawable/background_widget_header_timetable_dark.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_widget_item_timetable.xml b/app/src/main/res/drawable/background_widget_item_timetable.xml new file mode 100644 index 000000000..08854fba2 --- /dev/null +++ b/app/src/main/res/drawable/background_widget_item_timetable.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_widget_item_timetable_dark.xml b/app/src/main/res/drawable/background_widget_item_timetable_dark.xml new file mode 100644 index 000000000..e432a648c --- /dev/null +++ b/app/src/main/res/drawable/background_widget_item_timetable_dark.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_widget_timetable.xml b/app/src/main/res/drawable/background_widget_timetable.xml new file mode 100644 index 000000000..2267587d9 --- /dev/null +++ b/app/src/main/res/drawable/background_widget_timetable.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_widget_timetable_dark.xml b/app/src/main/res/drawable/background_widget_timetable_dark.xml new file mode 100644 index 000000000..6fe7d0ab2 --- /dev/null +++ b/app/src/main/res/drawable/background_widget_timetable_dark.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/img_timetable_widget_preview.png b/app/src/main/res/drawable/img_timetable_widget_preview.png old mode 100644 new mode 100755 index 550260258a82ae1fcb8c54530efff52985580153..8494301878a1bdcb838aee6ee82be8b42c3904cd GIT binary patch literal 25538 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 - - diff --git a/app/src/main/res/layout/item_widget_timetable_dark.xml b/app/src/main/res/layout/item_widget_timetable_dark.xml index 34535c694..06233244b 100644 --- a/app/src/main/res/layout/item_widget_timetable_dark.xml +++ b/app/src/main/res/layout/item_widget_timetable_dark.xml @@ -6,12 +6,12 @@ android:layout_height="wrap_content" android:minHeight="45dp" android:orientation="vertical" + android:background="@drawable/background_widget_item_timetable_dark" tools:context=".ui.modules.timetablewidget.TimetableWidgetFactory"> - - diff --git a/app/src/main/res/layout/item_widget_timetable_small.xml b/app/src/main/res/layout/item_widget_timetable_small.xml index 36a6bbddf..1bf4072dc 100644 --- a/app/src/main/res/layout/item_widget_timetable_small.xml +++ b/app/src/main/res/layout/item_widget_timetable_small.xml @@ -91,7 +91,7 @@ tools:text="@tools:sample/lorem/random" /> - diff --git a/app/src/main/res/layout/item_widget_timetable_small_dark.xml b/app/src/main/res/layout/item_widget_timetable_small_dark.xml index a02d85850..50bbbd031 100644 --- a/app/src/main/res/layout/item_widget_timetable_small_dark.xml +++ b/app/src/main/res/layout/item_widget_timetable_small_dark.xml @@ -90,7 +90,7 @@ tools:text="@tools:sample/lorem/random" /> - diff --git a/app/src/main/res/layout/widget_timetable.xml b/app/src/main/res/layout/widget_timetable.xml index 8a08b5d24..059bb741f 100644 --- a/app/src/main/res/layout/widget_timetable.xml +++ b/app/src/main/res/layout/widget_timetable.xml @@ -3,13 +3,13 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@android:color/white" + android:background="@drawable/background_widget_timetable" tools:context=".ui.modules.timetablewidget.TimetableWidgetProvider"> + android:background="@drawable/background_widget_header_timetable"> @@ -77,7 +78,12 @@ android:id="@+id/timetableWidgetList" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_marginTop="56dp" + android:layout_marginLeft="10dp" + android:layout_marginTop="66dp" + android:layout_marginRight="10dp" + android:layout_marginBottom="10dp" + android:divider="#00ffffff" + android:dividerHeight="4dp" tools:listitem="@layout/item_widget_timetable" /> diff --git a/app/src/main/res/layout/widget_timetable_dark.xml b/app/src/main/res/layout/widget_timetable_dark.xml index 5533eaeee..9c8b8c561 100644 --- a/app/src/main/res/layout/widget_timetable_dark.xml +++ b/app/src/main/res/layout/widget_timetable_dark.xml @@ -3,13 +3,13 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/colorWidgetBackground" + android:background="@drawable/background_widget_timetable_dark" tools:context=".ui.modules.timetablewidget.TimetableWidgetProvider"> + android:background="@drawable/background_widget_header_timetable_dark"> @@ -77,7 +78,12 @@ android:id="@+id/timetableWidgetList" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_marginTop="56dp" + android:layout_marginTop="66dp" + android:layout_marginBottom="10dp" + android:layout_marginLeft="10dp" + android:layout_marginRight="10dp" + android:divider="#00ffffff" + android:dividerHeight="4dp" tools:listitem="@layout/item_widget_timetable_dark" /> From 2979d8b62ac47cc034028e34286dd7805930866b Mon Sep 17 00:00:00 2001 From: Mateusz Idziejczak Date: Mon, 23 Aug 2021 18:16:41 +0200 Subject: [PATCH 145/869] Show information when the recipient has read the message (#1430) --- .../data/repositories/MessageRepository.kt | 5 +- .../message/preview/MessagePreviewAdapter.kt | 52 ++++++++++++++++--- .../main/res/layout/item_message_preview.xml | 13 ++++- app/src/main/res/values/strings.xml | 7 ++- 4 files changed, 64 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt index 2034000e2..2d70e26ee 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt @@ -20,14 +20,12 @@ import io.github.wulkanowy.data.pojos.MessageDraftJsonAdapter import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.Folder import io.github.wulkanowy.sdk.pojo.SentMessage -import io.github.wulkanowy.ui.modules.message.send.RecipientChipItem import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map import kotlinx.coroutines.sync.Mutex import timber.log.Timber import java.time.LocalDateTime.now @@ -79,8 +77,9 @@ class MessageRepository @Inject constructor( }, saveFetchResult = { old, (downloadedMessage, attachments) -> checkNotNull(old, { "Fetched message no longer exist!" }) - messagesDb.updateAll(listOf(old.message.copy(unread = !markAsRead).apply { + messagesDb.updateAll(listOf(old.message.apply { id = old.message.id + unread = !markAsRead content = content.ifBlank { downloadedMessage } })) messageAttachmentDao.insertAttachments(attachments) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewAdapter.kt index 206a74602..421453c94 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewAdapter.kt @@ -33,7 +33,8 @@ class MessagePreviewAdapter @Inject constructor() : private var attachments: List = emptyList() - override fun getItemCount() = if (messageWithAttachment == null) 0 else attachments.size + 1 + if (attachments.isNotEmpty()) 1 else 0 + override fun getItemCount() = + if (messageWithAttachment == null) 0 else attachments.size + 1 + if (attachments.isNotEmpty()) 1 else 0 override fun getItemViewType(position: Int) = when (position) { 0 -> ViewType.MESSAGE.id @@ -45,25 +46,60 @@ class MessagePreviewAdapter @Inject constructor() : val inflater = LayoutInflater.from(parent.context) return when (viewType) { - ViewType.MESSAGE.id -> MessageViewHolder(ItemMessagePreviewBinding.inflate(inflater, parent, false)) - ViewType.DIVIDER.id -> DividerViewHolder(ItemMessageDividerBinding.inflate(inflater, parent, false)) - ViewType.ATTACHMENT.id -> AttachmentViewHolder(ItemMessageAttachmentBinding.inflate(inflater, parent, false)) + ViewType.MESSAGE.id -> MessageViewHolder( + ItemMessagePreviewBinding.inflate(inflater, parent, false) + ) + ViewType.DIVIDER.id -> DividerViewHolder( + ItemMessageDividerBinding.inflate(inflater, parent, false) + ) + ViewType.ATTACHMENT.id -> AttachmentViewHolder( + ItemMessageAttachmentBinding.inflate(inflater, parent, false) + ) else -> throw IllegalStateException() } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { - is MessageViewHolder -> bindMessage(holder, requireNotNull(messageWithAttachment).message) - is AttachmentViewHolder -> bindAttachment(holder, requireNotNull(messageWithAttachment).attachments[position - 2]) + is MessageViewHolder -> bindMessage( + holder, + requireNotNull(messageWithAttachment).message + ) + is AttachmentViewHolder -> bindAttachment( + holder, + requireNotNull(messageWithAttachment).attachments[position - 2] + ) } } @SuppressLint("SetTextI18n") private fun bindMessage(holder: MessageViewHolder, message: Message) { + val context = holder.binding.root.context + val recipientCount = message.unreadBy + message.readBy + + val readText = when { + recipientCount > 1 -> { + context.resources.getQuantityString( + R.plurals.message_read_by, + message.readBy, + message.readBy, + recipientCount + ) + } + message.readBy == 1 -> { + context.getString(R.string.message_read, context.getString(R.string.all_yes)) + } + else -> context.getString(R.string.message_read, context.getString(R.string.all_no)) + } + with(holder.binding) { - messagePreviewSubject.text = message.subject.ifBlank { root.context.getString(R.string.message_no_subject) } - messagePreviewDate.text = root.context.getString(R.string.message_date, message.date.toFormattedString("yyyy-MM-dd HH:mm:ss")) + messagePreviewSubject.text = + message.subject.ifBlank { root.context.getString(R.string.message_no_subject) } + messagePreviewDate.text = root.context.getString( + R.string.message_date, + message.date.toFormattedString("yyyy-MM-dd HH:mm:ss") + ) + messagePreviewRead.text = readText messagePreviewContent.text = message.content messagePreviewFromSender.text = message.sender messagePreviewToRecipient.text = message.recipient diff --git a/app/src/main/res/layout/item_message_preview.xml b/app/src/main/res/layout/item_message_preview.xml index 83a4406bb..c5dc2fa90 100644 --- a/app/src/main/res/layout/item_message_preview.xml +++ b/app/src/main/res/layout/item_message_preview.xml @@ -78,6 +78,17 @@ app:layout_constraintTop_toBottomOf="@id/messagePreviewToLabel" tools:text="@tools:sample/date/ddmmyy" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8b3f3b8e8..b655769d4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -101,7 +101,7 @@ Predicted grade Calculated average Final average - From %d of %d subjects + From %1$d of %2$d subjects Summary Class Mark as read @@ -237,6 +237,11 @@ The message content must be at least 3 characters Only unread Only with attachments + Read: %s + + Read by: %1$d of %2$d people + Read by: %1$d of %2$d people + %d message %d messages From a6a2bcff3b2beec3b535f4e6364f30c28a7bf937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Tue, 24 Aug 2021 19:51:08 +0200 Subject: [PATCH 146/869] Remove Zachowanie from all count of subjects (#1447) --- .../ui/modules/grade/summary/GradeSummaryAdapter.kt | 9 +++++++-- app/src/main/res/values/strings.xml | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt index 9a888ddcc..0754361c9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt @@ -59,6 +59,7 @@ class GradeSummaryAdapter @Inject constructor( val context = binding.root.context val finalItemsCount = items.count { it.finalGrade.matches("[0-6][+-]?".toRegex()) } val calculatedItemsCount = items.count { value -> value.average != 0.0 } + val allItemsCount = items.count { !it.subject.equals("zachowanie", true) } val finalAverage = items.calcAverage( preferencesRepository.gradePlusModifier, preferencesRepository.gradeMinusModifier @@ -72,11 +73,15 @@ class GradeSummaryAdapter @Inject constructor( gradeSummaryScrollableHeaderFinal.text = formatAverage(finalAverage) gradeSummaryScrollableHeaderCalculated.text = formatAverage(calculatedAverage) gradeSummaryScrollableHeaderFinalSubjectCount.text = - context.getString(R.string.grade_summary_from_subjects, finalItemsCount, items.size) + context.getString( + R.string.grade_summary_from_subjects, + finalItemsCount, + allItemsCount + ) gradeSummaryScrollableHeaderCalculatedSubjectCount.text = context.getString( R.string.grade_summary_from_subjects, calculatedItemsCount, - items.size + allItemsCount ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b655769d4..ee911cfba 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -101,7 +101,7 @@ Predicted grade Calculated average Final average - From %1$d of %2$d subjects + from %1$d of %2$d subjects Summary Class Mark as read From b4b9d91ea6f706efe6bb007b4989e7c4f29b7e91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Wed, 25 Aug 2021 20:49:44 +0200 Subject: [PATCH 147/869] Update dependencies (#1448) --- app/build.gradle | 18 +++++------------- app/jacoco.gradle | 6 +++--- .../alarm/TimetableNotificationReceiver.kt | 4 ++-- .../wulkanowy/ui/modules/main/MainActivity.kt | 4 ++-- .../message/send/SendMessageActivity.kt | 4 +--- .../message/send/SendMessagePresenter.kt | 2 -- .../modules/message/tab/MessageTabFragment.kt | 2 -- .../modules/message/tab/MessageTabPresenter.kt | 3 +-- .../timetablewidget/TimetableWidgetProvider.kt | 2 ++ build.gradle | 2 +- gradle.properties | 8 +++++--- 11 files changed, 22 insertions(+), 33 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 409e6c4d1..d5fccac6f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,11 +3,11 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-kapt' apply plugin: 'dagger.hilt.android.plugin' +apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.firebase.crashlytics' apply plugin: 'com.github.triplet.play' apply plugin: 'ru.cian.huawei-publish' apply plugin: 'com.mikepenz.aboutlibraries.plugin' -apply plugin: 'com.google.gms.google-services' apply plugin: 'com.huawei.agconnect' apply from: 'jacoco.gradle' apply from: 'sonarqube.gradle' @@ -15,7 +15,6 @@ apply from: 'hooks.gradle' android { compileSdkVersion 30 - buildToolsVersion '30.0.3' defaultConfig { applicationId "io.github.wulkanowy" @@ -98,7 +97,7 @@ android { } buildFeatures { - viewBinding = true + viewBinding true } testOptions.unitTests { @@ -107,12 +106,12 @@ android { compileOptions { coreLibraryDesugaringEnabled true - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "11" freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn", "-Xjvm-default=all"] } @@ -128,12 +127,6 @@ android { kapt { correctErrorTypes true - - javacOptions { - //Bug workaround https://youtrack.jetbrains.com/issue/KT-47416 - option("-Adagger.fastInit=ENABLED") - option("-Adagger.hilt.android.internal.disableAndroidSuperclassValidation=true") - } } play { @@ -168,7 +161,6 @@ dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1" implementation "androidx.core:core-ktx:1.6.0" diff --git a/app/jacoco.gradle b/app/jacoco.gradle index 9c1f5b1c2..f253673e6 100644 --- a/app/jacoco.gradle +++ b/app/jacoco.gradle @@ -2,7 +2,7 @@ apply plugin: "jacoco" jacoco { toolVersion "0.8.7" - reportsDirectory = file("$buildDir/reports") + reportsDirectory.set(file("$buildDir/reports")) } tasks.withType(Test) { @@ -16,8 +16,8 @@ task jacocoTestReport(type: JacocoReport) { description = "Generate Jacoco coverage reports" reports { - xml.enabled = true - html.enabled = true + xml.required.set(true) + html.required.set(true) } def excludes = ['**/R.class', diff --git a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt index 8eefc032f..406d91f5f 100644 --- a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt +++ b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.services.alarm -import android.annotation.SuppressLint import android.app.PendingIntent import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.content.Context @@ -20,6 +19,7 @@ import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.getCompatColor import io.github.wulkanowy.utils.toLocalDateTime +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -50,7 +50,7 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { const val LESSON_END = "end_timestamp" } - @SuppressLint("CheckResult") + @OptIn(DelicateCoroutinesApi::class) override fun onReceive(context: Context, intent: Intent) { super.onReceive(context, intent) Timber.d("Receiving intent... ${intent.toUri(0)}") diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt index 188cf5fc9..b44bb4b21 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt @@ -230,8 +230,8 @@ class MainActivity : BaseActivity(), MainVie .setIcon(R.drawable.ic_main_more) } selectedItemId = startMenuIndex - setOnNavigationItemSelectedListener { presenter.onTabSelected(it.itemId, false) } - setOnNavigationItemReselectedListener { presenter.onTabSelected(it.itemId, true) } + setOnItemSelectedListener { presenter.onTabSelected(it.itemId, false) } + setOnItemReselectedListener { presenter.onTabSelected(it.itemId, true) } } with(navController) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt index f169de9fd..d49c9b833 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt @@ -23,8 +23,6 @@ import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.hideSoftInput import io.github.wulkanowy.utils.showSoftInput -import kotlinx.coroutines.FlowPreview -import timber.log.Timber import javax.inject.Inject @AndroidEntryPoint @@ -67,7 +65,6 @@ class SendMessageActivity : BaseActivity presenter.clearDraft() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt index b1cf3b642..f61542187 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.message.send -import io.github.wulkanowy.R import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Recipient @@ -41,7 +40,6 @@ class SendMessagePresenter @Inject constructor( private val messageUpdateChannel = Channel() - @FlowPreview fun onAttachView(view: SendMessageView, message: Message?, reply: Boolean?) { super.onAttachView(view) view.initView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt index dfd95b721..54ee74eb1 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt @@ -21,7 +21,6 @@ import io.github.wulkanowy.ui.modules.message.MessageFragment import io.github.wulkanowy.ui.modules.message.preview.MessagePreviewFragment import io.github.wulkanowy.ui.widgets.DividerItemDecoration import io.github.wulkanowy.utils.getThemeAttrColor -import kotlinx.coroutines.FlowPreview import javax.inject.Inject @AndroidEntryPoint @@ -58,7 +57,6 @@ class MessageTabFragment : BaseFragment(R.layout.frag setHasOptionsMenu(true) } - @FlowPreview override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentMessageTabBinding.bind(view) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt index 93c7408f6..3e5e09b48 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt @@ -44,7 +44,6 @@ class MessageTabPresenter @Inject constructor( private val searchChannel = Channel() - @FlowPreview fun onAttachView(view: MessageTabView, folder: MessageFolder) { super.onAttachView(view) view.initView() @@ -178,7 +177,7 @@ class MessageTabPresenter @Inject constructor( } } - @FlowPreview + @OptIn(FlowPreview::class) private fun initializeSearchStream() { launch { searchChannel.consumeAsFlow() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt index 733eb17fc..f9079b5f9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt @@ -35,6 +35,7 @@ import io.github.wulkanowy.utils.nextSchoolDay import io.github.wulkanowy.utils.nickOrName import io.github.wulkanowy.utils.previousSchoolDay import io.github.wulkanowy.utils.toFormattedString +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import timber.log.Timber @@ -81,6 +82,7 @@ class TimetableWidgetProvider : HiltBroadcastReceiver() { "timetable_widget_current_theme_$appWidgetId" } + @OptIn(DelicateCoroutinesApi::class) override fun onReceive(context: Context, intent: Intent) { super.onReceive(context, intent) GlobalScope.launch { diff --git a/build.gradle b/build.gradle index d169291d6..afe0285c8 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - kotlin_version = '1.5.21' + kotlin_version = '1.5.30' about_libraries = '8.9.1' hilt_version = "2.38.1" } diff --git a/gradle.properties b/gradle.properties index 998e31957..38603830b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,17 +4,19 @@ # any settings specified in this file. # For more details on how to configure your build environment visit # 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=-Xmx1536m +# android.enableJetifier=true android.useAndroidX=true +# kotlin.code.style=official -kapt.incremental.apt=true +# kapt.use.worker.api=true kapt.include.compile.classpath=false - +# # 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 From 4a38a0be709ac180cf8fb780da81b2852ecc2098 Mon Sep 17 00:00:00 2001 From: Piotr Romanowski Date: Thu, 26 Aug 2021 19:35:41 +0200 Subject: [PATCH 148/869] Add change password snackbar (#1336) --- .../github/wulkanowy/ui/base/BaseActivity.kt | 21 ++++++++++++------- .../wulkanowy/ui/base/BaseDialogFragment.kt | 4 ++++ .../github/wulkanowy/ui/base/BaseFragment.kt | 4 ++++ .../github/wulkanowy/ui/base/BasePresenter.kt | 1 + .../io/github/wulkanowy/ui/base/BaseView.kt | 2 ++ .../github/wulkanowy/ui/base/ErrorHandler.kt | 5 +++++ .../settings/advanced/AdvancedFragment.kt | 4 ++++ .../settings/appearance/AppearanceFragment.kt | 4 ++++ .../notifications/NotificationsFragment.kt | 4 ++++ .../ui/modules/settings/sync/SyncFragment.kt | 4 ++++ .../wulkanowy/utils/ContextExtension.kt | 2 +- app/src/main/res/values/strings.xml | 1 + 12 files changed, 47 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt index 9b93953d4..0521b4a08 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt @@ -3,8 +3,6 @@ package io.github.wulkanowy.ui.base import android.app.ActivityManager import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK import android.content.Intent.FLAG_ACTIVITY_NEW_TASK -import android.os.Build.VERSION.SDK_INT -import android.os.Build.VERSION_CODES.LOLLIPOP import android.os.Bundle import android.view.View import android.widget.Toast @@ -19,6 +17,7 @@ import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.utils.FragmentLifecycleLogger import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.lifecycleAwareVariable +import io.github.wulkanowy.utils.openInternetBrowser import javax.inject.Inject abstract class BaseActivity, VB : ViewBinding> : @@ -43,12 +42,10 @@ abstract class BaseActivity, VB : ViewBinding> : supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleLogger, true) AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) - if (SDK_INT >= LOLLIPOP) { - @Suppress("DEPRECATION") - setTaskDescription( - ActivityManager.TaskDescription(null, null, getThemeAttrColor(R.attr.colorSurface)) - ) - } + @Suppress("DEPRECATION") + setTaskDescription( + ActivityManager.TaskDescription(null, null, getThemeAttrColor(R.attr.colorSurface)) + ) } override fun showError(text: String, error: Throwable) { @@ -77,6 +74,14 @@ abstract class BaseActivity, VB : ViewBinding> : .show() } + override fun showChangePasswordSnackbar(redirectUrl: String) { + messageContainer?.let { + Snackbar.make(it, R.string.error_password_change_required, LENGTH_LONG) + .setAction(R.string.all_change) { openInternetBrowser(redirectUrl) } + .show() + } + } + override fun openClearLoginView() { startActivity(LoginActivity.getStartIntent(this) .apply { addFlags(FLAG_ACTIVITY_CLEAR_TASK or FLAG_ACTIVITY_NEW_TASK) }) diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt index 1c31976ee..25a53395d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt @@ -30,6 +30,10 @@ abstract class BaseDialogFragment : DialogFragment(), BaseView (activity as? BaseActivity<*, *>)?.openClearLoginView() } + override fun showChangePasswordSnackbar(redirectUrl: String) { + (activity as? BaseActivity<*, *>)?.showChangePasswordSnackbar(redirectUrl) + } + override fun showErrorDetailsDialog(error: Throwable) { ErrorDialog.newInstance(error).show(childFragmentManager, error.toString()) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt index c6a2e1d1c..dbc5af3a9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt @@ -45,4 +45,8 @@ abstract class BaseFragment(@LayoutRes layoutId: Int) : Fragme override fun openClearLoginView() { (activity as? BaseActivity<*, *>)?.openClearLoginView() } + + override fun showChangePasswordSnackbar(redirectUrl: String) { + (activity as? BaseActivity<*, *>)?.showChangePasswordSnackbar(redirectUrl) + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt index b222b0abb..be5300499 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt @@ -34,6 +34,7 @@ open class BasePresenter( showErrorMessage = view::showError onSessionExpired = view::showExpiredDialog onNoCurrentStudent = view::openClearLoginView + onPasswordChangeRequired = view::showChangePasswordSnackbar } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt index 0f4df92cd..d3165ea44 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt @@ -11,4 +11,6 @@ interface BaseView { fun openClearLoginView() fun showErrorDetailsDialog(error: Throwable) + + fun showChangePasswordSnackbar(redirectUrl: String) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt index 34dd3ec16..7c32ef184 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt @@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.base import android.content.res.Resources import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException +import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException import io.github.wulkanowy.utils.getString import io.github.wulkanowy.utils.security.ScramblerException import timber.log.Timber @@ -16,6 +17,8 @@ open class ErrorHandler @Inject constructor(protected val resources: Resources) var onNoCurrentStudent: () -> Unit = {} + var onPasswordChangeRequired: (String) -> Unit = {} + fun dispatch(error: Throwable) { Timber.e(error, "An exception occurred while the Wulkanowy was running") proceed(error) @@ -24,6 +27,7 @@ open class ErrorHandler @Inject constructor(protected val resources: Resources) protected open fun proceed(error: Throwable) { showErrorMessage(resources.getString(error), error) when (error) { + is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl) is ScramblerException, is BadCredentialsException -> onSessionExpired() is NoCurrentStudentException -> onNoCurrentStudent() } @@ -33,5 +37,6 @@ open class ErrorHandler @Inject constructor(protected val resources: Resources) showErrorMessage = { _, _ -> } onSessionExpired = {} onNoCurrentStudent = {} + onPasswordChangeRequired = {} } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt index 524d7ba6d..9f29731f6 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt @@ -58,6 +58,10 @@ class AdvancedFragment : PreferenceFragmentCompat(), (activity as? BaseActivity<*, *>)?.showExpiredDialog() } + override fun showChangePasswordSnackbar(redirectUrl: String) { + (activity as? BaseActivity<*, *>)?.showChangePasswordSnackbar(redirectUrl) + } + override fun openClearLoginView() { (activity as? BaseActivity<*, *>)?.openClearLoginView() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt index 5ac801dc6..a7ee800be 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt @@ -70,6 +70,10 @@ class AppearanceFragment : PreferenceFragmentCompat(), (activity as? BaseActivity<*, *>)?.showExpiredDialog() } + override fun showChangePasswordSnackbar(redirectUrl: String) { + (activity as? BaseActivity<*, *>)?.showChangePasswordSnackbar(redirectUrl) + } + override fun openClearLoginView() { (activity as? BaseActivity<*, *>)?.openClearLoginView() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt index 96d14a1b6..0fc7e68e7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt @@ -107,6 +107,10 @@ class NotificationsFragment : PreferenceFragmentCompat(), (activity as? BaseActivity<*, *>)?.showExpiredDialog() } + override fun showChangePasswordSnackbar(redirectUrl: String) { + (activity as? BaseActivity<*, *>)?.showChangePasswordSnackbar(redirectUrl) + } + override fun openClearLoginView() { (activity as? BaseActivity<*, *>)?.openClearLoginView() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt index 342988092..83caa3b0a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt @@ -86,6 +86,10 @@ class SyncFragment : PreferenceFragmentCompat(), (activity as? BaseActivity<*, *>)?.showExpiredDialog() } + override fun showChangePasswordSnackbar(redirectUrl: String) { + (activity as? BaseActivity<*, *>)?.showChangePasswordSnackbar(redirectUrl) + } + override fun openClearLoginView() { (activity as? BaseActivity<*, *>)?.openClearLoginView() } diff --git a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt index be664a470..cb31389e0 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt @@ -56,7 +56,7 @@ fun Context.getCompatDrawable(@DrawableRes drawableRes: Int, @ColorRes colorRes: fun Context.getCompatBitmap(@DrawableRes drawableRes: Int, @ColorRes colorRes: Int) = getCompatDrawable(drawableRes, colorRes)?.toBitmap() -fun Context.openInternetBrowser(uri: String, onActivityNotFound: (uri: String) -> Unit) { +fun Context.openInternetBrowser(uri: String, onActivityNotFound: (uri: String) -> Unit = {}) { Intent.parseUri(uri, 0).let { if (it.resolveActivity(packageManager) != null) startActivity(it) else onActivityNotFound(uri) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ee911cfba..39478a2ed 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -678,6 +678,7 @@ Copied Undo + Change From cebd1aa75d77b53eeb10b116d40fe7fda3154505 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sat, 28 Aug 2021 12:14:01 +0200 Subject: [PATCH 149/869] Remove lithuanian lang (#1449) --- .../res/values-lt-v29/preferences_values.xml | 9 - .../main/res/values-lt/preferences_values.xml | 48 -- app/src/main/res/values-lt/strings.xml | 548 ------------------ 3 files changed, 605 deletions(-) delete mode 100644 app/src/main/res/values-lt-v29/preferences_values.xml delete mode 100644 app/src/main/res/values-lt/preferences_values.xml delete mode 100644 app/src/main/res/values-lt/strings.xml diff --git a/app/src/main/res/values-lt-v29/preferences_values.xml b/app/src/main/res/values-lt-v29/preferences_values.xml deleted file mode 100644 index 18cbd4cf5..000000000 --- a/app/src/main/res/values-lt-v29/preferences_values.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - Sistemos tema - Šviesi - Tamsi - Juoda (AMOLED) - - diff --git a/app/src/main/res/values-lt/preferences_values.xml b/app/src/main/res/values-lt/preferences_values.xml deleted file mode 100644 index 5999a2fdd..000000000 --- a/app/src/main/res/values-lt/preferences_values.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - Šviesi - Tamsi - Juoda (AMOLED) - - - Sistemos kalba - Polski - English - Pусский - Українська - Deutsch - Čeština - Slovenčina - - - 15 minučių - 30 minučių - 1 valandą - 2 valandas - 6 valandas - 12 valandas - 24 valandas - - - 0,00 - 0,25 - 0,33 - 0,5 - 0,75 - - - Pagal abėcėlę - Pagal datą - - - Dzienniczek+ - Wulkanowy - Laipsnio spalvos registre - - - Antrojo semestro laipsnių vidurkis - Abiejų semestrų laipsnių vidurkis - Visų metų laipsnių vidurkis - - diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml deleted file mode 100644 index cf9d44d11..000000000 --- a/app/src/main/res/values-lt/strings.xml +++ /dev/null @@ -1,548 +0,0 @@ - - - - Prisijungti - Wulkanowy - Laipsnis - Attendance - Exams - Timetable - Nustatymai - Daugiau - Apie - Peržiūrėti žurnalą - Prisidėję - Licencijos - Žinutės - Nauja žinutė - Pastabos ir pasiekimai - Namų darbai - Paskyros valdymas - Pasirinkite paskyrą - Paskyros informacija - Studentų informacija - - Semestras %1$d, %2$d/%3$d - - Sign in with the student or parent account - Enter the symbol from the register page - Vartotojo vardas - El. paštas - Prisijunkite, PESEL arba el. paštas - Slaptažodis - UONET+ register variant - Mobile API - Scraper - Hibridas - Token - PIN - Symbol - Prisijungti - Slaptažodis per trumpas - Login details are incorrect. Make sure the correct UONET+ register variation is selected in the field below - Netinkamas PIN kodas - Invalid token - Token expired - Neteisingas el. paštas - Use the assigned login instead of email - Use the assigned login or email in @%1$s - Netinkamas simbolis - Student not found. Validate the symbol and the chosen variation of the UONET+ register - This field is required - Selected student is already logged in - The symbol can be found on the register page in Uczeń → Dostęp Mobilny → Zarejestruj urządzenie mobilne.\n\nMake sure that you have set the appropriate register variant in the UONET+ register variant field on the previous screen. Wulkanowy does not detect pre-school students at the moment - Select students to log in to the application - Papildomi nustatymai - In this mode, a lucky number does not work, a class grade stats, summary of attendance, excuse for absence, completed lessons, school information and preview of the list of registered devices - This mode displays the same data as it appears on the register website - The combination of the best features of the other two modes. It works faster than scraper and provides features not available in the Mobile API mode. It is in the experimental phase - Privatumo politika - Trouble signing in? Contact us! - El. paštas - Discord - Send email - Make sure you select the correct UONET+ register variation! - I forgot my password - Recover your account - Atkurti - Student is already signed in - - Paskyros valdymas - Prisijungti - Sesijos laikas baigėsi - Sesijos laikas baigėsi, prašome prisijungti iš naujo - - Laipsnis - Semestras %d - Keisti semestrą - Jokių laipsnių - Svoris - Svoris: %s - Pastabos - Number of new ratings: %1$d - Vidurkis: %1$.2f - Puantai: %s - Jokiu vidurkis - Bendras punktų skaičius - Galutinis laipsnis - Numatomas laipsnis - Calculated average - Final average - Summary - Class - Mark as read - Partial - Semester - Points - Legend - Average: %1$s - Class - Student - - %d grade - %d grades - %d grades - %d grades - - - New grade - New grades - New grades - New grades - - - New predicted grade - New predicted grades - New predicted grades - New predicted grades - - - New final grade - New final grades - New final grades - New final grades - - - You received %1$d grade - You received %1$d grades - You received %1$d grades - You received %1$d grades - - - You received %1$d predicted grade - You received %1$d predicted grades - You received %1$d predicted grades - You received %1$d predicted grades - - - You received %1$d final grade - You received %1$d final grades - You received %1$d final grades - You received %1$d final grades - - - Lesson - Room - Group - Hours - Changes - No lessons this day - %s min - %s sec - %1$s left - in %1$s - Finished - Now: %s - Next: %s - Later: %s - - Completed lessons - Show completed lessons - No info about completed lessons - Topic - Absence - Resources - - Additional lessons - Show additional lessons - No info about additional lessons - - Attendance summary - Absent for school reasons - Excused absence - Unexcused absence - Exemption - Excused lateness - Unexcused lateness - Present - Deleted - Unknown - Number of lesson - No entries - Absence reason (optional) - Send - Absence excuse request sent successfully! - You must select at least one absence! - Excuse - - Total - - No exams this week - Type - Entry date - - New exam - New exams - New exams - New exams - - - %d exam - %d exams - %d exams - %d exams - - - Inbox - Sent - Trash - (no subject) - No messages - From: - To: - Date: %s - Reply - Forward - Delete - Move to trash - Delete permanently - Message deleted successfully - Share - Print - Subject - Content - Message sent successfully - Message does not exist - You need to choose at least 1 recipient - The message content must be at least 3 characters - - %d message - %d messages - %d messages - %d messages - - - New message - New messages - New messages - New messages - - - You received %1$d message - You received %1$d messages - You received %1$d messages - You received %1$d messages - - - No info about notes - Points - - %d note - %d notes - %d notes - %d notes - - - New note - New notes - New notes - New notes - - - You received %1$d note - You received %1$d notes - You received %1$d notes - You received %1$d notes - - - - %d praise - %d praises - %d praises - %d praises - - - New praise - New praises - New praises - New praises - - - You received %1$d praise - You received %1$d praises - You received %1$d praises - You received %1$d praises - - - - %d neutral note - %d neutral notes - %d neutral notes - %d neutral notes - - - New neutral note - New neutral notes - New neutral notes - New neutral notes - - - You received %1$d neutral note - You received %1$d neutral notes - You received %1$d neutral notes - You received %1$d neutral notes - - - No info about homework - Mark as done - Mark as undone - Attachments - - New homework - New homework - New homework - New homework - - - %d homework - %d homework - %d homework - %d homework - - - Lucky number - Today\'s lucky number is - No info about the lucky number - Lucky number for today - Today\'s lucky number is: %s - Show history - - Lucky number history - No info about lucky numbers - - Mobile devices - No devices - Deregister - Device removed - QR code - Token - Symbol - PIN - - School and teachers - - School - No info about school - School name - School address - Telephone - Name of headmaster - Name of pedagogue - Show on map - Call - - Teachers - No info about teachers - No subject - - Conferences - No info about conferences - - Add account - Logout - Do you want to log out this student? - Student logout - Student account - Parent account - Edit data - Accounts manager - Select student - Family - Contact - Residence details - Personal information - - App version - Contributors - List of Wulkanowy developers - Report a bug - Send a bug report via e-mail - FAQ - Read Frequently Asked Questions - Discord server - Join the Wulkanowy community - Facebook fanpage - Like our facebook fanpage - Privacy policy - Rules for collecting personal data - System settings - Open system settings - Homepage - Visit the website and help develop the application - Licenses - Licenses of libraries used in the application - - License - - Avatar - See more on GitHub - - No info about student or student family - Name - Second name - Gender - Polish citizenship - Family name - Mother\'s and father\'s names - Phone - Cellphone - E-mail - Address of residence - Address of registration - Correspondence address - Surname and first name - Degree of kinship - Address - Phones - Male - Female - Last name - - Nick - Add nick - Choose avatar color - - Share logs - Refresh - - Check for updates - Before reporting a bug, check first if an update with the bug fix is available - - Content - Retry - Description - No description - Teacher - Date - Entry date - Color - Details - Category - Close - No data - Subject - Prev - Next - Search - Search… - Yes - No - Save - - No lessons - Choose theme - Light - Dark - System Theme - - App appearance & behavior - Default view - Calculation of the end-of-year average - Force average calculation by app - Show presence - Theme - Expand grades - Mark current lesson - Show groups next to subjects - Show chart list in class grades - Show subjects without grades - Grades color scheme - Subjects sorting - Language - Notifications - Show notifications - Show upcoming lesson notifications - Open system notification settings - Fix synchronization & notifications issues - Your device may have data synchronization issues and with notifications.\n\nTo fix them, you need to add Wulkanowy to the autostart and turn off battery optimization/saving in the phone settings. - Go to settings - Show debug notifications - Synchronization is disabled - Synchronization - Automatic update - Suspended on holidays - Updates interval - Wi-Fi only - Sync now - Synced! - Sync failed - Sync in progress - Value of the plus - Value of the minus - Reply with message history - Show arithmetic average when no weights provided - Advanced - Appearance & Behavior - Notifications - Synchronization - Grades - Attendance - Timetable - Grades - Messages - Appearance & Behavior - Languages, themes, subjects sorting - App notifications, fix problems - Notifications - Synchronization - Automatic update, synchronization interval - Plus and minus values, average calculation - Advanced - App version, contributors, social portals, licenses - - New grades - New homework - New exams - Lucky number - New messages - New notes - Push notifications - Upcoming lessons - Debug - - Black - Red - Blue - Green - Purple - No color - - Copied - Undo - - Download of updates has started… - An update has just been downloaded. - Restart - Update failed! Wulkanowy may not function properly. Consider updating - - No internet connection - Connection to register failed. Servers can be overloaded. Please try again later - Loading data failed. Please try again later - Register password change required - Maintenance underway UONET + register. Try again later - Unknown UONET + register error. Try again later - Unknown application error. Please try again later - An unexpected error occurred - Feature disabled by your school - Feature not available. Login in a mode other than Mobile API - From 55518cb0449f1073b056d0778a8f2618a924d7bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sat, 28 Aug 2021 21:43:10 +0200 Subject: [PATCH 150/869] Add missing dashboard item in default view settings (#1450) --- .../repositories/PreferencesRepository.kt | 1 + .../wulkanowy/ui/modules/exam/ExamFragment.kt | 10 +----- .../ui/modules/exam/ExamPresenter.kt | 10 ------ .../wulkanowy/ui/modules/exam/ExamView.kt | 2 -- .../wulkanowy/ui/modules/main/MainActivity.kt | 6 ++-- .../ui/modules/main/MainPresenter.kt | 10 +++--- .../wulkanowy/ui/modules/main/MainView.kt | 36 ++++++++++--------- .../wulkanowy/utils/FragmentExtension.kt | 4 +-- app/src/main/res/values/preferences_keys.xml | 2 +- .../main/res/values/preferences_values.xml | 2 +- 10 files changed, 33 insertions(+), 50 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index e725c42a0..7b3cd67b1 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -30,6 +30,7 @@ class PreferencesRepository @Inject constructor( @ApplicationContext val context: Context, moshi: Moshi ) { + @OptIn(ExperimentalStdlibApi::class) private val dashboardItemsPositionAdapter: JsonAdapter> = moshi.adapter() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt index 0940b0bdf..fb7939bc5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt @@ -20,7 +20,7 @@ import javax.inject.Inject @AndroidEntryPoint class ExamFragment : BaseFragment(R.layout.fragment_exam), ExamView, - MainView.MainChildView, MainView.TitledView { + MainView.TitledView { @Inject lateinit var presenter: ExamPresenter @@ -90,14 +90,6 @@ class ExamFragment : BaseFragment(R.layout.fragment_exam), } } - override fun resetView() { - binding.examRecycler.scrollToPosition(0) - } - - override fun onFragmentReselected() { - if (::presenter.isInitialized) presenter.onViewReselected() - } - override fun showEmpty(show: Boolean) { binding.examEmpty.visibility = if (show) VISIBLE else GONE } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamPresenter.kt index b70a648f4..582641fcf 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamPresenter.kt @@ -82,16 +82,6 @@ class ExamPresenter @Inject constructor( view?.showExamDialog(exam) } - fun onViewReselected() { - Timber.i("Exam view is reselected") - baseDate.also { - if (currentDate != it) { - reloadView(it) - loadData() - } else if (view?.isViewEmpty == false) view?.resetView() - } - } - private fun setBaseDateOnHolidays() { flow { val student = studentRepository.getCurrentStudent() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamView.kt index ac1a87fe9..45b9e788c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamView.kt @@ -17,8 +17,6 @@ interface ExamView : BaseView { fun showRefresh(show: Boolean) - fun resetView() - fun showEmpty(show: Boolean) fun showErrorView(show: Boolean) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt index b44bb4b21..cf31040a3 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt @@ -34,6 +34,7 @@ import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment import io.github.wulkanowy.ui.modules.conference.ConferenceFragment import io.github.wulkanowy.ui.modules.dashboard.DashboardFragment +import io.github.wulkanowy.ui.modules.exam.ExamFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment import io.github.wulkanowy.ui.modules.homework.HomeworkFragment import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment @@ -107,11 +108,12 @@ class MainActivity : BaseActivity(), MainVie private val moreMenuFragments = mapOf( MainView.Section.MESSAGE.id to MessageFragment.newInstance(), + MainView.Section.EXAM.id to ExamFragment.newInstance(), MainView.Section.HOMEWORK.id to HomeworkFragment.newInstance(), MainView.Section.NOTE.id to NoteFragment.newInstance(), - MainView.Section.LUCKY_NUMBER.id to LuckyNumberFragment.newInstance(), - MainView.Section.SCHOOL_ANNOUNCEMENT.id to SchoolAnnouncementFragment.newInstance(), MainView.Section.CONFERENCE.id to ConferenceFragment.newInstance(), + MainView.Section.SCHOOL_ANNOUNCEMENT.id to SchoolAnnouncementFragment.newInstance(), + MainView.Section.LUCKY_NUMBER.id to LuckyNumberFragment.newInstance(), ) @SuppressLint("NewApi") diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt index c0eb2b11d..a2fb07f66 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt @@ -118,11 +118,9 @@ class MainPresenter @Inject constructor( view?.showStudentAvatar(currentStudent) } - private fun getProperViewIndexes(initMenu: MainView.Section?): Pair { - return when (initMenu?.id) { - in 0..3 -> initMenu!!.id to -1 - in 4..10 -> 4 to initMenu!!.id - else -> prefRepository.startMenuIndex to -1 - } + private fun getProperViewIndexes(initMenu: MainView.Section?) = when (initMenu?.id) { + in 0..3 -> initMenu!!.id to -1 + in 4..100 -> 4 to initMenu!!.id + else -> prefRepository.startMenuIndex to -1 } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt index 44f37d50b..7402d37e0 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt @@ -56,22 +56,24 @@ interface MainView : BaseView { set(_) {} } - enum class Section(val id: Int) { - GRADE(0), - ATTENDANCE(1), - EXAM(2), - TIMETABLE(3), - MORE(4), - MESSAGE(5), - HOMEWORK(6), - NOTE(7), - LUCKY_NUMBER(8), - SETTINGS(9), - ABOUT(10), - SCHOOL(11), - ACCOUNT(12), - STUDENT_INFO(13), - CONFERENCE(14), - SCHOOL_ANNOUNCEMENT(15) + enum class Section { + DASHBOARD, + GRADE, + ATTENDANCE, + TIMETABLE, + MORE, + MESSAGE, + EXAM, + HOMEWORK, + NOTE, + CONFERENCE, + SCHOOL_ANNOUNCEMENT, + SCHOOL, + LUCKY_NUMBER, + ACCOUNT, + STUDENT_INFO, + SETTINGS; + + val id get() = ordinal } } diff --git a/app/src/main/java/io/github/wulkanowy/utils/FragmentExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/FragmentExtension.kt index 4651dffd2..210a62090 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/FragmentExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/FragmentExtension.kt @@ -1,11 +1,11 @@ package io.github.wulkanowy.utils import androidx.fragment.app.Fragment -import io.github.wulkanowy.ui.modules.about.AboutFragment import io.github.wulkanowy.ui.modules.account.AccountFragment import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsFragment import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment import io.github.wulkanowy.ui.modules.conference.ConferenceFragment +import io.github.wulkanowy.ui.modules.dashboard.DashboardFragment import io.github.wulkanowy.ui.modules.exam.ExamFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment import io.github.wulkanowy.ui.modules.homework.HomeworkFragment @@ -32,13 +32,13 @@ fun Fragment.toSection(): MainView.Section? { is NoteFragment -> MainView.Section.NOTE is LuckyNumberFragment -> MainView.Section.LUCKY_NUMBER is SettingsFragment -> MainView.Section.SETTINGS - is AboutFragment -> MainView.Section.ABOUT is SchoolAndTeachersFragment -> MainView.Section.SCHOOL is AccountFragment -> MainView.Section.ACCOUNT is AccountDetailsFragment -> MainView.Section.ACCOUNT is StudentInfoFragment -> MainView.Section.STUDENT_INFO is ConferenceFragment -> MainView.Section.CONFERENCE is SchoolAnnouncementFragment -> MainView.Section.SCHOOL_ANNOUNCEMENT + is DashboardFragment -> MainView.Section.DASHBOARD else -> null } } diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index aa60bb7e6..09dac7002 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -1,6 +1,6 @@ - start_menu + default_menu_index attendance_present app_theme dashboard_tiles diff --git a/app/src/main/res/values/preferences_values.xml b/app/src/main/res/values/preferences_values.xml index f5a2b864e..9c1a0421a 100644 --- a/app/src/main/res/values/preferences_values.xml +++ b/app/src/main/res/values/preferences_values.xml @@ -1,9 +1,9 @@ + @string/dashboard_title @string/grade_title @string/attendance_title - @string/exam_title @string/timetable_title From 04c727a0c8a5a8a5ed5476a2439eceb0b23d7249 Mon Sep 17 00:00:00 2001 From: Tomasz F Date: Sun, 29 Aug 2021 00:41:58 +0200 Subject: [PATCH 151/869] Exams and homework notification fixes (#1292) --- .../wulkanowy/data/repositories/ExamRepository.kt | 6 ++++-- .../data/repositories/HomeworkRepository.kt | 6 ++++-- .../sync/notifications/NewConferenceNotification.kt | 11 ++++++++--- .../sync/notifications/NewExamNotification.kt | 13 +++++++++---- .../sync/notifications/NewHomeworkNotification.kt | 13 +++++++++---- .../modules/debug/notification/mock/conference.kt | 2 +- app/src/main/res/values/strings.xml | 8 ++++++++ 7 files changed, 43 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt index 9406c77c6..93d5a47cb 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt @@ -40,8 +40,10 @@ class ExamRepository @Inject constructor( ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { - it.isEmpty() || forceRefresh - || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) + val isShouldBeRefreshed = refreshHelper.isShouldBeRefreshed( + key = getRefreshKey(cacheKey, semester, start, end) + ) + it.isEmpty() || forceRefresh || isShouldBeRefreshed }, query = { examDb.loadAll( diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt index 476a810c9..23dd74c2c 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt @@ -36,8 +36,10 @@ class HomeworkRepository @Inject constructor( ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { - it.isEmpty() || forceRefresh || - refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) + val isShouldBeRefreshed = refreshHelper.isShouldBeRefreshed( + key = getRefreshKey(cacheKey, semester, start, end) + ) + it.isEmpty() || forceRefresh || isShouldBeRefreshed }, query = { homeworkDb.loadAll( diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt index 25648b931..fda2922f9 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt @@ -8,6 +8,8 @@ import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.MultipleNotifications import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.utils.toFormattedString +import java.time.LocalDateTime import javax.inject.Inject class NewConferenceNotification @Inject constructor( @@ -16,6 +18,11 @@ class NewConferenceNotification @Inject constructor( ) : BaseNotification(context, notificationManager) { fun notify(items: List, student: Student) { + val today = LocalDateTime.now() + val lines = items.filter { !it.date.isBefore(today) }.map { + "${it.date.toFormattedString("dd.MM")} - ${it.title}: ${it.subject}" + }.ifEmpty { return } + val notification = MultipleNotifications( type = NotificationType.NEW_CONFERENCE, icon = R.drawable.ic_more_conferences, @@ -23,9 +30,7 @@ class NewConferenceNotification @Inject constructor( contentStringRes = R.plurals.conference_notify_new_items, summaryStringRes = R.plurals.conference_number_item, startMenu = MainView.Section.CONFERENCE, - lines = items.map { - "${it.title}: ${it.subject}" - } + lines = lines ) sendNotification(notification, student) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt index a41c44e58..d493c4d2e 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt @@ -8,6 +8,8 @@ import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.MultipleNotifications import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.utils.toFormattedString +import java.time.LocalDate import javax.inject.Inject class NewExamNotification @Inject constructor( @@ -16,16 +18,19 @@ class NewExamNotification @Inject constructor( ) : BaseNotification(context, notificationManager) { fun notify(items: List, student: Student) { + val today = LocalDate.now() + val lines = items.filter { !it.date.isBefore(today) }.map { + "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.description}" + }.ifEmpty { return } + val notification = MultipleNotifications( type = NotificationType.NEW_EXAM, icon = R.drawable.ic_main_exam, titleStringRes = R.plurals.exam_notify_new_item_title, - contentStringRes = R.plurals.exam_notify_new_item_title, // TODO add missing string + contentStringRes = R.plurals.exam_notify_new_item_content, summaryStringRes = R.plurals.exam_number_item, startMenu = MainView.Section.EXAM, - lines = items.map { - "${it.subject}: ${it.description}" - } + lines = lines ) sendNotification(notification, student) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt index 844aed979..fe973cade 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt @@ -8,6 +8,8 @@ import io.github.wulkanowy.data.db.entities.Homework import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.MultipleNotifications import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.utils.toFormattedString +import java.time.LocalDate import javax.inject.Inject class NewHomeworkNotification @Inject constructor( @@ -16,16 +18,19 @@ class NewHomeworkNotification @Inject constructor( ) : BaseNotification(context, notificationManager) { fun notify(items: List, student: Student) { + val today = LocalDate.now() + val lines = items.filter { !it.date.isBefore(today) }.map { + "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.content}" + }.ifEmpty { return } + val notification = MultipleNotifications( type = NotificationType.NEW_HOMEWORK, icon = R.drawable.ic_more_homework, titleStringRes = R.plurals.homework_notify_new_item_title, - contentStringRes = R.plurals.homework_notify_new_item_title, // todo: you received %d new homework + contentStringRes = R.plurals.homework_notify_new_item_content, summaryStringRes = R.plurals.homework_number_item, startMenu = MainView.Section.HOMEWORK, - lines = items.map { - "${it.subject}: ${it.content}" - } + lines = lines ) sendNotification(notification, student) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/conference.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/conference.kt index 969b1cf23..40af6bfbb 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/conference.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/conference.kt @@ -53,6 +53,6 @@ private fun generateConference(title: String, subject: String) = Conference( diaryId = 0, agenda = "", conferenceId = 0, - date = LocalDateTime.now(), + date = LocalDateTime.now().plusMinutes(10), presentOnConference = "", ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 39478a2ed..ea1187c84 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -206,6 +206,10 @@ New exam New exams + + You received %d new exam + You received %d new exams + %d exam %d exams @@ -312,6 +316,10 @@ New homework New homework + + You received %d new homework + You received %d new homework + %d homework %d homework From 765f8a2d1f0a18fb2cc2d48f9da7d3fb5d7602b6 Mon Sep 17 00:00:00 2001 From: Mateusz Idziejczak Date: Sun, 29 Aug 2021 00:43:58 +0200 Subject: [PATCH 152/869] Add in app review (#1435) --- app/build.gradle | 1 + .../wulkanowy/utils/InAppReviewHelper.kt | 17 +++++++++++ .../wulkanowy/utils/InAppReviewHelper.kt | 17 +++++++++++ .../repositories/PreferencesRepository.kt | 21 ++++++++++++++ .../wulkanowy/ui/modules/main/MainActivity.kt | 8 ++++++ .../ui/modules/main/MainPresenter.kt | 17 +++++++++++ .../wulkanowy/ui/modules/main/MainView.kt | 2 ++ .../wulkanowy/utils/InAppReviewHelper.kt | 28 +++++++++++++++++++ 8 files changed, 111 insertions(+) create mode 100644 app/src/fdroid/java/io/github/wulkanowy/utils/InAppReviewHelper.kt create mode 100644 app/src/hms/java/io/github/wulkanowy/utils/InAppReviewHelper.kt create mode 100644 app/src/play/java/io/github/wulkanowy/utils/InAppReviewHelper.kt diff --git a/app/build.gradle b/app/build.gradle index d5fccac6f..c05aa0341 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -215,6 +215,7 @@ dependencies { playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' + playImplementation 'com.google.android.play:core:1.10.0' playImplementation 'com.google.android.play:core-ktx:1.8.1' hmsImplementation 'com.huawei.hms:hianalytics:6.1.1.300' diff --git a/app/src/fdroid/java/io/github/wulkanowy/utils/InAppReviewHelper.kt b/app/src/fdroid/java/io/github/wulkanowy/utils/InAppReviewHelper.kt new file mode 100644 index 000000000..d052b54b8 --- /dev/null +++ b/app/src/fdroid/java/io/github/wulkanowy/utils/InAppReviewHelper.kt @@ -0,0 +1,17 @@ +package io.github.wulkanowy.utils + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.ui.modules.main.MainActivity +import javax.inject.Singleton +import javax.inject.Inject + +@Singleton +class InAppReviewHelper @Inject constructor( + @ApplicationContext private val context: Context +) { + + fun showInAppReview(activity: MainActivity) { + // do nothing + } +} \ No newline at end of file diff --git a/app/src/hms/java/io/github/wulkanowy/utils/InAppReviewHelper.kt b/app/src/hms/java/io/github/wulkanowy/utils/InAppReviewHelper.kt new file mode 100644 index 000000000..fb9bcae6c --- /dev/null +++ b/app/src/hms/java/io/github/wulkanowy/utils/InAppReviewHelper.kt @@ -0,0 +1,17 @@ +package io.github.wulkanowy.utils + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.ui.modules.main.MainActivity +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class InAppReviewHelper @Inject constructor( + @ApplicationContext private val context: Context +) { + + fun showInAppReview(activity: MainActivity) { + // do nothing + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index 7b3cd67b1..bc8100f22 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -10,14 +10,17 @@ import com.squareup.moshi.Moshi import com.squareup.moshi.adapter import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R +import io.github.wulkanowy.sdk.toLocalDate import io.github.wulkanowy.ui.modules.dashboard.DashboardItem import io.github.wulkanowy.ui.modules.grade.GradeAverageMode import io.github.wulkanowy.ui.modules.grade.GradeSortingMode +import io.github.wulkanowy.utils.toTimestamp import io.github.wulkanowy.utils.toLocalDateTime import io.github.wulkanowy.utils.toTimestamp import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import java.time.LocalDate import java.time.LocalDateTime import javax.inject.Inject import javax.inject.Singleton @@ -222,6 +225,18 @@ class PreferencesRepository @Inject constructor( return flowSharedPref.getStringSet(prefKey, defaultSet) } + var inAppReviewCount: Int + get() = sharedPref.getInt(PREF_KEY_IN_APP_REVIEW_COUNT, 0) + set(value) = sharedPref.edit().putInt(PREF_KEY_IN_APP_REVIEW_COUNT, value).apply() + + var inAppReviewDate: LocalDate? + get() = sharedPref.getLong(PREF_KEY_IN_APP_REVIEW_DATE, 0).takeIf { it != 0L }?.toLocalDate() + set(value) = sharedPref.edit().putLong(PREF_KEY_IN_APP_REVIEW_DATE, value!!.toTimestamp()).apply() + + var isAppReviewDone: Boolean + get() = sharedPref.getBoolean(PREF_KEY_IN_APP_REVIEW_DONE, false) + set(value) = sharedPref.edit().putBoolean(PREF_KEY_IN_APP_REVIEW_DONE, value).apply() + private fun getLong(id: Int, default: Int) = getLong(context.getString(id), default) private fun getLong(id: String, default: Int) = @@ -240,5 +255,11 @@ class PreferencesRepository @Inject constructor( private companion object { private const val PREF_KEY_DASHBOARD_ITEMS_POSITION = "dashboard_items_position" + + private const val PREF_KEY_IN_APP_REVIEW_COUNT = "in_app_review_count" + + private const val PREF_KEY_IN_APP_REVIEW_DATE = "in_app_review_date" + + private const val PREF_KEY_IN_APP_REVIEW_DONE = "in_app_review_done" } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt index cf31040a3..d758ac0da 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt @@ -45,6 +45,7 @@ import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragm import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AppInfo +import io.github.wulkanowy.utils.InAppReviewHelper import io.github.wulkanowy.utils.UpdateHelper import io.github.wulkanowy.utils.createNameInitialsDrawable import io.github.wulkanowy.utils.dpToPx @@ -68,6 +69,9 @@ class MainActivity : BaseActivity(), MainVie @Inject lateinit var updateHelper: UpdateHelper + @Inject + lateinit var inAppReviewHelper: InAppReviewHelper + @Inject lateinit var appInfo: AppInfo @@ -362,6 +366,10 @@ class MainActivity : BaseActivity(), MainVie } } + override fun showInAppReview() { + inAppReviewHelper.showInAppReview(this) + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) navController.onSaveInstanceState(outState) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt index a2fb07f66..4805b5a1c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt @@ -14,6 +14,7 @@ import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.flowWithResource import kotlinx.coroutines.flow.onEach import timber.log.Timber +import java.time.LocalDate import javax.inject.Inject class MainPresenter @Inject constructor( @@ -106,11 +107,27 @@ class MainPresenter @Inject constructor( } else { notifyMenuViewChanged() switchMenuView(index) + checkInAppReview() true } } == true } + private fun checkInAppReview() { + prefRepository.inAppReviewCount++ + + if (prefRepository.inAppReviewDate == null) { + prefRepository.inAppReviewDate = LocalDate.now() + } + + if (!prefRepository.isAppReviewDone && prefRepository.inAppReviewCount >= 50 && + LocalDate.now().minusDays(14).isAfter(prefRepository.inAppReviewDate) + ) { + view?.showInAppReview() + prefRepository.isAppReviewDone = true + } + } + private fun showCurrentStudentAvatar() { val currentStudent = studentsWitSemesters?.singleOrNull { it.student.isCurrent }?.student ?: return diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt index 7402d37e0..8851f5878 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt @@ -40,6 +40,8 @@ interface MainView : BaseView { fun showStudentAvatar(student: Student) + fun showInAppReview() + interface MainChildView { fun onFragmentReselected() diff --git a/app/src/play/java/io/github/wulkanowy/utils/InAppReviewHelper.kt b/app/src/play/java/io/github/wulkanowy/utils/InAppReviewHelper.kt new file mode 100644 index 000000000..1dcece99d --- /dev/null +++ b/app/src/play/java/io/github/wulkanowy/utils/InAppReviewHelper.kt @@ -0,0 +1,28 @@ +package io.github.wulkanowy.utils + +import android.content.Context +import com.google.android.play.core.review.ReviewManagerFactory +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.ui.modules.main.MainActivity +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class InAppReviewHelper @Inject constructor( + @ApplicationContext private val context: Context +) { + + fun showInAppReview(activity: MainActivity) { + val manager = ReviewManagerFactory.create(context) + val request = manager.requestReviewFlow() + request.addOnCompleteListener { task -> + if (task.isSuccessful) { + val reviewInfo = task.result + manager.launchReviewFlow(activity, reviewInfo) + } else { + Timber.e(task.exception) + } + } + } +} \ No newline at end of file From 2f43b6e55247167852e1f8f4c4396f63c4edec6d Mon Sep 17 00:00:00 2001 From: Daniel Olczyk <44818681+MRmlik12@users.noreply.github.com> Date: Sun, 29 Aug 2021 14:08:48 +0200 Subject: [PATCH 153/869] Change display name for MRmlik12 (#1451) --- app/src/main/assets/contributors.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/contributors.json b/app/src/main/assets/contributors.json index c1e7e209d..b2849931a 100644 --- a/app/src/main/assets/contributors.json +++ b/app/src/main/assets/contributors.json @@ -36,7 +36,7 @@ "githubUsername": "Luncenok" }, { - "displayName": "MRmlik12", + "displayName": "Daniel Olczyk", "githubUsername": "MRmlik12" }, { From 57d11e825be154c8d40046add5d133e422f12c95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sun, 29 Aug 2021 15:40:28 +0200 Subject: [PATCH 154/869] Update readBy and unreadBy fields during message list fetch (#1452) --- .../data/repositories/MessageRepository.kt | 83 ++++++++-- .../repositories/MessageRepositoryTest.kt | 156 ++++++++++++++++-- 2 files changed, 205 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt index 2d70e26ee..9977e1d57 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt @@ -4,10 +4,12 @@ import android.content.Context import com.squareup.moshi.Moshi import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R +import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.dao.MessageAttachmentDao import io.github.wulkanowy.data.db.dao.MessagesDao import io.github.wulkanowy.data.db.entities.Message +import io.github.wulkanowy.data.db.entities.MessageWithAttachment import io.github.wulkanowy.data.db.entities.Recipient import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student @@ -48,22 +50,54 @@ class MessageRepository @Inject constructor( private val cacheKey = "message" @Suppress("UNUSED_PARAMETER") - fun getMessages(student: Student, semester: Semester, folder: MessageFolder, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource( + fun getMessages( + student: Student, semester: Semester, + folder: MessageFolder, forceRefresh: Boolean, notify: Boolean = false + ): Flow>> = networkBoundResource( mutex = saveFetchResultMutex, - shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student, folder)) }, + shouldFetch = { + it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed( + getRefreshKey(cacheKey, student, folder) + ) + }, query = { messagesDb.loadAll(student.id.toInt(), folder.id) }, - fetch = { sdk.init(student).getMessages(Folder.valueOf(folder.name), now().minusMonths(3), now()).mapToEntities(student) }, + fetch = { + sdk.init(student).getMessages(Folder.valueOf(folder.name), now().minusMonths(3), now()) + .mapToEntities(student) + }, saveFetchResult = { old, new -> messagesDb.deleteAll(old uniqueSubtract new) messagesDb.insertAll((new uniqueSubtract old).onEach { it.isNotified = !notify }) + messagesDb.updateAll(getMessagesWithReadByChange(old, new, !notify)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student, folder)) } ) - fun getMessage(student: Student, message: Message, markAsRead: Boolean = false) = networkBoundResource( + private fun getMessagesWithReadByChange( + old: List, new: List, + setNotified: Boolean + ): List { + val oldMeta = old.map { Triple(it, it.readBy, it.unreadBy) } + val newMeta = new.map { Triple(it, it.readBy, it.unreadBy) } + + val updatedItems = newMeta uniqueSubtract oldMeta + + return updatedItems.map { + val oldItem = old.find { item -> item.messageId == it.first.messageId } + it.first.apply { + id = oldItem?.id ?: 0 + isNotified = oldItem?.isNotified ?: setNotified + content = oldItem?.content.orEmpty() + } + } + } + + fun getMessage( + student: Student, message: Message, markAsRead: Boolean = false + ): Flow> = networkBoundResource( shouldFetch = { checkNotNull(it, { "This message no longer exist!" }) Timber.d("Message content in db empty: ${it.message.content.isEmpty()}") @@ -71,7 +105,12 @@ class MessageRepository @Inject constructor( }, query = { messagesDb.loadMessageWithAttachment(student.id.toInt(), message.messageId) }, fetch = { - sdk.init(student).getMessageDetails(it!!.message.messageId, message.folderId, markAsRead, message.realId).let { details -> + sdk.init(student).getMessageDetails( + messageId = it!!.message.messageId, + folderId = message.folderId, + read = markAsRead, + id = message.realId + ).let { details -> details.content to details.attachments.mapToEntities() } }, @@ -95,26 +134,34 @@ class MessageRepository @Inject constructor( return messagesDb.updateAll(messages) } - suspend fun sendMessage(student: Student, subject: String, content: String, recipients: List): SentMessage { - return sdk.init(student).sendMessage( - subject = subject, - content = content, - recipients = recipients.mapFromEntities() - ) - } + suspend fun sendMessage( + student: Student, subject: String, content: String, + recipients: List + ): SentMessage = sdk.init(student).sendMessage( + subject = subject, + content = content, + recipients = recipients.mapFromEntities() + ) suspend fun deleteMessage(student: Student, message: Message) { - val isDeleted = sdk.init(student).deleteMessages(listOf(message.messageId), message.folderId) + val isDeleted = sdk.init(student).deleteMessages( + messages = listOf(message.messageId), message.folderId + ) - if (message.folderId != MessageFolder.TRASHED.id) { - if (isDeleted) messagesDb.updateAll(listOf(message.copy(folderId = MessageFolder.TRASHED.id).apply { + if (message.folderId != MessageFolder.TRASHED.id && isDeleted) { + val deletedMessage = message.copy(folderId = MessageFolder.TRASHED.id).apply { id = message.id content = message.content - })) + } + messagesDb.updateAll(listOf(deletedMessage)) } else messagesDb.deleteAll(listOf(message)) } var draftMessage: MessageDraft? - get() = sharedPrefProvider.getString(context.getString(R.string.pref_key_message_send_draft))?.let { MessageDraftJsonAdapter(moshi).fromJson(it) } - set(value) = sharedPrefProvider.putString(context.getString(R.string.pref_key_message_send_draft), value?.let { MessageDraftJsonAdapter(moshi).toJson(it) }) + get() = sharedPrefProvider.getString(context.getString(R.string.pref_key_message_send_draft)) + ?.let { MessageDraftJsonAdapter(moshi).fromJson(it) } + set(value) = sharedPrefProvider.putString( + context.getString(R.string.pref_key_message_send_draft), + value?.let { MessageDraftJsonAdapter(moshi).toJson(it) } + ) } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt index 8b72479f1..cadc4225a 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt @@ -8,19 +8,25 @@ import io.github.wulkanowy.data.db.dao.MessageAttachmentDao import io.github.wulkanowy.data.db.dao.MessagesDao import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.MessageWithAttachment +import io.github.wulkanowy.data.enums.MessageFolder +import io.github.wulkanowy.getSemesterEntity import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.sdk.pojo.Folder import io.github.wulkanowy.sdk.pojo.MessageDetails +import io.github.wulkanowy.sdk.pojo.Sender import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.toFirstResult import io.mockk.MockKAnnotations import io.mockk.Runs +import io.mockk.checkEquals import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.SpyK import io.mockk.just +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking @@ -29,6 +35,7 @@ import org.junit.Before import org.junit.Test import java.net.UnknownHostException import java.time.LocalDateTime +import kotlin.test.assertTrue class MessageRepositoryTest { @@ -52,7 +59,9 @@ class MessageRepositoryTest { private val student = getStudentEntity() - private lateinit var messageRepository: MessageRepository + private val semester = getSemesterEntity() + + private lateinit var repository: MessageRepository @MockK private lateinit var moshi: Moshi @@ -62,15 +71,92 @@ class MessageRepositoryTest { MockKAnnotations.init(this) every { refreshHelper.isShouldBeRefreshed(any()) } returns false - messageRepository = MessageRepository(messageDb, messageAttachmentDao, sdk, context, refreshHelper, sharedPrefProvider, moshi) + repository = MessageRepository( + messagesDb = messageDb, + messageAttachmentDao = messageAttachmentDao, + sdk = sdk, + context = context, + refreshHelper = refreshHelper, + sharedPrefProvider = sharedPrefProvider, + moshi = moshi, + ) + } + + @Test + fun `get messages when read by values was changed on already read message`() = runBlocking { + every { messageDb.loadAll(any(), any()) } returns flow { + val dbMessage = getMessageEntity(3, "", false).apply { + unreadBy = 10 + readBy = 5 + isNotified = true + } + emit(listOf(dbMessage)) + } + coEvery { sdk.getMessages(Folder.RECEIVED, any(), any()) } returns listOf( + getMessageDto(messageId = 3, content = "", unread = false).copy( + unreadBy = 5, + readBy = 10, + ) + ) + coEvery { messageDb.deleteAll(any()) } just Runs + coEvery { messageDb.insertAll(any()) } returns listOf() + + repository.getMessages( + student = student, + semester = semester, + folder = MessageFolder.RECEIVED, + forceRefresh = true, + notify = true, // all new messages will be marked as not notified + ).toFirstResult().data.orEmpty() + + coVerify(exactly = 1) { messageDb.deleteAll(emptyList()) } + coVerify(exactly = 1) { messageDb.insertAll(emptyList()) } + coVerify(exactly = 1) { + messageDb.updateAll(withArg { + assertEquals(1, it.size) + assertEquals(5, it.single().unreadBy) + assertEquals(10, it.single().readBy) + }) + } + } + + @Test + fun `get messages when fetched completely new message without notify`() = runBlocking { + every { messageDb.loadAll(any(), any()) } returns flowOf(emptyList()) + coEvery { sdk.getMessages(Folder.RECEIVED, any(), any()) } returns listOf( + getMessageDto(messageId = 4, content = "Test", unread = true).copy( + unreadBy = 5, + readBy = 10, + ) + ) + coEvery { messageDb.deleteAll(any()) } just Runs + coEvery { messageDb.insertAll(any()) } returns listOf() + + repository.getMessages( + student = student, + semester = semester, + folder = MessageFolder.RECEIVED, + forceRefresh = true, + notify = false, + ).toFirstResult().data.orEmpty() + + coVerify(exactly = 1) { messageDb.deleteAll(withArg { checkEquals(emptyList()) }) } + coVerify { + messageDb.insertAll(withArg { + assertEquals(4, it.single().messageId) + assertTrue(it.single().isNotified) + }) + } } @Test(expected = NoSuchElementException::class) fun `throw error when message is not in the db`() { val testMessage = getMessageEntity(1, "", false) - coEvery { messageDb.loadMessageWithAttachment(1, 1) } throws NoSuchElementException("No message in database") + coEvery { + messageDb.loadMessageWithAttachment(1, 1) + } throws NoSuchElementException("No message in database") - runBlocking { messageRepository.getMessage(student, testMessage).toFirstResult() } + runBlocking { repository.getMessage(student, testMessage).toFirstResult() } } @Test @@ -78,9 +164,11 @@ class MessageRepositoryTest { val testMessage = getMessageEntity(123, "Test", false) val messageWithAttachment = MessageWithAttachment(testMessage, emptyList()) - coEvery { messageDb.loadMessageWithAttachment(1, testMessage.messageId) } returns flowOf(messageWithAttachment) + coEvery { messageDb.loadMessageWithAttachment(1, testMessage.messageId) } returns flowOf( + messageWithAttachment + ) - val res = runBlocking { messageRepository.getMessage(student, testMessage).toFirstResult() } + val res = runBlocking { repository.getMessage(student, testMessage).toFirstResult() } assertEquals(null, res.error) assertEquals(Status.SUCCESS, res.status) @@ -95,12 +183,24 @@ class MessageRepositoryTest { val mWa = MessageWithAttachment(testMessage, emptyList()) val mWaWithContent = MessageWithAttachment(testMessageWithContent, emptyList()) - coEvery { messageDb.loadMessageWithAttachment(1, testMessage.messageId) } returnsMany listOf(flowOf(mWa), flowOf(mWaWithContent)) - coEvery { sdk.getMessageDetails(testMessage.messageId, 1, false, testMessage.realId) } returns MessageDetails("Test", emptyList()) + coEvery { + messageDb.loadMessageWithAttachment( + 1, + testMessage.messageId + ) + } returnsMany listOf(flowOf(mWa), flowOf(mWaWithContent)) + coEvery { + sdk.getMessageDetails( + messageId = testMessage.messageId, + folderId = 1, + read = false, + id = testMessage.realId + ) + } returns MessageDetails("Test", emptyList()) coEvery { messageDb.updateAll(any()) } just Runs coEvery { messageAttachmentDao.insertAttachments(any()) } returns listOf(1) - val res = runBlocking { messageRepository.getMessage(student, testMessage).toFirstResult() } + val res = runBlocking { repository.getMessage(student, testMessage).toFirstResult() } assertEquals(null, res.error) assertEquals(Status.SUCCESS, res.status) @@ -112,18 +212,22 @@ class MessageRepositoryTest { fun `get message when content in db is empty and there is no internet connection`() { val testMessage = getMessageEntity(123, "", false) - coEvery { messageDb.loadMessageWithAttachment(1, testMessage.messageId) } throws UnknownHostException() + coEvery { + messageDb.loadMessageWithAttachment(1, testMessage.messageId) + } throws UnknownHostException() - runBlocking { messageRepository.getMessage(student, testMessage).toFirstResult() } + runBlocking { repository.getMessage(student, testMessage).toFirstResult() } } @Test(expected = UnknownHostException::class) fun `get message when content in db is empty, unread and there is no internet connection`() { val testMessage = getMessageEntity(123, "", true) - coEvery { messageDb.loadMessageWithAttachment(1, testMessage.messageId) } throws UnknownHostException() + coEvery { + messageDb.loadMessageWithAttachment(1, testMessage.messageId) + } throws UnknownHostException() - runBlocking { messageRepository.getMessage(student, testMessage).toList()[1] } + runBlocking { repository.getMessage(student, testMessage).toList()[1] } } private fun getMessageEntity( @@ -135,10 +239,10 @@ class MessageRepositoryTest { realId = 1, messageId = messageId, sender = "", - senderId = 1, - recipient = "", + senderId = 0, + recipient = "Wielu adresatów", subject = "", - date = LocalDateTime.now(), + date = LocalDateTime.MAX, folderId = 1, unread = unread, removed = false, @@ -148,4 +252,24 @@ class MessageRepositoryTest { unreadBy = 1 readBy = 1 } + + private fun getMessageDto( + messageId: Int, + content: String, + unread: Boolean, + ) = io.github.wulkanowy.sdk.pojo.Message( + id = 1, + messageId = messageId, + sender = Sender("", "", 0, 0, 0, ""), + recipients = listOf(), + subject = "", + content = content, + date = LocalDateTime.MAX, + folderId = 1, + unread = unread, + unreadBy = 0, + readBy = 0, + removed = false, + hasAttachments = false, + ) } From 4aa6b0b99519db7e16dfec03cd72a14b6776b475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sun, 29 Aug 2021 19:00:30 +0200 Subject: [PATCH 155/869] Hide keyboard on opening login host dropdown (#1455) --- .../wulkanowy/ui/modules/login/form/LoginFormFragment.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt index 5250ceb6a..e383072ec 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt @@ -81,6 +81,9 @@ class LoginFormFragment : BaseFragment(R.layout.fragme loginFormRecoverLink.setOnClickListener { presenter.onRecoverClick() } loginFormRecoverLinkSecond.setOnClickListener { presenter.onRecoverClick() } loginFormPass.setOnEditorDoneSignIn { loginFormSignIn.callOnClick() } + loginFormHost.setOnFocusChangeListener { _, hasFocus -> + if (hasFocus) requireActivity().hideSoftInput() + } } with(binding.loginFormHost) { From ea0fb00bdebaa48e9c7ac6dfa01ee39bc67e6bdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sun, 29 Aug 2021 19:31:28 +0200 Subject: [PATCH 156/869] Fix crash on opening date pickers during holidays (#1456) --- .../modules/attendance/AttendanceFragment.kt | 30 ++++++++++--------- .../history/LuckyNumberHistoryFragment.kt | 20 ++++++------- .../history/LuckyNumberHistoryPresenter.kt | 18 +++++++++++ .../ui/modules/timetable/TimetableFragment.kt | 21 +++++++------ 4 files changed, 54 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt index 10cab3df0..7ae1e0586 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt @@ -35,7 +35,8 @@ import java.time.LocalDate import javax.inject.Inject @AndroidEntryPoint -class AttendanceFragment : BaseFragment(R.layout.fragment_attendance), AttendanceView, MainView.MainChildView, +class AttendanceFragment : BaseFragment(R.layout.fragment_attendance), + AttendanceView, MainView.MainChildView, MainView.TitledView { @Inject @@ -118,7 +119,9 @@ class AttendanceFragment : BaseFragment(R.layout.frag with(binding) { attendanceSwipe.setOnRefreshListener(presenter::onSwipeRefresh) attendanceSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) - attendanceSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh)) + attendanceSwipe.setProgressBackgroundColorSchemeColor( + requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh) + ) attendanceErrorRetry.setOnClickListener { presenter.onRetry() } attendanceErrorDetails.setOnClickListener { presenter.onDetailsClick() } @@ -208,7 +211,7 @@ class AttendanceFragment : BaseFragment(R.layout.frag } override fun showNextButton(show: Boolean) { - binding. attendanceNextButton.visibility = if (show) VISIBLE else INVISIBLE + binding.attendanceNextButton.visibility = if (show) VISIBLE else INVISIBLE } override fun showExcuseButton(show: Boolean) { @@ -220,20 +223,19 @@ class AttendanceFragment : BaseFragment(R.layout.frag } override fun showDatePickerDialog(currentDate: LocalDate) { - val now = LocalDate.now() - val startOfSchoolYear = now.schoolYearStart.toTimestamp() - val endWeek = now.plusWeeks(1).toTimestamp() + val baseDate = currentDate.schoolYearStart + val rangeStart = baseDate.toTimestamp() + val rangeEnd = LocalDate.now().plusWeeks(1).toTimestamp() val constraintsBuilder = CalendarConstraints.Builder().apply { - setValidator(SchoolDaysValidator(startOfSchoolYear, endWeek)) - setStart(startOfSchoolYear) - setEnd(endWeek) + setValidator(SchoolDaysValidator(rangeStart, rangeEnd)) + setStart(rangeStart) + setEnd(rangeEnd) } - val datePicker = - MaterialDatePicker.Builder.datePicker() - .setCalendarConstraints(constraintsBuilder.build()) - .setSelection(currentDate.toTimestamp()) - .build() + val datePicker = MaterialDatePicker.Builder.datePicker() + .setCalendarConstraints(constraintsBuilder.build()) + .setSelection(currentDate.toTimestamp()) + .build() datePicker.addOnPositiveButtonClickListener { val date = it.toLocalDateTime() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt index 4981aad25..dc141f8d3 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt @@ -112,19 +112,19 @@ class LuckyNumberHistoryFragment : } override fun showDatePickerDialog(currentDate: LocalDate) { - val now = LocalDate.now() - val startOfSchoolYear = now.schoolYearStart.toTimestamp() + val baseDate = currentDate.schoolYearStart + val rangeStart = baseDate.toTimestamp() + val rangeEnd = LocalDate.now().plusWeeks(1).toTimestamp() val constraintsBuilder = CalendarConstraints.Builder().apply { - setValidator(SchoolDaysValidator(startOfSchoolYear, now.toTimestamp())) - setStart(startOfSchoolYear) - setEnd(now.toTimestamp()) + setValidator(SchoolDaysValidator(rangeStart, rangeEnd)) + setStart(rangeStart) + setEnd(rangeEnd) } - val datePicker = - MaterialDatePicker.Builder.datePicker() - .setCalendarConstraints(constraintsBuilder.build()) - .setSelection(currentDate.toTimestamp()) - .build() + val datePicker = MaterialDatePicker.Builder.datePicker() + .setCalendarConstraints(constraintsBuilder.build()) + .setSelection(currentDate.toTimestamp()) + .build() datePicker.addOnPositiveButtonClickListener { val date = it.toLocalDateTime() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryPresenter.kt index 556dda759..c45cb69a7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryPresenter.kt @@ -2,18 +2,22 @@ package io.github.wulkanowy.ui.modules.luckynumber.history import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.repositories.LuckyNumberRepository +import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.flowWithResource +import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday import io.github.wulkanowy.utils.isHolidays import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.previousOrSameSchoolDay import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.toFormattedString +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onEach import timber.log.Timber import java.time.LocalDate @@ -22,6 +26,7 @@ import javax.inject.Inject class LuckyNumberHistoryPresenter @Inject constructor( errorHandler: ErrorHandler, studentRepository: StudentRepository, + private val semesterRepository: SemesterRepository, private val luckyNumberRepository: LuckyNumberRepository, private val analytics: AnalyticsHelper ) : BasePresenter(errorHandler, studentRepository) { @@ -40,6 +45,19 @@ class LuckyNumberHistoryPresenter @Inject constructor( Timber.i("Lucky number history view was initialized") errorHandler.showErrorMessage = ::showErrorViewOnError loadData() + if (currentDate.isHolidays) setBaseDateOnHolidays() + } + + private fun setBaseDateOnHolidays() { + flow { + val student = studentRepository.getCurrentStudent() + emit(semesterRepository.getCurrentSemester(student)) + }.catch { + Timber.i("Loading semester result: An exception occurred") + }.onEach { + currentDate = currentDate.getLastSchoolDayIfHoliday(it.schoolYear) + reloadNavigation() + }.launch("holidays") } private fun loadData() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt index a374e166c..a65d69211 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt @@ -185,20 +185,19 @@ class TimetableFragment : BaseFragment(R.layout.fragme } override fun showDatePickerDialog(currentDate: LocalDate) { - val now = LocalDate.now() - val startOfSchoolYear = now.schoolYearStart.toTimestamp() - val endOfSchoolYear = now.schoolYearEnd.toTimestamp() + val baseDate = currentDate.schoolYearStart + val rangeStart = baseDate.toTimestamp() + val rangeEnd = LocalDate.now().schoolYearEnd.toTimestamp() val constraintsBuilder = CalendarConstraints.Builder().apply { - setValidator(SchoolDaysValidator(startOfSchoolYear, endOfSchoolYear)) - setStart(startOfSchoolYear) - setEnd(endOfSchoolYear) + setValidator(SchoolDaysValidator(rangeStart, rangeEnd)) + setStart(rangeStart) + setEnd(rangeEnd) } - val datePicker = - MaterialDatePicker.Builder.datePicker() - .setCalendarConstraints(constraintsBuilder.build()) - .setSelection(currentDate.toTimestamp()) - .build() + val datePicker = MaterialDatePicker.Builder.datePicker() + .setCalendarConstraints(constraintsBuilder.build()) + .setSelection(currentDate.toTimestamp()) + .build() datePicker.addOnPositiveButtonClickListener { val date = it.toLocalDateTime() From 98dcc62bb73de7b5eaef04dfe6ff1437eeb8fd2c Mon Sep 17 00:00:00 2001 From: Mateusz Idziejczak Date: Sun, 29 Aug 2021 19:47:14 +0200 Subject: [PATCH 157/869] Add excuse function to "not excusable" account (#1429) --- .../modules/attendance/AttendanceAdapter.kt | 3 +- .../modules/attendance/AttendanceFragment.kt | 25 ++++++++++++++-- .../modules/attendance/AttendancePresenter.kt | 30 +++++++++++++++++-- .../ui/modules/attendance/AttendanceView.kt | 2 ++ .../message/send/SendMessageActivity.kt | 8 +++++ .../message/send/SendMessagePresenter.kt | 6 +++- .../wulkanowy/utils/AttendanceExtension.kt | 3 ++ app/src/main/res/values/strings.xml | 2 ++ 8 files changed, 72 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt index 03ec1c842..6cee23963 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt @@ -10,6 +10,7 @@ import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.enums.SentExcuseStatus import io.github.wulkanowy.databinding.ItemAttendanceBinding import io.github.wulkanowy.utils.description +import io.github.wulkanowy.utils.isExcusableOrNotExcused import javax.inject.Inject class AttendanceAdapter @Inject constructor() : @@ -56,7 +57,7 @@ class AttendanceAdapter @Inject constructor() : attendanceItemExcuseInfo.visibility = View.VISIBLE } else -> { - if (item.excusable && excuseActionMode) { + if (item.isExcusableOrNotExcused && excuseActionMode) { attendanceItemNumber.visibility = View.GONE attendanceItemExcuseCheckbox.visibility = View.VISIBLE } else { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt index 7ae1e0586..058946796 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt @@ -24,6 +24,7 @@ import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.ui.modules.message.send.SendMessageActivity import io.github.wulkanowy.ui.widgets.DividerItemDecoration import io.github.wulkanowy.utils.SchoolDaysValidator import io.github.wulkanowy.utils.dpToPx @@ -120,7 +121,9 @@ class AttendanceFragment : BaseFragment(R.layout.frag attendanceSwipe.setOnRefreshListener(presenter::onSwipeRefresh) attendanceSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) attendanceSwipe.setProgressBackgroundColorSchemeColor( - requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh) + requireContext().getThemeAttrColor( + R.attr.colorSwipeRefresh + ) ) attendanceErrorRetry.setOnClickListener { presenter.onRetry() } attendanceErrorDetails.setOnClickListener { presenter.onDetailsClick() } @@ -253,8 +256,13 @@ class AttendanceFragment : BaseFragment(R.layout.frag .setNegativeButton(android.R.string.cancel) { _, _ -> } .create() .apply { - setButton(BUTTON_POSITIVE, getString(R.string.attendance_excuse_dialog_submit)) { _, _ -> - presenter.onExcuseDialogSubmit(dialogBinding.excuseReason.text?.toString().orEmpty()) + setButton( + BUTTON_POSITIVE, + getString(R.string.attendance_excuse_dialog_submit) + ) { _, _ -> + presenter.onExcuseDialogSubmit( + dialogBinding.excuseReason.text?.toString().orEmpty() + ) } }.show() } @@ -267,6 +275,17 @@ class AttendanceFragment : BaseFragment(R.layout.frag actionMode = (activity as MainActivity?)?.startSupportActionMode(actionModeCallback) } + override fun startSendMessageIntent(date: LocalDate, numbers: String, reason: String) { + val reasonFullText = getString( + R.string.attendance_excuse_formula, + date, + numbers, + if (reason.isNotBlank()) " ${getString(R.string.attendance_excuse_reason)} " else "", + reason.ifBlank { "" } + ) + startActivity(SendMessageActivity.getStartIntent(requireContext(), reasonFullText)) + } + override fun showExcuseCheckboxes(show: Boolean) { attendanceAdapter.apply { excuseActionMode = show diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt index 333e7103a..9a1598128 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt @@ -15,6 +15,7 @@ import io.github.wulkanowy.utils.capitalise import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.flowWithResourceIn import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday +import io.github.wulkanowy.utils.isExcusableOrNotExcused import io.github.wulkanowy.utils.isHolidays import io.github.wulkanowy.utils.nextSchoolDay import io.github.wulkanowy.utils.previousOrSameSchoolDay @@ -47,6 +48,8 @@ class AttendancePresenter @Inject constructor( private val attendanceToExcuseList = mutableListOf() + private var isVulcanExcusedFunctionEnabled = false + fun onAttachView(view: AttendanceView, date: Long?) { super.onAttachView(view) view.initView() @@ -148,7 +151,21 @@ class AttendancePresenter @Inject constructor( fun onExcuseDialogSubmit(reason: String) { view?.finishActionMode() - excuseAbsence(if (reason != "") reason else null, attendanceToExcuseList.toList()) + + if (isVulcanExcusedFunctionEnabled) { + excuseAbsence( + reason = reason.takeIf { it.isNotBlank() }, + toExcuseList = attendanceToExcuseList.toList() + ) + } else { + val attendanceToExcuseNumbers = attendanceToExcuseList.map { it.number } + + view?.startSendMessageIntent( + date = attendanceToExcuseList[0].date, + numbers = attendanceToExcuseNumbers.joinToString(", "), + reason = reason + ) + } } fun onPrepareActionMode(): Boolean { @@ -188,8 +205,12 @@ class AttendancePresenter @Inject constructor( private fun loadData(forceRefresh: Boolean = false) { Timber.i("Loading attendance data started") + var isParent = false + flowWithResourceIn { val student = studentRepository.getCurrentStudent() + isParent = student.isParent + val semester = semesterRepository.getCurrentSemester(student) attendanceRepository.getAttendance( student, @@ -227,12 +248,17 @@ class AttendancePresenter @Inject constructor( it.data?.filter { item -> !item.presence }.orEmpty() } + isVulcanExcusedFunctionEnabled = + filteredAttendance.any { item -> item.excusable } + view?.apply { updateData(filteredAttendance.sortedBy { item -> item.number }) showEmpty(filteredAttendance.isEmpty()) showErrorView(false) showContent(filteredAttendance.isNotEmpty()) - showExcuseButton(filteredAttendance.any { item -> item.excusable }) + showExcuseButton(filteredAttendance.any { item -> + (!isParent && isVulcanExcusedFunctionEnabled) || (isParent && item.isExcusableOrNotExcused) + }) } analytics.logEvent( "load_data", diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt index 0459dfcf6..738f2ff86 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt @@ -54,6 +54,8 @@ interface AttendanceView : BaseView { fun openSummaryView() + fun startSendMessageIntent(date: LocalDate, numbers: String, reason: String) + fun startActionMode() fun showExcuseCheckboxes(show: Boolean) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt index d49c9b833..1432a9945 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt @@ -35,6 +35,8 @@ class SendMessageActivity : BaseActivity() - fun onAttachView(view: SendMessageView, message: Message?, reply: Boolean?) { + fun onAttachView(view: SendMessageView, reason: String?, message: Message?, reply: Boolean?) { super.onAttachView(view) view.initView() initializeSubjectStream() @@ -50,6 +50,10 @@ class SendMessagePresenter @Inject constructor( if (messageRepository.draftMessage != null && reply == null) { view.showMessageBackupDialog() } + reason?.let { + setSubject("Usprawiedliwenie") + setContent(it) + } message?.let { setSubject(when (reply) { true -> "RE: " diff --git a/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt index 8bccec620..479cc5188 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt @@ -16,6 +16,9 @@ private inline val AttendanceSummary.allPresences: Double private inline val AttendanceSummary.allAbsences: Double get() = absence.toDouble() + absenceExcused +inline val Attendance.isExcusableOrNotExcused: Boolean + get() = excusable || ((absence || lateness) && !excused) + fun AttendanceSummary.calculatePercentage() = calculatePercentage(allPresences, allAbsences) fun List.calculatePercentage(): Double { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ea1187c84..08776c1ab 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -192,6 +192,8 @@ Absence excuse request sent successfully! You must select at least one absence! Excuse + z powodu + Dzień dobry,\nProszę o usprawiedliwienie mojego dziecka w dniu %s z lekcji %s%s%s.\n\nPozdrawiam. From 79e9e1a780c8c600d9380ffe3ad3ddd273955295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sun, 29 Aug 2021 20:01:36 +0200 Subject: [PATCH 158/869] New Crowdin updates (#1321) --- .../main/res/values-cs/preferences_values.xml | 11 + app/src/main/res/values-cs/strings.xml | 163 ++++++++++++++- .../main/res/values-de/preferences_values.xml | 11 + app/src/main/res/values-de/strings.xml | 147 +++++++++++-- .../main/res/values-pl/preferences_values.xml | 11 + app/src/main/res/values-pl/strings.xml | 153 +++++++++++++- .../main/res/values-ru/preferences_values.xml | 11 + app/src/main/res/values-ru/strings.xml | 183 ++++++++++++++-- .../main/res/values-sk/preferences_values.xml | 11 + app/src/main/res/values-sk/strings.xml | 163 ++++++++++++++- .../main/res/values-uk/preferences_values.xml | 11 + app/src/main/res/values-uk/strings.xml | 195 +++++++++++++++--- 12 files changed, 999 insertions(+), 71 deletions(-) diff --git a/app/src/main/res/values-cs/preferences_values.xml b/app/src/main/res/values-cs/preferences_values.xml index ce32f22bc..fb938f092 100644 --- a/app/src/main/res/values-cs/preferences_values.xml +++ b/app/src/main/res/values-cs/preferences_values.xml @@ -45,4 +45,15 @@ Průměr známek z obou semestrů Průměr známek z celého roku + + Šťastné číslo + Nepřečtené zprávy + Docházka + Lekce + Známky + Domácí úkoly + Školní oznámení + Zkoušky + Setkání + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 8929f6c20..3b8476551 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -11,6 +11,8 @@ Více O aplikaci Prohlížeč protokolů + Ladění + Ladění oznámení Tvůrci Licence Zprávy @@ -21,6 +23,7 @@ Vyberte účet Podrobnosti účtu Informace o žáku + Domů Semestr %1$d, %2$d/%3$d @@ -88,6 +91,7 @@ Předpokládaná známka Vypočítaný průměr Konečný průměr + z %1$d z %2$d předmětů Shrnutí Třída Označit jako přečtené @@ -196,6 +200,12 @@ Nové zkoušky Nové zkoušky + + Máte %d novou zkoušku + Máte %d nové zkoušky + Máte %d nových zkoušek + Máte %d nových zkoušek + %d zkouška %d zkoušky @@ -203,7 +213,7 @@ %d zkoušek - Doručená pošta + Doručené Odesláno Koš (žádný předmět) @@ -225,6 +235,15 @@ Zpráva neexistuje Musíte vybrat alespoň 1 příjemce Obsah zprávy musí mít alespoň 3 znaky + Pouze nepřečtené + Pouze s přílohami + Přečtena: %s + + Přečtena přes: %1$d z %2$d osob + Přečtena přes: %1$d z %2$d osob + Přečtena přes: %1$d z %2$d osob + Přečtena přes: %1$d z %2$d osob + %d zpráva %d zprávy @@ -237,6 +256,8 @@ Nové zprávy Nové zprávy + Chcete obnovit koncept zprávy? + Chcete obnovit koncept zprávy s příjemci: %s? Máte %1$d novou zprávu Máte %1$d nové zprávy @@ -313,6 +334,12 @@ Nové domácí úkoly Nové domácí úkoly + + Máte %d nový domácí úkol + Máte %d nové domácí úkoly + Máte %d nových domácích úkolů + Máte %d nových domácích úkolů + %d domácí úkol %d domácí úkoly @@ -324,7 +351,7 @@ Dnešní šťastné číslo je Žádné informace o šťastném čísle Šťastné číslo pro dnešek - Dnes je šťastným číslem: %s + Dnešní šťastné číslo je: %s Zobrazit historii Historie šťastných čísel @@ -357,6 +384,45 @@ Setkání Žádné informace o setkáních + + %d setkání + %d setkání + %d setkání + %d setkání + + + Nové setkání + Nová setkání + Nová setkání + Nová setkání + + + Máte %1$d nové setkání + Máte %1$d nové setkání + Máte %1$d nových setkání + Máte %1$d nových setkání + + + Školní oznámení + Žádná školní oznámení + + %d školní oznámení + %d školní oznámení + %d školní oznámení + %d školní oznámení + + + Nové školní oznámení + Nová školní oznámení + Nová školní oznámení + Nová školní oznámení + + + Máte %1$d nové školní oznámení + Máte %1$d nové školní oznámení + Máte %1$d nových školních oznámení + Máte %1$d nových školních oznámení + Přidat účet Odhlásit @@ -382,6 +448,8 @@ Server Discord Připojte se ke komunitě Wulkanového Facebooková fanpage + Twitter stránka + Sledujte nás na Twitteru Stejně jako naše facebooková fanpage Zásady ochrany osobních údajů Pravidla pro shromažďování osobních údajů @@ -417,6 +485,7 @@ Muž Žena Příjmení + Opatrovník Přezdívka Přidat přezdívku @@ -424,6 +493,80 @@ Sdílet protokoly Obnovit + + Lekce + (Zítra) + %1$s (%2$s) + Za chvíli: + Brzy: + První: + Teď: + + za %1$d minutu + za %1$d minuty + za %1$d minut + za %1$d minut + + + ještě %1$d minutu + ještě %1$d minuty + ještě %1$d minut + ještě %1$d minut + + Konec lekcí + Další: + Později: + + ještě %1$d další lekce + ještě %1$d další lekce + ještě %1$d dalších lekcí + ještě %1$d dalších lekcí + + do %1$s + Žádné nadcházející lekce + Při načítání lekcí došlo k chybě + Domácí úkoly + Žádné domácí úkoly do vykonána + Při načítání domácích úkolů došlo k chybě + + Ještě %1$d další domácí úkol + Ještě %1$d další domácí úkoly + Ještě %1$d dalších domácích úkolů + Ještě %1$d dalších domácích úkolů + + do %1$s + Poslední známky + Žádné nové známky + Při načítání známek došlo k chybě + Školní oznámení + Žádná aktuální oznámení + Při načítání oznámení došlo k chybě + + Ještě %1$d další oznámení + Ještě %1$d další oznámení + Ještě %1$d dalších oznámení + Ještě %1$d dalších oznámení + + Zkoušky + Žádné nadcházející zkoušky + Při načítání zkoušek došlo k chybě + + Ještě %1$d další zkouška + Ještě %1$d další zkoušky + Ještě %1$d dalších zkoušek + Ještě %1$d dalších zkoušek + + Setkání + Žádná nadcházející setkání + Při načítání setkání došlo k chybě + + Ještě %1$d další setkání + Ještě %1$d další setkání + Ještě %1$d dalších setkání + Ještě %1$d dalších setkání + + Při načítání dat došlo k chybě + Žádné Zkontrolovat aktualizace Před hlášením chyby zkontrolujte, zda je k dispozici aktualizace s opravou chyb @@ -487,6 +630,7 @@ Synchronizováno! Synchronizace selhala Probíhá synchronizace + Poslední úplná synchronizace: %s Hodnota plusu Hodnota mínusu Odpovědět s historií zpráv @@ -496,6 +640,8 @@ Upozornění Synchronizace Známky + Domů + Viditelnost dlaždic Docházka Plán lekce Známky @@ -512,23 +658,26 @@ Nové známky Nové domácí úkoly + Nová setkání Nové zkoušky Šťastné číslo Nové zprávy Nové poznámky + Nové školní oznámení Push upozornění Nadcházející lekce Ladění - Černý - Červený - Modrý - Zelený - Fialový + Černá + Červená + Modrá + Zelená + Fialová Žádná barva Zkopírováno Vrátit + Změnit Stahování aktualizací začalo… Aktualizace byla stažena. diff --git a/app/src/main/res/values-de/preferences_values.xml b/app/src/main/res/values-de/preferences_values.xml index 35e930594..53faaf9b0 100644 --- a/app/src/main/res/values-de/preferences_values.xml +++ b/app/src/main/res/values-de/preferences_values.xml @@ -45,4 +45,15 @@ Durchschnitt der Noten aus beiden Semestern Durchschnitt der Noten aus dem ganzen Jahr + + Lucky number + Unread messages + Attendance + Lessons + Grades + Homework + School announcements + Exams + Conferences + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 7399e1dea..4bbf7767b 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -11,6 +11,8 @@ Mehr Über die Applikation Log Viewer + Debuggen + Benachrichtigungen debuggen Mitarbeiter Lizenzen Nachrichten @@ -21,6 +23,7 @@ Konto auswählen Kontodetails Schülerinfo + Übersicht Semester %1$d, %2$d/%3$d @@ -88,6 +91,7 @@ Vorhergesagte Note Berechnender Durchschnitt Finaler Durchschnitt + aus %1$d von %2$d Schulfächern Zusammenfassung Klasse Als gelesen markieren @@ -177,12 +181,16 @@ Form Eintrittsdatum - New exam - New exams + Neue prüfung + Neue prüfungen + + + You received %d new exam + Du hast %d neue Prüfungen bekommen - %d exam - %d exams + %d prüfung + %d prüfungen Posteingang @@ -207,6 +215,13 @@ Nachricht existiert nicht Sie müssen mindestens 1 Empfänger auswählen. Der Inhalt der Nachricht muss mindestens 3 Zeichen lang sein. + Nur ungelesen + Nur mit Anhängen + Lesen: %s + + Read by: %1$d of %2$d people + Read by: %1$d of %2$d people + %d nachricht %d nachrichten @@ -215,6 +230,8 @@ Neu nachricht Neue nachrichten + Do you want to restore draft message? + Do you want to restore draft message with recipients: %s? Du hast %1$d nachricht bekommen Du hast %1$d nachrichten bekommen @@ -266,19 +283,23 @@ Unvollständig Anhänge - New homework - New homework + Neue hausaufgaben + Neue hausaufgaben + + + You received %d new homework + Du hast %d neue Hausaufgaben bekommen - %d homework - %d homework + %d hausaufgaben + %d hausaufgaben Glückliche Nummer Die heutige Glücksnummer ist Keine Information über die Glücksnummer. Glücksnummer für heute - Die heutige Glücksnummer ist: %s + Heutige Glückszahl ist: %s Verlauf anzeigen Glücksnummerverlauf @@ -311,6 +332,33 @@ Sitzungen Keine Informationen über Sitzungen + + %d konferenz + %d konferenzen + + + Neue konferenz + Neue konferenzen + + + Sie haben %1$d neue konferenz + Sie haben %1$d neue konferenzen + + + Schulankündigungen + Keine schulankündigungen + + %d schulankündigung + %d schulankündigungen + + + Ankündigung der neuen schule + Neue schulankündigungen + + + Sie haben %1$d neue schulmitteilungen + Sie haben %1$d neue schulankündigungen + Konto hinzufügen Abmelden @@ -336,11 +384,13 @@ Discord server Treten Sie der Wulkanowy-Gemeinschaft bei Facebook fanpage + Twitter-Seite + Folgen Sie uns bei Twitter Gefällt unsere Facebook-Fanpage Datenschutzerklärung Regeln für die Sammlung persönlicher Daten - System settings - Open system settings + Systemeinstellungen + Systemeinstellungen öffnen Startseite Besuchen Sie die Website und helfen Sie bei der Entwicklung der Anwendung Lizenzen @@ -371,6 +421,7 @@ Mann Frau Nachname + Wächter Nick Nick hinzufügen @@ -378,6 +429,66 @@ Logs teilen Aktualisieren + + Lektionen + (Morgen) + %1$s (%2$s) + Gleich: + Bald: + Erstens: + Jetzt: + + in %1$d Minute + in %1$d Minuten + + + Noch %1$d Minute + Noch %1$d Minuten + + Ende der Lektion + Nächste: + Später: + + Noch %1$d Lektion + %1$d more lessons + + until %1$s + No upcoming lessons + An error occurred while loading the lessons + Homework + No homework to do + An error occurred while loading the homework + + %1$d more homework + %1$d more homework + + due %1$s + Last grades + No new grades + An error occurred while loading the grades + School announcements + No current announcements + An error occurred while loading the announcements + + %1$d more announcement + %1$d more announcements + + Exams + No upcoming exams + An error occurred while loading the exams + + %1$d more exam + %1$d more exams + + Conferences + No upcoming conferences + An error occurred while loading the conferences + + %1$d more conference + %1$d more conferences + + An error occurred while loading data + None Auf Updates prüfen Bevor Sie einen Fehler melden, prüfen Sie zuerst, ob ein Update mit der Fehlerbehebung verfügbar ist @@ -426,7 +537,7 @@ Benachrichtigungen Benachrichtigungen anzeigen Benachrichtigungen über bevorstehende Lektionen anzeigen - Open system notification settings + Systembenachrichtigungseinstellungen öffnen Synchronisierungs- und Benachrichtigungsprobleme reparieren Ihr Gerät hat möglicherweise Probleme mit der Datensynchronisierung und Benachrichtigungen.\n\nUm diese zu reparieren, fügen Sie Wulkanowy zum Autostart hinzu und deaktivieren Sie die Batterieoptimierung in den Systemeinstellungen des Geräts. Gehe zu den Einstellungen @@ -441,15 +552,18 @@ Synchronisiert! Synchronisierung fehlgeschlagen Synchronisierung läuft + Last full sync: %s Wert des Plus Wert des Minus Antwort mit Nachrichtenhistorie - Show arithmetic average when no weights provided + Arithmetisches Mittel anzeigen, wenn keine Gewichte angegeben sind Erweitert Aussehen & Verhalten Benachrichtigungen Synchronisierung Noten + Dashboard + Tiles visibility Schulbesuch Zeitplan Noten @@ -465,11 +579,13 @@ App-Version, Mitarbeiter, soziale Portale, Lizenzen Neue Noten - New homework - New exams + Neue Hausaufgaben + Neue Konferenzen + Neue Prüfungen Glückliche Nummer Neue Nachrichten Neue Eintragen + Neue Schulankündigungen Push-Benachrichtigungen Bevorstehende Lektionen Debuggen @@ -483,6 +599,7 @@ Kopiert lösen + Change Download der Updates wurde gestartet… Ein Update wurde gerade heruntergeladen. diff --git a/app/src/main/res/values-pl/preferences_values.xml b/app/src/main/res/values-pl/preferences_values.xml index 918697856..12cfda8c9 100644 --- a/app/src/main/res/values-pl/preferences_values.xml +++ b/app/src/main/res/values-pl/preferences_values.xml @@ -45,4 +45,15 @@ Średnia średnich z obu semestrów Średnia wszystkich ocen z całego roku + + Szczęśliwy numerek + Nieprzeczytane wiadomości + Frekwencja + Lekcje + Oceny + Zadania domowe + Ogłoszenia szkolne + Sprawdziany + Zebrania + diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index a855ebd1c..74627ed1f 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -11,6 +11,8 @@ Więcej O aplikacji Przeglądarka logów + Debugowanie + Debugowanie powiadomień Twórcy Licencje Wiadomości @@ -21,6 +23,7 @@ Wybierz konto Szczegóły konta Informacje o uczniu + Start Semestr %1$d, %2$d/%3$d @@ -88,6 +91,7 @@ Przewidywana ocena Obliczona średnia Końcowa średnia + z %1$d na %2$d przedmiotów Podsumowanie Klasa Oznacz jako przeczytane @@ -196,6 +200,12 @@ Nowe sprawdziany Nowe sprawdziany + + Masz %d nowy sprawdzian + Masz %d nowe sprawdziany + Masz %d nowych sprawdzianów + Masz %d nowych sprawdzianów + %d sprawdzian %d sprawdziany @@ -225,6 +235,15 @@ Wiadomość nie istnieje Musisz wybrać co najmniej 1 adresata Treść wiadomości musi zawierać co najmniej 3 znaki + Tylko nieprzeczytane + Tylko z załącznikami + Przeczytana: %s + + Przeczytana przez: %1$d z %2$d osób + Przeczytana przez: %1$d z %2$d osób + Przeczytana przez: %1$d z %2$d osób + Przeczytana przez: %1$d z %2$d osób + %d wiadomość %d wiadomości @@ -237,6 +256,8 @@ Nowe wiadomości Nowe wiadomości + Czy chcesz przywrócić wersję roboczą wiadomości? + Czy chcesz przywrócić wersję roboczą wiadomości z adresatami: %s? Masz %1$d nową wiadomość Masz %1$d nowe wiadomości @@ -313,6 +334,12 @@ Nowe zadania domowe Nowe zadania domowe + + Masz %d nowe zadanie domowe + Masz %d nowe zadania domowe + Masz %d nowych zadań domowych + Masz %d nowych zadań domowych + %d zadanie domowe %d zadania domowe @@ -324,7 +351,7 @@ Dzisiejszym szczęśliwym numerkiem jest Brak informacji o szczęśliwym numerku Szczęśliwy numerek na dzisiaj - Dziś szczęśliwym numerkiem jest: %s + Dzisiejszym szczęśliwym numerkiem jest: %s Pokaż historię Historia numerków @@ -357,6 +384,45 @@ Zebrania Brak informacji o zebraniach + + %d zebranie + %d zebrania + %d zebrań + %d zebrań + + + Nowe zebranie + Nowe zebrania + Nowe zebrania + Nowe zebrania + + + Masz %1$d nowe zebranie + Masz %1$d nowe zebrania + Masz %1$d nowych zebrań + Masz %1$d nowych zebrań + + + Ogłoszenia szkolne + Brak ogłoszeń szkolnych + + %d ogłoszenie szkolne + %d ogłoszenia szkolne + %d ogłoszeń szkolnych + %d ogłoszeń szkolnych + + + Nowe ogłoszenie szkolne + Nowe ogłoszenia szkolne + Nowe ogłoszenia szkolne + Nowe ogłoszenia szkolne + + + Masz %1$d nowe ogłoszenie szkolne + Masz %1$d nowe ogłoszenia szkolne + Masz %1$d nowych ogłoszeń szkolnych + Masz %1$d nowych ogłoszeń szkolnych + Dodaj konto Wyloguj @@ -382,6 +448,8 @@ Serwer Discord Dołącz do społeczności Wulkanowego Fanpage na Facebooku + Strona na Twitterze + Śledź nas na Twitterze Polub nasz fanpage na Facebooku Polityka prywatności Zasady zbierania danych osobowych @@ -417,6 +485,7 @@ Mężczyzna Kobieta Nazwisko + Opiekun Pseudonim Dodaj pseudonim @@ -424,6 +493,80 @@ Udostępnij logi Odśwież + + Lekcje + (Jutro) + %1$s (%2$s) + Za chwilę: + Wkrótce: + Pierwsza: + Teraz: + + za %1$d minutę + za %1$d minuty + za %1$d minut + za %1$d minut + + + jeszcze %1$d minuta + jeszcze %1$d minuty + jeszcze %1$d minut + jeszcze %1$d minut + + Koniec lekcji + Następnie: + Później: + + jeszcze %1$d dodatkowa lekcja + jeszcze %1$d dodatkowe lekcje + jeszcze %1$d dodatkowych lekcji + jeszcze %1$d dodatkowych lekcji + + do %1$s + Brak nadchodzących lekcji + Wystąpił błąd podczas ładowania lekcji + Zadania domowe + Brak prac domowych do wykonania + Wystąpił błąd podczas ładowania zadań domowych + + Jeszcze %1$d dodatkowe zadanie domowe + Jeszcze %1$d dodatkowe zadania domowe + Jeszcze %1$d dodatkowych zadań domowych + Jeszcze %1$d dodatkowych zadań domowych + + do %1$s + Ostatnie oceny + Brak nowych ocen + Wystąpił błąd podczas ładowania ocen + Ogłoszenia szkolne + Brak aktualnych ogłoszeń + Wystąpił błąd podczas ładowania ogłoszeń + + Jeszcze %1$d dodatkowe ogłoszenie + Jeszcze %1$d dodatkowe ogłoszenia + Jeszcze %1$d dodatkowych ogłoszeń + Jeszcze %1$d dodatkowych ogłoszeń + + Sprawdziany + Brak nadchodzących sprawdzianów + Wystąpił błąd podczas ładowania sprawdzianów + + Jeszcze %1$d dodatkowy sprawdzian + Jeszcze %1$d dodatkowe sprawdziany + Jeszcze %1$d dodatkowych sprawdzianów + Jeszcze %1$d dodatkowych sprawdzianów + + Zebrania + Brak nadchodzących zebrań + Wystąpił błąd podczas ładowania zebrań + + Jeszcze %1$d dodatkowe zebranie + Jeszcze %1$d dodatkowe zebrania + Jeszcze %1$d dodatkowych zebrań + Jeszcze %1$d dodatkowych zebrań + + Wystąpił błąd podczas ładowania danych + Brak Sprawdź dostępność aktualizacji Przed zgłoszeniem błędu sprawdź wcześniej, czy dostępna jest już aktualizacja z poprawką błędu @@ -472,7 +615,7 @@ Powiadomienia Pokazuj powiadomienia Pokazuj powiadomienia o nadchodzących lekcjach - Otwórz ustawienia systemowe powiadomień + Otwórz systemowe ustawienia powiadomień Napraw problemy z synchronizacją i powiadomieniami Na twoim urządzeniu mogą występować problemy z synchronizacją danych i powiadomieniami.\n\nBy je naprawić, dodaj Wulkanowego do autostartu i wyłącz optymalizację/oszczędzanie baterii w ustawieniach systemowych telefonu. Przejdź do ustawień @@ -487,6 +630,7 @@ Zsynchronizowano! Synchronizacja nie powiodła się Synchronizacja w trakcie + Ostatnia pełna synchronizacja: %s Wartość plusa Wartość minusa Odpowiadaj z historią wiadomości @@ -496,6 +640,8 @@ Powiadomienia Synchronizacja Oceny + Start + Widoczność kafelków Frekwencja Plan lekcji Oceny @@ -512,10 +658,12 @@ Nowe oceny Nowe zadania domowe + Nowe zebrania Nowe sprawdziany Szczęśliwy numerek Nowe wiadomości Nowe uwagi + Nowe ogłoszenia szkolne Powiadomienia push Nadchodzące lekcje Debugowanie @@ -529,6 +677,7 @@ Skopiowano Cofnij + Zmień Rozpoczęto pobieranie aktualizacji… Aktualizacja została pobrana. diff --git a/app/src/main/res/values-ru/preferences_values.xml b/app/src/main/res/values-ru/preferences_values.xml index 4ea5a6146..7c4d14df6 100644 --- a/app/src/main/res/values-ru/preferences_values.xml +++ b/app/src/main/res/values-ru/preferences_values.xml @@ -45,4 +45,15 @@ Средняя оценка с двух семестров Средняя оценок со всего года + + Счастливый номер + Непрочитанные сообщения + Посещаемость + Уроки + Оценки + Домашняя работа + Объявления школ + Экзамены + Конференции + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 1c67d2841..e9349313b 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -11,6 +11,8 @@ Ещё О приложении Просмотр журнала + Отладка + Отладка уведомления Разработчики Лицензии Сообщения @@ -21,6 +23,7 @@ Выбор учетной записи Данные аккаунта Информация о студенте + Панель %1$d семестр, %2$d/%3$d @@ -88,6 +91,7 @@ Ожидаемая оценка Рассчитанная средняя оценка Итоговая средняя оценка + от %1$d из %2$d субъектов Итоги Класс Пометить как \"прочитанное\" @@ -191,16 +195,22 @@ Тип Дата записи - New exam - New exams - New exams - New exams + Новый экзамен + Новый экзамен + Новый экзамен + Новые экзамены + + + Вы получили %d новый экзамен + Вы получили %d новый экзамен + Вы получили %d новый экзамен + Вы получили %d новых экзаменов - %d exam - %d exams - %d exams - %d exams + %d экзамен + %d экзамен + %d экзамен + %d экзаменов Полученные @@ -225,6 +235,15 @@ Сообщения не существует Вы должны выбрать как минимум одного получателя Текст сообщения должен содержать как минимум 3 знака + Только непрочитанные + Только с вложениями + Чтение: %s + + Прочитано: %1$d из %2$d человек + Прочитано: %1$d из %2$d человек + Прочитано: %1$d из %2$d человек + Прочитано: %1$d из %2$d человек + %d сообщение %d сообщения @@ -237,6 +256,8 @@ Новые сообщения Новые сообщения + Вы хотите восстановить черновичное сообщение? + Вы хотите восстановить черновик сообщения с получателями: %s? Вы получили %1$d новое сообщение Вы получили %1$d новых сообщения @@ -308,23 +329,29 @@ Отметить как невыполненное Вложения - New homework - New homework - New homework - New homework + Новая домашняя работа + Новая домашняя работа + Новая домашняя работа + Новая домашняя работа + + + Вы получили %d новых домашних заданий + Вы получили %d новых домашних заданий + Вы получили %d новых домашних заданий + Вы получили %d новых домашних заданий - %d homework - %d homework - %d homework - %d homework + %d домашних заданий + %d домашних заданий + %d домашних заданий + %d домашних заданий Счастливый номер Сегодняшний счастливый номер это Нет данных о счастливом номере Сегодняшний счастливый номер - Сегодняшний счастливый номер это: %s + Сегодняшний номер удачи: %s Показать историю История удачных чисел @@ -357,6 +384,45 @@ Встречи Нет информации о встречах + + %d конференция + %d конференция + %d конференция + %d конференций + + + Новая конференция + Новая конференция + Новая конференция + Новые конференции + + + У вас %1$d новая конференция + У вас %1$d новая конференция + У вас %1$d новая конференция + У вас %1$d новых конференций + + + Объявления школ + Нет объявлений о школе + + Объявление о школе %d + Объявление о школе %d + Объявление о школе %d + Объявления школы %d + + + Объявление о новой школе + New school announcements + New school announcements + New school announcements + + + You have %1$d new school announcement + You have %1$d new school announcements + You have %1$d new school announcements + You have %1$d new school announcements + Добавить аккаунт Выйти @@ -382,6 +448,8 @@ Сервер Discord Присоединиться к сообществу приложения Facebook фан-страница + Twitter page + Follow us on twitter Поставьте лайк на нашей странице в Facebook Политика приватности Правила хранения личных данных @@ -417,6 +485,7 @@ Муж Женская Фамилия + Guardian Ник Добавить ник @@ -424,6 +493,80 @@ Поделиться логами Обновить + + Lessons + (Tomorrow) + %1$s (%2$s) + In a moment: + Soon: + First: + Now: + + in %1$d minute + in %1$d minutes + in %1$d minutes + in %1$d minutes + + + %1$d more minute + %1$d more minutes + %1$d more minutes + %1$d more minutes + + End of lessons + Next: + Later: + + %1$d more lesson + %1$d more lessons + %1$d more lessons + %1$d more lessons + + until %1$s + No upcoming lessons + An error occurred while loading the lessons + Homework + No homework to do + An error occurred while loading the homework + + %1$d more homework + %1$d more homework + %1$d more homework + %1$d more homework + + due %1$s + Last grades + No new grades + An error occurred while loading the grades + School announcements + No current announcements + An error occurred while loading the announcements + + %1$d more announcement + %1$d more announcements + %1$d more announcements + %1$d more announcements + + Exams + No upcoming exams + An error occurred while loading the exams + + %1$d more exam + %1$d more exams + %1$d more exams + %1$d more exams + + Conferences + No upcoming conferences + An error occurred while loading the conferences + + %1$d more conference + %1$d more conferences + %1$d more conferences + %1$d more conferences + + An error occurred while loading data + None Проверить наличие обновлений Прежде чем сообщать об ошибке, проверьте наличие обновлений @@ -487,6 +630,7 @@ Синхронизировано! Синхронизация не удалась Идёт синхронизация + Last full sync: %s Стоимость плюса Стоимость минуса Отвечать с историей сообщений @@ -496,6 +640,8 @@ Уведомления Синхронизация Оценки + Dashboard + Tiles visibility Посещаемость Расписание Оценки @@ -512,10 +658,12 @@ Новые оценки New homework + New conferences New exams Счастливый номер Новые сообщения Новые заметки + New school announcements Показывать push-уведомления Будущие уроки Дебаг @@ -529,6 +677,7 @@ Скопировано Отменить + Change Загрузка обновлений началась… Только что было скачано обновление. diff --git a/app/src/main/res/values-sk/preferences_values.xml b/app/src/main/res/values-sk/preferences_values.xml index b51bfa404..108af555d 100644 --- a/app/src/main/res/values-sk/preferences_values.xml +++ b/app/src/main/res/values-sk/preferences_values.xml @@ -45,4 +45,15 @@ Priemer známok z oboch semestrov Priemer známok z celého roka + + Šťastné číslo + Neprečítané správy + Dochádzka + Lekcie + Známky + Domáce úlohy + Školské oznámenia + Skúšky + Stretnutie + diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index a9656eab7..81a76cf35 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -11,6 +11,8 @@ Viac O aplikácii Prehliadač protokolov + Ladenie + Ladenie oznámení Prispievatelia Licencie Správy @@ -21,6 +23,7 @@ Vyberte účet Podrobnosti účtu Informácie o žiakovi + Domov Semester %1$d, %2$d/%3$d @@ -88,6 +91,7 @@ Predpokladaná známka Vypočítaný priemer Konečný priemer + z %1$d z %2$d predmetov Zhrnutie Trieda Označiť ako prečítané @@ -196,6 +200,12 @@ Nové skúšky Nové skúšky + + You received %d new exam + You received %d new exams + You received %d new exams + You received %d new exams + %d skúška %d skúšky @@ -203,7 +213,7 @@ %d skúšok - Doručená pošta + Doručené Odoslané Kôš (žiadny predmet) @@ -225,6 +235,15 @@ Správa neexistuje Musíte vybrať aspoň 1 príjemca Obsah správy musí mať aspoň 3 znaky + Iba neprečítané + Iba s prílohami + Prečítaná: %s + + Prečítaná cez: %1$d z %2$d osôb + Prečítaná cez: %1$d z %2$d osôb + Prečítaná cez: %1$d z %2$d osôb + Prečítaná cez: %1$d z %2$d osôb + %d správa %d správy @@ -237,6 +256,8 @@ Nové správy Nové správy + Chcete obnoviť koncept správy? + Chcete obnoviť koncept správy s príjemcami:%s? Máte %1$d novú správu Máte %1$d nové správy @@ -313,6 +334,12 @@ Nové domáce úlohy Nové domáce úlohy + + You received %d new homework + You received %d new homework + You received %d new homework + You received %d new homework + %d domáci úloh %d domáce úlohy @@ -324,7 +351,7 @@ Dnešné šťastné číslo je Žiadne informácie o šťastnom čísle Šťastné číslo pre dnešok - Dnes je šťastným číslom: %s + Dnešné šťastné číslo je: %s Zobraziť históriu História šťastných čísel @@ -357,6 +384,45 @@ Stretnutie Žiadne informácie o stretnutiach + + %d stretnutie + %d stretnutia + %d stretnutí + %d stretnutí + + + Nové stretnutie + Nová stretnutia + Nová stretnutia + Nová stretnutia + + + Máte %1$d nové stretnutie + Máte %1$d nové stretnutia + Máte %1$d nových stretnutí + Máte %1$d nových stretnutí + + + Školské oznámenia + Žiadne školské oznámenia + + %d školské oznámenie + %d školské oznámenia + %d školských oznámení + %d školských oznámení + + + Nové školské oznámenie + Nové školské oznámenia + Nové školské oznámenia + Nové školské oznámenia + + + Máte %1$d nové školské oznámenie + Máte %1$d nové školské oznámenia + Máte %1$d nových školských oznámení + Máte %1$d nových školských oznámení + Pridať účet Odhlásiť @@ -382,6 +448,8 @@ Server Discord Pripojte sa ku komunite Wulkanového Facebooková fanpage + Twitter stránka + Sledujte nás na Twitteri Rovnako ako naše facebooková fanpage Zásady ochrany osobných údajov Pravidlá pre zhromažďovanie osobných údajov @@ -417,6 +485,7 @@ Muž Žena Priezvisko + Opatrovník Prezývka Pridať prezývku @@ -424,6 +493,80 @@ Zdieľať protokoly Obnoviť + + Lekcie + (Zajtra) + %1$s (%2$s) + Za chvíľu: + Čoskoro: + Prvá: + Teraz: + + za %1$d minútu + za %1$d minúty + za %1$d minút + za %1$d minút + + + ešte %1$d minútu + ešte %1$d minúty + ešte %1$d minút + ešte %1$d minút + + Koniec lekcií + Ďalej: + Neskôr: + + ešte %1$d ďalší lekcia + ešte %1$d ďalšie lekcie + ešte %1$d ďalších lekcií + ešte %1$d ďalších lekcií + + do %1$s + Žiadne nadchádzajúce lekcie + Pri načítaní lekcií došlo k chybe + Domáce úlohy + Žiadne domáce úlohy do vykonaná + Pri načítaní domácich úloh došlo k chybe + + Ešte %1$d ďalšia domáca úloha + Ešte %1$d ďalšie domáce úlohy + Ešte %1$d ďalších domácich úloh + Ešte %1$d ďalších domácich úloh + + do %1$s + Posledné známky + Žiadne nové známky + Pri načítaní známok došlo k chybe + Školské oznámenia + Žiadne aktuálne oznámenia + Pri načítaní oznámení došlo k chybe + + Ešte %1$d ďalšie oznámenie + Ešte %1$d ďalšie oznámenia + Ešte %1$d ďalších oznámení + Ešte %1$d ďalších oznámení + + Skúšky + Žiadne nadchádzajúce skúšky + Pri načítaní skúšok došlo k chybe + + Ešte %1$d ďalšia skúška + Ešte %1$d ďalšie skúšky + Ešte %1$d ďalších skúšok + Ešte %1$d ďalších skúšok + + Stretnutie + Žiadna nadchádzajúce stretnutie + Pri načítaní stretnutí došlo k chybe + + Ešte %1$d ďalšie stretnutie + Ešte %1$d ďalšie stretnutia + Ešte %1$d ďalších stretnutí + Ešte %1$d ďalších stretnutí + + Pri načítaní dát došlo k chybe + Žiadne Skontrolovať aktualizácie Pred hlásením chyby skontrolujte, či je k dispozícii aktualizácia s opravou chýb @@ -487,6 +630,7 @@ Synchronizovano! Synchronizácia zlyhala Prebieha synchronizácia + Posledná úplná synchronizácia: %s Hodnota plusu Hodnota mínusu Odpovedať s históriou správ @@ -496,6 +640,8 @@ Upozornenia Synchronizácia Známky + Domov + Viditeľnosť dlaždíc Dochádzka Plán lekcie Známky @@ -512,23 +658,26 @@ Nové známky Nové domáce úlohy + Nová stretnutia Nové skúšky Šťastné číslo Nové správy Nové poznámky + Nové školské oznámenia Push upozornenia Nadchádzajúce lekcie Ladenie - Čierny - Červený - Modrý - Zelený - Fialový + Čierna + Červená + Modrá + Zelená + Fialová Žiadna farba Skopírované Vrátiť + Zmeniť Sťahovanie aktualizácií začalo… Aktualizácia bola stiahnutá. diff --git a/app/src/main/res/values-uk/preferences_values.xml b/app/src/main/res/values-uk/preferences_values.xml index 5c70bd53a..f6f5b984f 100644 --- a/app/src/main/res/values-uk/preferences_values.xml +++ b/app/src/main/res/values-uk/preferences_values.xml @@ -45,4 +45,15 @@ Середнє оцінювання за обидва семестри Середнє оцінювання за весь рік + + Щасливий номер + Непрочитані повідомлення + Відвідуваність + Уроки + Оцінки + Домашні завдання + Оголошення школи + Тести + Конференції + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 8bc1b8e31..4850e2396 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -11,6 +11,8 @@ Інше Про додаток Переглядач журналів + Відладка + Налагодження сповіщень Розробники Ліцензії Повідомлення @@ -21,6 +23,7 @@ Виберіть обліковий запис Деталі облікового запису Інформація про учня + Дошка %1$d семестр, %2$d/%3$d @@ -88,6 +91,7 @@ Очікувана оцінка Розрахована середня оцінка Підсумкова середня оцінка + з %1$d із %2$d тем Підсумок Клас Позначити як прочитане @@ -191,16 +195,22 @@ Тип Дата запису - New exam - New exams - New exams - New exams + Новий іспит + Новий іспит + Новий іспит + Нові іспити + + + Ви отримали %d новий іспит + Ви отримали %d новий іспит + Ви отримали %d новий іспит + Ви отримали %d нових іспитів - %d exam - %d exams - %d exams - %d exams + %d екзамен + %d екзамен + %d екзамен + %d іспити Отримані @@ -225,6 +235,15 @@ Такого повідомлення не існує Необхідно обрати принаймні 1 адресата Зміст повідомлення мусить складатися принаймні з 3 знаків + Лише непрочитані + Тільки з вкладеннями + Читання: %s + + Прочитані: %1$d з %2$d осіб + Прочитані: %1$d з %2$d осіб + Прочитані: %1$d з %2$d осіб + Прочитані: %1$d з %2$d осіб + %d повідомлення %d повідомлення @@ -237,6 +256,8 @@ Нові повідомлення Нові повідомлення + Ви хочете відновити повідомлення в чернетці? + Ви хочете відновити чернетку повідомлення з отримувачами: %s? Ви отримали %1$d нове повідомлення Ви отримали %1$d нових повідомлення @@ -308,23 +329,29 @@ Позначити як не зроблене Додатки - New homework - New homework - New homework - New homework + Нова домашня робота + Нова домашня робота + Нова домашня робота + Нова домашня робота + + + Ви отримали %d нове домашнє завдання + Ви отримали %d нове домашнє завдання + Ви отримали %d нове домашнє завдання + Ви отримали %d нове домашнє завдання - %d homework - %d homework - %d homework - %d homework + %d домашнє завдання + %d домашнє завдання + %d домашнє завдання + %d домашнє завдання Щасливий номер Сьогоднішній щасливий номер Брак інформації о щасливому номері Сьогоднішній щасливий номер - Сьогоднішнім щасливим номером є %s + Сьогоднішній щасливий номер: %s Показати історію Історія щасливих чисел @@ -357,6 +384,45 @@ Зустрічі Немає інформації про зустрічі + + %d конференція + %d конференція + %d конференція + %d конференцій + + + Нова конференція + Нова конференція + Нова конференція + Нові конференції + + + У вас є %1$d нова конференція + У вас є %1$d нова конференція + У вас є %1$d нова конференція + У вас є %1$d нових конференцій + + + Оголошення школи + Жодних навчальних оголошень + + %d оголошення про школу + %d оголошення про школу + %d оголошення про школу + Оголошення нової школи + + + Оголошення нової школи + Оголошення нової школи + Нові шкільні оголошення + Нові шкільні оголошення + + + У вас є %1$d нове оголошення про школу + У вас є %1$d нове оголошення про школу + У вас є %1$d нове оголошення про школу + У вас є %1$d нових оголошень про школи + Додати аккаунт Вийти @@ -382,11 +448,13 @@ Сервер Discord Приєднатися до спільноти додатка Фен-сторінка Facebook + Сторінка Twitter + Стежте за нами у Твіттері Вподобати нашу фансторінку у Facebook Політика конфіденційності Правила зберігання особистих даних - System settings - Open system settings + Налаштування системи + Відкрити налаштування системи Домашня сторінка Допомогти розвитку додатка Ліцензії @@ -417,6 +485,7 @@ Чоловіча Жінка Прізвище + Охоронець Псевдонім Додати псевдонім @@ -424,6 +493,80 @@ Поділитися логами Оновити + + Уроки + (Завтра) + %1$s (%2$s) + Через мить: + Незабаром: + Перше: + Зараз: + + через %1$d хвилину + через %1$d хвилину + через %1$d хвилину + через %1$d хвилин + + + %1$d більше хвилини + %1$d більше хвилини + %1$d більше хвилини + %1$d ще хвилин + + Кінець уроків + Далі: + Пізніше : + + %1$d більше уроку + %1$d більше уроку + %1$d більше уроку + %1$d більше уроків + + до %1$s + Немає майбутніх уроків + Сталася помилка під час завантаження уроків + Домашня робота + Немає домашнього завдання для виконання + Помилка при завантаженні домашньої роботи + + Ще %1$d домашнє завдання + Ще %1$d домашнє завдання + Ще %1$d домашнє завдання + Ще %1$d домашнє завдання + + до %1$s + Останні оцінки + Немає нових оцінок + Помилка при завантаженні класів + Оголошення школи + Немає поточних оголошень + Помилка при завантаженні анонсів + + %1$d нових оголошень + %1$d нових оголошень + %1$d нових оголошень + %1$d нових оголошень + + Іспити + Немає майбутніх іспитів + Помилка при завантаженні іспитів + + %1$d ще екзамен + %1$d ще екзамен + %1$d ще екзамен + %1$d ще іспитів + + Конференції + Немає майбутніх конференцій + Помилка при завантаженні конференцій + + Ще %1$d конференція + Ще %1$d конференція + Ще %1$d конференція + %1$d більше конференцій + + Помилка при завантаженні даних + Нічого Провірити наявність оновлень Перед тим, як повідомлювати о помілці, перевірте наявність оновлень @@ -472,7 +615,7 @@ Повідомлення Показувати повідомлення Показувати повідомлення о наступних уроках - Open system notification settings + Відкрити налаштування сповіщень системи Виправити помилки з синхронізацією і повідомленнями На вашому пристрої можуть бути помилки з синхронізацією і повідомленнями\n\nЩоб виправити іх, вам необхідно додати Wulkanowy в авто-старт и вимкнути оптимізацію/экономію батареї в налаштуваннях пристрою. Перейти до налаштувань @@ -487,15 +630,18 @@ Синхронізовано! Синхронізація не вдалася Триває синхронізація + Остання синхронізація: %s Вартість плюсу Вага мінуса Відповісти з історією повідомлень - Show arithmetic average when no weights provided + Показувати в середньому арифметику, якщо немає ваги Додатково Вигляд & Поведінка Повідомлення Синхронізація Оцінки + Дошка + Видимість плиток Відвідуваність Розклад Класи @@ -511,11 +657,13 @@ Версія програми, учасники, соціальні портали, ліцензії Нові оцінки - New homework - New exams + Нова домашня робота + Нові конференції + Нові іспити Щасливий номер Нові повідомлення Нові нотатки + Оголошення нової школи Показувати push-повідомлення Наступні уроки Дебаг @@ -529,6 +677,7 @@ Скопійовано Відмінити + Змінити Завантаження оновлень розпочато… Щойно завантажено оновлення. From 170b7c43798606aa1ef12cc0e7013788286bfe7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sun, 29 Aug 2021 21:06:33 +0200 Subject: [PATCH 159/869] New Crowdin updates (#1459) --- .../main/res/values-de/preferences_values.xml | 18 +- app/src/main/res/values-de/strings.xml | 82 ++++----- app/src/main/res/values-ru/strings.xml | 160 +++++++++--------- app/src/main/res/values-sk/strings.xml | 16 +- 4 files changed, 138 insertions(+), 138 deletions(-) diff --git a/app/src/main/res/values-de/preferences_values.xml b/app/src/main/res/values-de/preferences_values.xml index 53faaf9b0..1e0df8dea 100644 --- a/app/src/main/res/values-de/preferences_values.xml +++ b/app/src/main/res/values-de/preferences_values.xml @@ -46,14 +46,14 @@ Durchschnitt der Noten aus dem ganzen Jahr - Lucky number - Unread messages - Attendance - Lessons - Grades - Homework - School announcements - Exams - Conferences + Glückszahl + Ungelesene Nachrichten + Schulbesuch + Lektionen + Noten + Hausaufgaben + Schulankündigungen + Prüfungen + Sitzungen diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4bbf7767b..29d5c7636 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -185,8 +185,8 @@ Neue prüfungen - You received %d new exam - Du hast %d neue Prüfungen bekommen + Du hast %d neue Prüfung erhalten + Sie haben %d neue Prüfungen erhalten %d prüfung @@ -219,8 +219,8 @@ Nur mit Anhängen Lesen: %s - Read by: %1$d of %2$d people - Read by: %1$d of %2$d people + Lesen von: %1$d von %2$d Personen + Lesen von: %1$d von %2$d Personen %d nachricht @@ -230,8 +230,8 @@ Neu nachricht Neue nachrichten - Do you want to restore draft message? - Do you want to restore draft message with recipients: %s? + Möchten Sie den Entwurf der Nachricht wiederherstellen? + Möchten Sie die Entwurfsnachricht mit den Empfängern wiederherstellen: %s? Du hast %1$d nachricht bekommen Du hast %1$d nachrichten bekommen @@ -287,8 +287,8 @@ Neue hausaufgaben - You received %d new homework - Du hast %d neue Hausaufgaben bekommen + Du hast %d neue Hausaufgaben erhalten + Du hast %d neue Hausaufgaben erhalten %d hausaufgaben @@ -450,45 +450,45 @@ Später: Noch %1$d Lektion - %1$d more lessons + Noch %1$d Lektionen - until %1$s - No upcoming lessons - An error occurred while loading the lessons - Homework - No homework to do - An error occurred while loading the homework + bis %1$s + Keine anstehenden Lektionen + Fehler beim Laden des Lektionen + Hausaufgaben + Keine Hausaufgaben zu tun + Fehler beim Laden der Hausaufgaben - %1$d more homework - %1$d more homework + Noch %1$d Hausaufgabe + Noch %1$d Hausaufgaben - due %1$s - Last grades - No new grades - An error occurred while loading the grades - School announcements - No current announcements - An error occurred while loading the announcements + bis %1$s + Letzte Noten + Keine neuen Noten + Ein Fehler ist beim Laden der Noten + Schulankündigungen + Keine aktuellen Ankündigungen + Fehler beim Laden der Ankündigungen - %1$d more announcement - %1$d more announcements + Noch %1$d Schulankündigung + Noch %1$d Schulankündigungen - Exams - No upcoming exams - An error occurred while loading the exams + Prüfungen + Keine bevorstehenden Prüfungen + Fehler beim Laden der Prüfungen - %1$d more exam - %1$d more exams + Noch %1$d Prüfung + Noch %1$d Prüfungen - Conferences - No upcoming conferences - An error occurred while loading the conferences + Sitzungen + Keine bevorstehenden Ankündigungen + Beim Laden der Konferenzen trat ein Fehler auf - %1$d more conference - %1$d more conferences + %1$d weitere Konferenz + %1$d weitere Konferenzen - An error occurred while loading data - None + Fehler beim Laden der Daten + Keine Auf Updates prüfen Bevor Sie einen Fehler melden, prüfen Sie zuerst, ob ein Update mit der Fehlerbehebung verfügbar ist @@ -552,7 +552,7 @@ Synchronisiert! Synchronisierung fehlgeschlagen Synchronisierung läuft - Last full sync: %s + Letzte vollständige Synchronisierung: %s Wert des Plus Wert des Minus Antwort mit Nachrichtenhistorie @@ -563,7 +563,7 @@ Synchronisierung Noten Dashboard - Tiles visibility + Sichtbarkeit der Kacheln Schulbesuch Zeitplan Noten @@ -599,7 +599,7 @@ Kopiert lösen - Change + Ändern Download der Updates wurde gestartet… Ein Update wurde gerade heruntergeladen. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index e9349313b..db78f5952 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -413,15 +413,15 @@ Объявление о новой школе - New school announcements - New school announcements - New school announcements + Объявление о новой школе + Объявление о новой школе + Объявления о новых школах - You have %1$d new school announcement - You have %1$d new school announcements - You have %1$d new school announcements - You have %1$d new school announcements + У вас %1$d объявление о новой школе + У вас %1$d объявление о новой школе + У тебя %1$d новых школьных объявлений + У тебя %1$d новых школьных объявлений Добавить аккаунт @@ -448,13 +448,13 @@ Сервер Discord Присоединиться к сообществу приложения Facebook фан-страница - Twitter page - Follow us on twitter + Странница в твиттере + Подпишись на нас в твиттере Поставьте лайк на нашей странице в Facebook Политика приватности Правила хранения личных данных - System settings - Open system settings + Параметры системы + Открыть системные настройки Домашняя страница Помочь в развитии приложения Лицензии @@ -485,7 +485,7 @@ Муж Женская Фамилия - Guardian + Опекун Ник Добавить ник @@ -494,79 +494,79 @@ Поделиться логами Обновить - Lessons - (Tomorrow) + Уроки + (Завтра) %1$s (%2$s) - In a moment: - Soon: - First: - Now: + Сейчас: + Скоро: + Первый: + Сейчас: - in %1$d minute - in %1$d minutes - in %1$d minutes - in %1$d minutes + через %1$d минуту + через %1$d минуту + через %1$d минуту + через %1$d минут - %1$d more minute - %1$d more minutes - %1$d more minutes - %1$d more minutes + Еще %1$d минута + Еще %1$d минута + Еще %1$d минута + Ещё %1$d минут - End of lessons - Next: - Later: + Окончание уроков + Далее: + Позднее: - %1$d more lesson - %1$d more lessons - %1$d more lessons - %1$d more lessons + Еще %1$d урок + Еще %1$d урок + Еще %1$d урок + Ещё %1$d уроков - until %1$s - No upcoming lessons - An error occurred while loading the lessons - Homework - No homework to do - An error occurred while loading the homework + до %1$s + Нет предстоящих занятий + Произошла ошибка при загрузке уроков + Домашняя работа + Нет домашних заданий + Произошла ошибка при загрузке домашнего задания - %1$d more homework - %1$d more homework - %1$d more homework - %1$d more homework + Ещё %1$d домашних заданий + Ещё %1$d домашних заданий + Ещё %1$d домашних заданий + Ещё %1$d домашних заданий - due %1$s - Last grades - No new grades - An error occurred while loading the grades - School announcements - No current announcements - An error occurred while loading the announcements + срок до %1$s + Последние оценки + Нет новых оценок + Произошла ошибка при загрузке оценок + Объявления школ + Нет текущих объявлений + Произошла ошибка при загрузке объявлений - %1$d more announcement - %1$d more announcements - %1$d more announcements - %1$d more announcements + Ещё %1$d объявление + Ещё %1$d объявление + Ещё %1$d объявление + Ещё %1$d объявлений - Exams - No upcoming exams - An error occurred while loading the exams + Экзамены + Нет предстоящих экзаменов + Произошла ошибка при загрузке экзамена - %1$d more exam - %1$d more exams - %1$d more exams - %1$d more exams + Еще %1$d экзамен + Еще %1$d экзамен + Еще %1$d экзамен + ещё %1$d экзаменов - Conferences - No upcoming conferences - An error occurred while loading the conferences + Конференции + Нет предстоящих конференций + Произошла ошибка при загрузке конференций - %1$d more conference - %1$d more conferences - %1$d more conferences - %1$d more conferences + Еще %1$d конференция + Еще %1$d конференция + Еще %1$d конференция + Еще %1$d конференций - An error occurred while loading data - None + Произошла ошибка при загрузке данных + Отсутствует Проверить наличие обновлений Прежде чем сообщать об ошибке, проверьте наличие обновлений @@ -615,7 +615,7 @@ Уведомления Показывать уведомления Показывать уведомления о будущих уроках - Open system notification settings + Открыть настройки уведомлений системы Исправить проблемы с синхронизацией и уведомлениями На вашем устройстве могут быть проблемы с синхронизацией данных и уведомлениями.\n\nЧтобы их исправить, вам необходимо добавить Wulkanowy в авто-старт и выключить оптимизацию/экономию батареи в настройках устройства. Перейти в настройски @@ -630,18 +630,18 @@ Синхронизировано! Синхронизация не удалась Идёт синхронизация - Last full sync: %s + Последняя полная синхронизация: %s Стоимость плюса Стоимость минуса Отвечать с историей сообщений - Show arithmetic average when no weights provided + Показывать среднее арифметическое при отсутствии весов Расширенные Внешний вид & Поведение Уведомления Синхронизация Оценки - Dashboard - Tiles visibility + Панель + Видимость плиток Посещаемость Расписание Оценки @@ -657,13 +657,13 @@ Версия приложения, участники, социальные порталы, лицензии Новые оценки - New homework - New conferences - New exams + Новая домашняя работа + Новые конференции + Новые экзамены Счастливый номер Новые сообщения Новые заметки - New school announcements + Объявления о новых школах Показывать push-уведомления Будущие уроки Дебаг @@ -677,7 +677,7 @@ Скопировано Отменить - Change + Изменить Загрузка обновлений началась… Только что было скачано обновление. diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 81a76cf35..72899b78b 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -201,10 +201,10 @@ Nové skúšky - You received %d new exam - You received %d new exams - You received %d new exams - You received %d new exams + Máte %d novú skúšku + Máte %d nové skúšky + Máte %d nových skúšok + Máte %d nových skúšok %d skúška @@ -335,10 +335,10 @@ Nové domáce úlohy - You received %d new homework - You received %d new homework - You received %d new homework - You received %d new homework + Máte %d novú domácu úlohu + Máte %d nové domáce úlohy + Máte %d nových domácich úloh + Máte %d nových domácich úloh %d domáci úloh From 72d8b4aa8480a4d7c32b1ccb225b7a2498dbe24c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sun, 29 Aug 2021 21:08:08 +0200 Subject: [PATCH 160/869] Version 1.2.0 --- app/build.gradle | 10 +++++----- app/src/main/play/release-notes/pl-PL/default.txt | 10 +++++++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c05aa0341..61e47534c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,8 +21,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 30 - versionCode 92 - versionName "1.1.6" + versionCode 93 + versionName "1.2.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true @@ -133,8 +133,8 @@ play { serviceAccountEmail = System.getenv("PLAY_SERVICE_ACCOUNT_EMAIL") ?: "jan@fakelog.cf" serviceAccountCredentials = file('key.p12') defaultToAppBundles = false - track = 'production' - updatePriority = 5 + track = 'beta' + updatePriority = 3 } huaweiPublish { @@ -157,7 +157,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:b991d0c" + implementation "io.github.wulkanowy:sdk:1.2.0" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index a6f3f44ef..42fd22292 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,6 +1,10 @@ -Wersja 1.1.6 +Wersja 1.2.0 -- naprawiliśmy błąd przy wysyłaniu wiadomości -- naprawiliśmy oznaczanie odwołanych lekcji jako zastępstwa +- dodaliśmy nowy ekran startowy 🎉 +- usprawniliśmy powiadomienia +- dodaliśmy wersje robocze, filtrowanie oraz informację o odczytaniu przez odbiorcę w wiadomościach +- dodaliśmy informacje o liczeniu średniej w podsumowaniu ocen +- dodaliśmy opcję generowania wiadomości z usprawiedliwieniem dni w szkołach pozbawionych funkcji usprawiedliwiania przez zakładkę frekwencja +- oraz wiele wiele innych ulepszeń i poprawek Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases From e557021ad9793954563a4cf3c844f3f8227db060 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 29 Aug 2021 19:37:01 +0000 Subject: [PATCH 161/869] Bump huawei-publish-gradle-plugin from 1.2.4 to 1.3.0 (#1458) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index afe0285c8..840d90f42 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ buildscript { classpath 'com.huawei.agconnect:agcp:1.6.0.300' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1' classpath "com.github.triplet.gradle:play-publisher:2.8.0" - classpath "ru.cian:huawei-publish-gradle-plugin:1.2.4" + classpath "ru.cian:huawei-publish-gradle-plugin:1.3.0" classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3" classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0" classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries" From d139c22782660abb9452d1a6f6b7d51a4ba797d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 29 Aug 2021 19:37:18 +0000 Subject: [PATCH 162/869] Bump hianalytics from 6.1.1.300 to 6.2.0.300 (#1457) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 61e47534c..7b2b2e695 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -218,7 +218,7 @@ dependencies { playImplementation 'com.google.android.play:core:1.10.0' playImplementation 'com.google.android.play:core-ktx:1.8.1' - hmsImplementation 'com.huawei.hms:hianalytics:6.1.1.300' + hmsImplementation 'com.huawei.hms:hianalytics:6.2.0.300' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.0.300' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From d87283eb3151780fb85adc6f3458ec4d12f12b7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Mon, 30 Aug 2021 00:20:13 +0200 Subject: [PATCH 163/869] Fix opening twitter link from about on android 11 (#1460) --- .../java/io/github/wulkanowy/utils/ContextExtension.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt index cb31389e0..68d3afe83 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.utils import android.annotation.SuppressLint +import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.graphics.Bitmap @@ -58,8 +59,11 @@ fun Context.getCompatBitmap(@DrawableRes drawableRes: Int, @ColorRes colorRes: I fun Context.openInternetBrowser(uri: String, onActivityNotFound: (uri: String) -> Unit = {}) { Intent.parseUri(uri, 0).let { - if (it.resolveActivity(packageManager) != null) startActivity(it) - else onActivityNotFound(uri) + try { + startActivity(it) + } catch (e: ActivityNotFoundException) { + onActivityNotFound(uri) + } } } From c3adb9b6d6f6b972656262f63a52efe1e9db80e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Fri, 3 Sep 2021 22:54:29 +0200 Subject: [PATCH 164/869] Bump agp to 7.0.2 (#1469) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 840d90f42..7fb02eb9d 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ buildscript { } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.android.tools.build:gradle:7.0.1' + classpath 'com.android.tools.build:gradle:7.0.2' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.10' classpath 'com.huawei.agconnect:agcp:1.6.0.300' From 8d7b611c4483326ee7e2cb999f811b80e0f3ba83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sat, 4 Sep 2021 15:54:05 +0200 Subject: [PATCH 165/869] Fix showing error view in timetable (#1472) --- app/build.gradle | 2 +- .../github/wulkanowy/ui/modules/timetable/TimetableFragment.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 7b2b2e695..4513be16d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -157,7 +157,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:1.2.0" + implementation "io.github.wulkanowy:sdk:7c399ffaea" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt index a65d69211..a79661901 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt @@ -49,7 +49,7 @@ class TimetableFragment : BaseFragment(R.layout.fragme override val titleStringId get() = R.string.timetable_title - override val isViewEmpty get() = timetableAdapter.itemCount > 0 + override val isViewEmpty get() = timetableAdapter.itemCount == 0 override val currentStackSize get() = (activity as? MainActivity)?.currentStackSize From 45d1727dbeccaa66f08bd1822e665737c9d4cc25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sat, 4 Sep 2021 15:54:37 +0200 Subject: [PATCH 166/869] Add missing school announcement dialog (#1470) --- .../SchoolAnnouncementAdapter.kt | 10 +- .../SchoolAnnouncementDialog.kt | 54 +++++++ .../SchoolAnnouncementFragment.kt | 9 +- .../SchoolAnnouncementPresenter.kt | 5 + .../SchoolAnnouncementView.kt | 2 + .../ui/modules/timetable/TimetableFragment.kt | 6 +- .../res/layout/dialog_school_announcement.xml | 148 ++++++++++++++++++ 7 files changed, 225 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementDialog.kt create mode 100644 app/src/main/res/layout/dialog_school_announcement.xml diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementAdapter.kt index e7c202676..62f6251ec 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementAdapter.kt @@ -2,7 +2,7 @@ package io.github.wulkanowy.ui.modules.schoolannouncement import android.view.LayoutInflater import android.view.ViewGroup -import androidx.core.text.HtmlCompat +import androidx.core.text.parseAsHtml import androidx.recyclerview.widget.RecyclerView import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.databinding.ItemSchoolAnnouncementBinding @@ -14,6 +14,8 @@ class SchoolAnnouncementAdapter @Inject constructor() : var items = emptyList() + var onItemClickListener: (SchoolAnnouncement) -> Unit = {} + override fun getItemCount() = items.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( @@ -26,9 +28,9 @@ class SchoolAnnouncementAdapter @Inject constructor() : with(holder.binding) { schoolAnnouncementItemDate.text = item.date.toFormattedString() schoolAnnouncementItemType.text = item.subject - schoolAnnouncementItemContent.text = HtmlCompat.fromHtml( - item.content, HtmlCompat.FROM_HTML_MODE_COMPACT - ) + schoolAnnouncementItemContent.text = item.content.parseAsHtml() + + root.setOnClickListener { onItemClickListener(item) } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementDialog.kt new file mode 100644 index 000000000..ed4b0ac98 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementDialog.kt @@ -0,0 +1,54 @@ +package io.github.wulkanowy.ui.modules.schoolannouncement + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.text.parseAsHtml +import androidx.fragment.app.DialogFragment +import io.github.wulkanowy.data.db.entities.SchoolAnnouncement +import io.github.wulkanowy.databinding.DialogSchoolAnnouncementBinding +import io.github.wulkanowy.utils.lifecycleAwareVariable +import io.github.wulkanowy.utils.toFormattedString + +class SchoolAnnouncementDialog : DialogFragment() { + + private var binding: DialogSchoolAnnouncementBinding by lifecycleAwareVariable() + + private lateinit var announcement: SchoolAnnouncement + + companion object { + + private const val ARGUMENT_KEY = "item" + + fun newInstance(exam: SchoolAnnouncement) = SchoolAnnouncementDialog().apply { + arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NO_TITLE, 0) + arguments?.run { + announcement = getSerializable(ARGUMENT_KEY) as SchoolAnnouncement + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = DialogSchoolAnnouncementBinding.inflate(inflater).apply { binding = this }.root + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + with(binding) { + announcementDialogSubjectValue.text = announcement.subject + announcementDialogDateValue.text = announcement.date.toFormattedString() + announcementDialogDescriptionValue.text = announcement.content.parseAsHtml() + + announcementDialogClose.setOnClickListener { dismiss() } + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementFragment.kt index a5a53aefc..baf2824ba 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementFragment.kt @@ -8,6 +8,7 @@ import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.databinding.FragmentSchoolAnnouncementBinding import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.widgets.DividerItemDecoration import io.github.wulkanowy.utils.getThemeAttrColor @@ -43,7 +44,9 @@ class SchoolAnnouncementFragment : override fun initView() { with(binding.directorInformationRecycler) { layoutManager = LinearLayoutManager(context) - adapter = schoolAnnouncementAdapter + adapter = schoolAnnouncementAdapter.apply { + onItemClickListener = presenter::onItemClickListener + } addItemDecoration(DividerItemDecoration(context)) } with(binding) { @@ -99,6 +102,10 @@ class SchoolAnnouncementFragment : binding.directorInformationSwipe.isRefreshing = show } + override fun openSchoolAnnouncementDialog(item: SchoolAnnouncement) { + (activity as? MainActivity)?.showDialogFragment(SchoolAnnouncementDialog.newInstance(item)) + } + override fun onDestroyView() { presenter.onDetachView() super.onDestroyView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementPresenter.kt index ba89ab1f4..88ad81465 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementPresenter.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.ui.modules.schoolannouncement import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.data.repositories.SchoolAnnouncementRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter @@ -46,6 +47,10 @@ class SchoolAnnouncementPresenter @Inject constructor( view?.showErrorDetailsDialog(lastError) } + fun onItemClickListener(item: SchoolAnnouncement) { + view?.openSchoolAnnouncementDialog(item) + } + private fun loadData(forceRefresh: Boolean = false) { Timber.i("Loading School announcement data started") diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementView.kt index 0553df1e7..383d0f294 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementView.kt @@ -19,6 +19,8 @@ interface SchoolAnnouncementView : BaseView { fun setErrorDetails(message: String) + fun openSchoolAnnouncementDialog(item: SchoolAnnouncement) + fun showProgress(show: Boolean) fun enableSwipe(enable: Boolean) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt index a79661901..1e1084a8d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt @@ -7,7 +7,7 @@ import android.view.MenuItem import android.view.View import android.view.View.GONE import android.view.View.VISIBLE -import androidx.core.text.HtmlCompat +import androidx.core.text.parseAsHtml import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.datepicker.CalendarConstraints import com.google.android.material.datepicker.MaterialDatePicker @@ -147,9 +147,7 @@ class TimetableFragment : BaseFragment(R.layout.fragme override fun setDayHeaderMessage(message: String?) { binding.timetableEmptyMessage.visibility = if (message.isNullOrEmpty()) GONE else VISIBLE - binding.timetableEmptyMessage.text = HtmlCompat.fromHtml( - message.orEmpty(), HtmlCompat.FROM_HTML_MODE_COMPACT - ) + binding.timetableEmptyMessage.text = message.orEmpty().parseAsHtml() } override fun showErrorView(show: Boolean) { diff --git a/app/src/main/res/layout/dialog_school_announcement.xml b/app/src/main/res/layout/dialog_school_announcement.xml new file mode 100644 index 000000000..cbce4e025 --- /dev/null +++ b/app/src/main/res/layout/dialog_school_announcement.xml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + From 3b9451184c60ff9feb1bad8b036f08b6603c85b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sun, 5 Sep 2021 03:15:40 +0200 Subject: [PATCH 167/869] Fix preview of second student guardian when first guardian is null (#1473) --- .../modules/studentinfo/StudentInfoAdapter.kt | 4 +- .../studentinfo/StudentInfoFragment.kt | 69 ++++++++----------- .../ui/modules/studentinfo/StudentInfoItem.kt | 3 +- .../studentinfo/StudentInfoPresenter.kt | 25 ++++--- .../ui/modules/studentinfo/StudentInfoView.kt | 4 +- 5 files changed, 48 insertions(+), 57 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoAdapter.kt index 2d8387491..60912200c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoAdapter.kt @@ -13,7 +13,7 @@ class StudentInfoAdapter @Inject constructor() : var items = listOf() - var onItemClickListener: (position: Int) -> Unit = {} + var onItemClickListener: (StudentInfoView.Type?) -> Unit = {} var onItemLongClickListener: (text: String) -> Unit = {} @@ -32,7 +32,7 @@ class StudentInfoAdapter @Inject constructor() : studentInfoItemArrow.visibility = if (item.showArrow) VISIBLE else GONE with(root) { - setOnClickListener { onItemClickListener(position) } + setOnClickListener { onItemClickListener(item.viewType) } setOnLongClickListener { onItemLongClickListener(studentInfoItemSubtitle.text.toString()) true diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoFragment.kt index e9ef41372..361a59440 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoFragment.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.studentinfo -import android.annotation.SuppressLint import android.content.ClipData import android.content.ClipboardManager import android.os.Bundle @@ -130,9 +129,9 @@ class StudentInfoFragment : getString(R.string.student_info_parents_name) to studentInfo.parentsNames ).map { StudentInfoItem( - it.first, - it.second.ifBlank { getString(R.string.all_no_data) }, - false, + title = it.first, + subtitle = it.second.ifBlank { getString(R.string.all_no_data) }, + showArrow = false, ) } ) @@ -146,25 +145,33 @@ class StudentInfoFragment : getString(R.string.student_info_email) to studentInfo.email ).map { StudentInfoItem( - it.first, - it.second.ifBlank { getString(R.string.all_no_data) }, - false, + title = it.first, + subtitle = it.second.ifBlank { getString(R.string.all_no_data) }, + showArrow = false, ) } ) } - @SuppressLint("DefaultLocale") + @OptIn(ExperimentalStdlibApi::class) override fun showFamilyTypeData(studentInfo: StudentInfo) { + val items = buildList { + add(studentInfo.firstGuardian?.let { + Triple(it.kinship.capitalise(), it.fullName, StudentInfoView.Type.FIRST_GUARDIAN) + }) + + add(studentInfo.secondGuardian?.let { + Triple(it.kinship.capitalise(), it.fullName, StudentInfoView.Type.SECOND_GUARDIAN) + }) + }.filterNotNull() + updateData( - listOfNotNull( - studentInfo.firstGuardian?.let { it.kinship.capitalise() to it.fullName }, - studentInfo.secondGuardian?.let { it.kinship.capitalise() to it.fullName }, - ).map { (title, value) -> + items.map { (title, value, type) -> StudentInfoItem( - title.ifBlank { getString(R.string.all_no_data) }, - value.ifBlank { getString(R.string.all_no_data) }, - true, + title = title.ifBlank { getString(R.string.all_no_data) }, + subtitle = value.ifBlank { getString(R.string.all_no_data) }, + showArrow = true, + viewType = type, ) } ) @@ -178,15 +185,15 @@ class StudentInfoFragment : getString(R.string.student_info_correspondence_address) to studentInfo.correspondenceAddress ).map { StudentInfoItem( - it.first, - it.second.ifBlank { getString(R.string.all_no_data) }, - false, + title = it.first, + subtitle = it.second.ifBlank { getString(R.string.all_no_data) }, + showArrow = false, ) } ) } - override fun showFirstGuardianTypeData(studentGuardian: StudentGuardian) { + override fun showGuardianTypeData(studentGuardian: StudentGuardian) { updateData( listOf( getString(R.string.student_info_full_name) to studentGuardian.fullName, @@ -196,27 +203,9 @@ class StudentInfoFragment : getString(R.string.student_info_email) to studentGuardian.email ).map { StudentInfoItem( - it.first, - it.second.ifBlank { getString(R.string.all_no_data) }, - false, - ) - } - ) - } - - override fun showSecondGuardianTypeData(studentGuardian: StudentGuardian) { - updateData( - listOf( - getString(R.string.student_info_full_name) to studentGuardian.fullName, - getString(R.string.student_info_kinship) to studentGuardian.kinship, - getString(R.string.student_info_guardian_address) to studentGuardian.address, - getString(R.string.student_info_phones) to studentGuardian.phones, - getString(R.string.student_info_email) to studentGuardian.email - ).map { - StudentInfoItem( - it.first, - it.second.ifBlank { getString(R.string.all_no_data) }, - false, + title = it.first, + subtitle = it.second.ifBlank { getString(R.string.all_no_data) }, + showArrow = false, ) } ) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoItem.kt index bb149b2b1..21226539b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoItem.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoItem.kt @@ -3,5 +3,6 @@ package io.github.wulkanowy.ui.modules.studentinfo data class StudentInfoItem( val title: String, val subtitle: String, - val showArrow: Boolean + val showArrow: Boolean, + val viewType: StudentInfoView.Type? = null, ) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoPresenter.kt index 55ac66d0b..80798b11e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoPresenter.kt @@ -58,13 +58,12 @@ class StudentInfoPresenter @Inject constructor( view?.showErrorDetailsDialog(lastError) } - fun onItemSelected(position: Int) { - if (infoType != StudentInfoView.Type.FAMILY) return + fun onItemSelected(viewType: StudentInfoView.Type?) { + viewType ?: return view?.openStudentInfoView( - if (position == 0) StudentInfoView.Type.FIRST_GUARDIAN - else StudentInfoView.Type.SECOND_GUARDIAN, - studentWithSemesters + studentWithSemesters = studentWithSemesters, + infoType = viewType, ) } @@ -76,15 +75,19 @@ class StudentInfoPresenter @Inject constructor( flowWithResourceIn { val semester = studentWithSemesters.semesters.getCurrentOrLast() studentInfoRepository.getStudentInfo( - studentWithSemesters.student, - semester, - forceRefresh + student = studentWithSemesters.student, + semester = semester, + forceRefresh = forceRefresh ) }.onEach { when (it.status) { Status.LOADING -> Timber.i("Loading student info $infoType started") Status.SUCCESS -> { - if (it.data != null && !(infoType == StudentInfoView.Type.FAMILY && it.data.firstGuardian == null && it.data.secondGuardian == null)) { + val isFamily = infoType == StudentInfoView.Type.FAMILY + val isFirstGuardianEmpty = it.data?.firstGuardian == null + val isSecondGuardianEmpty = it.data?.secondGuardian == null + + if (it.data != null && !(isFamily && isFirstGuardianEmpty && isSecondGuardianEmpty)) { Timber.i("Loading student info $infoType result: Success") showCorrectData(it.data) view?.run { @@ -122,8 +125,8 @@ class StudentInfoPresenter @Inject constructor( StudentInfoView.Type.CONTACT -> view?.showContactTypeData(studentInfo) StudentInfoView.Type.ADDRESS -> view?.showAddressTypeData(studentInfo) StudentInfoView.Type.FAMILY -> view?.showFamilyTypeData(studentInfo) - StudentInfoView.Type.SECOND_GUARDIAN -> view?.showSecondGuardianTypeData(studentInfo.secondGuardian!!) - StudentInfoView.Type.FIRST_GUARDIAN -> view?.showFirstGuardianTypeData(studentInfo.firstGuardian!!) + StudentInfoView.Type.SECOND_GUARDIAN -> view?.showGuardianTypeData(studentInfo.secondGuardian!!) + StudentInfoView.Type.FIRST_GUARDIAN -> view?.showGuardianTypeData(studentInfo.firstGuardian!!) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoView.kt index 87613e626..c20359df1 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/studentinfo/StudentInfoView.kt @@ -25,9 +25,7 @@ interface StudentInfoView : BaseView { fun showFamilyTypeData(studentInfo: StudentInfo) - fun showFirstGuardianTypeData(studentGuardian: StudentGuardian) - - fun showSecondGuardianTypeData(studentGuardian: StudentGuardian) + fun showGuardianTypeData(studentGuardian: StudentGuardian) fun openStudentInfoView(infoType: Type, studentWithSemesters: StudentWithSemesters) From e6c9abb4e5e7552a32a2c89f8088be709539d119 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 5 Sep 2021 18:09:42 +0000 Subject: [PATCH 168/869] Bump core from 1.10.0 to 1.10.1 (#1480) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 4513be16d..b175a274c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -215,7 +215,7 @@ dependencies { playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' - playImplementation 'com.google.android.play:core:1.10.0' + playImplementation 'com.google.android.play:core:1.10.1' playImplementation 'com.google.android.play:core-ktx:1.8.1' hmsImplementation 'com.huawei.hms:hianalytics:6.2.0.300' From a43acaaa07e516c637ec61a2e4ce62912a378020 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 5 Sep 2021 18:10:05 +0000 Subject: [PATCH 169/869] Bump kotlinx-coroutines-android from 1.5.1 to 1.5.2 (#1479) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index b175a274c..17685c164 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -161,7 +161,7 @@ dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2" implementation "androidx.core:core-ktx:1.6.0" implementation "androidx.activity:activity-ktx:1.3.1" From 0008a72be127eae827a89c14a37db40dea1727c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 5 Sep 2021 18:10:22 +0000 Subject: [PATCH 170/869] Bump kotlinx-coroutines-test from 1.5.1 to 1.5.2 (#1477) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 17685c164..57395372f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -228,7 +228,7 @@ dependencies { testImplementation "junit:junit:4.13.2" testImplementation "io.mockk:mockk:$mockk" - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2' testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" testImplementation 'org.robolectric:robolectric:4.6.1' From 44a9db48a67a8ac9632f12ebe90711a0b52c4983 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 5 Sep 2021 18:17:18 +0000 Subject: [PATCH 171/869] Bump hianalytics from 6.2.0.300 to 6.2.0.301 (#1476) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 57395372f..e1d1be79b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -218,7 +218,7 @@ dependencies { playImplementation 'com.google.android.play:core:1.10.1' playImplementation 'com.google.android.play:core-ktx:1.8.1' - hmsImplementation 'com.huawei.hms:hianalytics:6.2.0.300' + hmsImplementation 'com.huawei.hms:hianalytics:6.2.0.301' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.0.300' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 49ebae6e6381cae38240e523a4c2702ae57dd435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sun, 5 Sep 2021 21:59:03 +0200 Subject: [PATCH 172/869] Fix overlaping empty and error view in grade statistics (#1475) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mikołaj Pich --- .../res/layout/fragment_grade_statistics.xml | 170 ++++++++++-------- 1 file changed, 97 insertions(+), 73 deletions(-) diff --git a/app/src/main/res/layout/fragment_grade_statistics.xml b/app/src/main/res/layout/fragment_grade_statistics.xml index 3bd57b4b1..981ee48f3 100644 --- a/app/src/main/res/layout/fragment_grade_statistics.xml +++ b/app/src/main/res/layout/fragment_grade_statistics.xml @@ -36,7 +36,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - @@ -44,91 +44,115 @@ android:id="@+id/gradeStatisticsProgress" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="center" android:indeterminate="true" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" tools:visibility="gone" /> + android:layout_height="wrap_content" + app:layout_constraintTop_toTopOf="parent" + tools:listitem="@layout/item_grade_statistics_pie" + tools:visibility="visible" /> - + android:layout_height="0dp" + android:layout_marginTop="12dp" + android:fillViewport="true" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toBottomOf="@id/gradeStatisticsRecycler"> - - - - - - - - - - - - + android:layout_height="wrap_content"> - + android:gravity="center" + android:orientation="vertical" + android:visibility="invisible" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:ignore="UseCompoundDrawables" + tools:visibility="gone"> - + + + + + - - - + android:gravity="center" + android:orientation="vertical" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:ignore="UseCompoundDrawables" + tools:visibility="gone"> + + + + + + + + + + + + + + + From 2b55ec02ff5da46f8f3713f91032aa15ee827c98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sun, 5 Sep 2021 23:06:44 +0200 Subject: [PATCH 173/869] New translations strings.xml (Polish) (#1474) --- app/src/main/res/values-pl/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 74627ed1f..2a0f3cb77 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -53,7 +53,7 @@ Nie znaleziono ucznia. Sprawdź poprawność symbolu i wybranej odmiany dziennika UONET+ To pole jest wymagane Wybrany uczeń jest już zalogowany - Symbol znajdziesz na stronie dziennika w Uczeń → Dostęp Mobilny → Zarejestruj urządzenie mobilne.\n\nUpewnij się, że w polu Dziennik UONET+ na poprzednim ekranie została ustawiona odpowiednia odmiana dziennika. Wulkanowy na chwilę obecną nie wykrywa uczniów przedszkolnych + Symbol znajdziesz na stronie dziennika w Uczeń → Dostęp Mobilny → Zarejestruj urządzenie mobilne.\n\nUpewnij się, że w polu Dziennik UONET+ na poprzednim ekranie została ustawiona odpowiednia odmiana dziennika.\n\nWulkanowy na chwilę obecną nie wykrywa uczniów przedszkolnych (z zerówki) Wybierz uczniów do zalogowania w aplikacji Inne opcje W tym trybie nie działa szczęśliwy numerek, uczeń na tle klasy, podsumowanie frekwencji, usprawiedliwianie nieobecności, lekcje zrealizowane, informacje o szkole i podgląd listy zarejestrowanych urządzeń From 77c5330f91f1f026fb6ebd9118b99e69f196bc39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sun, 5 Sep 2021 23:24:03 +0200 Subject: [PATCH 174/869] Dashboard fixes (#1463) --- .../data/repositories/GradeRepository.kt | 29 +- .../data/repositories/SemesterRepository.kt | 30 +- .../github/wulkanowy/ui/base/BasePresenter.kt | 2 +- .../ui/modules/account/AccountFragment.kt | 8 +- .../ui/modules/account/AccountPresenter.kt | 2 +- .../ui/modules/account/AccountView.kt | 4 +- .../accountdetails/AccountDetailsFragment.kt | 6 +- .../accountdetails/AccountDetailsPresenter.kt | 5 +- .../ui/modules/dashboard/DashboardAdapter.kt | 74 +- .../ui/modules/dashboard/DashboardFragment.kt | 6 +- .../ui/modules/dashboard/DashboardItem.kt | 3 + .../modules/dashboard/DashboardPresenter.kt | 630 +++++++++--------- .../wulkanowy/utils/TimetableExtension.kt | 2 +- .../item_dashboard_horizontal_group.xml | 14 +- app/src/main/res/values-cs/strings.xml | 3 +- app/src/main/res/values-de/strings.xml | 3 +- app/src/main/res/values-pl/strings.xml | 3 +- app/src/main/res/values-ru/strings.xml | 3 +- app/src/main/res/values-sk/strings.xml | 3 +- app/src/main/res/values-uk/strings.xml | 3 +- app/src/main/res/values/strings.xml | 3 +- .../wulkanowy/utils/TimetableExtensionTest.kt | 17 +- 22 files changed, 433 insertions(+), 420 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt index 0b8a0e710..d8417f8a9 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt @@ -33,10 +33,16 @@ class GradeRepository @Inject constructor( private val cacheKey = "grade" - fun getGrades(student: Student, semester: Semester, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource( + fun getGrades( + student: Student, + semester: Semester, + forceRefresh: Boolean, + notify: Boolean = false + ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { (details, summaries) -> - val isShouldBeRefreshed = refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) + val isShouldBeRefreshed = + refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) details.isEmpty() || summaries.isEmpty() || forceRefresh || isShouldBeRefreshed }, query = { @@ -59,8 +65,14 @@ class GradeRepository @Inject constructor( } ) - private suspend fun refreshGradeDetails(student: Student, oldGrades: List, newDetails: List, notify: Boolean) { - val notifyBreakDate = oldGrades.maxByOrNull { it.date }?.date ?: student.registrationDate.toLocalDate() + private suspend fun refreshGradeDetails( + student: Student, + oldGrades: List, + newDetails: List, + notify: Boolean + ) { + val notifyBreakDate = + oldGrades.maxByOrNull { it.date }?.date ?: student.registrationDate.toLocalDate() gradeDb.deleteAll(oldGrades uniqueSubtract newDetails) gradeDb.insertAll((newDetails uniqueSubtract oldGrades).onEach { if (it.date >= notifyBreakDate) it.apply { @@ -70,10 +82,15 @@ class GradeRepository @Inject constructor( }) } - private suspend fun refreshGradeSummaries(oldSummaries: List, newSummary: List, notify: Boolean) { + private suspend fun refreshGradeSummaries( + oldSummaries: List, + newSummary: List, + notify: Boolean + ) { gradeSummaryDb.deleteAll(oldSummaries uniqueSubtract newSummary) gradeSummaryDb.insertAll((newSummary uniqueSubtract oldSummaries).onEach { summary -> - val oldSummary = oldSummaries.find { oldSummary -> oldSummary.subject == summary.subject } + val oldSummary = + oldSummaries.find { oldSummary -> oldSummary.subject == summary.subject } summary.isPredictedGradeNotified = when { summary.predictedGrade.isEmpty() -> true notify && oldSummary?.predictedGrade != summary.predictedGrade -> false diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt index 8942391c8..4336877a5 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt @@ -22,7 +22,11 @@ class SemesterRepository @Inject constructor( private val dispatchers: DispatchersProvider ) { - suspend fun getSemesters(student: Student, forceRefresh: Boolean = false, refreshOnNoCurrent: Boolean = false) = withContext(dispatchers.backgroundThread) { + suspend fun getSemesters( + student: Student, + forceRefresh: Boolean = false, + refreshOnNoCurrent: Boolean = false + ) = withContext(dispatchers.backgroundThread) { val semesters = semesterDb.loadAll(student.studentId, student.classId) if (isShouldFetch(student, semesters, forceRefresh, refreshOnNoCurrent)) { @@ -31,14 +35,21 @@ class SemesterRepository @Inject constructor( } else semesters } - private fun isShouldFetch(student: Student, semesters: List, forceRefresh: Boolean, refreshOnNoCurrent: Boolean): Boolean { + private fun isShouldFetch( + student: Student, + semesters: List, + forceRefresh: Boolean, + refreshOnNoCurrent: Boolean + ): Boolean { val isNoSemesters = semesters.isEmpty() - val isRefreshOnModeChangeRequired = if (Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { - semesters.firstOrNull { it.isCurrent }?.diaryId == 0 - } else false + val isRefreshOnModeChangeRequired = + if (Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { + semesters.firstOrNull { it.isCurrent }?.diaryId == 0 + } else false - val isRefreshOnNoCurrentAppropriate = refreshOnNoCurrent && !semesters.any { semester -> semester.isCurrent } + val isRefreshOnNoCurrentAppropriate = + refreshOnNoCurrent && !semesters.any { semester -> semester.isCurrent } return forceRefresh || isNoSemesters || isRefreshOnModeChangeRequired || isRefreshOnNoCurrentAppropriate } @@ -52,7 +63,8 @@ class SemesterRepository @Inject constructor( semesterDb.insertSemesters(new.uniqueSubtract(old)) } - suspend fun getCurrentSemester(student: Student, forceRefresh: Boolean = false) = withContext(dispatchers.backgroundThread) { - getSemesters(student, forceRefresh).getCurrentOrLast() - } + suspend fun getCurrentSemester(student: Student, forceRefresh: Boolean = false) = + withContext(dispatchers.backgroundThread) { + getSemesters(student, forceRefresh).getCurrentOrLast() + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt index be5300499..6f363bfc4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt @@ -18,7 +18,7 @@ open class BasePresenter( protected val studentRepository: StudentRepository ) : CoroutineScope { - private var job: Job = Job() + private var job = Job() private val jobs = mutableMapOf() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountFragment.kt index 7a8f8585f..051c93c95 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountFragment.kt @@ -8,7 +8,7 @@ import androidx.core.view.get import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R -import io.github.wulkanowy.data.db.entities.StudentWithSemesters +import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.databinding.FragmentAccountBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsFragment @@ -75,9 +75,7 @@ class AccountFragment : BaseFragment(R.layout.fragment_a } } - override fun openAccountDetailsView(studentWithSemesters: StudentWithSemesters) { - (activity as? MainActivity)?.pushView( - AccountDetailsFragment.newInstance(studentWithSemesters) - ) + override fun openAccountDetailsView(student: Student) { + (activity as? MainActivity)?.pushView(AccountDetailsFragment.newInstance(student)) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountPresenter.kt index 8d1651395..7fe77ca7a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountPresenter.kt @@ -28,7 +28,7 @@ class AccountPresenter @Inject constructor( } fun onItemSelected(studentWithSemesters: StudentWithSemesters) { - view?.openAccountDetailsView(studentWithSemesters) + view?.openAccountDetailsView(studentWithSemesters.student) } private fun loadData() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountView.kt index d7deefafd..56fcb0a35 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/AccountView.kt @@ -1,6 +1,6 @@ package io.github.wulkanowy.ui.modules.account -import io.github.wulkanowy.data.db.entities.StudentWithSemesters +import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.ui.base.BaseView interface AccountView : BaseView { @@ -11,5 +11,5 @@ interface AccountView : BaseView { fun openLoginView() - fun openAccountDetailsView(studentWithSemesters: StudentWithSemesters) + fun openAccountDetailsView(student: Student) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt index f1c7f7bd1..a9890ad9a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt @@ -37,9 +37,9 @@ class AccountDetailsFragment : private const val ARGUMENT_KEY = "Data" - fun newInstance(studentWithSemesters: StudentWithSemesters) = + fun newInstance(student: Student) = AccountDetailsFragment().apply { - arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, studentWithSemesters) } + arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, student) } } } @@ -51,7 +51,7 @@ class AccountDetailsFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentAccountDetailsBinding.bind(view) - presenter.onAttachView(this, requireArguments()[ARGUMENT_KEY] as StudentWithSemesters) + presenter.onAttachView(this, requireArguments()[ARGUMENT_KEY] as Student) } override fun initView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsPresenter.kt index cc53c9b63..d4cba580a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsPresenter.kt @@ -2,6 +2,7 @@ package io.github.wulkanowy.ui.modules.account.accountdetails import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.services.sync.SyncManager @@ -27,9 +28,9 @@ class AccountDetailsPresenter @Inject constructor( private var studentId: Long? = null - fun onAttachView(view: AccountDetailsView, studentWithSemesters: StudentWithSemesters) { + fun onAttachView(view: AccountDetailsView, student: Student) { super.onAttachView(view) - studentId = studentWithSemesters.student.id + studentId = student.id view.initView() errorHandler.showErrorMessage = ::showErrorViewOnError diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt index fa081ce7f..56503d027 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt @@ -14,6 +14,7 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.db.entities.TimetableHeader import io.github.wulkanowy.databinding.ItemDashboardAccountBinding @@ -41,7 +42,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter Unit = {} + var onAccountTileClickListener: (Student) -> Unit = {} var onLuckyNumberTileClickListener: () -> Unit = {} @@ -152,7 +153,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter { + attendancePercentage == null || attendancePercentage == .0 -> { + context.getThemeAttrColor(R.attr.colorOnSurface) + } + attendancePercentage <= ATTENDANCE_SECOND_WARNING_THRESHOLD -> { context.getThemeAttrColor(R.attr.colorPrimary) } - attendancePercentage ?: 0.0 <= ATTENDANCE_FIRST_WARNING_THRESHOLD -> { + attendancePercentage <= ATTENDANCE_FIRST_WARNING_THRESHOLD -> { context.getThemeAttrColor(R.attr.colorTimetableChange) } else -> context.getThemeAttrColor(R.attr.colorOnSurface) } + val attendanceString = if (attendancePercentage == null || attendancePercentage == .0) { + context.getString(R.string.dashboard_horizontal_group_no_data) + } else { + "%.2f%%".format(attendancePercentage) + } with(binding.dashboardHorizontalGroupItemAttendanceValue) { - text = "%.2f%%".format(attendancePercentage) + text = attendanceString setTextColor(attendanceColor) } with(binding) { dashboardHorizontalGroupItemMessageValue.text = unreadMessagesCount.toString() - dashboardHorizontalGroupItemLuckyValue.text = if (luckyNumber == -1) { - context.getString(R.string.dashboard_horizontal_group_no_lukcy_number) + dashboardHorizontalGroupItemLuckyValue.text = if (luckyNumber == 0) { + context.getString(R.string.dashboard_horizontal_group_no_data) } else luckyNumber?.toString() - if (dashboardHorizontalGroupItemInfoContainer.isVisible != (error != null || isLoading)) { - dashboardHorizontalGroupItemInfoContainer.isVisible = error != null || isLoading - } - - if (dashboardHorizontalGroupItemInfoProgress.isVisible != isLoading) { - dashboardHorizontalGroupItemInfoProgress.isVisible = isLoading - } - + dashboardHorizontalGroupItemInfoContainer.isVisible = error != null || isLoading + dashboardHorizontalGroupItemInfoProgress.isVisible = + (isLoading && !item.isDataLoaded) || (isLoading && !item.isFullDataLoaded) dashboardHorizontalGroupItemInfoErrorText.isVisible = error != null with(dashboardHorizontalGroupItemLuckyContainer) { - isVisible = error == null && !isLoading && luckyNumber != null + isVisible = luckyNumber != null && luckyNumber != -1 setOnClickListener { onLuckyNumberTileClickListener() } updateLayoutParams { @@ -216,7 +220,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter { matchConstraintPercentWidth = when { luckyNumber == null && unreadMessagesCount == null -> 1.0f @@ -228,7 +232,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter { - updateLessonView(item, emptyList(), binding, currentDayHeader) - binding.dashboardLessonsItemTitleTomorrow.isVisible = false - } tomorrowTimetable.isNotEmpty() -> { updateLessonView(item, tomorrowTimetable, binding) binding.dashboardLessonsItemTitleTomorrow.isVisible = true } + currentDayHeader != null && currentDayHeader.content.isNotBlank() -> { + updateLessonView(item, emptyList(), binding, currentDayHeader) + binding.dashboardLessonsItemTitleTomorrow.isVisible = false + } tomorrowDayHeader != null && tomorrowDayHeader.content.isNotBlank() -> { updateLessonView(item, emptyList(), binding, tomorrowDayHeader) binding.dashboardLessonsItemTitleTomorrow.isVisible = true @@ -348,6 +352,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter 60) { val formattedStartTime = firstLesson.start.toFormattedString("HH:mm") val formattedEndTime = firstLesson.end.toFormattedString("HH:mm") - firstTimeRangeText = "${formattedStartTime}-${formattedEndTime}" + firstTimeRangeText = "$formattedStartTime - $formattedEndTime" firstTimeText = "" isFirstTimeRangeVisible = true @@ -421,7 +426,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter(R.layout.fragme ) dashboardAdapter.apply { - onAccountTileClickListener = { mainActivity.pushView(AccountFragment.newInstance()) } + onAccountTileClickListener = { + mainActivity.pushView(AccountDetailsFragment.newInstance(it)) + } onLuckyNumberTileClickListener = { mainActivity.pushView(LuckyNumberFragment.newInstance()) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt index 2948b42fa..cf99f0c9a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt @@ -35,6 +35,9 @@ sealed class DashboardItem(val type: Type) { override val isDataLoaded get() = unreadMessagesCount != null || attendancePercentage != null || luckyNumber != null + + val isFullDataLoaded + get() = luckyNumber != -1 && attendancePercentage != -1.0 && unreadMessagesCount != -1 } data class Grades( diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt index 0e24f0a14..027bcc053 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt @@ -2,6 +2,8 @@ package io.github.wulkanowy.ui.modules.dashboard import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.db.entities.LuckyNumber +import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository import io.github.wulkanowy.data.repositories.ConferenceRepository @@ -18,11 +20,18 @@ import io.github.wulkanowy.data.repositories.TimetableRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.calculatePercentage -import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.flowWithResourceIn import io.github.wulkanowy.utils.nextOrSameSchoolDay +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.filterNot +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import timber.log.Timber import java.time.LocalDate import java.time.LocalDateTime @@ -48,9 +57,11 @@ class DashboardPresenter @Inject constructor( private val dashboardItemRefreshLoadedList = mutableListOf() - private lateinit var dashboardItemsToLoad: Set + private var dashboardItemsToLoad = emptySet() - private var dashboardTilesToLoad: Set = emptySet() + private var dashboardTileLoadedList = emptySet() + + private val firstLoadedItemList = mutableListOf() private lateinit var lastError: Throwable @@ -69,8 +80,10 @@ class DashboardPresenter @Inject constructor( } fun onDragAndDropEnd(list: List) { - dashboardItemLoadedList.clear() - dashboardItemLoadedList.addAll(list) + with(dashboardItemLoadedList) { + clear() + addAll(list) + } val positionList = list.mapIndexed { index, dashboardItem -> Pair(dashboardItem.type, index) }.toMap() @@ -78,87 +91,102 @@ class DashboardPresenter @Inject constructor( preferencesRepository.dashboardItemsPosition = positionList } - fun loadData(forceRefresh: Boolean = false, tilesToLoad: Set) { - val oldDashboardDataToLoad = dashboardTilesToLoad + fun loadData( + tilesToLoad: Set, + forceRefresh: Boolean = false, + ) { + val oldDashboardTileLoadedList = dashboardTileLoadedList + dashboardItemsToLoad = tilesToLoad.map { it.toDashboardItemType() }.toSet() + dashboardTileLoadedList = tilesToLoad - dashboardTilesToLoad = tilesToLoad - dashboardItemsToLoad = dashboardTilesToLoad.map { it.toDashboardItemType() }.toSet() + val itemsToLoad = generateDashboardTileListToLoad( + dashboardTilesToLoad = tilesToLoad, + dashboardLoadedTiles = oldDashboardTileLoadedList, + forceRefresh = forceRefresh + ).map { it.toDashboardItemType() } - removeUnselectedTiles() - - val newTileList = generateTileListToLoad(oldDashboardDataToLoad, forceRefresh) - loadTiles(forceRefresh, newTileList) + removeUnselectedTiles(tilesToLoad.toList()) + loadTiles(tileList = itemsToLoad, forceRefresh = forceRefresh) } - private fun removeUnselectedTiles() { - val isLuckyNumberToLoad = - dashboardTilesToLoad.any { it == DashboardItem.Tile.LUCKY_NUMBER } - val isMessagesToLoad = - dashboardTilesToLoad.any { it == DashboardItem.Tile.MESSAGES } - val isAttendanceToLoad = - dashboardTilesToLoad.any { it == DashboardItem.Tile.ATTENDANCE } + private fun generateDashboardTileListToLoad( + dashboardTilesToLoad: Set, + dashboardLoadedTiles: Set, + forceRefresh: Boolean + ) = dashboardTilesToLoad.filter { newItemToLoad -> + dashboardLoadedTiles.none { it == newItemToLoad } || forceRefresh + } + private fun removeUnselectedTiles(tilesToLoad: List) { dashboardItemLoadedList.removeAll { loadedTile -> dashboardItemsToLoad.none { it == loadedTile.type } } val horizontalGroup = dashboardItemLoadedList.find { it is DashboardItem.HorizontalGroup } as DashboardItem.HorizontalGroup? if (horizontalGroup != null) { - val horizontalIndex = dashboardItemLoadedList.indexOf(horizontalGroup) - dashboardItemLoadedList.remove(horizontalGroup) + val isLuckyNumberToLoad = DashboardItem.Tile.LUCKY_NUMBER in tilesToLoad + val isMessagesToLoad = DashboardItem.Tile.MESSAGES in tilesToLoad + val isAttendanceToLoad = DashboardItem.Tile.ATTENDANCE in tilesToLoad - var updatedHorizontalGroup = horizontalGroup + val horizontalGroupIndex = dashboardItemLoadedList.indexOf(horizontalGroup) - if (horizontalGroup.luckyNumber != null && !isLuckyNumberToLoad) { - updatedHorizontalGroup = updatedHorizontalGroup.copy(luckyNumber = null) + val newHorizontalGroup = horizontalGroup.copy( + attendancePercentage = horizontalGroup.attendancePercentage.takeIf { isAttendanceToLoad }, + unreadMessagesCount = horizontalGroup.unreadMessagesCount.takeIf { isMessagesToLoad }, + luckyNumber = horizontalGroup.luckyNumber.takeIf { isLuckyNumberToLoad } + ) + + with(dashboardItemLoadedList) { + removeAt(horizontalGroupIndex) + add(horizontalGroupIndex, newHorizontalGroup) } - - if (horizontalGroup.attendancePercentage != null && !isAttendanceToLoad) { - updatedHorizontalGroup = updatedHorizontalGroup.copy(attendancePercentage = null) - } - - if (horizontalGroup.unreadMessagesCount != null && !isMessagesToLoad) { - updatedHorizontalGroup = updatedHorizontalGroup.copy(unreadMessagesCount = null) - } - - if (horizontalGroup.error != null) { - updatedHorizontalGroup = updatedHorizontalGroup.copy(error = null, isLoading = true) - } - - dashboardItemLoadedList.add(horizontalIndex, updatedHorizontalGroup) } view?.updateData(dashboardItemLoadedList) } - private fun loadTiles(forceRefresh: Boolean, tileList: List) { - tileList.forEach { - when (it) { - DashboardItem.Tile.ACCOUNT -> loadCurrentAccount(forceRefresh) - DashboardItem.Tile.LUCKY_NUMBER -> loadLuckyNumber(forceRefresh) - DashboardItem.Tile.MESSAGES -> loadMessages(forceRefresh) - DashboardItem.Tile.ATTENDANCE -> loadAttendance(forceRefresh) - DashboardItem.Tile.LESSONS -> loadLessons(forceRefresh) - DashboardItem.Tile.GRADES -> loadGrades(forceRefresh) - DashboardItem.Tile.HOMEWORK -> loadHomework(forceRefresh) - DashboardItem.Tile.ANNOUNCEMENTS -> loadSchoolAnnouncements(forceRefresh) - DashboardItem.Tile.EXAMS -> loadExams(forceRefresh) - DashboardItem.Tile.CONFERENCES -> loadConferences(forceRefresh) - DashboardItem.Tile.ADS -> TODO() + private fun loadTiles( + tileList: List, + forceRefresh: Boolean + ) { + launch { + Timber.i("Loading dashboard account data started") + val student = runCatching { studentRepository.getCurrentStudent(true) } + .onFailure { + Timber.i("Loading dashboard account result: An exception occurred") + errorHandler.dispatch(it) + updateData(DashboardItem.Account(error = it), forceRefresh) + } + .onSuccess { Timber.i("Loading dashboard account result: Success") } + .getOrNull() ?: return@launch + + tileList.forEach { + when (it) { + DashboardItem.Type.ACCOUNT -> { + updateData(DashboardItem.Account(student), forceRefresh) + } + DashboardItem.Type.HORIZONTAL_GROUP -> { + loadHorizontalGroup(student, forceRefresh) + } + DashboardItem.Type.LESSONS -> loadLessons(student, forceRefresh) + DashboardItem.Type.GRADES -> loadGrades(student, forceRefresh) + DashboardItem.Type.HOMEWORK -> loadHomework(student, forceRefresh) + DashboardItem.Type.ANNOUNCEMENTS -> { + loadSchoolAnnouncements(student, forceRefresh) + } + DashboardItem.Type.EXAMS -> loadExams(student, forceRefresh) + DashboardItem.Type.CONFERENCES -> { + loadConferences(student, forceRefresh) + } + DashboardItem.Type.ADS -> TODO() + } } } } - private fun generateTileListToLoad( - oldDashboardTileToLoad: Set, - forceRefresh: Boolean - ) = dashboardTilesToLoad.filter { newTileToLoad -> - oldDashboardTileToLoad.none { it == newTileToLoad } || forceRefresh - } - fun onSwipeRefresh() { Timber.i("Force refreshing the dashboard") - loadData(true, preferencesRepository.selectedDashboardTiles) + loadData(preferencesRepository.selectedDashboardTiles, forceRefresh = true) } fun onRetry() { @@ -166,7 +194,7 @@ class DashboardPresenter @Inject constructor( showErrorView(false) showProgress(true) } - loadData(true, preferencesRepository.selectedDashboardTiles) + loadData(preferencesRepository.selectedDashboardTiles, forceRefresh = true) } fun onViewReselected() { @@ -192,139 +220,86 @@ class DashboardPresenter @Inject constructor( }.toSet() } - private fun loadCurrentAccount(forceRefresh: Boolean) { - flowWithResource { studentRepository.getCurrentStudent(false) } + private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) { + flow { + val semester = semesterRepository.getCurrentSemester(student) + val selectedTiles = preferencesRepository.selectedDashboardTiles + + val luckyNumberFlow = luckyNumberRepository.getLuckyNumber(student, forceRefresh) + .map { + if (it.data == null) { + it.copy(data = LuckyNumber(0, LocalDate.now(), 0)) + } else it + } + .takeIf { DashboardItem.Tile.LUCKY_NUMBER in selectedTiles } ?: flowOf(null) + + val messageFLow = messageRepository.getMessages( + student = student, + semester = semester, + folder = MessageFolder.RECEIVED, + forceRefresh = forceRefresh + ).takeIf { DashboardItem.Tile.MESSAGES in selectedTiles } ?: flowOf(null) + + val attendanceFlow = attendanceSummaryRepository.getAttendanceSummary( + student = student, + semester = semester, + subjectId = -1, + forceRefresh = forceRefresh + ).takeIf { DashboardItem.Tile.ATTENDANCE in selectedTiles } ?: flowOf(null) + + emitAll( + combine( + luckyNumberFlow, + messageFLow, + attendanceFlow + ) { luckyNumberResource, messageResource, attendanceResource -> + val error = + luckyNumberResource?.error ?: messageResource?.error ?: attendanceResource?.error + error?.let { throw it } + + val luckyNumber = luckyNumberResource?.data?.luckyNumber + val messageCount = messageResource?.data?.count { it.unread } + val attendancePercentage = attendanceResource?.data?.calculatePercentage() + + val isLoading = + luckyNumberResource?.status == Status.LOADING || messageResource?.status == Status.LOADING || attendanceResource?.status == Status.LOADING + + DashboardItem.HorizontalGroup( + isLoading = isLoading, + attendancePercentage = if (attendancePercentage == 0.0 && isLoading) -1.0 else attendancePercentage, + unreadMessagesCount = if (messageCount == 0 && isLoading) -1 else messageCount, + luckyNumber = if (luckyNumber == 0 && isLoading) -1 else luckyNumber + ) + }) + } + .filterNot { it.isLoading && forceRefresh } + .distinctUntilChanged() .onEach { - when (it.status) { - Status.LOADING -> { - Timber.i("Loading dashboard account data started") - if (forceRefresh) return@onEach - updateData(DashboardItem.Account(it.data, isLoading = true), forceRefresh) - } - Status.SUCCESS -> { - Timber.i("Loading dashboard account result: Success") - updateData(DashboardItem.Account(it.data), forceRefresh) - } - Status.ERROR -> { - Timber.i("Loading dashboard account result: An exception occurred") - errorHandler.dispatch(it.error!!) - updateData(DashboardItem.Account(error = it.error), forceRefresh) + updateData(it, forceRefresh) + + if (it.isLoading) { + Timber.i("Loading horizontal group data started") + + if (it.isFullDataLoaded) { + firstLoadedItemList += DashboardItem.Type.HORIZONTAL_GROUP } + } else { + Timber.i("Loading horizontal group result: Success") } } - .launch("dashboard_account") - } - - private fun loadLuckyNumber(forceRefresh: Boolean) { - flowWithResourceIn { - val student = studentRepository.getCurrentStudent(true) - - luckyNumberRepository.getLuckyNumber(student, forceRefresh) - }.onEach { - when (it.status) { - Status.LOADING -> { - Timber.i("Loading dashboard lucky number data started") - if (forceRefresh) return@onEach - processHorizontalGroupData( - luckyNumber = it.data?.luckyNumber, - isLoading = true, - forceRefresh = forceRefresh - ) - } - Status.SUCCESS -> { - Timber.i("Loading dashboard lucky number result: Success") - processHorizontalGroupData( - luckyNumber = it.data?.luckyNumber ?: -1, - forceRefresh = forceRefresh - ) - } - Status.ERROR -> { - Timber.i("Loading dashboard lucky number result: An exception occurred") - errorHandler.dispatch(it.error!!) - processHorizontalGroupData(error = it.error, forceRefresh = forceRefresh) - } + .catch { + Timber.i("Loading horizontal group result: An exception occurred") + updateData( + DashboardItem.HorizontalGroup(error = it), + forceRefresh, + ) + errorHandler.dispatch(it) } - }.launch("dashboard_lucky_number") + .launch("horizontal_group") } - private fun loadMessages(forceRefresh: Boolean) { + private fun loadGrades(student: Student, forceRefresh: Boolean) { flowWithResourceIn { - val student = studentRepository.getCurrentStudent(true) - val semester = semesterRepository.getCurrentSemester(student) - - messageRepository.getMessages(student, semester, MessageFolder.RECEIVED, forceRefresh) - }.onEach { - when (it.status) { - Status.LOADING -> { - Timber.i("Loading dashboard messages data started") - if (forceRefresh) return@onEach - val unreadMessagesCount = it.data?.count { message -> message.unread } - - processHorizontalGroupData( - unreadMessagesCount = unreadMessagesCount, - isLoading = true, - forceRefresh = forceRefresh - ) - } - Status.SUCCESS -> { - Timber.i("Loading dashboard messages result: Success") - val unreadMessagesCount = it.data?.count { message -> message.unread } - - processHorizontalGroupData( - unreadMessagesCount = unreadMessagesCount, - forceRefresh = forceRefresh - ) - } - Status.ERROR -> { - Timber.i("Loading dashboard messages result: An exception occurred") - errorHandler.dispatch(it.error!!) - processHorizontalGroupData(error = it.error, forceRefresh = forceRefresh) - } - } - }.launch("dashboard_messages") - } - - private fun loadAttendance(forceRefresh: Boolean) { - flowWithResourceIn { - val student = studentRepository.getCurrentStudent(true) - val semester = semesterRepository.getCurrentSemester(student) - - attendanceSummaryRepository.getAttendanceSummary(student, semester, -1, forceRefresh) - }.onEach { - when (it.status) { - Status.LOADING -> { - Timber.i("Loading dashboard attendance data started") - if (forceRefresh) return@onEach - val attendancePercentage = it.data?.calculatePercentage() - - processHorizontalGroupData( - attendancePercentage = attendancePercentage, - isLoading = true, - forceRefresh = forceRefresh - ) - } - Status.SUCCESS -> { - Timber.i("Loading dashboard attendance result: Success") - val attendancePercentage = it.data?.calculatePercentage() - - processHorizontalGroupData( - attendancePercentage = attendancePercentage, - forceRefresh = forceRefresh - ) - } - Status.ERROR -> { - Timber.i("Loading dashboard attendance result: An exception occurred") - errorHandler.dispatch(it.error!!) - - processHorizontalGroupData(error = it.error, forceRefresh = forceRefresh) - } - } - }.launch("dashboard_attendance") - } - - private fun loadGrades(forceRefresh: Boolean) { - flowWithResourceIn { - val student = studentRepository.getCurrentStudent(true) val semester = semesterRepository.getCurrentSemester(student) gradeRepository.getGrades(student, semester, forceRefresh) @@ -353,6 +328,7 @@ class DashboardPresenter @Inject constructor( Status.LOADING -> { Timber.i("Loading dashboard grades data started") if (forceRefresh) return@onEach + updateData( DashboardItem.Grades( subjectWithGrades = it.data, @@ -360,6 +336,10 @@ class DashboardPresenter @Inject constructor( isLoading = true ), forceRefresh ) + + if (!it.data.isNullOrEmpty()) { + firstLoadedItemList += DashboardItem.Type.GRADES + } } Status.SUCCESS -> { Timber.i("Loading dashboard grades result: Success") @@ -367,7 +347,8 @@ class DashboardPresenter @Inject constructor( DashboardItem.Grades( subjectWithGrades = it.data, gradeTheme = preferencesRepository.gradeColorTheme - ), forceRefresh + ), + forceRefresh ) } Status.ERROR -> { @@ -379,9 +360,8 @@ class DashboardPresenter @Inject constructor( }.launch("dashboard_grades") } - private fun loadLessons(forceRefresh: Boolean) { + private fun loadLessons(student: Student, forceRefresh: Boolean) { flowWithResourceIn { - val student = studentRepository.getCurrentStudent(true) val semester = semesterRepository.getCurrentSemester(student) val date = LocalDate.now().nextOrSameSchoolDay @@ -398,24 +378,34 @@ class DashboardPresenter @Inject constructor( Status.LOADING -> { Timber.i("Loading dashboard lessons data started") if (forceRefresh) return@onEach - updateData(DashboardItem.Lessons(it.data, isLoading = true), forceRefresh) + updateData( + DashboardItem.Lessons(it.data, isLoading = true), + forceRefresh + ) + + if (!it.data?.lessons.isNullOrEmpty()) { + firstLoadedItemList += DashboardItem.Type.LESSONS + } } Status.SUCCESS -> { Timber.i("Loading dashboard lessons result: Success") - updateData(DashboardItem.Lessons(it.data), forceRefresh) + updateData( + DashboardItem.Lessons(it.data), forceRefresh + ) } Status.ERROR -> { Timber.i("Loading dashboard lessons result: An exception occurred") errorHandler.dispatch(it.error!!) - updateData(DashboardItem.Lessons(error = it.error), forceRefresh) + updateData( + DashboardItem.Lessons(error = it.error), forceRefresh + ) } } }.launch("dashboard_lessons") } - private fun loadHomework(forceRefresh: Boolean) { + private fun loadHomework(student: Student, forceRefresh: Boolean) { flowWithResourceIn { - val student = studentRepository.getCurrentStudent(true) val semester = semesterRepository.getCurrentSemester(student) val date = LocalDate.now().nextOrSameSchoolDay @@ -443,6 +433,10 @@ class DashboardPresenter @Inject constructor( DashboardItem.Homework(it.data ?: emptyList(), isLoading = true), forceRefresh ) + + if (!it.data.isNullOrEmpty()) { + firstLoadedItemList += DashboardItem.Type.HOMEWORK + } } Status.SUCCESS -> { Timber.i("Loading dashboard homework result: Success") @@ -457,10 +451,8 @@ class DashboardPresenter @Inject constructor( }.launch("dashboard_homework") } - private fun loadSchoolAnnouncements(forceRefresh: Boolean) { + private fun loadSchoolAnnouncements(student: Student, forceRefresh: Boolean) { flowWithResourceIn { - val student = studentRepository.getCurrentStudent(true) - schoolAnnouncementRepository.getSchoolAnnouncements(student, forceRefresh) }.onEach { when (it.status) { @@ -468,11 +460,13 @@ class DashboardPresenter @Inject constructor( Timber.i("Loading dashboard announcements data started") if (forceRefresh) return@onEach updateData( - DashboardItem.Announcements( - it.data ?: emptyList(), - isLoading = true - ), forceRefresh + DashboardItem.Announcements(it.data ?: emptyList(), isLoading = true), + forceRefresh ) + + if (!it.data.isNullOrEmpty()) { + firstLoadedItemList += DashboardItem.Type.ANNOUNCEMENTS + } } Status.SUCCESS -> { Timber.i("Loading dashboard announcements result: Success") @@ -487,9 +481,8 @@ class DashboardPresenter @Inject constructor( }.launch("dashboard_announcements") } - private fun loadExams(forceRefresh: Boolean) { + private fun loadExams(student: Student, forceRefresh: Boolean) { flowWithResourceIn { - val student = studentRepository.getCurrentStudent(true) val semester = semesterRepository.getCurrentSemester(student) examRepository.getExams( @@ -508,6 +501,10 @@ class DashboardPresenter @Inject constructor( DashboardItem.Exams(it.data.orEmpty(), isLoading = true), forceRefresh ) + + if (!it.data.isNullOrEmpty()) { + firstLoadedItemList += DashboardItem.Type.EXAMS + } } Status.SUCCESS -> { Timber.i("Loading dashboard exams result: Success") @@ -522,9 +519,8 @@ class DashboardPresenter @Inject constructor( }.launch("dashboard_exams") } - private fun loadConferences(forceRefresh: Boolean) { + private fun loadConferences(student: Student, forceRefresh: Boolean) { flowWithResourceIn { - val student = studentRepository.getCurrentStudent(true) val semester = semesterRepository.getCurrentSemester(student) conferenceRepository.getConferences( @@ -542,6 +538,10 @@ class DashboardPresenter @Inject constructor( DashboardItem.Conferences(it.data ?: emptyList(), isLoading = true), forceRefresh ) + + if (!it.data.isNullOrEmpty()) { + firstLoadedItemList += DashboardItem.Type.CONFERENCES + } } Status.SUCCESS -> { Timber.i("Loading dashboard conferences result: Success") @@ -556,145 +556,119 @@ class DashboardPresenter @Inject constructor( }.launch("dashboard_conferences") } - private fun processHorizontalGroupData( - luckyNumber: Int? = null, - unreadMessagesCount: Int? = null, - attendancePercentage: Double? = null, - error: Throwable? = null, - isLoading: Boolean = false, - forceRefresh: Boolean - ) { - val isLuckyNumberToLoad = - dashboardTilesToLoad.any { it == DashboardItem.Tile.LUCKY_NUMBER } - val isMessagesToLoad = - dashboardTilesToLoad.any { it == DashboardItem.Tile.MESSAGES } - val isAttendanceToLoad = - dashboardTilesToLoad.any { it == DashboardItem.Tile.ATTENDANCE } - val isPushedToList = - dashboardItemLoadedList.any { it.type == DashboardItem.Type.HORIZONTAL_GROUP } + private fun updateData(dashboardItem: DashboardItem, forceRefresh: Boolean) { + val isForceRefreshError = forceRefresh && dashboardItem.error != null + val isFirstRunDataLoadedError = + dashboardItem.type in firstLoadedItemList && dashboardItem.error != null - if (error != null) { - updateData(DashboardItem.HorizontalGroup(error = error), forceRefresh) - return + with(dashboardItemLoadedList) { + removeAll { it.type == dashboardItem.type && !isForceRefreshError && !isFirstRunDataLoadedError } + if (!isForceRefreshError && !isFirstRunDataLoadedError) add(dashboardItem) } - if (isLoading) { - val horizontalGroup = - dashboardItemLoadedList.find { it is DashboardItem.HorizontalGroup } as DashboardItem.HorizontalGroup? - val updatedHorizontalGroup = - horizontalGroup?.copy(isLoading = true) ?: DashboardItem.HorizontalGroup(isLoading = true) + sortDashboardItems() - updateData(updatedHorizontalGroup, forceRefresh) - } - - if (forceRefresh && !isPushedToList) { - updateData(DashboardItem.HorizontalGroup(), forceRefresh) - } - - val horizontalGroup = - dashboardItemLoadedList.single { it is DashboardItem.HorizontalGroup } as DashboardItem.HorizontalGroup - - when { - luckyNumber != null -> { - updateData(horizontalGroup.copy(luckyNumber = luckyNumber), forceRefresh) - } - unreadMessagesCount != null -> { - updateData( - horizontalGroup.copy(unreadMessagesCount = unreadMessagesCount), - forceRefresh - ) - } - attendancePercentage != null -> { - updateData( - horizontalGroup.copy(attendancePercentage = attendancePercentage), - forceRefresh - ) - } - } - - val isHorizontalGroupLoaded = dashboardItemLoadedList.any { - if (it !is DashboardItem.HorizontalGroup) return@any false - - val isLuckyNumberStateCorrect = (it.luckyNumber != null) == isLuckyNumberToLoad - val isMessagesStateCorrect = (it.unreadMessagesCount != null) == isMessagesToLoad - val isAttendanceStateCorrect = (it.attendancePercentage != null) == isAttendanceToLoad - - isLuckyNumberStateCorrect && isAttendanceStateCorrect && isMessagesStateCorrect - } - - if (isHorizontalGroupLoaded) { - val updatedHorizontalGroup = - dashboardItemLoadedList.single { it is DashboardItem.HorizontalGroup } as DashboardItem.HorizontalGroup - - updateData(updatedHorizontalGroup.copy(isLoading = false, error = null), forceRefresh) + if (forceRefresh) { + updateForceRefreshData(dashboardItem) + } else { + updateNormalData() } } - private fun updateData(dashboardItem: DashboardItem, forceRefresh: Boolean) { - val isForceRefreshError = forceRefresh && dashboardItem.error != null - val dashboardItemsPosition = preferencesRepository.dashboardItemsPosition - - with(dashboardItemLoadedList) { - removeAll { it.type == dashboardItem.type && !isForceRefreshError } - if (!isForceRefreshError) add(dashboardItem) - sortBy { tile -> dashboardItemsToLoad.single { it == tile.type }.ordinal } + private fun updateNormalData() { + val isItemsLoaded = + dashboardItemsToLoad.all { type -> dashboardItemLoadedList.any { it.type == type } } + val isItemsDataLoaded = isItemsLoaded && dashboardItemLoadedList.all { + it.isDataLoaded || it.error != null } - if (forceRefresh) { - with(dashboardItemRefreshLoadedList) { - removeAll { it.type == dashboardItem.type } - add(dashboardItem) + if (isItemsDataLoaded) { + view?.run { + showProgress(false) + showErrorView(false) + showContent(true) + updateData(dashboardItemLoadedList.toList()) } } + showErrorIfExists( + isItemsLoaded = isItemsLoaded, + itemsLoadedList = dashboardItemLoadedList, + forceRefresh = false + ) + } + + private fun updateForceRefreshData(dashboardItem: DashboardItem) { + with(dashboardItemRefreshLoadedList) { + removeAll { it.type == dashboardItem.type } + add(dashboardItem) + } + + val isRefreshItemLoaded = + dashboardItemsToLoad.all { type -> dashboardItemRefreshLoadedList.any { it.type == type } } + val isRefreshItemsDataLoaded = isRefreshItemLoaded && dashboardItemRefreshLoadedList.all { + it.isDataLoaded || it.error != null + } + + if (isRefreshItemsDataLoaded) { + view?.run { + showRefresh(false) + showErrorView(false) + showContent(true) + updateData(dashboardItemLoadedList.toList()) + } + } + + showErrorIfExists( + isItemsLoaded = isRefreshItemLoaded, + itemsLoadedList = dashboardItemRefreshLoadedList, + forceRefresh = true + ) + + if (isRefreshItemsDataLoaded) dashboardItemRefreshLoadedList.clear() + } + + private fun showErrorIfExists( + isItemsLoaded: Boolean, + itemsLoadedList: List, + forceRefresh: Boolean + ) { + val filteredItems = itemsLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT } + val isAccountItemError = + itemsLoadedList.find { it.type == DashboardItem.Type.ACCOUNT }?.error != null + val isGeneralError = + filteredItems.none { it.error == null } && filteredItems.isNotEmpty() || isAccountItemError + val errorMessage = itemsLoadedList.map { it.error?.stackTraceToString() }.toString() + + val filteredOriginalLoadedList = + dashboardItemLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT } + val wasAccountItemError = + dashboardItemLoadedList.find { it.type == DashboardItem.Type.ACCOUNT }?.error != null + val wasGeneralError = + filteredOriginalLoadedList.none { it.error == null } && filteredOriginalLoadedList.isNotEmpty() || wasAccountItemError + + if (isGeneralError && isItemsLoaded) { + lastError = Exception(errorMessage) + + view?.run { + showProgress(false) + showRefresh(false) + if ((forceRefresh && wasGeneralError) || !forceRefresh) { + showContent(false) + showErrorView(true) + } + } + } + } + + private fun sortDashboardItems() { + val dashboardItemsPosition = preferencesRepository.dashboardItemsPosition + dashboardItemLoadedList.sortBy { tile -> dashboardItemsPosition?.getOrDefault( tile.type, tile.type.ordinal + 100 ) ?: tile.type.ordinal } - - val isItemsLoaded = - dashboardItemsToLoad.all { type -> dashboardItemLoadedList.any { it.type == type } } - val isRefreshItemLoaded = - dashboardItemsToLoad.all { type -> dashboardItemRefreshLoadedList.any { it.type == type } } - val isItemsDataLoaded = isItemsLoaded && dashboardItemLoadedList.all { - it.isDataLoaded || it.error != null - } - val isRefreshItemsDataLoaded = isRefreshItemLoaded && dashboardItemRefreshLoadedList.all { - it.isDataLoaded || it.error != null - } - - if (isRefreshItemsDataLoaded) { - view?.showRefresh(false) - dashboardItemRefreshLoadedList.clear() - } - - view?.run { - if (!forceRefresh) { - showProgress(!isItemsDataLoaded) - showContent(isItemsDataLoaded) - } - updateData(dashboardItemLoadedList.toList()) - } - - if (isItemsLoaded) { - val filteredItems = - dashboardItemLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT } - val isAccountItemError = - dashboardItemLoadedList.single { it.type == DashboardItem.Type.ACCOUNT }.error != null - val isGeneralError = - filteredItems.all { it.error != null } && filteredItems.isNotEmpty() || isAccountItemError - - val errorMessage = filteredItems.map { it.error?.stackTraceToString() }.toString() - - lastError = Exception(errorMessage) - - view?.run { - showProgress(false) - showContent(!isGeneralError) - showErrorView(isGeneralError) - } - } } } \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/utils/TimetableExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/TimetableExtension.kt index f3591306e..9d15216c6 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/TimetableExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/TimetableExtension.kt @@ -18,7 +18,7 @@ inline val Timetable.left: Duration? get() = when { canceled -> null !isStudentPlan -> null - end.isAfter(now()) && start.isBefore(now()) -> between(now(), end) + end >= now() && start <= now() -> between(now(), end) else -> null } diff --git a/app/src/main/res/layout/item_dashboard_horizontal_group.xml b/app/src/main/res/layout/item_dashboard_horizontal_group.xml index bbd8f3517..1d43d5115 100644 --- a/app/src/main/res/layout/item_dashboard_horizontal_group.xml +++ b/app/src/main/res/layout/item_dashboard_horizontal_group.xml @@ -4,14 +4,14 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingHorizontal="12dp" android:layout_marginVertical="2dp" - android:clipToPadding="false"> + android:clipToPadding="false" + android:paddingHorizontal="12dp"> Lekce (Zítra) - %1$s (%2$s) Za chvíli: Brzy: První: @@ -566,7 +565,7 @@ Ještě %1$d dalších setkání Při načítání dat došlo k chybě - Žádné + Žádné Zkontrolovat aktualizace Před hlášením chyby zkontrolujte, zda je k dispozici aktualizace s opravou chyb diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 29d5c7636..5f14a4258 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -432,7 +432,6 @@ Lektionen (Morgen) - %1$s (%2$s) Gleich: Bald: Erstens: @@ -488,7 +487,7 @@ %1$d weitere Konferenzen Fehler beim Laden der Daten - Keine + Keine Auf Updates prüfen Bevor Sie einen Fehler melden, prüfen Sie zuerst, ob ein Update mit der Fehlerbehebung verfügbar ist diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 2a0f3cb77..14f23cd29 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -496,7 +496,6 @@ Lekcje (Jutro) - %1$s (%2$s) Za chwilę: Wkrótce: Pierwsza: @@ -566,7 +565,7 @@ Jeszcze %1$d dodatkowych zebrań Wystąpił błąd podczas ładowania danych - Brak + Brak Sprawdź dostępność aktualizacji Przed zgłoszeniem błędu sprawdź wcześniej, czy dostępna jest już aktualizacja z poprawką błędu diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index db78f5952..09b8a146d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -496,7 +496,6 @@ Уроки (Завтра) - %1$s (%2$s) Сейчас: Скоро: Первый: @@ -566,7 +565,7 @@ Еще %1$d конференций Произошла ошибка при загрузке данных - Отсутствует + Отсутствует Проверить наличие обновлений Прежде чем сообщать об ошибке, проверьте наличие обновлений diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 72899b78b..ad078e33f 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -496,7 +496,6 @@ Lekcie (Zajtra) - %1$s (%2$s) Za chvíľu: Čoskoro: Prvá: @@ -566,7 +565,7 @@ Ešte %1$d ďalších stretnutí Pri načítaní dát došlo k chybe - Žiadne + Žiadne Skontrolovať aktualizácie Pred hlásením chyby skontrolujte, či je k dispozícii aktualizácia s opravou chýb diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 4850e2396..41f1c300c 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -496,7 +496,6 @@ Уроки (Завтра) - %1$s (%2$s) Через мить: Незабаром: Перше: @@ -566,7 +565,7 @@ %1$d більше конференцій Помилка при завантаженні даних - Нічого + Нічого Провірити наявність оновлень Перед тим, як повідомлювати о помілці, перевірте наявність оновлень diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 08776c1ab..cf8a07604 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -495,7 +495,6 @@ Lessons (Tomorrow) - %1$s (%2$s) In a moment: Soon: First: @@ -557,7 +556,7 @@ An error occurred while loading data - None + None diff --git a/app/src/test/java/io/github/wulkanowy/utils/TimetableExtensionTest.kt b/app/src/test/java/io/github/wulkanowy/utils/TimetableExtensionTest.kt index 84b61a904..b7fa58c6f 100644 --- a/app/src/test/java/io/github/wulkanowy/utils/TimetableExtensionTest.kt +++ b/app/src/test/java/io/github/wulkanowy/utils/TimetableExtensionTest.kt @@ -32,7 +32,22 @@ class TimetableExtensionTest { assertEquals(null, getTimetableEntity(canceled = true).left) assertEquals(null, getTimetableEntity(start = now().plusMinutes(5), end = now().plusMinutes(50)).left) assertEquals(null, getTimetableEntity(start = now().minusMinutes(1), end = now().plusMinutes(44), isStudentPlan = false).left) - assertNotEquals(null, getTimetableEntity(start = now().minusMinutes(1), end = now().plusMinutes(44), isStudentPlan = true).left) + assertNotEquals( + null, + getTimetableEntity( + start = now().minusMinutes(1), + end = now().plusMinutes(44), + isStudentPlan = true + ).left + ) + assertNotEquals( + null, + getTimetableEntity( + start = now(), + end = now().plusMinutes(45), + isStudentPlan = true + ).left + ) } @Test From b64b41c11c0830d24e7f1c7adea48cb36895029f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sun, 5 Sep 2021 23:29:15 +0200 Subject: [PATCH 175/869] Version 1.2.1 --- app/build.gradle | 8 ++++---- app/src/main/play/release-notes/pl-PL/default.txt | 11 ++++------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index e1d1be79b..695dc6399 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,8 +21,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 30 - versionCode 93 - versionName "1.2.0" + versionCode 94 + versionName "1.2.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true @@ -133,7 +133,7 @@ play { serviceAccountEmail = System.getenv("PLAY_SERVICE_ACCOUNT_EMAIL") ?: "jan@fakelog.cf" serviceAccountCredentials = file('key.p12') defaultToAppBundles = false - track = 'beta' + track = 'production' updatePriority = 3 } @@ -157,7 +157,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:7c399ffaea" + implementation "io.github.wulkanowy:sdk:1.2.1" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index 42fd22292..aeef2a77e 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,10 +1,7 @@ -Wersja 1.2.0 +Wersja 1.2.1 -- dodaliśmy nowy ekran startowy 🎉 -- usprawniliśmy powiadomienia -- dodaliśmy wersje robocze, filtrowanie oraz informację o odczytaniu przez odbiorcę w wiadomościach -- dodaliśmy informacje o liczeniu średniej w podsumowaniu ocen -- dodaliśmy opcję generowania wiadomości z usprawiedliwieniem dni w szkołach pozbawionych funkcji usprawiedliwiania przez zakładkę frekwencja -- oraz wiele wiele innych ulepszeń i poprawek +- dodaliśmy brakujące okienka z podglądem szczegółów ogłoszeń szkolnych +- naprawiliśmy rzucające się w oczy błędy na ekranie startowym +- naprawiliśmy też inne drobne błędy w wyglądzie i stabilności aplikacji Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases From b47f26684b03c37a58564416d3694d3406ed62e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Mon, 6 Sep 2021 03:27:54 +0200 Subject: [PATCH 176/869] Change AppGallery deploy format to aab (#1483) --- .github/workflows/deploy-store.yml | 2 +- app/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-store.yml b/.github/workflows/deploy-store.yml index e7ed6b49a..8015ef640 100644 --- a/.github/workflows/deploy-store.yml +++ b/.github/workflows/deploy-store.yml @@ -71,4 +71,4 @@ jobs: PLAY_KEY_ALIAS: ${{ secrets.PLAY_KEY_ALIAS }} PLAY_KEY_PASSWORD: ${{ secrets.PLAY_KEY_PASSWORD }} PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }} - run: ./gradlew assembleHmsRelease --stacktrace && ./gradlew publishHuaweiAppGalleryHmsRelease --stacktrace + run: ./gradlew bundleHmsRelease --stacktrace && ./gradlew publishHuaweiAppGalleryHmsRelease --stacktrace diff --git a/app/build.gradle b/app/build.gradle index 695dc6399..2b0f01514 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -141,7 +141,7 @@ huaweiPublish { instances { hmsRelease { credentialsPath = "$rootDir/app/src/release/agconnect-credentials.json" - buildFormat = "apk" + buildFormat = "aab" deployType = "draft" } } From f5e9197f98c5952d71de02fd83da226262eb049f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Sep 2021 23:38:10 +0000 Subject: [PATCH 177/869] Bump work_manager from 2.5.0 to 2.6.0 (#1478) --- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2b0f01514..dac488b23 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -148,7 +148,7 @@ huaweiPublish { } ext { - work_manager = "2.5.0" + work_manager = "2.6.0" android_hilt = "1.0.0" room = "2.3.0" chucker = "3.5.2" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a331c41f6..ad5adaf2e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -119,11 +119,9 @@ - Date: Wed, 8 Sep 2021 09:13:52 +0200 Subject: [PATCH 178/869] Update material chips input (#1495) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index dac488b23..d1975e1ab 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -177,7 +177,7 @@ dependencies { implementation "androidx.constraintlayout:constraintlayout:2.1.0" implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0" implementation "com.google.android.material:material:1.4.0" - implementation "com.github.wulkanowy:material-chips-input:2.2.0" + implementation "com.github.wulkanowy:material-chips-input:2.3.1" implementation "com.github.PhilJay:MPAndroidChart:v3.1.0" implementation 'com.github.lopspower:CircularImageView:4.2.0' From 16a5d88dfb39179565b12642c9f9f4a79a761d21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Fri, 10 Sep 2021 00:25:23 +0200 Subject: [PATCH 179/869] Fix overlapping shadow in dashboard (#1494) --- .../ui/modules/dashboard/DashboardAdapter.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt index 56503d027..8c03e3644 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt @@ -170,6 +170,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter { context.getThemeAttrColor(R.attr.colorOnSurface) @@ -199,13 +201,12 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter { @@ -220,7 +221,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter { matchConstraintPercentWidth = when { luckyNumber == null && unreadMessagesCount == null -> 1.0f @@ -232,7 +234,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter Date: Fri, 10 Sep 2021 00:27:48 +0200 Subject: [PATCH 180/869] Fix NPE in timetable dashboard tile (#1498) --- .../wulkanowy/ui/modules/dashboard/DashboardAdapter.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt index 8c03e3644..0eda49326 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt @@ -31,6 +31,7 @@ import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.left import io.github.wulkanowy.utils.nickOrName import io.github.wulkanowy.utils.toFormattedString +import timber.log.Timber import java.time.Duration import java.time.LocalDate import java.time.LocalDateTime @@ -429,7 +430,10 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter Date: Fri, 10 Sep 2021 00:36:44 +0200 Subject: [PATCH 181/869] Fix overlapping error view (#1493) --- .../wulkanowy/ui/modules/attendance/AttendancePresenter.kt | 1 + .../wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt | 1 + .../ui/modules/schoolannouncement/SchoolAnnouncementPresenter.kt | 1 + .../github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt | 1 + 4 files changed, 4 insertions(+) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt index 9a1598128..fc37e50de 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt @@ -234,6 +234,7 @@ class AttendancePresenter @Inject constructor( enableSwipe(true) showRefresh(true) showProgress(false) + showErrorView(false) showEmpty(filteredAttendance.isEmpty()) showContent(filteredAttendance.isNotEmpty()) updateData(filteredAttendance.sortedBy { item -> item.number }) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt index 3e5e09b48..a24f9b79f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt @@ -117,6 +117,7 @@ class MessageTabPresenter @Inject constructor( if (!it.data.isNullOrEmpty()) { view?.run { enableSwipe(true) + showErrorView(false) showRefresh(true) showProgress(false) showContent(true) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementPresenter.kt index 88ad81465..d6a32e3cc 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementPresenter.kt @@ -64,6 +64,7 @@ class SchoolAnnouncementPresenter @Inject constructor( view?.run { enableSwipe(true) showRefresh(true) + showErrorView(false) showProgress(false) showContent(true) updateData(it.data) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt index fa1bbfb28..86e993982 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt @@ -149,6 +149,7 @@ class TimetablePresenter @Inject constructor( view?.run { enableSwipe(true) showRefresh(true) + showErrorView(false) showProgress(false) showContent(true) updateData(it.data!!.lessons) From e665a8f18b9a0bfea8629636b8501140912e1a42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Fri, 10 Sep 2021 00:48:29 +0200 Subject: [PATCH 182/869] Fix error view in attendance summary (#1492) --- .../attendance/summary/AttendanceSummaryPresenter.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryPresenter.kt index e53cda749..8b603837b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryPresenter.kt @@ -82,7 +82,13 @@ class AttendanceSummaryPresenter @Inject constructor( flowWithResourceIn { val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student) - attendanceSummaryRepository.getAttendanceSummary(student, semester, subjectId, forceRefresh) + + attendanceSummaryRepository.getAttendanceSummary( + student = student, + semester = semester, + subjectId = subjectId, + forceRefresh = forceRefresh + ) }.onEach { when (it.status) { Status.LOADING -> { @@ -92,6 +98,7 @@ class AttendanceSummaryPresenter @Inject constructor( showRefresh(true) showProgress(false) showContent(true) + showErrorView(false) updateDataSet(sortItems(it.data)) } } @@ -99,6 +106,7 @@ class AttendanceSummaryPresenter @Inject constructor( Status.SUCCESS -> { Timber.i("Loading attendance summary result: Success") view?.apply { + showErrorView(false) showEmpty(it.data!!.isEmpty()) showContent(it.data.isNotEmpty()) updateDataSet(sortItems(it.data)) From 8528e0beff83ea39c678005879ffef77294d8451 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Fri, 10 Sep 2021 11:49:22 +0200 Subject: [PATCH 183/869] Fix crash in school info when dialer is unavailable (#1500) --- .../main/java/io/github/wulkanowy/utils/ContextExtension.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt index 68d3afe83..2cd4459eb 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt @@ -102,7 +102,9 @@ fun Context.openNavigation(location: String) { fun Context.openDialer(phone: String) { val intentUri = Uri.parse("tel:$phone") val intent = Intent(Intent.ACTION_DIAL, intentUri) - startActivity(intent) + if (intent.resolveActivity(packageManager) != null) { + startActivity(intent) + } } fun Context.shareText(text: String, subject: String?) { From 0389642543f909a4fab31bbf6bf00b9d847bbdc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sat, 11 Sep 2021 19:40:09 +0200 Subject: [PATCH 184/869] Fix empty list on excuse submit (#1501) --- .../wulkanowy/ui/modules/attendance/AttendancePresenter.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt index fc37e50de..03545b25b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt @@ -152,6 +152,8 @@ class AttendancePresenter @Inject constructor( fun onExcuseDialogSubmit(reason: String) { view?.finishActionMode() + if (attendanceToExcuseList.isEmpty()) return + if (isVulcanExcusedFunctionEnabled) { excuseAbsence( reason = reason.takeIf { it.isNotBlank() }, From 91f631089273c98eed453e581c2bc12cdacf3b77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sat, 11 Sep 2021 19:43:05 +0200 Subject: [PATCH 185/869] Restore lucky number in more view (#1504) --- .../io/github/wulkanowy/ui/modules/more/MoreFragment.kt | 8 ++++++++ .../io/github/wulkanowy/ui/modules/more/MorePresenter.kt | 2 ++ .../java/io/github/wulkanowy/ui/modules/more/MoreView.kt | 4 ++++ 3 files changed, 14 insertions(+) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreFragment.kt index 2f0957c46..145b12a35 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreFragment.kt @@ -11,6 +11,7 @@ import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.conference.ConferenceFragment import io.github.wulkanowy.ui.modules.exam.ExamFragment import io.github.wulkanowy.ui.modules.homework.HomeworkFragment +import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.message.MessageFragment @@ -66,6 +67,9 @@ class MoreFragment : BaseFragment(R.layout.fragment_more), override val examRes: Pair? get() = context?.run { getString(R.string.exam_title) to getCompatDrawable(R.drawable.ic_main_exam) } + override val luckyNumberRes: Pair? + get() = context?.run { getString(R.string.lucky_number_title) to getCompatDrawable(R.drawable.ic_more_lucky_number) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentMoreBinding.bind(view) @@ -128,6 +132,10 @@ class MoreFragment : BaseFragment(R.layout.fragment_more), (activity as? MainActivity)?.pushView(ExamFragment.newInstance()) } + override fun openLuckyNumberView() { + (activity as? MainActivity)?.pushView(LuckyNumberFragment.newInstance()) + } + override fun popView(depth: Int) { (activity as? MainActivity)?.popView(depth) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt index a2b7f204e..92551d6e9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt @@ -31,6 +31,7 @@ class MorePresenter @Inject constructor( schoolAndTeachersRes?.first -> openSchoolAndTeachersView() mobileDevicesRes?.first -> openMobileDevicesView() settingsRes?.first -> openSettingsView() + luckyNumberRes?.first -> openLuckyNumberView() } } } @@ -48,6 +49,7 @@ class MorePresenter @Inject constructor( examRes, homeworkRes, noteRes, + luckyNumberRes, conferencesRes, schoolAnnouncementRes, schoolAndTeachersRes, diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt index c4a07bdcc..cb895de28 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt @@ -23,6 +23,8 @@ interface MoreView : BaseView { val examRes: Pair? + val luckyNumberRes: Pair? + fun initView() fun updateData(data: List>) @@ -46,4 +48,6 @@ interface MoreView : BaseView { fun openMobileDevicesView() fun openExamView() + + fun openLuckyNumberView() } From dddeff802ffd348958cebf6508ff0eb13ffd829b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sun, 12 Sep 2021 17:29:46 +0200 Subject: [PATCH 186/869] Fix date picker crash after saved state (#1502) --- .../wulkanowy/ui/modules/attendance/AttendanceFragment.kt | 4 +++- .../modules/luckynumber/history/LuckyNumberHistoryFragment.kt | 4 +++- .../wulkanowy/ui/modules/timetable/TimetableFragment.kt | 4 +++- .../modules/timetable/additional/AdditionalLessonsFragment.kt | 4 +++- .../modules/timetable/completed/CompletedLessonsFragment.kt | 4 +++- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt index 058946796..3fbdaec5a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt @@ -245,7 +245,9 @@ class AttendanceFragment : BaseFragment(R.layout.frag presenter.onDateSet(date.year, date.monthValue, date.dayOfMonth) } - datePicker.show(this@AttendanceFragment.parentFragmentManager, null) + if (!parentFragmentManager.isStateSaved) { + datePicker.show(parentFragmentManager, null) + } } override fun showExcuseDialog() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt index dc141f8d3..3a84b2dd5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt @@ -131,7 +131,9 @@ class LuckyNumberHistoryFragment : presenter.onDateSet(date.year, date.monthValue, date.dayOfMonth) } - datePicker.show(this@LuckyNumberHistoryFragment.parentFragmentManager, null) + if (!parentFragmentManager.isStateSaved) { + datePicker.show(parentFragmentManager, null) + } } override fun showContent(show: Boolean) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt index 1e1084a8d..83218a0da 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt @@ -202,7 +202,9 @@ class TimetableFragment : BaseFragment(R.layout.fragme presenter.onDateSet(date.year, date.monthValue, date.dayOfMonth) } - datePicker.show(this@TimetableFragment.parentFragmentManager, null) + if (!parentFragmentManager.isStateSaved) { + datePicker.show(parentFragmentManager, null) + } } override fun openAdditionalLessonsView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt index a4e1f0fc0..47bee1e39 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt @@ -152,7 +152,9 @@ class AdditionalLessonsFragment : presenter.onDateSet(date.year, date.monthValue, date.dayOfMonth) } - datePicker.show(this@AdditionalLessonsFragment.parentFragmentManager, null) + if (!parentFragmentManager.isStateSaved) { + datePicker.show(parentFragmentManager, null) + } } override fun onSaveInstanceState(outState: Bundle) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsFragment.kt index ad698c1cf..b8da1c0fd 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsFragment.kt @@ -173,7 +173,9 @@ class CompletedLessonsFragment : presenter.onDateSet(date.year, date.monthValue, date.dayOfMonth) } - datePicker.show(this@CompletedLessonsFragment.parentFragmentManager, null) + if (!parentFragmentManager.isStateSaved) { + datePicker.show(parentFragmentManager, null) + } } override fun onSaveInstanceState(outState: Bundle) { From 5a7f52c773aad5aba85b5086a78e5cd458ad7732 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Mon, 13 Sep 2021 14:19:24 +0200 Subject: [PATCH 187/869] Update help email pre-filled content (#1507) --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cf8a07604..295d83193 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -69,7 +69,7 @@ Discord Send email Zgłoszenie: Problemy z logowaniem - Informacje o aplikacji:\n\nUrządzenie: %1$s\nWersja SDK: %2$s\nWersja aplikacji: %3$s\nDodatkowe informacje: %4$s\nOstatni błąd: %5$s\n\nOpis problemu: + Informacje o aplikacji:\n\nUrządzenie: %1$s\nWersja SDK: %2$s\nWersja aplikacji: %3$s\nDodatkowe informacje: %4$s\nOstatni błąd: %5$s\n\nOpis problemu (pełna nazwa szkoły, klasa ucznia): Make sure you select the correct UONET+ register variation! I forgot my password Recover your account From 19c96ee83ff7f6e5685ec41543c6f5d2460eb890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Mon, 13 Sep 2021 14:19:46 +0200 Subject: [PATCH 188/869] Unlock sunday in navigation datepicker (#1506) --- .../java/io/github/wulkanowy/utils/SchooldaysValidator.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/utils/SchooldaysValidator.kt b/app/src/main/java/io/github/wulkanowy/utils/SchooldaysValidator.kt index 00fccfc8d..b6dd528f5 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/SchooldaysValidator.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/SchooldaysValidator.kt @@ -2,7 +2,6 @@ package io.github.wulkanowy.utils import com.google.android.material.datepicker.CalendarConstraints import kotlinx.parcelize.Parcelize -import java.time.DayOfWeek import java.time.temporal.ChronoUnit @Parcelize @@ -12,7 +11,6 @@ class SchoolDaysValidator(val start: Long, val end: Long) : CalendarConstraints. val date = dateLong.toLocalDateTime() return date.until(end.toLocalDateTime(), ChronoUnit.DAYS) >= 0 && - date.until(start.toLocalDateTime(), ChronoUnit.DAYS) <= 0 && - date.dayOfWeek != DayOfWeek.SUNDAY + date.until(start.toLocalDateTime(), ChronoUnit.DAYS) <= 0 } -} \ No newline at end of file +} From 827fb33eeb382ceb0ddaa6d54807cb48a455c471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Mon, 13 Sep 2021 14:36:31 +0200 Subject: [PATCH 189/869] Fix login process after was interrupted (#1505) --- .../wulkanowy/data/db/dao/StudentDao.kt | 26 ++++---- .../data/repositories/StudentRepository.kt | 24 +++++--- .../modules/login/form/LoginFormPresenter.kt | 8 +-- .../LoginStudentSelectPresenter.kt | 60 ++++++++++--------- .../data/repositories/StudentTest.kt | 3 +- .../LoginStudentSelectPresenterTest.kt | 31 +++------- 6 files changed, 76 insertions(+), 76 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt index 0ad2ee590..3dda8a44b 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt @@ -14,33 +14,39 @@ import javax.inject.Singleton @Singleton @Dao -interface StudentDao { +abstract class StudentDao { @Insert(onConflict = ABORT) - suspend fun insertAll(student: List): List + abstract suspend fun insertAll(student: List): List @Delete - suspend fun delete(student: Student) + abstract suspend fun delete(student: Student) @Update(entity = Student::class) - suspend fun update(studentNickAndAvatar: StudentNickAndAvatar) + abstract suspend fun update(studentNickAndAvatar: StudentNickAndAvatar) @Query("SELECT * FROM Students WHERE is_current = 1") - suspend fun loadCurrent(): Student? + abstract suspend fun loadCurrent(): Student? @Query("SELECT * FROM Students WHERE id = :id") - suspend fun loadById(id: Long): Student? + abstract suspend fun loadById(id: Long): Student? @Query("SELECT * FROM Students") - suspend fun loadAll(): List + abstract suspend fun loadAll(): List @Transaction @Query("SELECT * FROM Students") - suspend fun loadStudentsWithSemesters(): List + abstract suspend fun loadStudentsWithSemesters(): List @Query("UPDATE Students SET is_current = 1 WHERE id = :id") - suspend fun updateCurrent(id: Long) + abstract suspend fun updateCurrent(id: Long) @Query("UPDATE Students SET is_current = 0") - suspend fun resetCurrent() + abstract suspend fun resetCurrent() + + @Transaction + open suspend fun switchCurrent(id: Long) { + resetCurrent() + updateCurrent(id) + } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt index c2f364b3d..2ac892d01 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt @@ -1,7 +1,9 @@ package io.github.wulkanowy.data.repositories import android.content.Context +import androidx.room.withTransaction import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.dao.SemesterDao import io.github.wulkanowy.data.db.dao.StudentDao import io.github.wulkanowy.data.db.entities.Student @@ -25,7 +27,8 @@ class StudentRepository @Inject constructor( private val studentDb: StudentDao, private val semesterDb: SemesterDao, private val sdk: Sdk, - private val appInfo: AppInfo + private val appInfo: AppInfo, + private val appDatabase: AppDatabase ) { suspend fun isStudentSaved() = getSavedStudents(false).isNotEmpty() @@ -92,7 +95,7 @@ class StudentRepository @Inject constructor( return student } - suspend fun saveStudents(studentsWithSemesters: List): List { + suspend fun saveStudents(studentsWithSemesters: List) { val semesters = studentsWithSemesters.flatMap { it.semesters } val students = studentsWithSemesters.map { it.student } .map { @@ -104,16 +107,21 @@ class StudentRepository @Inject constructor( } } } + .mapIndexed { index, student -> + if (index == 0) { + student.copy(isCurrent = true).apply { avatarColor = student.avatarColor } + } else student + } - semesterDb.insertSemesters(semesters) - return studentDb.insertAll(students) + appDatabase.withTransaction { + studentDb.resetCurrent() + semesterDb.insertSemesters(semesters) + studentDb.insertAll(students) + } } suspend fun switchStudent(studentWithSemesters: StudentWithSemesters) { - with(studentDb) { - resetCurrent() - updateCurrent(studentWithSemesters.student.id) - } + studentDb.switchCurrent(studentWithSemesters.student.id) } suspend fun logoutStudent(student: Student) = studentDb.delete(student) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt index 99dcf1bb3..d79c422d4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt @@ -90,10 +90,10 @@ class LoginFormPresenter @Inject constructor( flowWithResource { studentRepository.getStudentsScrapper( - email, - password, - host, - symbol + email = email, + password = password, + scrapperBaseUrl = host, + symbol = symbol ) }.onEach { when (it.status) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt index c344bf441..f0f5586cc 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt @@ -78,7 +78,9 @@ class LoginStudentSelectPresenter @Inject constructor( when (it.status) { Status.LOADING -> Timber.d("Login student select students load started") Status.SUCCESS -> view?.updateData(studentsWithSemesters.map { studentWithSemesters -> - studentWithSemesters to it.data!!.any { item -> compareStudents(studentWithSemesters.student, item.student) } + studentWithSemesters to it.data!!.any { item -> + compareStudents(studentWithSemesters.student, item.student) + } }) Status.ERROR -> { errorHandler.dispatch(it.error!!) @@ -95,35 +97,32 @@ class LoginStudentSelectPresenter @Inject constructor( } private fun registerStudents(studentsWithSemesters: List) { - flowWithResource { - val savedStudents = studentRepository.saveStudents(studentsWithSemesters) - val firstRegistered = studentsWithSemesters.first().apply { student.id = savedStudents.first() } - studentRepository.switchStudent(firstRegistered) - }.onEach { - when (it.status) { - Status.LOADING -> view?.run { - Timber.i("Registration started") - showProgress(true) - showContent(false) - } - Status.SUCCESS -> { - Timber.i("Registration result: Success") - view?.openMainView() - logRegisterEvent(studentsWithSemesters) - } - Status.ERROR -> { - Timber.i("Registration result: An exception occurred ") - view?.apply { - showProgress(false) - showContent(true) - showContact(true) + flowWithResource { studentRepository.saveStudents(studentsWithSemesters) } + .onEach { + when (it.status) { + Status.LOADING -> view?.run { + Timber.i("Registration started") + showProgress(true) + showContent(false) + } + Status.SUCCESS -> { + Timber.i("Registration result: Success") + view?.openMainView() + logRegisterEvent(studentsWithSemesters) + } + Status.ERROR -> { + Timber.i("Registration result: An exception occurred ") + view?.apply { + showProgress(false) + showContent(true) + showContact(true) + } + lastError = it.error + loginErrorHandler.dispatch(it.error!!) + logRegisterEvent(studentsWithSemesters, it.error) } - lastError = it.error - loginErrorHandler.dispatch(it.error!!) - logRegisterEvent(studentsWithSemesters, it.error) } - } - }.launch("register") + }.launch("register") } fun onDiscordClick() { @@ -134,7 +133,10 @@ class LoginStudentSelectPresenter @Inject constructor( view?.openEmail(lastError?.message.ifNullOrBlank { "empty" }) } - private fun logRegisterEvent(studentsWithSemesters: List, error: Throwable? = null) { + private fun logRegisterEvent( + studentsWithSemesters: List, + error: Throwable? = null + ) { studentsWithSemesters.forEach { student -> analytics.logEvent( "registration_student_select", diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt index 402b22723..b34bbf1b2 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt @@ -37,7 +37,8 @@ class StudentTest { studentDb, semesterDb, mockSdk, - AppInfo() + AppInfo(), + mockk() ) } diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt index 9e34235e7..c79d739e8 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt @@ -89,24 +89,11 @@ class LoginStudentSelectPresenterTest { @Test fun onSelectedStudentTest() { coEvery { - studentRepository.saveStudents( - listOf( - StudentWithSemesters( - testStudent, - emptyList() - ) - ) - ) - } returns listOf(1L) - coEvery { - studentRepository.switchStudent( - StudentWithSemesters( - testStudent, - emptyList() - ) - ) + studentRepository.saveStudents(listOf(StudentWithSemesters(testStudent, emptyList()))) } just Runs + every { loginStudentSelectView.openMainView() } just Runs + presenter.onItemSelected(StudentWithSemesters(testStudent, emptyList()), false) presenter.onSignIn() @@ -118,18 +105,14 @@ class LoginStudentSelectPresenterTest { @Test fun onSelectedStudentErrorTest() { coEvery { - studentRepository.saveStudents( - listOf( - StudentWithSemesters( - testStudent, - emptyList() - ) - ) - ) + studentRepository.saveStudents(listOf(StudentWithSemesters(testStudent, emptyList()))) } throws testException + coEvery { studentRepository.logoutStudent(testStudent) } just Runs + presenter.onItemSelected(StudentWithSemesters(testStudent, emptyList()), false) presenter.onSignIn() + verify { loginStudentSelectView.showContent(false) } verify { loginStudentSelectView.showProgress(true) } verify { errorHandler.dispatch(match { testException.message == it.message }) } From 957adaf6ee2708c71dcc624bfce9409e3ff5e0db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Mon, 13 Sep 2021 14:53:27 +0200 Subject: [PATCH 190/869] Version 1.2.2 --- app/build.gradle | 6 +++--- app/src/main/play/release-notes/pl-PL/default.txt | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d1975e1ab..745859988 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,8 +21,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 30 - versionCode 94 - versionName "1.2.1" + versionCode 95 + versionName "1.2.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true @@ -157,7 +157,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:1.2.1" + implementation "io.github.wulkanowy:sdk:1.2.2" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index aeef2a77e..3456c3d03 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,7 +1,8 @@ -Wersja 1.2.1 +Wersja 1.2.2 -- dodaliśmy brakujące okienka z podglądem szczegółów ogłoszeń szkolnych -- naprawiliśmy rzucające się w oczy błędy na ekranie startowym -- naprawiliśmy też inne drobne błędy w wyglądzie i stabilności aplikacji +- naprawiliśmy problem z widocznością zadań w aplikacji gdy widoczne są one na stronie www dziennika (nadal pozostaje błąd z zadaniami widocznymi tylko w oficjalnej aplikacji - czekamy na poprawkę po stronie VULCANa) +- odblokowaliśmy niedzielę w wyborze daty w planie lekcji i innych zakładkach +- przywróciliśmy odnośnik do szczęśliwego numerka w menu Więcej +- naprawiliśmy drobne błędy ze stabilnością i wyglądem Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases From 7ec7afed87333af6ee460e87a57bb3bf3cb3a8fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Sep 2021 08:22:06 +0000 Subject: [PATCH 191/869] Bump firebase-bom from 28.4.0 to 28.4.1 (#1520) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 745859988..8e8be8bb1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -211,7 +211,7 @@ dependencies { implementation 'me.xdrop:fuzzywuzzy:1.3.1' implementation 'com.fredporciuncula:flow-preferences:1.5.0' - playImplementation platform('com.google.firebase:firebase-bom:28.4.0') + playImplementation platform('com.google.firebase:firebase-bom:28.4.1') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' From 037dbd792f6d5b5381419ee6529cfc9156598e83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 16 Sep 2021 10:51:38 +0200 Subject: [PATCH 192/869] Add conference dialog (#1519) --- .../modules/conference/ConferenceAdapter.kt | 7 +- .../ui/modules/conference/ConferenceDialog.kt | 60 +++++ .../modules/conference/ConferenceFragment.kt | 13 +- .../modules/conference/ConferencePresenter.kt | 5 + .../ui/modules/conference/ConferenceView.kt | 2 + .../SchoolAnnouncementDialog.kt | 2 +- app/src/main/res/layout/dialog_conference.xml | 211 ++++++++++++++++++ .../res/layout/dialog_school_announcement.xml | 1 + app/src/main/res/values/strings.xml | 6 +- 9 files changed, 303 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceDialog.kt create mode 100644 app/src/main/res/layout/dialog_conference.xml diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceAdapter.kt index c87286149..f63b293cf 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceAdapter.kt @@ -14,6 +14,8 @@ class ConferenceAdapter @Inject constructor() : var items = emptyList() + var onItemClickListener: (Conference) -> Unit = {} + override fun getItemCount() = items.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder( @@ -28,7 +30,10 @@ class ConferenceAdapter @Inject constructor() : conferenceItemTitle.text = item.title conferenceItemSubject.text = item.subject conferenceItemContent.text = item.agenda - conferenceItemContent.visibility = if (item.agenda.isBlank()) View.GONE else View.VISIBLE + conferenceItemContent.visibility = + if (item.agenda.isBlank()) View.GONE else View.VISIBLE + + root.setOnClickListener { onItemClickListener(item) } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceDialog.kt new file mode 100644 index 000000000..477b762b9 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceDialog.kt @@ -0,0 +1,60 @@ +package io.github.wulkanowy.ui.modules.conference + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.fragment.app.DialogFragment +import io.github.wulkanowy.data.db.entities.Conference +import io.github.wulkanowy.databinding.DialogConferenceBinding +import io.github.wulkanowy.utils.lifecycleAwareVariable +import io.github.wulkanowy.utils.toFormattedString + +class ConferenceDialog : DialogFragment() { + + private var binding: DialogConferenceBinding by lifecycleAwareVariable() + + private lateinit var conference: Conference + + companion object { + + private const val ARGUMENT_KEY = "item" + + fun newInstance(conference: Conference) = ConferenceDialog().apply { + arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, conference) } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NO_TITLE, 0) + arguments?.let { + conference = it.getSerializable(ARGUMENT_KEY) as Conference + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = DialogConferenceBinding.inflate(inflater).also { binding = it }.root + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + with(binding) { + conferenceDialogClose.setOnClickListener { dismiss() } + + conferenceDialogSubjectValue.text = conference.subject + conferenceDialogDateValue.text = conference.date.toFormattedString("dd.MM.yyyy HH:mm") + conferenceDialogHeaderValue.text = conference.title + conferenceDialogAgendaValue.text = conference.agenda + conferenceDialogPresentValue.text = conference.presentOnConference + conferenceDialogPresentValue.isVisible = conference.presentOnConference.isNotBlank() + conferenceDialogPresentTitle.isVisible = conference.presentOnConference.isNotBlank() + conferenceDialogAgendaValue.isVisible = conference.agenda.isNotBlank() + conferenceDialogAgendaTitle.isVisible = conference.agenda.isNotBlank() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceFragment.kt index dd10a65e0..b9642b1c7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceFragment.kt @@ -8,6 +8,7 @@ import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.databinding.FragmentConferenceBinding import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.widgets.DividerItemDecoration import io.github.wulkanowy.utils.getThemeAttrColor @@ -41,6 +42,8 @@ class ConferenceFragment : BaseFragment(R.layout.frag } override fun initView() { + conferencesAdapter.onItemClickListener = presenter::onItemSelected + with(binding.conferenceRecycler) { layoutManager = LinearLayoutManager(context) adapter = conferencesAdapter @@ -50,7 +53,11 @@ class ConferenceFragment : BaseFragment(R.layout.frag with(binding) { conferenceSwipe.setOnRefreshListener(presenter::onSwipeRefresh) conferenceSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) - conferenceSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh)) + conferenceSwipe.setProgressBackgroundColorSchemeColor( + requireContext().getThemeAttrColor( + R.attr.colorSwipeRefresh + ) + ) conferenceErrorRetry.setOnClickListener { presenter.onRetry() } conferenceErrorDetails.setOnClickListener { presenter.onDetailsClick() } } @@ -98,6 +105,10 @@ class ConferenceFragment : BaseFragment(R.layout.frag binding.conferenceRecycler.visibility = if (show) View.VISIBLE else View.GONE } + override fun openConferenceDialog(conference: Conference) { + (activity as? MainActivity)?.showDialogFragment(ConferenceDialog.newInstance(conference)) + } + override fun onDestroyView() { presenter.onDetachView() super.onDestroyView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferencePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferencePresenter.kt index cc7e50db5..dab170daa 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferencePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferencePresenter.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.ui.modules.conference import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.data.repositories.ConferenceRepository import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.StudentRepository @@ -43,6 +44,10 @@ class ConferencePresenter @Inject constructor( loadData(true) } + fun onItemSelected(conference: Conference) { + view?.openConferenceDialog(conference) + } + fun onDetailsClick() { view?.showErrorDetailsDialog(lastError) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceView.kt index f3d1b3b3f..4f73394df 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/conference/ConferenceView.kt @@ -26,4 +26,6 @@ interface ConferenceView : BaseView { fun enableSwipe(enable: Boolean) fun showContent(show: Boolean) + + fun openConferenceDialog(conference: Conference) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementDialog.kt index ed4b0ac98..7dcd51cea 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolannouncement/SchoolAnnouncementDialog.kt @@ -38,7 +38,7 @@ class SchoolAnnouncementDialog : DialogFragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ) = DialogSchoolAnnouncementBinding.inflate(inflater).apply { binding = this }.root + ) = DialogSchoolAnnouncementBinding.inflate(inflater).also { binding = it }.root override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/app/src/main/res/layout/dialog_conference.xml b/app/src/main/res/layout/dialog_conference.xml new file mode 100644 index 000000000..d08edf4f7 --- /dev/null +++ b/app/src/main/res/layout/dialog_conference.xml @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_school_announcement.xml b/app/src/main/res/layout/dialog_school_announcement.xml index cbce4e025..96c11d4a4 100644 --- a/app/src/main/res/layout/dialog_school_announcement.xml +++ b/app/src/main/res/layout/dialog_school_announcement.xml @@ -118,6 +118,7 @@ android:layout_marginEnd="24dp" android:paddingStart="0dp" android:paddingEnd="16dp" + tools:maxLines="5" android:text="@string/all_no_data" android:textIsSelectable="true" android:textSize="16sp" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 295d83193..de85614bc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -336,10 +336,12 @@ Today\'s lucky number is: %s Show history + Lucky number history No info about lucky numbers + Mobile devices No devices @@ -388,7 +390,8 @@ You have %1$d new conference You have %1$d new conferences - + Present at conference + Agenda School announcements @@ -585,6 +588,7 @@ Yes No Save + Title From da668f93cf309f4ae4c86f492b23849ca3c66dc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 16 Sep 2021 11:24:52 +0200 Subject: [PATCH 193/869] Fix bugs in dashboard (#1517) --- .../ui/modules/dashboard/DashboardAdapter.kt | 11 +++- .../ui/modules/dashboard/DashboardFragment.kt | 9 ++-- .../modules/dashboard/DashboardPresenter.kt | 50 +++++++++++-------- .../ui/modules/timetable/TimetableFragment.kt | 14 +++++- .../res/layout/subitem_dashboard_grades.xml | 4 +- 5 files changed, 56 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt index 0eda49326..11b575c1c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt @@ -53,7 +53,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter Unit = {} - var onLessonsTileClickListener: () -> Unit = {} + var onLessonsTileClickListener: (LocalDate) -> Unit = {} var onHomeworkTileClickListener: () -> Unit = {} @@ -275,10 +275,12 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter { + dateToNavigate = currentDate updateLessonView(item, currentTimetable, binding) binding.dashboardLessonsItemTitleTomorrow.isVisible = false } tomorrowTimetable.isNotEmpty() -> { + dateToNavigate = tomorrowDate updateLessonView(item, tomorrowTimetable, binding) binding.dashboardLessonsItemTitleTomorrow.isVisible = true } currentDayHeader != null && currentDayHeader.content.isNotBlank() -> { + dateToNavigate = currentDate updateLessonView(item, emptyList(), binding, currentDayHeader) binding.dashboardLessonsItemTitleTomorrow.isVisible = false } tomorrowDayHeader != null && tomorrowDayHeader.content.isNotBlank() -> { + dateToNavigate = tomorrowDate updateLessonView(item, emptyList(), binding, tomorrowDayHeader) binding.dashboardLessonsItemTitleTomorrow.isVisible = true } else -> { + dateToNavigate = tomorrowDate updateLessonView(item, emptyList(), binding) binding.dashboardLessonsItemTitleTomorrow.isVisible = !(item.isLoading && item.error == null) @@ -326,7 +333,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter(R.layout.fragme override fun initView() { val mainActivity = requireActivity() as MainActivity val itemTouchHelper = ItemTouchHelper( - DashboardItemMoveCallback( - dashboardAdapter, - presenter::onDragAndDropEnd - ) + DashboardItemMoveCallback(dashboardAdapter, presenter::onDragAndDropEnd) ) dashboardAdapter.apply { @@ -87,7 +84,9 @@ class DashboardFragment : BaseFragment(R.layout.fragme onAttendanceTileClickListener = { mainActivity.pushView(AttendanceSummaryFragment.newInstance()) } - onLessonsTileClickListener = { mainActivity.pushView(TimetableFragment.newInstance()) } + onLessonsTileClickListener = { + mainActivity.pushView(TimetableFragment.newInstance(it)) + } onGradeTileClickListener = { mainActivity.pushView(GradeFragment.newInstance()) } onHomeworkTileClickListener = { mainActivity.pushView(HomeworkFragment.newInstance()) } onAnnouncementsTileClickListener = { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt index 027bcc053..108a086b7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt @@ -492,31 +492,37 @@ class DashboardPresenter @Inject constructor( end = LocalDate.now().plusDays(7), forceRefresh = forceRefresh ) - }.onEach { - when (it.status) { - Status.LOADING -> { - Timber.i("Loading dashboard exams data started") - if (forceRefresh) return@onEach - updateData( - DashboardItem.Exams(it.data.orEmpty(), isLoading = true), - forceRefresh - ) + } + .map { examResource -> + val sortedExams = examResource.data?.sortedBy { it.date } - if (!it.data.isNullOrEmpty()) { - firstLoadedItemList += DashboardItem.Type.EXAMS + examResource.copy(data = sortedExams) + } + .onEach { + when (it.status) { + Status.LOADING -> { + Timber.i("Loading dashboard exams data started") + if (forceRefresh) return@onEach + updateData( + DashboardItem.Exams(it.data.orEmpty(), isLoading = true), + forceRefresh + ) + + if (!it.data.isNullOrEmpty()) { + firstLoadedItemList += DashboardItem.Type.EXAMS + } + } + Status.SUCCESS -> { + Timber.i("Loading dashboard exams result: Success") + updateData(DashboardItem.Exams(it.data ?: emptyList()), forceRefresh) + } + Status.ERROR -> { + Timber.i("Loading dashboard exams result: An exception occurred") + errorHandler.dispatch(it.error!!) + updateData(DashboardItem.Exams(error = it.error), forceRefresh) } } - Status.SUCCESS -> { - Timber.i("Loading dashboard exams result: Success") - updateData(DashboardItem.Exams(it.data ?: emptyList()), forceRefresh) - } - Status.ERROR -> { - Timber.i("Loading dashboard exams result: An exception occurred") - errorHandler.dispatch(it.error!!) - updateData(DashboardItem.Exams(error = it.error), forceRefresh) - } - } - }.launch("dashboard_exams") + }.launch("dashboard_exams") } private fun loadConferences(student: Student, forceRefresh: Boolean) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt index 83218a0da..4478a2a66 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt @@ -44,7 +44,13 @@ class TimetableFragment : BaseFragment(R.layout.fragme companion object { private const val SAVED_DATE_KEY = "CURRENT_DATE" - fun newInstance() = TimetableFragment() + private const val ARGUMENT_DATE_KEY = "ARGUMENT_DATE" + + fun newInstance(date: LocalDate? = null) = TimetableFragment().apply { + arguments = Bundle().apply { + date?.let { putLong(ARGUMENT_DATE_KEY, it.toEpochDay()) } + } + } } override val titleStringId get() = R.string.timetable_title @@ -62,7 +68,11 @@ class TimetableFragment : BaseFragment(R.layout.fragme super.onViewCreated(view, savedInstanceState) binding = FragmentTimetableBinding.bind(view) messageContainer = binding.timetableRecycler - presenter.onAttachView(this, savedInstanceState?.getLong(SAVED_DATE_KEY)) + + val initDate = savedInstanceState?.getLong(SAVED_DATE_KEY) + ?: arguments?.getLong(ARGUMENT_DATE_KEY)?.takeUnless { it == 0L } + + presenter.onAttachView(this, initDate) } override fun initView() { diff --git a/app/src/main/res/layout/subitem_dashboard_grades.xml b/app/src/main/res/layout/subitem_dashboard_grades.xml index 61faa6ac2..9354be3d6 100644 --- a/app/src/main/res/layout/subitem_dashboard_grades.xml +++ b/app/src/main/res/layout/subitem_dashboard_grades.xml @@ -11,12 +11,13 @@ android:layout_height="wrap_content" android:ellipsize="end" android:includeFontPadding="false" + android:maxEms="15" android:maxLines="1" android:textSize="13sp" app:layout_constraintBottom_toBottomOf="@id/dashboard_grades_subitem_grade_container" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@id/dashboard_grades_subitem_grade_container" - tools:text="Urządzenia techniki kompu..." /> + tools:text="Urządzenia techniki komputerowych" /> From c568bc1515c3937e9b5bc5b3da959a27bdc28757 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 16 Sep 2021 11:29:11 +0200 Subject: [PATCH 194/869] Fix ghost account after logout not current student (#1518) --- .../accountdetails/AccountDetailsFragment.kt | 6 +++++- .../accountdetails/AccountDetailsPresenter.kt | 15 +++++++++++---- .../account/accountdetails/AccountDetailsView.kt | 4 +++- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt index a9890ad9a..c3137ec58 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsFragment.kt @@ -121,10 +121,14 @@ class AccountDetailsFragment : } } - override fun popView() { + override fun popViewToMain() { (requireActivity() as MainActivity).popView(2) } + override fun popViewToAccounts() { + (requireActivity() as MainActivity).popView(1) + } + override fun recreateMainView() { requireActivity().recreate() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsPresenter.kt index d4cba580a..1f44cbbc3 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsPresenter.kt @@ -119,7 +119,7 @@ class AccountDetailsPresenter @Inject constructor( } } }.afterLoading { - view?.popView() + view?.popViewToMain() }.launch("switch") } @@ -152,11 +152,14 @@ class AccountDetailsPresenter @Inject constructor( syncManager.stopSyncWorker() openClearLoginView() } - studentWithSemesters!!.student.isCurrent -> { + studentWithSemesters?.student?.isCurrent == true -> { Timber.i("Logout result: Logout student and switch to another") recreateMainView() } - else -> Timber.i("Logout result: Logout student") + else -> { + Timber.i("Logout result: Logout student") + recreateMainView() + } } } Status.ERROR -> { @@ -165,7 +168,11 @@ class AccountDetailsPresenter @Inject constructor( } } }.afterLoading { - view?.popView() + if (studentWithSemesters?.student?.isCurrent == true) { + view?.popViewToMain() + } else { + view?.popViewToAccounts() + } }.launch("logout") } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsView.kt index 652f0c1aa..aeb743fa5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountdetails/AccountDetailsView.kt @@ -15,7 +15,9 @@ interface AccountDetailsView : BaseView { fun showLogoutConfirmDialog() - fun popView() + fun popViewToMain() + + fun popViewToAccounts() fun recreateMainView() From 258782c6481e13935ca6724bbd17b820f5dead80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 16 Sep 2021 11:30:05 +0200 Subject: [PATCH 195/869] New Crowdin updates (#1482) --- app/src/main/res/values-cs/strings.xml | 43 ++++++++++++++------------ app/src/main/res/values-de/strings.xml | 3 ++ app/src/main/res/values-pl/strings.xml | 37 ++++++++++++---------- app/src/main/res/values-ru/strings.xml | 3 ++ app/src/main/res/values-sk/strings.xml | 43 ++++++++++++++------------ app/src/main/res/values-uk/strings.xml | 3 ++ 6 files changed, 75 insertions(+), 57 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index fedbc9037..33ac36161 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -402,6 +402,8 @@ Máte %1$d nových setkání Máte %1$d nových setkání + Present at conference + Agenda Školní oznámení Žádná školní oznámení @@ -516,10 +518,10 @@ Další: Později: - ještě %1$d další lekce - ještě %1$d další lekce - ještě %1$d dalších lekcí - ještě %1$d dalších lekcí + ještě %1$d lekce + ještě %1$d lekce + ještě %1$d lekcí + ještě %1$d lekcí do %1$s Žádné nadcházející lekce @@ -528,10 +530,10 @@ Žádné domácí úkoly do vykonána Při načítání domácích úkolů došlo k chybě - Ještě %1$d další domácí úkol - Ještě %1$d další domácí úkoly - Ještě %1$d dalších domácích úkolů - Ještě %1$d dalších domácích úkolů + Ještě %1$d domácí úkol + Ještě %1$d domácí úkoly + Ještě %1$d domácích úkolů + Ještě %1$d domácích úkolů do %1$s Poslední známky @@ -541,28 +543,28 @@ Žádná aktuální oznámení Při načítání oznámení došlo k chybě - Ještě %1$d další oznámení - Ještě %1$d další oznámení - Ještě %1$d dalších oznámení - Ještě %1$d dalších oznámení + Ještě %1$d oznámení + Ještě %1$d oznámení + Ještě %1$d oznámení + Ještě %1$d oznámení Zkoušky Žádné nadcházející zkoušky Při načítání zkoušek došlo k chybě - Ještě %1$d další zkouška - Ještě %1$d další zkoušky - Ještě %1$d dalších zkoušek - Ještě %1$d dalších zkoušek + Ještě %1$d zkouška + Ještě %1$d zkoušky + Ještě %1$d zkoušek + Ještě %1$d zkoušek Setkání Žádná nadcházející setkání Při načítání setkání došlo k chybě - Ještě %1$d další setkání - Ještě %1$d další setkání - Ještě %1$d dalších setkání - Ještě %1$d dalších setkání + Ještě %1$d setkání + Ještě %1$d setkání + Ještě %1$d setkání + Ještě %1$d setkání Při načítání dat došlo k chybě Žádné @@ -590,6 +592,7 @@ Ano Ne Uložit + Title Žádné lekce Vybrat motiv diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 5f14a4258..f86c3076a 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -344,6 +344,8 @@ Sie haben %1$d neue konferenz Sie haben %1$d neue konferenzen + Present at conference + Agenda Schulankündigungen Keine schulankündigungen @@ -512,6 +514,7 @@ Ja Nein Speichern + Title Keine Lektionen Thema wählen diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 14f23cd29..cfc6810e7 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -402,6 +402,8 @@ Masz %1$d nowych zebrań Masz %1$d nowych zebrań + Obecność na zebraniu + Agenda Ogłoszenia szkolne Brak ogłoszeń szkolnych @@ -516,10 +518,10 @@ Następnie: Później: - jeszcze %1$d dodatkowa lekcja - jeszcze %1$d dodatkowe lekcje - jeszcze %1$d dodatkowych lekcji - jeszcze %1$d dodatkowych lekcji + jeszcze %1$d lekcja + jeszcze %1$d lekcje + jeszcze %1$d lekcji + jeszcze %1$d lekcji do %1$s Brak nadchodzących lekcji @@ -528,10 +530,10 @@ Brak prac domowych do wykonania Wystąpił błąd podczas ładowania zadań domowych - Jeszcze %1$d dodatkowe zadanie domowe - Jeszcze %1$d dodatkowe zadania domowe - Jeszcze %1$d dodatkowych zadań domowych - Jeszcze %1$d dodatkowych zadań domowych + Jeszcze %1$d zadanie domowe + Jeszcze %1$d zadania domowe + Jeszcze %1$d zadań domowych + Jeszcze %1$d zadań domowych do %1$s Ostatnie oceny @@ -541,19 +543,19 @@ Brak aktualnych ogłoszeń Wystąpił błąd podczas ładowania ogłoszeń - Jeszcze %1$d dodatkowe ogłoszenie - Jeszcze %1$d dodatkowe ogłoszenia - Jeszcze %1$d dodatkowych ogłoszeń - Jeszcze %1$d dodatkowych ogłoszeń + Jeszcze %1$d ogłoszenie + Jeszcze %1$d ogłoszenia + Jeszcze %1$d ogłoszeń + Jeszcze %1$d ogłoszeń Sprawdziany Brak nadchodzących sprawdzianów Wystąpił błąd podczas ładowania sprawdzianów - Jeszcze %1$d dodatkowy sprawdzian - Jeszcze %1$d dodatkowe sprawdziany - Jeszcze %1$d dodatkowych sprawdzianów - Jeszcze %1$d dodatkowych sprawdzianów + Jeszcze %1$d sprawdzian + Jeszcze %1$d sprawdziany + Jeszcze %1$d sprawdzianów + Jeszcze %1$d sprawdzianów Zebrania Brak nadchodzących zebrań @@ -562,7 +564,7 @@ Jeszcze %1$d dodatkowe zebranie Jeszcze %1$d dodatkowe zebrania Jeszcze %1$d dodatkowych zebrań - Jeszcze %1$d dodatkowych zebrań + Jeszcze %1$d zebrań Wystąpił błąd podczas ładowania danych Brak @@ -590,6 +592,7 @@ Tak Nie Zapisz + Tytuł Brak lekcji Wybierz motyw diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 09b8a146d..4e41088fd 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -402,6 +402,8 @@ У вас %1$d новая конференция У вас %1$d новых конференций + Present at conference + Agenda Объявления школ Нет объявлений о школе @@ -590,6 +592,7 @@ Да Нет Сохранить + Title Нет уроков Выбрать тему diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index ad078e33f..75a42467d 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -402,6 +402,8 @@ Máte %1$d nových stretnutí Máte %1$d nových stretnutí + Present at conference + Agenda Školské oznámenia Žiadne školské oznámenia @@ -516,10 +518,10 @@ Ďalej: Neskôr: - ešte %1$d ďalší lekcia - ešte %1$d ďalšie lekcie - ešte %1$d ďalších lekcií - ešte %1$d ďalších lekcií + ešte %1$d lekcia + ešte %1$d lekcie + ešte %1$d lekcií + ešte %1$d lekcií do %1$s Žiadne nadchádzajúce lekcie @@ -528,10 +530,10 @@ Žiadne domáce úlohy do vykonaná Pri načítaní domácich úloh došlo k chybe - Ešte %1$d ďalšia domáca úloha - Ešte %1$d ďalšie domáce úlohy - Ešte %1$d ďalších domácich úloh - Ešte %1$d ďalších domácich úloh + Ešte %1$d domáca úloha + Ešte %1$d domáce úlohy + Ešte %1$d domácich úloh + Ešte %1$d domácich úloh do %1$s Posledné známky @@ -541,28 +543,28 @@ Žiadne aktuálne oznámenia Pri načítaní oznámení došlo k chybe - Ešte %1$d ďalšie oznámenie - Ešte %1$d ďalšie oznámenia - Ešte %1$d ďalších oznámení - Ešte %1$d ďalších oznámení + Ešte %1$d oznámenie + Ešte %1$d oznámenia + Ešte %1$d oznámení + Ešte %1$d oznámení Skúšky Žiadne nadchádzajúce skúšky Pri načítaní skúšok došlo k chybe - Ešte %1$d ďalšia skúška - Ešte %1$d ďalšie skúšky - Ešte %1$d ďalších skúšok - Ešte %1$d ďalších skúšok + Ešte %1$d skúška + Ešte %1$d skúšky + Ešte %1$d skúšok + Ešte %1$d skúšok Stretnutie Žiadna nadchádzajúce stretnutie Pri načítaní stretnutí došlo k chybe - Ešte %1$d ďalšie stretnutie - Ešte %1$d ďalšie stretnutia - Ešte %1$d ďalších stretnutí - Ešte %1$d ďalších stretnutí + Ešte %1$d stretnutie + Ešte %1$d stretnutia + Ešte %1$d stretnutí + Ešte %1$d stretnutí Pri načítaní dát došlo k chybe Žiadne @@ -590,6 +592,7 @@ Áno Nie Uložiť + Title Žiadne lekcie Vybrať motív diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 41f1c300c..51f258818 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -402,6 +402,8 @@ У вас є %1$d нова конференція У вас є %1$d нових конференцій + Present at conference + Agenda Оголошення школи Жодних навчальних оголошень @@ -590,6 +592,7 @@ Так Ні Зберегти + Title Брак уроків Увібрати тему From 5ba8289c8707840ec45b1ba6d182d5052513c027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Thu, 16 Sep 2021 11:59:23 +0200 Subject: [PATCH 196/869] Display info in timetable as-is when lesson has change flag (#1521) --- app/build.gradle | 2 +- .../wulkanowy/ui/modules/timetable/TimetableDialog.kt | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 8e8be8bb1..7436ffcda 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -157,7 +157,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:1.2.2" + implementation "io.github.wulkanowy:sdk:d74acd3989" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableDialog.kt index 3f8622f85..bead8fb2c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableDialog.kt @@ -13,7 +13,6 @@ import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.databinding.DialogTimetableBinding import io.github.wulkanowy.utils.capitalise -import io.github.wulkanowy.utils.decapitalise import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.lifecycleAwareVariable import io.github.wulkanowy.utils.toFormattedString @@ -52,7 +51,7 @@ class TimetableDialog : DialogFragment() { super.onViewCreated(view, savedInstanceState) with(lesson) { - setInfo(info, teacher, canceled, changes) + setInfo(info, canceled, changes) setSubject(subject, subjectOld) setTeacher(teacher, teacherOld) setGroup(group) @@ -80,7 +79,7 @@ class TimetableDialog : DialogFragment() { } @SuppressLint("DefaultLocale") - private fun setInfo(info: String, teacher: String, canceled: Boolean, changes: Boolean) { + private fun setInfo(info: String, canceled: Boolean, changes: Boolean) { with(binding) { when { info.isNotBlank() -> { @@ -102,8 +101,6 @@ class TimetableDialog : DialogFragment() { timetableDialogChangesValue.text = when { canceled && !changes -> "Lekcja odwołana: $info" - changes && teacher.isNotBlank() -> "Zastępstwo: $teacher" - changes && teacher.isBlank() -> "Zastępstwo, ${info.decapitalise()}" else -> info.capitalise() } } From 9cb4754132e5b787b29d47eabe6c8c7990b3e8a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Thu, 16 Sep 2021 12:01:49 +0200 Subject: [PATCH 197/869] Version 1.2.3 --- app/build.gradle | 6 +++--- app/src/main/play/release-notes/pl-PL/default.txt | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 7436ffcda..814fce3e0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,8 +21,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 30 - versionCode 95 - versionName "1.2.2" + versionCode 96 + versionName "1.2.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true @@ -157,7 +157,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:d74acd3989" + implementation "io.github.wulkanowy:sdk:1.2.3" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index 3456c3d03..7de10a26b 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,8 +1,8 @@ -Wersja 1.2.2 +Wersja 1.2.3 -- naprawiliśmy problem z widocznością zadań w aplikacji gdy widoczne są one na stronie www dziennika (nadal pozostaje błąd z zadaniami widocznymi tylko w oficjalnej aplikacji - czekamy na poprawkę po stronie VULCANa) -- odblokowaliśmy niedzielę w wyborze daty w planie lekcji i innych zakładkach -- przywróciliśmy odnośnik do szczęśliwego numerka w menu Więcej -- naprawiliśmy drobne błędy ze stabilnością i wyglądem +- naprawiliśmy pomieszane imiona nauczycieli z salami w planie lekcji +- dodaliśmy brakujące okienka ze szczegółami na ekranie zebrań +- klikając w kafelek z lekcjami na jutro aplikacja teraz przekierowuje na ekran z planem na jutro +- naprawiliśmy błąd przy wylogowywaniu innego niż bieżący uczeń Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases From 6e5481f3459981d20ac989ca214cad6123528de3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Mon, 20 Sep 2021 11:38:13 +0200 Subject: [PATCH 198/869] Upgrade Gradle Play Publisher to 3.6.0 (#1526) --- .github/workflows/deploy-store.yml | 10 ++++------ app/build.gradle | 7 +++++-- app/key.p12.gpg | Bin 5129 -> 0 bytes build.gradle | 2 +- 4 files changed, 10 insertions(+), 9 deletions(-) delete mode 100644 app/key.p12.gpg diff --git a/.github/workflows/deploy-store.yml b/.github/workflows/deploy-store.yml index 8015ef640..068b9c106 100644 --- a/.github/workflows/deploy-store.yml +++ b/.github/workflows/deploy-store.yml @@ -28,15 +28,14 @@ jobs: SERVICES_ENCRYPT_KEY: ${{ secrets.SERVICES_ENCRYPT_KEY }} run: | gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/google-services.json.gpg - gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/key.p12.gpg gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg - name: Upload apk to google play env: + PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }} PLAY_KEY_ALIAS: ${{ secrets.PLAY_KEY_ALIAS }} PLAY_KEY_PASSWORD: ${{ secrets.PLAY_KEY_PASSWORD }} - PLAY_SERVICE_ACCOUNT_EMAIL: ${{ secrets.PLAY_SERVICE_ACCOUNT_EMAIL }} - PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }} - run: ./gradlew publishPlayRelease -PenableFirebase --stacktrace; + ANDROID_PUBLISHER_CREDENTIALS: ${{ secrets.ANDROID_PUBLISHER_CREDENTIALS }} + run: ./gradlew publishPlayReleaseApps -PenableFirebase --stacktrace; deploy-app-gallery: name: Deploy to AppGallery @@ -60,7 +59,6 @@ jobs: SERVICES_ENCRYPT_KEY: ${{ secrets.SERVICES_ENCRYPT_KEY }} run: | gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/agconnect-services.json.gpg - gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/key.p12.gpg gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg - name: Prepare credentials env: @@ -68,7 +66,7 @@ jobs: run: echo $AGC_CREDENTIALS > ./app/src/release/agconnect-credentials.json - name: Build and publish HMS version env: + PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }} PLAY_KEY_ALIAS: ${{ secrets.PLAY_KEY_ALIAS }} PLAY_KEY_PASSWORD: ${{ secrets.PLAY_KEY_PASSWORD }} - PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }} run: ./gradlew bundleHmsRelease --stacktrace && ./gradlew publishHuaweiAppGalleryHmsRelease --stacktrace diff --git a/app/build.gradle b/app/build.gradle index 814fce3e0..466924b0f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -96,6 +96,10 @@ android { } } + playConfigs { + play { enabled.set(true) } + } + buildFeatures { viewBinding true } @@ -130,11 +134,10 @@ kapt { } play { - serviceAccountEmail = System.getenv("PLAY_SERVICE_ACCOUNT_EMAIL") ?: "jan@fakelog.cf" - serviceAccountCredentials = file('key.p12') defaultToAppBundles = false track = 'production' updatePriority = 3 + enabled.set(false) } huaweiPublish { diff --git a/app/key.p12.gpg b/app/key.p12.gpg deleted file mode 100644 index e9b6d06ebe2c5f9e8f062549a09b948be52be482..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5129 zcmeC-W#MFITEnt&@~l4PN&hdsVRSLL5xZsG&kIirLW3?%RSMDfJh=Vim4w%BsTwYe zRycjwxR7h3t5}cjqdyq(syKH@*w*0lLu>Yac8 zb$NfD5ZPK@ay~smie78 zAIj6Vnm_m?daE*0ivP2#%KqaflA>>`BX0{===_>!!QaKU^Y7!00t#Mhud`iPviZ33 z!B37_g_C^i_%^kcbqc@UxU=$l_nniG>u={Au3_p4j}P&^dstqGMJ3xTL*f0|P8*Xo zh3=jgJNcX1f_Wy+RCZ)dHe3H8`h7&Hit(YF0xKrkCSJ0$c&WWLu#@4puAxYtklpw7 zQ%)RV5MI6ZQ|G;HxATdj-4r|eokw||FE&pfx|Hl8OcUAj7>J(6QgwnpTLux=H< zmvidw?^B=UizOb_&6}Rp6?*pH_xWx=9X4*{6ngOCi(cq=w#D10dDJx4Pi4ti>#$v6 zHj^cf!qpD<;DbTC_Ldx#|K}j-5I7^m`|r{dMYT)Ak{bLc^G>XPeSAi`|A|}AZicLX zBh1;fP*$+|PJ6^0sV83V3VEN)hznUIRIKCUT(qcmQP8#0sS|iQuBfkT*e0D`^LNFT zvRll9#mcA+GcEzdmi;{Qlonhw_`2D-J?9+GVo)echRplp4wdC|} zEZS>iwNvugwvMz2xc~f9dD58~$>i6rIAljOFgRwC9Gb7nbgr zBmTX;`eNMD%%~UP=J);zR+zizes|E)=_>yI_Rbx-@9k=T-%S-eSi?8%>SVsU930J%nBFHYnO`t{y3HW;`OzXDOvS~hnp2o7uNpR`Juk!zSmE~3kh4ooA-xFo}b@8 zmzn=UK+2{q0o&iJeTjXOvxqj$5g`**QJI!}*S0 z(lwd%o^rh!w-a~Y^0M|@c}_hx-^=~uf^#|62mDvpw90d=vsfSEqq11@Z?(}9>6u42 zyt2wX+#?^N_$1=ZB9rxT-)c8`nZG#@{zPTQw@0>|rjZig^0z6p1^)ke@yk5_HEX8K zwA_98^POo-ftekV-wooW7BV(_t}=S5*c87lb?#HK)BZAdOV~Gc8Q-j5_1fpy)~iCX zSv)pLcS6OK!Y-PxxpqWaDvs+p(MTzx=7ZYkX(4XT5q7 zULi8+fo{%<!n$v1-kGUp# z9^Gvznf(6Bf8j$<6;$t39^Cq;cR}>$Qx`S7`xtq*turin-?Fp$3(NcuON3TOe{3lU zyYj34W7wH6kxK^J4L&FAV`Bbao43?0!PwREix``%@r~8Cb5FlosUG7|d#4=Hu#uM9ci%j}?Y#eX^)G@g z3zMSP&)?g8SM*H5(t_q`NB%ZR2)E6@VI0a(o$mbi%lvnTm+<+s_#4)J{5aFI`D@O% zHR}Y#4*ig_4H0`#ul%U3$ytJBh47(SXYFQG_j4WyEPhe;zOH-4)9I&pmkC7PHVF7J z&AV{=IriL>`Q{O_&%7TUYq8mAcv3t_&3V$9^>6gguGQwPIcR+_g=OT~0 zdDC}YN$!&0JcDDk{*thqWpkJ{4R+}4Vb~}6*1F%R=5&C!44cLJ;(f>G&Eu4~llN5V z@x50ih1aey7?y6T*Uj*L+r3HH`Qx0*t0$+e_x9ebDOHb{Tb)ND2T@LS+mAsPX3Rh?9`iYlqs+pXx zI9G4ym(+>DC-?upnYQhH{EC&kCZ9MPd*h&HS>`|X%d$)6r2PFg<%EQ@b71n4E0Qyk z4!5qJTqhJQUG}~_XWOl9`<6IqxIf#m$JzD8F6ZSh<_87+=)4w^mhfTsT;DIvEAH8E zF5PnEo>-@D`eBtd%PN-(?c1HV^w^b99WH&@3s3(mdI(FD9^h1#+O+@kvxWPD?=GqF zFX-64p*h=&Ti@WVI8XM}Kuz6mvC5)XrzCD=tnQb4&ssT!Yl^(Nt+v>?+w11|m1&zf zifgO$Jh$HvYTa`0uY(2j$H+b^D z&pp`snDvtKNA-(-bwUqZYqK9MS?;VI(_F=W_wdV!$I9LBT)omiBXq+p`^Zb7oV#A^ zsVZ^pw73xy&8;B1!G%{vV&2L!LFuk*3ybY%e~DiZUHgr<(J{ltNm1b2fpc59Y`(MJ zGno-{~wRDlfO?nyrJg6E`4p54eRD692YIGsjJ$d-}3yyjTiR(E7oP^A6eyH z$RH=8R{2*?`eHT9v#>+kwG~Cgl_>XS2_G6~6D> zEG^Crp?edQrKl1%KdyeC^zEvu_QvLaV@m<{Q5ckt6 zE;qMLVZxlHjN4hB9Ge`WpUwHY^#W5%%o5r86JJDBzp@tX%a6J(6O+vv665gPWp%QT zyvIy`d#me-?JQ6Kom-Zmc2+z-PGA*pY(#|evBx~}-FarM{BF(rw&eW&*7?Zt_Oy~p zvA9LwGg#T*%)R;2gFSa!$@WhQ#Y~Z~wHPd$Ms$3k&aQL(Q74 zspgOSlOIUwCv7oqU&CORxuuI|tJsfiW&*yIKf9KQoV31Ha#Lg55`|k_dDrjudWkVy zs9d4>j$2>(7YH zJb3OkvqL_A^@&^_ zw-vNxCpjgpNjtgI@=%iFj1LLsHM!=+7m^E)yZyXi^St-DrpXK4^xa3j zEZN0;*e37v*!%UIRaCfNKktV5Ut``eO#OTG*Yqe>VT15E-<26IpSc=dFT4=7eG2m- zyW4Z#{=FmYvusxLtJ7DwckGhr5I7}nbVXs+&UP7tQ;$4K(!1C!_oRF@&YUr2p5E=e zjf!IbojfJ-6Zu3I?<#lx>U_HMwU?9RM$7%u%!MgiyH>izZb-P>ed*$qPg{?y-(ElI zMBAa+4_udh)2rXO+WN}5jq_pPY%rP9VS z{vv)CMf&EsPu7*RS9&_{%KYSS>wQ$e>)g0m|JdB;js3^WvR&b)j9osjvs@Z+F5Gm* ztYE&ZlNZjLybZa~n|}V+wd&yQ(9Vmp@aW`|zuD(%#p5ug+XEW4RPtke-ZT zY=K4prbVB)1RuNp|9w#H@r2y7%rXInt1Sa=t}0&AxO;+NXWR32Cv1OrocMD8{mjw> zJ!`{P7k~H8>vdpXFx!pkw8p2A59hejB}>>g#WIx_vi7wUH~*lj%~R>%X_( zs=C9=t2B#*13Z@VEL1e+=TAPSbbIv|4ed__(pwj5{4y~Jage^UTx;PmXJO5^i}$aJ zvf|z&8hBUoX?mb?Q2&X2M)G_Ldjh__-QpX(c}b?stLLY_C$lxOPUD=sJU@T-rz`jG zNG5EHXk9qRMm3&)&W40vS6^xGvDsi4w&&!NFvkB?AB%Nb!}$&#_71xFqUH3i*(b7s zzV)QVoZc<6ZStAMQ_Q@)9Q$VWI6w9a6W_T0#75g{-k^*%6GiSM$$krZcUPsVJzqrX zpWn&jD`p9Yd}y4ZqqkzW`^2DW-8tLrQm$r}@F~l6OMSYe=$-aP?%NcLyd5`T zS$%Sq%&fE1T?}&`eR!UB**3_mB)V^5?UcD!Uo1}1n4S+?v%J7wlO7G_}4>^<8N4RMjSl--)ef5uIO3)PoL+ySWj&F zKBI7jl&YgLSL45rrkhx^GFIIBB4Kf+VMdxll|0AP%S!gkW-sk9u&-owUzd>J*;2Cg zM95a(^K&Xxo%0N4vgawRF7H0SHRJTRFE5$mwcS+xudXq2*U`C9W#8a3VNR{PLPU1G z+Ous^dw$s|{yge-dsCUo!;G0pRc|7luTMRHEoHx|ctVi%R_pqe`POXZnLDN4&6V7u z9=W41jVV;lW(wbyDaV~ozskzryxg(sRVC*O!@c&CD%*TG(&Hssg%(-<%y+P<*;^EP zp35X}9Use^dFnSf-R51(nDII$*0^R;p!-Ip-EQZ${nHVi*;hT)e^tVQZo>m}?Ejou z{jucQ(NqtOn926C6E@oXS=w%*eQnl00pUl!66#EEnjM*5<>@SQtT5a^<+A?2sa7mS zZizfg4s`}EpQ0bBcfaVsmjzqC&OOB5)TdG%ocn86fL!PKXk#87rw=;~w?-e3Pu@5G zn{ED^p!GRckCt(juMTXtVzSh0p1IB;;mIFP!Fvxv&fb-l>-ra$_VCWE*@^oc!?!z4 zexM`uBcmm29=qq3?G~3FKc2k2W4}y$@X|w34}EJ>5BD8r{h+hd()|vnqJbu5~ zU~9^B-2d5cqB~p7*BO<+#F-aA`?cL5Qd}p;d>h-0*1HetpJ~V3Og-cu zrm{VGz2+B_O@&WFl+OBREELMxaj&swz0%ThW^JJx_R|{kInO%ge{o}sN_^6Vzk%3$1XZq-|^| z&!0V6VKwoF`jq6w?y?VmUw7m#y485J@P~~`$v!{fPurthc-|k}ZenRPSI8k|Q{9ZJ zpRbp1IwyJ7<)g@t&5<<|@=H1Q{+?{*y>Gou@fz>``Ztx?B@dn*T`3N$OSr diff --git a/build.gradle b/build.gradle index 7fb02eb9d..e425974d8 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { classpath 'com.google.gms:google-services:4.3.10' classpath 'com.huawei.agconnect:agcp:1.6.0.300' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1' - classpath "com.github.triplet.gradle:play-publisher:2.8.0" + classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.3.0" classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3" classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0" From 36daa7ccc1c25878fdcaf5f8475ecd6e6240745f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Wed, 22 Sep 2021 09:25:16 +0200 Subject: [PATCH 199/869] Always include all language resources in app bundle (#1527) --- app/build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index 466924b0f..bcb84c19b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -104,6 +104,12 @@ android { viewBinding true } + bundle { + language { + enableSplit = false + } + } + testOptions.unitTests { includeAndroidResources = true } From 6615e684303bc0938ed8e706ccd400cdc6877855 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Sep 2021 09:25:54 +0200 Subject: [PATCH 200/869] Bump kotlin_version from 1.5.30 to 1.5.31 (#1528) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e425974d8..15d3c792b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - kotlin_version = '1.5.30' + kotlin_version = '1.5.31' about_libraries = '8.9.1' hilt_version = "2.38.1" } From a43ffcdef482a295623dee33fb67e1c7e177aea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Fri, 24 Sep 2021 21:02:51 +0200 Subject: [PATCH 201/869] Display bad credentials error in the message box above login form (#1525) --- app/build.gradle | 2 +- .../ui/modules/login/LoginActivity.kt | 5 +- .../ui/modules/login/LoginErrorHandler.kt | 4 +- .../login/advanced/LoginAdvancedFragment.kt | 67 +++++++++++++------ .../login/advanced/LoginAdvancedPresenter.kt | 4 +- .../login/advanced/LoginAdvancedView.kt | 2 +- .../modules/login/form/LoginFormFragment.kt | 48 ++++++++++--- .../modules/login/form/LoginFormPresenter.kt | 2 +- .../ui/modules/login/form/LoginFormView.kt | 2 +- .../main/res/layout/fragment_login_form.xml | 28 +++++--- app/src/main/res/values-cs/strings.xml | 3 +- app/src/main/res/values-de/strings.xml | 3 +- app/src/main/res/values-pl/strings.xml | 3 +- app/src/main/res/values-ru/strings.xml | 3 +- app/src/main/res/values-sk/strings.xml | 3 +- app/src/main/res/values-uk/strings.xml | 3 +- app/src/main/res/values/strings.xml | 3 +- 17 files changed, 126 insertions(+), 59 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index bcb84c19b..cea9ae810 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -166,7 +166,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:1.2.3" + implementation "io.github.wulkanowy:sdk:8f3721f1f9" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt index 8d96a498f..10f6c0737 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt @@ -103,9 +103,8 @@ class LoginActivity : BaseActivity(), Logi } override fun notifyInitSymbolFragment(loginData: Triple) { - (loginAdapter.getFragmentInstance(1) as? LoginSymbolFragment)?.onParentInitSymbolFragment( - loginData - ) + (loginAdapter.getFragmentInstance(1) as? LoginSymbolFragment) + ?.onParentInitSymbolFragment(loginData) } override fun notifyInitStudentSelectFragment(studentsWithSemesters: List) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt index ed4563246..2f76cd51e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt @@ -13,7 +13,7 @@ import javax.inject.Inject class LoginErrorHandler @Inject constructor(resources: Resources) : ErrorHandler(resources) { - var onBadCredentials: () -> Unit = {} + var onBadCredentials: (String?) -> Unit = {} var onInvalidToken: (String) -> Unit = {} @@ -25,7 +25,7 @@ class LoginErrorHandler @Inject constructor(resources: Resources) : ErrorHandler override fun proceed(error: Throwable) { when (error) { - is BadCredentialsException -> onBadCredentials() + is BadCredentialsException -> onBadCredentials(error.message) is SQLiteConstraintException -> onStudentDuplicate(resources.getString(R.string.login_duplicate_student)) is TokenDeadException -> onInvalidToken(resources.getString(R.string.login_expired_token)) is InvalidTokenException -> onInvalidToken(resources.getString(R.string.login_invalid_token)) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt index 9231914c8..0672d75fa 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt @@ -51,10 +51,12 @@ class LoginAdvancedFragment : private lateinit var hostSymbols: Array override val formHostValue: String - get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty() + get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())) + .orEmpty() override val formHostSymbol: String - get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty() + get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())) + .orEmpty() override val formPinValue: String get() = binding.loginFormPin.text.toString().trim() @@ -92,39 +94,62 @@ class LoginAdvancedFragment : loginFormSignIn.setOnClickListener { presenter.onSignInClick() } loginTypeSwitch.setOnCheckedChangeListener { _, checkedId -> - presenter.onLoginModeSelected(when (checkedId) { - R.id.loginTypeApi -> Sdk.Mode.API - R.id.loginTypeScrapper -> Sdk.Mode.SCRAPPER - else -> Sdk.Mode.HYBRID - }) + presenter.onLoginModeSelected( + when (checkedId) { + R.id.loginTypeApi -> Sdk.Mode.API + R.id.loginTypeScrapper -> Sdk.Mode.SCRAPPER + else -> Sdk.Mode.HYBRID + } + ) } loginFormPin.setOnEditorDoneSignIn { loginFormSignIn.callOnClick() } loginFormPass.setOnEditorDoneSignIn { loginFormSignIn.callOnClick() } - loginFormSymbol.setAdapter(ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, resources.getStringArray(R.array.symbols_values))) + loginFormSymbol.setAdapter( + ArrayAdapter( + requireContext(), + android.R.layout.simple_list_item_1, + resources.getStringArray(R.array.symbols_values) + ) + ) } with(binding.loginFormHost) { setText(hostKeys.getOrNull(0).orEmpty()) - setAdapter(LoginSymbolAdapter(context, R.layout.support_simple_spinner_dropdown_item, hostKeys)) + setAdapter( + LoginSymbolAdapter( + context, + R.layout.support_simple_spinner_dropdown_item, + hostKeys + ) + ) setOnClickListener { if (binding.loginFormContainer.visibility == GONE) dismissDropDown() } } } override fun showMobileApiWarningMessage() { - binding.loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_mobile_api) + binding.loginFormAdvancedWarningInfo.text = + getString(R.string.login_advanced_warning_mobile_api) } override fun showScraperWarningMessage() { - binding.loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_scraper) + binding.loginFormAdvancedWarningInfo.text = + getString(R.string.login_advanced_warning_scraper) } override fun showHybridWarningMessage() { - binding.loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_hybrid) + binding.loginFormAdvancedWarningInfo.text = + getString(R.string.login_advanced_warning_hybrid) } - override fun setDefaultCredentials(username: String, pass: String, symbol: String, token: String, pin: String) { + override fun setDefaultCredentials( + username: String, + pass: String, + symbol: String, + token: String, + pin: String + ) { with(binding) { loginFormUsername.setText(username) loginFormPass.setText(pass) @@ -177,10 +202,10 @@ class LoginAdvancedFragment : } } - override fun setErrorPassIncorrect() { + override fun setErrorPassIncorrect(message: String?) { with(binding.loginFormPassLayout) { requestFocus() - error = getString(R.string.login_incorrect_password) + error = message ?: getString(R.string.login_incorrect_password) } } @@ -296,11 +321,13 @@ class LoginAdvancedFragment : } override fun notifyParentAccountLogged(studentsWithSemesters: List) { - (activity as? LoginActivity)?.onFormFragmentAccountLogged(studentsWithSemesters, Triple( - binding.loginFormUsername.text.toString(), - binding.loginFormPass.text.toString(), - resources.getStringArray(R.array.hosts_values)[1] - )) + (activity as? LoginActivity)?.onFormFragmentAccountLogged( + studentsWithSemesters, Triple( + binding.loginFormUsername.text.toString(), + binding.loginFormPass.text.toString(), + resources.getStringArray(R.array.hosts_values)[1] + ) + ) } override fun onResume() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt index 891a6b0bb..17d8c5ecb 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt @@ -34,9 +34,9 @@ class LoginAdvancedPresenter @Inject constructor( } } - private fun onBadCredentials() { + private fun onBadCredentials(message: String?) { view?.run { - setErrorPassIncorrect() + setErrorPassIncorrect(message) showSoftKeyboard() Timber.i("Entered wrong username or password") } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedView.kt index 029a6b4d4..1d2b2856d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedView.kt @@ -49,7 +49,7 @@ interface LoginAdvancedView : BaseView { fun setErrorPassInvalid(focus: Boolean) - fun setErrorPassIncorrect() + fun setErrorPassIncorrect(message: String?) fun clearUsernameError() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt index e383072ec..6e0294a4e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt @@ -5,6 +5,7 @@ import android.os.Bundle import android.view.View import android.view.View.GONE import android.view.View.VISIBLE +import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -41,10 +42,12 @@ class LoginFormFragment : BaseFragment(R.layout.fragme get() = binding.loginFormPass.text.toString() override val formHostValue: String - get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty() + get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())) + .orEmpty() override val formHostSymbol: String - get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty() + get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())) + .orEmpty() override val nicknameLabel: String get() = getString(R.string.login_nickname_hint) @@ -88,7 +91,13 @@ class LoginFormFragment : BaseFragment(R.layout.fragme with(binding.loginFormHost) { setText(hostKeys.getOrNull(0).orEmpty()) - setAdapter(LoginSymbolAdapter(context, R.layout.support_simple_spinner_dropdown_item, hostKeys)) + setAdapter( + LoginSymbolAdapter( + context, + R.layout.support_simple_spinner_dropdown_item, + hostKeys + ) + ) setOnClickListener { if (binding.loginFormContainer.visibility == GONE) dismissDropDown() } } } @@ -142,24 +151,31 @@ class LoginFormFragment : BaseFragment(R.layout.fragme } } - override fun setErrorPassIncorrect() { - with(binding.loginFormPassLayout) { - error = getString(R.string.login_incorrect_password) + override fun setErrorPassIncorrect(message: String?) { + val error = message ?: getString(R.string.login_incorrect_password_default) + + with(binding) { + loginFormUsernameLayout.error = " " + loginFormPassLayout.error = " " + loginFormErrorBox.text = getString(R.string.login_incorrect_password, error) + loginFormErrorBox.isVisible = true } } override fun setErrorEmailInvalid(domain: String) { with(binding.loginFormUsernameLayout) { - error = getString(R.string.login_invalid_custom_email,domain) + error = getString(R.string.login_invalid_custom_email, domain) } } override fun clearUsernameError() { binding.loginFormUsernameLayout.error = null + binding.loginFormErrorBox.isVisible = false } override fun clearPassError() { binding.loginFormPassLayout.error = null + binding.loginFormErrorBox.isVisible = false } override fun showSoftKeyboard() { @@ -183,12 +199,18 @@ class LoginFormFragment : BaseFragment(R.layout.fragme binding.loginFormVersion.text = "v${appInfo.versionName}" } - override fun notifyParentAccountLogged(studentsWithSemesters: List, loginData: Triple) { + override fun notifyParentAccountLogged( + studentsWithSemesters: List, + loginData: Triple + ) { (activity as? LoginActivity)?.onFormFragmentAccountLogged(studentsWithSemesters, loginData) } override fun openPrivacyPolicyPage() { - context?.openInternetBrowser("https://wulkanowy.github.io/polityka-prywatnosci.html", ::showMessage) + context?.openInternetBrowser( + "https://wulkanowy.github.io/polityka-prywatnosci.html", + ::showMessage + ) } override fun showContact(show: Boolean) { @@ -210,7 +232,10 @@ class LoginFormFragment : BaseFragment(R.layout.fragme } override fun openFaqPage() { - context?.openInternetBrowser("https://wulkanowy.github.io/czesto-zadawane-pytania/dlaczego-nie-moge-sie-zalogowac", ::showMessage) + context?.openInternetBrowser( + "https://wulkanowy.github.io/czesto-zadawane-pytania/dlaczego-nie-moge-sie-zalogowac", + ::showMessage + ) } override fun onResume() { @@ -223,7 +248,8 @@ class LoginFormFragment : BaseFragment(R.layout.fragme chooserTitle = requireContext().getString(R.string.login_email_intent_title), email = "wulkanowyinc@gmail.com", subject = requireContext().getString(R.string.login_email_subject), - body = requireContext().getString(R.string.login_email_text, + body = requireContext().getString( + R.string.login_email_text, "${appInfo.systemManufacturer} ${appInfo.systemModel}", appInfo.systemVersion.toString(), appInfo.versionName, diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt index d79c422d4..bd876b849 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt @@ -30,7 +30,7 @@ class LoginFormPresenter @Inject constructor( showVersion() loginErrorHandler.onBadCredentials = { - setErrorPassIncorrect() + setErrorPassIncorrect(it) showSoftKeyboard() Timber.i("Entered wrong username or password") } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt index 079629ef6..efdaa082b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt @@ -37,7 +37,7 @@ interface LoginFormView : BaseView { fun setErrorPassInvalid(focus: Boolean) - fun setErrorPassIncorrect() + fun setErrorPassIncorrect(message: String?) fun setErrorEmailInvalid(domain: String) diff --git a/app/src/main/res/layout/fragment_login_form.xml b/app/src/main/res/layout/fragment_login_form.xml index 06d1fa5e9..d1c997ff8 100644 --- a/app/src/main/res/layout/fragment_login_form.xml +++ b/app/src/main/res/layout/fragment_login_form.xml @@ -110,10 +110,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="32dp" - android:layout_marginLeft="32dp" android:layout_marginTop="32dp" android:layout_marginEnd="32dp" - android:layout_marginRight="32dp" android:gravity="center_horizontal" android:text="@string/login_header_default" android:textSize="16sp" @@ -126,6 +124,20 @@ app:layout_constraintVertical_chainStyle="packed" app:layout_goneMarginTop="64dp" /> + + app:layout_constraintTop_toBottomOf="@+id/loginFormErrorBox" + app:layout_goneMarginTop="48dp"> @@ -217,7 +227,6 @@ android:layout_marginRight="24dp" android:hint="@string/login_host_hint" android:orientation="vertical" - app:layout_constraintBottom_toTopOf="@+id/loginFormAdvancedButton" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/loginFormRecoverLink"> @@ -262,14 +271,13 @@ android:id="@+id/loginFormPrivacyLink" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginTop="24dp" android:gravity="start|center_vertical" android:text="@string/login_privacy_policy" android:textColor="?android:textColorSecondary" android:textSize="12sp" app:fontFamily="sans-serif-medium" app:layout_constraintStart_toStartOf="@id/loginFormAdvancedButton" - app:layout_constraintTop_toBottomOf="@+id/loginFormAdvancedButton" + app:layout_constraintTop_toTopOf="@+id/loginFormVersion" tools:visibility="visible" /> Symbol Přihlásit Toto heslo je příliš krátké - Přihlašovací údaje jsou nesprávné. Ujistěte se, že je v poli níže vybrána správná variace deníku UONET+ + Přihlašovací údaje jsou nesprávné + %1$s. Ujistěte se, že je v poli níže vybrána správná variace deníku UONET+ Neplatný PIN Neplatný token Token vypršel diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index f86c3076a..8c30c7a99 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -42,7 +42,8 @@ Symbol Anmelden Passwort ist zu kurz - Anmeldedaten sind falsch. Stellen Sie sicher, dass die richtige UONET+ Registervariation im unteren Feld ausgewählt ist + Anmeldedaten sind falsch + %1$s. Stellen Sie sicher, dass die richtige UONET+ Registervariation im unteren Feld ausgewählt ist Ungültige PIN Ungültige token Token ist nicht mehr gültig diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index cfc6810e7..6ac94a4ed 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -42,7 +42,8 @@ Symbol Zaloguj To hasło jest za krótkie - Dane logowania są niepoprawne. Upewnij się, że została wybrana odpowiednia odmiana dziennika UONET+ w polu poniżej + Dane logowania są niepoprawne + %1$s. Upewnij się, że poniżej została wybrana odpowiednia odmiana dziennika UONET+ Nieprawidłowy PIN Nieprawidłowy token Token stracił ważność diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 4e41088fd..54ae5e1ba 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -42,7 +42,8 @@ Symbol Войти Слишком короткий пароль - Данные для входа неверны. Убедитесь, что в поле ниже выбран правильный вариант регистра UONET+ + Данные для входа неверны + %1$s. Убедитесь, что в поле ниже выбран правильный вариант регистра UONET+ Неправильный PIN Неверный token Token просрочен diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 75a42467d..b1fd78ae3 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -42,7 +42,8 @@ Symbol Prihlásiť Toto heslo je príliš krátke - Prihlasovacie údaje sú nesprávne. Uistite sa, že je v poli nižšie vybraná správna variácie denníka UONET+ + Prihlasovacie údaje sú nesprávne + %1$s. Uistite sa, že je v poli nižšie vybraná správna variácie denníka UONET+ Neplatný PIN Neplatný token Platnosť tokenu vypršala diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 51f258818..70f40b66c 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -42,7 +42,8 @@ Symbol Увійти Занадто короткий пароль - Дані для входу неправильні. Переконайтеся, що у полі нижче вказано правильний варіант реєстрації UONET+ + Дані для входу неправильні + %1$s. Переконайтеся, що у полі нижче вказано правильний варіант реєстрації UONET+ Неправильний PIN Неправильний token Минув термін дії токену diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index de85614bc..575401100 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -46,7 +46,8 @@ Symbol Sign in Password too short - Login details are incorrect. Make sure the correct UONET+ register variation is selected in the field below + Login details are incorrect + %1$s. Make sure the correct UONET+ register variation is selected below Invalid PIN Invalid token Token expired From 2cb11e443cadde38b90b01f6946644a0f4b3a193 Mon Sep 17 00:00:00 2001 From: Mateusz Idziejczak Date: Sat, 25 Sep 2021 13:46:11 +0200 Subject: [PATCH 202/869] Mark teacher with yellow when new and old are the same (#1529) --- app/build.gradle | 2 +- .../ui/modules/timetable/TimetableAdapter.kt | 2 +- .../ui/modules/timetable/TimetableDialog.kt | 21 +++++++++++++++++-- .../timetablewidget/TimetableWidgetFactory.kt | 2 +- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index cea9ae810..435a2f567 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -166,7 +166,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:8f3721f1f9" + implementation "io.github.wulkanowy:sdk:230d2075df" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt index 87b3362db..4a5a06995 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt @@ -360,7 +360,7 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter { + timetableDialogTeacherValue.run { + visibility = GONE + } + timetableDialogTeacherNewValue.run { + visibility = VISIBLE + text = teacher + } + } teacher.isNotBlank() -> timetableDialogTeacherValue.text = teacher else -> { timetableDialogTeacherTitle.visibility = GONE diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt index 45b79b50f..c8dda23ce 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt @@ -173,7 +173,7 @@ class TimetableWidgetFactory( updateNotCanceledLessonNumberColor(this, lesson) updateNotCanceledSubjectColor(this, lesson) - val teacherChange = lesson.teacherOld.isNotBlank() && lesson.teacher != lesson.teacherOld + val teacherChange = lesson.teacherOld.isNotBlank() updateNotCanceledRoom(this, lesson, teacherChange) updateNotCanceledTeacher(this, lesson, teacherChange) } From de6131f4f574540357537429985e27cc8247b6dc Mon Sep 17 00:00:00 2001 From: Mateusz Idziejczak Date: Sat, 25 Sep 2021 13:46:35 +0200 Subject: [PATCH 203/869] Add transparency to lucky number widget (#1530) --- app/src/main/res/drawable/background_luckynumber_widget.xml | 4 ++-- .../main/res/drawable/background_luckynumber_widget_dark.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/drawable/background_luckynumber_widget.xml b/app/src/main/res/drawable/background_luckynumber_widget.xml index f29744d0c..367c55275 100644 --- a/app/src/main/res/drawable/background_luckynumber_widget.xml +++ b/app/src/main/res/drawable/background_luckynumber_widget.xml @@ -1,6 +1,6 @@ - - + + diff --git a/app/src/main/res/drawable/background_luckynumber_widget_dark.xml b/app/src/main/res/drawable/background_luckynumber_widget_dark.xml index fa15fd857..cb094b57e 100644 --- a/app/src/main/res/drawable/background_luckynumber_widget_dark.xml +++ b/app/src/main/res/drawable/background_luckynumber_widget_dark.xml @@ -1,6 +1,6 @@ - - + + From 9211baf7ec54d1e4774e53340e43a997bead1e6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sat, 25 Sep 2021 14:02:38 +0200 Subject: [PATCH 204/869] Add notification piggyback (#1503) --- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 7 ++++ .../repositories/PreferencesRepository.kt | 21 ++++++---- .../VulcanNotificationListenerService.kt | 24 +++++++++++ .../wulkanowy/services/sync/SyncManager.kt | 24 +++++++---- .../notifications/NotificationsFragment.kt | 41 +++++++++++++++++-- .../notifications/NotificationsPresenter.kt | 22 ++++++++++ .../notifications/NotificationsView.kt | 6 +++ app/src/main/res/values-pl/strings.xml | 4 ++ .../main/res/values/preferences_defaults.xml | 1 + app/src/main/res/values/preferences_keys.xml | 1 + app/src/main/res/values/strings.xml | 4 ++ .../xml/scheme_preferences_notifications.xml | 6 +++ 13 files changed, 145 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/io/github/wulkanowy/services/piggyback/VulcanNotificationListenerService.kt diff --git a/app/build.gradle b/app/build.gradle index 435a2f567..713756583 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -166,7 +166,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:230d2075df" + implementation "io.github.wulkanowy:sdk:49c2071d10" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ad5adaf2e..77ce3506c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -93,6 +93,13 @@ + + + + + ? @@ -230,8 +235,10 @@ class PreferencesRepository @Inject constructor( set(value) = sharedPref.edit().putInt(PREF_KEY_IN_APP_REVIEW_COUNT, value).apply() var inAppReviewDate: LocalDate? - get() = sharedPref.getLong(PREF_KEY_IN_APP_REVIEW_DATE, 0).takeIf { it != 0L }?.toLocalDate() - set(value) = sharedPref.edit().putLong(PREF_KEY_IN_APP_REVIEW_DATE, value!!.toTimestamp()).apply() + get() = sharedPref.getLong(PREF_KEY_IN_APP_REVIEW_DATE, 0).takeIf { it != 0L } + ?.toLocalDate() + set(value) = sharedPref.edit().putLong(PREF_KEY_IN_APP_REVIEW_DATE, value!!.toTimestamp()) + .apply() var isAppReviewDone: Boolean get() = sharedPref.getBoolean(PREF_KEY_IN_APP_REVIEW_DONE, false) diff --git a/app/src/main/java/io/github/wulkanowy/services/piggyback/VulcanNotificationListenerService.kt b/app/src/main/java/io/github/wulkanowy/services/piggyback/VulcanNotificationListenerService.kt new file mode 100644 index 000000000..c7df2dbc1 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/piggyback/VulcanNotificationListenerService.kt @@ -0,0 +1,24 @@ +package io.github.wulkanowy.services.piggyback + +import android.service.notification.NotificationListenerService +import android.service.notification.StatusBarNotification +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.services.sync.SyncManager +import javax.inject.Inject + +@AndroidEntryPoint +class VulcanNotificationListenerService : NotificationListenerService() { + + @Inject + lateinit var syncManager: SyncManager + + @Inject + lateinit var preferenceRepository: PreferencesRepository + + override fun onNotificationPosted(statusBarNotification: StatusBarNotification?) { + if (statusBarNotification?.packageName == "pl.edu.vulcan.hebe" && preferenceRepository.isNotificationPiggybackEnabled) { + syncManager.startOneTimeSyncWorker() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt b/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt index b94d97e33..02d8b964f 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt @@ -57,14 +57,20 @@ class SyncManager @Inject constructor( fun startPeriodicSyncWorker(restart: Boolean = false) { if (preferencesRepository.isServiceEnabled && !now().isHolidays) { - workManager.enqueueUniquePeriodicWork(SyncWorker::class.java.simpleName, if (restart) REPLACE else KEEP, - PeriodicWorkRequestBuilder(preferencesRepository.servicesInterval, MINUTES) + val serviceInterval = preferencesRepository.servicesInterval + + workManager.enqueueUniquePeriodicWork( + SyncWorker::class.java.simpleName, if (restart) REPLACE else KEEP, + PeriodicWorkRequestBuilder(serviceInterval, MINUTES) .setInitialDelay(10, MINUTES) .setBackoffCriteria(EXPONENTIAL, 30, MINUTES) - .setConstraints(Constraints.Builder() - .setRequiredNetworkType(if (preferencesRepository.isServicesOnlyWifi) UNMETERED else CONNECTED) - .build()) - .build()) + .setConstraints( + Constraints.Builder() + .setRequiredNetworkType(if (preferencesRepository.isServicesOnlyWifi) UNMETERED else CONNECTED) + .build() + ) + .build() + ) } } @@ -77,7 +83,11 @@ class SyncManager @Inject constructor( ) .build() - workManager.enqueueUniqueWork("${SyncWorker::class.java.simpleName}_one_time", ExistingWorkPolicy.REPLACE, work) + workManager.enqueueUniqueWork( + "${SyncWorker::class.java.simpleName}_one_time", + ExistingWorkPolicy.REPLACE, + work + ) return workManager.getWorkInfoByIdLiveData(work.id).asFlow() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt index 0fc7e68e7..207d587de 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt @@ -10,9 +10,12 @@ import android.provider.Settings import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog +import androidx.core.app.NotificationManagerCompat import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SwitchPreferenceCompat import androidx.recyclerview.widget.RecyclerView import com.thelittlefireman.appkillermanager.AppKillerManager import com.thelittlefireman.appkillermanager.exceptions.NoActionFoundException @@ -43,6 +46,21 @@ class NotificationsFragment : PreferenceFragmentCompat(), override val titleStringId get() = R.string.pref_settings_notifications_title + override val isNotificationPermissionGranted: Boolean + get() { + val packageNameList = + NotificationManagerCompat.getEnabledListenerPackages(requireContext()) + val appPackageName = requireContext().packageName + + return appPackageName in packageNameList + } + + private val notificationSettingsContract = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + + presenter.onNotificationPermissionResult() + } + override fun initView(showDebugNotificationSwitch: Boolean) { findPreference(getString(R.string.pref_key_notification_debug))?.isVisible = showDebugNotificationSwitch @@ -57,12 +75,11 @@ class NotificationsFragment : PreferenceFragmentCompat(), } } - findPreference(getString(R.string.pref_key_notifications_system_settings))?.run { - setOnPreferenceClickListener { + findPreference(getString(R.string.pref_key_notifications_system_settings)) + ?.setOnPreferenceClickListener { presenter.onOpenSystemSettingsClicked() true } - } } override fun onCreateRecyclerView( @@ -157,6 +174,24 @@ class NotificationsFragment : PreferenceFragmentCompat(), } } + override fun openNotificationPermissionDialog() { + AlertDialog.Builder(requireContext()) + .setTitle(getString(R.string.pref_notification_piggyback_popup_title)) + .setMessage(getString(R.string.pref_notification_piggyback_popup_description)) + .setPositiveButton(getString(R.string.pref_notification_piggyback_popup_positive)) { _, _ -> + notificationSettingsContract.launch(Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS")) + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + setNotificationPiggybackPreferenceChecked(false) + } + .show() + } + + override fun setNotificationPiggybackPreferenceChecked(isChecked: Boolean) { + findPreference(getString(R.string.pref_key_notifications_piggyback))?.isChecked = + isChecked + } + override fun onResume() { super.onResume() preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt index 8366d3094..19d2f5591 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt @@ -31,6 +31,9 @@ class NotificationsPresenter @Inject constructor( ) initView(appInfo.isDebug) } + + checkNotificationPiggybackState() + Timber.i("Settings notifications view was initialized") } @@ -47,6 +50,11 @@ class NotificationsPresenter @Inject constructor( isDebugNotificationEnableKey -> { chuckerCollector.showNotification = isDebugNotificationEnable } + isNotificationPiggybackEnabledKey -> { + if (isNotificationPiggybackEnabled && view?.isNotificationPermissionGranted == false) { + view?.openNotificationPermissionDialog() + } + } } } analytics.logEvent("setting_changed", "name" to key) @@ -59,4 +67,18 @@ class NotificationsPresenter @Inject constructor( fun onOpenSystemSettingsClicked() { view?.openSystemSettings() } + + fun onNotificationPermissionResult() { + view?.run { + setNotificationPiggybackPreferenceChecked(isNotificationPermissionGranted) + } + } + + private fun checkNotificationPiggybackState() { + if (preferencesRepository.isNotificationPiggybackEnabled) { + view?.run { + setNotificationPiggybackPreferenceChecked(isNotificationPermissionGranted) + } + } + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsView.kt index 2ab9b0352..42862b380 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsView.kt @@ -4,6 +4,8 @@ import io.github.wulkanowy.ui.base.BaseView interface NotificationsView : BaseView { + val isNotificationPermissionGranted: Boolean + fun initView(showDebugNotificationSwitch: Boolean) fun showFixSyncDialog() @@ -11,4 +13,8 @@ interface NotificationsView : BaseView { fun openSystemSettings() fun enableNotification(notificationKey: String, enable: Boolean) + + fun openNotificationPermissionDialog() + + fun setNotificationPiggybackPreferenceChecked(isChecked: Boolean) } diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 6ac94a4ed..d1f44e5c2 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -624,6 +624,10 @@ Przejdź do ustawień Pokazuj powiadomienia debugowania Synchronizacja jest wyłączona + Przechwytywanie powiadomień oficjalnej aplikacji + Przechwytywanie powiadomień + Dzięki tej funkcji możesz zyskać namiastkę powiadomień push takich, jak w oficjalnej aplikacji. Wystarczy że zezwolisz Wulkanowemu na odbieranie wszystkich powiadomień w ustawieniach systemowych.\n\nJak to działa?\nGdy dostaniesz powiadomienie w Dzienniczku VULCANa Wulkanowy zostanie o tym powiadomiony (po to te dodatkowe uprawnienia) i uruchomi synchronizację, dzięki czemu będzie mógł wysłać własne powiadomienie.\n\nTYLKO DLA ZAAWANSOWANYCH UŻYTKOWNIKÓW + Przejdź do ustawień Synchronizacja Automatyczna aktualizacja Zawieszona na wakacjach diff --git a/app/src/main/res/values/preferences_defaults.xml b/app/src/main/res/values/preferences_defaults.xml index 1ba9359cd..9286052d8 100644 --- a/app/src/main/res/values/preferences_defaults.xml +++ b/app/src/main/res/values/preferences_defaults.xml @@ -26,6 +26,7 @@ false false 0 + false LUCKY_NUMBER MESSAGES diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index 09dac7002..bae6a617b 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -32,4 +32,5 @@ message_send_is_draft message_send_recipients last_sync_date + notifications_piggyback diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 575401100..af2006037 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -625,6 +625,10 @@ Go to settings Show debug notifications Synchronization is disabled + Capture official app notifications + Capture notifications + With this feature you can gain a substitute of push notifications like in the official app. All you need to do is allow Wulkanowy to receive all notifications in your system settings.\n\nHow it works?\nWhen you get a notification in Dziennik VULCAN, Wulkanowy will be notified (that\'s what these extra permissions are for) and will trigger a sync so that can send its own notification.\n\nFOR ADVANCED USERS ONLY + Go to settings Synchronization Automatic update diff --git a/app/src/main/res/xml/scheme_preferences_notifications.xml b/app/src/main/res/xml/scheme_preferences_notifications.xml index ac88746c0..24b83169b 100644 --- a/app/src/main/res/xml/scheme_preferences_notifications.xml +++ b/app/src/main/res/xml/scheme_preferences_notifications.xml @@ -14,6 +14,12 @@ app:key="@string/pref_key_notifications_upcoming_lessons_enable" app:singleLineTitle="false" app:title="@string/pref_notify_upcoming_lessons_switch" /> + Date: Sat, 25 Sep 2021 15:18:40 +0200 Subject: [PATCH 205/869] Don't stop loading the timetable when error occurs in upcoming lessons notification scheduling (#1532) --- .../TimetableNotificationSchedulerHelper.kt | 67 +++++++++++-------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt index 98bd93eb8..a42a0ab4b 100644 --- a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt +++ b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt @@ -31,6 +31,7 @@ import io.github.wulkanowy.utils.nickOrName import io.github.wulkanowy.utils.toTimestamp import kotlinx.coroutines.withContext import timber.log.Timber +import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalDateTime.now import javax.inject.Inject @@ -57,10 +58,13 @@ class TimetableNotificationSchedulerHelper @Inject constructor( lessons.sortedBy { it.start }.forEachIndexed { index, lesson -> val upcomingTime = getUpcomingLessonTime(index, lessons, lesson) cancelScheduledTo( - upcomingTime..lesson.start, - getRequestCode(upcomingTime, studentId) + range = upcomingTime..lesson.start, + requestCode = getRequestCode(upcomingTime, studentId) + ) + cancelScheduledTo( + range = lesson.start..lesson.end, + requestCode = getRequestCode(lesson.start, studentId) ) - cancelScheduledTo(lesson.start..lesson.end, getRequestCode(lesson.start, studentId)) Timber.d("TimetableNotification canceled: type 1 & 2, subject: ${lesson.subject}, start: ${lesson.start}, student: $studentId") } @@ -82,6 +86,11 @@ class TimetableNotificationSchedulerHelper @Inject constructor( return cancelScheduled(lessons, student) } + if (lessons.firstOrNull()?.date?.isAfter(LocalDate.now().plusDays(2)) == true) { + Timber.d("Timetable notification scheduling skipped - lessons are too far") + return + } + withContext(dispatchersProvider.backgroundThread) { lessons.groupBy { it.date } .map { it.value.sortedBy { lesson -> lesson.start } } @@ -96,26 +105,26 @@ class TimetableNotificationSchedulerHelper @Inject constructor( if (lesson.start > now()) { scheduleBroadcast( - intent, - student.studentId, - NOTIFICATION_TYPE_UPCOMING, - getUpcomingLessonTime(index, active, lesson) + intent = intent, + studentId = student.studentId, + notificationType = NOTIFICATION_TYPE_UPCOMING, + time = getUpcomingLessonTime(index, active, lesson) ) } if (lesson.end > now()) { scheduleBroadcast( - intent, - student.studentId, - NOTIFICATION_TYPE_CURRENT, - lesson.start + intent = intent, + studentId = student.studentId, + notificationType = NOTIFICATION_TYPE_CURRENT, + time = lesson.start ) if (active.lastIndex == index) { scheduleBroadcast( - intent, - student.studentId, - NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION, - lesson.end + intent = intent, + studentId = student.studentId, + notificationType = NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION, + time = lesson.end ) } } @@ -143,17 +152,21 @@ class TimetableNotificationSchedulerHelper @Inject constructor( notificationType: Int, time: LocalDateTime ) { - AlarmManagerCompat.setExactAndAllowWhileIdle( - alarmManager, RTC_WAKEUP, time.toTimestamp(), - PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also { - it.putExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id) - it.putExtra(LESSON_TYPE, notificationType) - }, FLAG_UPDATE_CURRENT) - ) - Timber.d( - "TimetableNotification scheduled: type: $notificationType, subject: ${ - intent.getStringExtra(LESSON_TITLE) - }, start: $time, student: $studentId" - ) + try { + AlarmManagerCompat.setExactAndAllowWhileIdle( + alarmManager, RTC_WAKEUP, time.toTimestamp(), + PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also { + it.putExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id) + it.putExtra(LESSON_TYPE, notificationType) + }, FLAG_UPDATE_CURRENT) + ) + Timber.d( + "TimetableNotification scheduled: type: $notificationType, subject: ${ + intent.getStringExtra(LESSON_TITLE) + }, start: $time, student: $studentId" + ) + } catch (e: IllegalStateException) { + Timber.e(e) + } } } From 7a46ef5f1924dcb7b4a470c9c801bc0a0d07142f Mon Sep 17 00:00:00 2001 From: Piotr Romanowski Date: Sat, 25 Sep 2021 17:19:21 +0200 Subject: [PATCH 206/869] Add calculated average help dialog (#1379) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rafał Borcz --- .../ui/modules/grade/GradeAverageProvider.kt | 10 ++- .../grade/summary/GradeSummaryAdapter.kt | 11 +++- .../grade/summary/GradeSummaryFragment.kt | 28 ++++++++- .../grade/summary/GradeSummaryPresenter.kt | 8 +++ .../modules/grade/summary/GradeSummaryView.kt | 4 ++ .../message/preview/MessagePreviewAdapter.kt | 7 +-- .../github/wulkanowy/utils/GradeExtension.kt | 5 +- app/src/main/res/drawable/ic_help.xml | 10 +++ .../scrollable_header_grade_summary.xml | 61 +++++++++++++++---- .../main/res/values-pl/preferences_values.xml | 2 +- app/src/main/res/values-pl/strings.xml | 6 +- .../main/res/values/preferences_values.xml | 4 +- app/src/main/res/values/strings.xml | 11 ++-- .../wulkanowy/utils/GradeExtensionTest.kt | 16 ++--- 14 files changed, 139 insertions(+), 44 deletions(-) create mode 100644 app/src/main/res/drawable/ic_help.xml diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt index 4a3049721..2784f429f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt @@ -131,7 +131,9 @@ class GradeAverageProvider @Inject constructor( val updatedFirstSemesterGrades = firstSemesterSubject?.grades?.updateModifiers(student).orEmpty() - (updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage(isOptionalArithmeticAverage) + (updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage( + isOptionalArithmeticAverage + ) } else { secondSemesterSubject.average } @@ -147,7 +149,8 @@ class GradeAverageProvider @Inject constructor( return if (!isAnyVulcanAverage || isGradeAverageForceCalc) { val secondSemesterAverage = - secondSemesterSubject.grades.updateModifiers(student).calcAverage(isOptionalArithmeticAverage) + secondSemesterSubject.grades.updateModifiers(student) + .calcAverage(isOptionalArithmeticAverage) val firstSemesterAverage = firstSemesterSubject?.grades?.updateModifiers(student) ?.calcAverage(isOptionalArithmeticAverage) ?: secondSemesterAverage @@ -213,7 +216,8 @@ class GradeAverageProvider @Inject constructor( proposedPoints = "", finalPoints = "", pointsSum = "", - average = if (calcAverage) details.updateModifiers(student).calcAverage(isOptionalArithmeticAverage) else .0 + average = if (calcAverage) details.updateModifiers(student) + .calcAverage(isOptionalArithmeticAverage) else .0 ) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt index 0754361c9..082c847e5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt @@ -10,7 +10,7 @@ import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.databinding.ItemGradeSummaryBinding import io.github.wulkanowy.databinding.ScrollableHeaderGradeSummaryBinding -import io.github.wulkanowy.utils.calcAverage +import io.github.wulkanowy.utils.calcFinalAverage import java.util.Locale import javax.inject.Inject @@ -25,6 +25,10 @@ class GradeSummaryAdapter @Inject constructor( var items = emptyList() + var onCalculatedHelpClickListener: () -> Unit = {} + + var onFinalHelpClickListener: () -> Unit = {} + override fun getItemCount() = items.size + if (items.isNotEmpty()) 1 else 0 override fun getItemViewType(position: Int) = when (position) { @@ -60,7 +64,7 @@ class GradeSummaryAdapter @Inject constructor( val finalItemsCount = items.count { it.finalGrade.matches("[0-6][+-]?".toRegex()) } val calculatedItemsCount = items.count { value -> value.average != 0.0 } val allItemsCount = items.count { !it.subject.equals("zachowanie", true) } - val finalAverage = items.calcAverage( + val finalAverage = items.calcFinalAverage( preferencesRepository.gradePlusModifier, preferencesRepository.gradeMinusModifier ) @@ -83,6 +87,9 @@ class GradeSummaryAdapter @Inject constructor( calculatedItemsCount, allItemsCount ) + + gradeSummaryCalculatedAverageHelp.setOnClickListener { onCalculatedHelpClickListener() } + gradeSummaryFinalAverageHelp.setOnClickListener { onFinalHelpClickListener() } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt index 0ac16fb36..3810902ff 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt @@ -5,6 +5,7 @@ import android.view.View import android.view.View.GONE import android.view.View.INVISIBLE import android.view.View.VISIBLE +import androidx.appcompat.app.AlertDialog import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -48,6 +49,11 @@ class GradeSummaryFragment : } override fun initView() { + with(gradeSummaryAdapter) { + onCalculatedHelpClickListener = presenter::onCalculatedAverageHelpClick + onFinalHelpClickListener = presenter::onFinalAverageHelpClick + } + with(binding.gradeSummaryRecycler) { layoutManager = LinearLayoutManager(context) adapter = gradeSummaryAdapter @@ -55,7 +61,11 @@ class GradeSummaryFragment : with(binding) { gradeSummarySwipe.setOnRefreshListener(presenter::onSwipeRefresh) gradeSummarySwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) - gradeSummarySwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh)) + gradeSummarySwipe.setProgressBackgroundColorSchemeColor( + requireContext().getThemeAttrColor( + R.attr.colorSwipeRefresh + ) + ) gradeSummaryErrorRetry.setOnClickListener { presenter.onRetry() } gradeSummaryErrorDetails.setOnClickListener { presenter.onDetailsClick() } } @@ -107,6 +117,22 @@ class GradeSummaryFragment : binding.gradeSummarySwipe.isRefreshing = show } + override fun showCalculatedAverageHelpDialog() { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.grade_summary_calculated_average_help_dialog_title) + .setMessage(R.string.grade_summary_calculated_average_help_dialog_message) + .setPositiveButton(R.string.all_close) { _, _ -> } + .show() + } + + override fun showFinalAverageHelpDialog() { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.grade_summary_final_average_help_dialog_title) + .setMessage(R.string.grade_summary_final_average_help_dialog_message) + .setPositiveButton(R.string.all_close) { _, _ -> } + .show() + } + override fun onParentLoadData(semesterId: Int, forceRefresh: Boolean) { presenter.onParentViewLoadData(semesterId, forceRefresh) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt index 7adfd7e58..933633dca 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt @@ -135,6 +135,14 @@ class GradeSummaryPresenter @Inject constructor( cancelJobs("load") } + fun onCalculatedAverageHelpClick() { + view?.showCalculatedAverageHelpDialog() + } + + fun onFinalAverageHelpClick() { + view?.showFinalAverageHelpDialog() + } + private fun createGradeSummaryItems(items: List): List { return items .filter { !checkEmpty(it) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryView.kt index 974d91415..156731c31 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryView.kt @@ -33,6 +33,10 @@ interface GradeSummaryView : BaseView { fun showEmpty(show: Boolean) + fun showCalculatedAverageHelpDialog() + + fun showFinalAverageHelpDialog() + fun notifyParentDataLoaded(semesterId: Int) fun notifyParentRefresh() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewAdapter.kt index 421453c94..d75128be1 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewAdapter.kt @@ -79,12 +79,7 @@ class MessagePreviewAdapter @Inject constructor() : val readText = when { recipientCount > 1 -> { - context.resources.getQuantityString( - R.plurals.message_read_by, - message.readBy, - message.readBy, - recipientCount - ) + context.getString(R.string.message_read_by, message.readBy, recipientCount) } message.readBy == 1 -> { context.getString(R.string.message_read, context.getString(R.string.all_yes)) diff --git a/app/src/main/java/io/github/wulkanowy/utils/GradeExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/GradeExtension.kt index 820e7f435..1be3093fe 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/GradeExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/GradeExtension.kt @@ -3,7 +3,7 @@ package io.github.wulkanowy.utils import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.GradeSummary -import io.github.wulkanowy.sdk.scrapper.grades.* +import io.github.wulkanowy.sdk.scrapper.grades.isGradeValid fun List.calcAverage(isOptionalArithmeticAverage: Boolean): Double { val isArithmeticAverage = isOptionalArithmeticAverage && !any { it.weightValue != .0 } @@ -18,8 +18,7 @@ fun List.calcAverage(isOptionalArithmeticAverage: Boolean): Double { return if (denominator != 0.0) counter / denominator else 0.0 } -@JvmName("calcSummaryAverage") -fun List.calcAverage(plusModifier: Double, minusModifier: Double) = asSequence() +fun List.calcFinalAverage(plusModifier: Double, minusModifier: Double) = asSequence() .mapNotNull { if (it.finalGrade.matches("[0-6][+-]?".toRegex())) { when { diff --git a/app/src/main/res/drawable/ic_help.xml b/app/src/main/res/drawable/ic_help.xml new file mode 100644 index 000000000..9c6ba2925 --- /dev/null +++ b/app/src/main/res/drawable/ic_help.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/scrollable_header_grade_summary.xml b/app/src/main/res/layout/scrollable_header_grade_summary.xml index 29657ba1a..049219a98 100644 --- a/app/src/main/res/layout/scrollable_header_grade_summary.xml +++ b/app/src/main/res/layout/scrollable_header_grade_summary.xml @@ -1,5 +1,6 @@ - + android:layout_gravity="center" + android:orientation="horizontal"> + + + + + - + android:layout_gravity="center" + android:orientation="horizontal"> + + + + + Kolory ocen w dzienniku - Średnia ocen z drugiego semestru + Średnia ocen z wybranego semestru Średnia średnich z obu semestrów Średnia wszystkich ocen z całego roku diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index d1f44e5c2..d7a49c11d 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -91,7 +91,11 @@ Ocena końcowa Przewidywana ocena Obliczona średnia + Jak działa Obliczona średnia? + Obliczona średnia to średnia arytmetyczna wyliczona ze średnich przedmiotów. Pozwala ona na poznanie przybliżonej średniej końcowej. Jest obliczana w sposób wybrany przez użytkownika w ustawieniach aplikacji. Zalecane jest, aby wybrać odpowiednią opcję. Wynika to z tego, że sposoby obliczania średnich w szkołach różnią się. Dodatkowo jeśli twoja szkoła podaje średnią z przedmiotów na stronie dziennika aplikacja pobiera je i nie wylicza tych średnich. Można to zmienić wymuszając liczenie średniej w ustawieniach aplikacji.\n\nŚrednia ocen z aktualnie wybranego semestru:\n1. Obliczanie średniej ważonej dla każdego przedmiotu w danym semestrze\n2. Sumowanie obliczonych średnich\n3. Obliczenie średniej arytmetycznej z zsumowanych średnich\n\nŚrednia średnich z obu semestrów:\n1. Obliczanie średniej ważonej dla każdego przedmiotu w semestrze 1 i 2\n2. Obliczanie średniej arytmetycznej z obliczonych średnich semestrów 1 i 2 dla każdego przedmiotu.\n3. Sumowanie obliczonych średnich\n4. Obliczenie średniej arytmetycznej z zsumowanych średnich\n\nŚrednia wszystkich ocen z całego roku:\n1. Obliczanie średniej ważonej z całego roku dla każdego przedmiotu. Średnia końcowa w 1 semestrze jest bez znaczenia.\n3. Sumowanie obliczonych średnich\n4. Obliczenie średniej arytmetycznej z zsumowanych średnich Końcowa średnia + Jak działa Końcowa średnia? + Końcowa średnia to średnia arytmetyczna wyliczona ze wszystkich aktualnie dostępnych ocen końcowych w danym semestrze.\n\nSchemat obliczania składa się z następujących kroków:\n1. Zsumowanie ocen końcowych wystawionych przez nauczycieli\n2. Podzielenie przez liczbę przedmiotów, z których oceny zostały już wystawione z %1$d na %2$d przedmiotów Podsumowanie Klasa @@ -603,7 +607,7 @@ Wygląd i zachowanie aplikacji Domyślny widok - Obliczanie średniej końcoworocznej + Opcje średniej obliczonej Wymuś obliczanie średniej przez aplikację Pokazuj obecność Motyw diff --git a/app/src/main/res/values/preferences_values.xml b/app/src/main/res/values/preferences_values.xml index 9c1a0421a..bd3e8b47d 100644 --- a/app/src/main/res/values/preferences_values.xml +++ b/app/src/main/res/values/preferences_values.xml @@ -100,8 +100,8 @@ - Average of grades only from the 2nd semester - Average of grades from both semesters + Average of grades only from selected semester + Average of averages from both semesters Average of grades from the whole year diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index af2006037..ed2369322 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -101,6 +101,10 @@ Final grade Predicted grade Calculated average + How does Calculated Average work? + The Calculated Average is the arithmetic average calculated from the subjects averages. It allows you to know the approximate final average. It is calculated in a way selected by the user in the application settings. It is recommended that you choose the appropriate option. This is because the calculation of school averages differs. Additionally, if your school reports the average of the subjects on the Vulcan page, the application downloads them and does not calculate these averages. This can be changed by forcing the calculation of the average in the application settings.\n\nAverage of grades only from selected semester:\n1. Calculating the weighted average for each subject in a given semester\n2.Adding calculated averages\n3. Calculation of the arithmetic average of the summed averages\n\nAverage of averages from both semesters:\n1.Calculating the weighted average for each subject in semester 1 and 2\n2. Calculating the arithmetic average of the calculated averages for semesters 1 and 2 for each subject.\n3. Adding calculated averages\n4. Calculation of the arithmetic average of the summed averages\n\nAverage of grades from the whole year:\n1. Calculating weighted average over the year for each subject. The final average in the 1st semester is irrelevant.\n3. Adding calculated averages\n4. Calculating the arithmetic average of summed averages + How does the Final Average work? + The Final Average is the arithmetic average calculated from all currently available final grades in the given semester.\n\nThe calculation scheme consists of the following steps:\n1. Summing up the final grades given by teachers\n2. Divide by the number of subjects that have already been graded Final average from %1$d of %2$d subjects Summary @@ -245,10 +249,7 @@ Only unread Only with attachments Read: %s - - Read by: %1$d of %2$d people - Read by: %1$d of %2$d people - + Read by: %1$d of %2$d people %d message %d messages @@ -603,7 +604,7 @@ App appearance & behavior Default view - Calculation of the end-of-year average + Calculated average options Force average calculation by app Show presence Theme diff --git a/app/src/test/java/io/github/wulkanowy/utils/GradeExtensionTest.kt b/app/src/test/java/io/github/wulkanowy/utils/GradeExtensionTest.kt index 32b1602e9..39d4c3bdb 100644 --- a/app/src/test/java/io/github/wulkanowy/utils/GradeExtensionTest.kt +++ b/app/src/test/java/io/github/wulkanowy/utils/GradeExtensionTest.kt @@ -33,13 +33,15 @@ class GradeExtensionTest { @Test fun calcSummaryAverage() { - assertEquals(3.5, listOf( - createGradeSummary("4"), - createGradeSummary("5+"), - createGradeSummary("5-"), - createGradeSummary("test"), - createGradeSummary("0") - ).calcAverage(0.5, 0.5), 0.005) + assertEquals( + 3.5, listOf( + createGradeSummary("4"), + createGradeSummary("5+"), + createGradeSummary("5-"), + createGradeSummary("test"), + createGradeSummary("0") + ).calcFinalAverage(0.5, 0.5), 0.005 + ) } @Test From a6a1678b47ebc694d89d47a6b090a1f478cbde75 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Sep 2021 15:51:06 +0000 Subject: [PATCH 207/869] Bump core from 1.10.1 to 1.10.2 (#1536) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 713756583..09c9e8e12 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -224,7 +224,7 @@ dependencies { playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' - playImplementation 'com.google.android.play:core:1.10.1' + playImplementation 'com.google.android.play:core:1.10.2' playImplementation 'com.google.android.play:core-ktx:1.8.1' hmsImplementation 'com.huawei.hms:hianalytics:6.2.0.301' From b552dbc9048231f5923aec8f509c0de6e75332cb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Sep 2021 15:56:11 +0000 Subject: [PATCH 208/869] Bump constraintlayout from 2.1.0 to 2.1.1 (#1535) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 09c9e8e12..8414ff5d0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -183,7 +183,7 @@ dependencies { implementation "androidx.recyclerview:recyclerview:1.2.1" implementation "androidx.viewpager:viewpager:1.0.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" - implementation "androidx.constraintlayout:constraintlayout:2.1.0" + implementation "androidx.constraintlayout:constraintlayout:2.1.1" implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0" implementation "com.google.android.material:material:1.4.0" implementation "com.github.wulkanowy:material-chips-input:2.3.1" From dc90549b9da55d606664cfeac516708e4d31c16c Mon Sep 17 00:00:00 2001 From: Mateusz Idziejczak Date: Mon, 27 Sep 2021 17:56:43 +0200 Subject: [PATCH 209/869] Fix hiding last element in messages (#1538) --- app/src/main/res/layout/fragment_message_tab.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/layout/fragment_message_tab.xml b/app/src/main/res/layout/fragment_message_tab.xml index 4a6948f09..3a30cff8a 100644 --- a/app/src/main/res/layout/fragment_message_tab.xml +++ b/app/src/main/res/layout/fragment_message_tab.xml @@ -13,6 +13,8 @@ android:id="@+id/messageTabRecycler" android:layout_width="match_parent" android:layout_height="match_parent" + android:clipToPadding="false" + android:paddingBottom="64dp" tools:listitem="@layout/item_message" /> From d69118b0859640fa8b783b9fb38ca959fba0e3e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Mon, 27 Sep 2021 20:58:25 +0200 Subject: [PATCH 210/869] Add notifications center (#1524) --- .../40.json | 2316 +++++++++++++++++ app/src/main/AndroidManifest.xml | 13 + .../github/wulkanowy/data/RepositoryModule.kt | 6 +- .../github/wulkanowy/data/db/AppDatabase.kt | 13 +- .../wulkanowy/data/db/dao/NotificationDao.kt | 15 + .../data/db/entities/Notification.kt | 27 + .../data/db/migrations/Migration40.kt | 23 + .../{Notification.kt => NotificationData.kt} | 10 +- .../repositories/NotificationRepository.kt | 15 + .../notifications/AppNotificationManager.kt | 145 ++ .../sync/notifications/BaseNotification.kt | 102 - .../NewConferenceNotification.kt | 16 +- .../sync/notifications/NewExamNotification.kt | 16 +- .../notifications/NewGradeNotification.kt | 28 +- .../notifications/NewHomeworkNotification.kt | 16 +- .../NewLuckyNumberNotification.kt | 30 +- .../notifications/NewMessageNotification.kt | 16 +- .../sync/notifications/NewNoteNotification.kt | 16 +- .../NewSchoolAnnouncementNotification.kt | 34 +- .../sync/notifications/NotificationType.kt | 4 +- .../services/sync/works/AttendanceWork.kt | 7 +- .../ui/modules/dashboard/DashboardFragment.kt | 6 + .../modules/dashboard/DashboardPresenter.kt | 5 + .../ui/modules/dashboard/DashboardView.kt | 2 + .../NotificationDebugPresenter.kt | 2 +- .../NotificationsCenterAdapter.kt | 62 + .../NotificationsCenterFragment.kt | 108 + .../NotificationsCenterPresenter.kt | 83 + .../NotificationsCenterView.kt | 23 + .../notifications/NotificationsFragment.kt | 1 + .../layout/fragment_notifications_center.xml | 106 + .../res/layout/item_notifications_center.xml | 70 + .../main/res/menu/action_menu_dashboard.xml | 9 +- app/src/main/res/menu/action_menu_main.xml | 2 +- app/src/main/res/values/strings.xml | 1 + .../services/messaging/AppMessagingService.kt | 57 + 36 files changed, 3186 insertions(+), 219 deletions(-) create mode 100644 app/schemas/io.github.wulkanowy.data.db.AppDatabase/40.json create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/dao/NotificationDao.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/entities/Notification.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration40.kt rename app/src/main/java/io/github/wulkanowy/data/pojos/{Notification.kt => NotificationData.kt} (87%) create mode 100644 app/src/main/java/io/github/wulkanowy/data/repositories/NotificationRepository.kt create mode 100644 app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt delete mode 100644 app/src/main/java/io/github/wulkanowy/services/sync/notifications/BaseNotification.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterAdapter.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterFragment.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterPresenter.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterView.kt create mode 100644 app/src/main/res/layout/fragment_notifications_center.xml create mode 100644 app/src/main/res/layout/item_notifications_center.xml create mode 100644 app/src/play/java/io/github/wulkanowy/services/messaging/AppMessagingService.kt diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/40.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/40.json new file mode 100644 index 000000000..362c7f0e0 --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/40.json @@ -0,0 +1,2316 @@ +{ + "formatVersion": 1, + "database": { + "version": 40, + "identityHash": "e2fba6244951713b4e9b217adc5d1a23", + "entities": [ + { + "tableName": "Students", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrapper_base_url` TEXT NOT NULL, `mobile_base_url` TEXT NOT NULL, `login_type` TEXT NOT NULL, `login_mode` TEXT NOT NULL, `certificate_key` TEXT NOT NULL, `private_key` TEXT NOT NULL, `is_parent` INTEGER NOT NULL, `email` TEXT NOT NULL, `password` TEXT NOT NULL, `symbol` TEXT NOT NULL, `student_id` INTEGER NOT NULL, `user_login_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `student_name` TEXT NOT NULL, `school_id` TEXT NOT NULL, `school_short` TEXT NOT NULL, `school_name` TEXT NOT NULL, `class_name` TEXT NOT NULL, `class_id` INTEGER NOT NULL, `is_current` INTEGER NOT NULL, `registration_date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `nick` TEXT NOT NULL, `avatar_color` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "scrapperBaseUrl", + "columnName": "scrapper_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mobileBaseUrl", + "columnName": "mobile_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginType", + "columnName": "login_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginMode", + "columnName": "login_mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificateKey", + "columnName": "certificate_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "private_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isParent", + "columnName": "is_parent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "user_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "student_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolSymbol", + "columnName": "school_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "school_short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolName", + "columnName": "school_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "className", + "columnName": "class_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCurrent", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registrationDate", + "columnName": "registration_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nick", + "columnName": "nick", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarColor", + "columnName": "avatar_color", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Students_email_symbol_student_id_school_id_class_id", + "unique": true, + "columnNames": [ + "email", + "symbol", + "student_id", + "school_id", + "class_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryName", + "columnName": "diary_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolYear", + "columnName": "school_year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterName", + "columnName": "semester_name", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "semester_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `semester_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Exams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Timetable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `number` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `subjectOld` TEXT NOT NULL, `group` TEXT NOT NULL, `room` TEXT NOT NULL, `roomOld` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacherOld` TEXT NOT NULL, `info` TEXT NOT NULL, `student_plan` INTEGER NOT NULL, `changes` INTEGER NOT NULL, `canceled` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectOld", + "columnName": "subjectOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "room", + "columnName": "room", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roomOld", + "columnName": "roomOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherOld", + "columnName": "teacherOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isStudentPlan", + "columnName": "student_plan", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "changes", + "columnName": "changes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canceled", + "columnName": "canceled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Attendance", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `time_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `excusable` INTEGER NOT NULL, `excuse_status` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeId", + "columnName": "time_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excusable", + "columnName": "excusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excuseStatus", + "columnName": "excuse_status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AttendanceSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `subject_id` INTEGER NOT NULL, `month` INTEGER NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `absence_excused` INTEGER NOT NULL, `absence_for_school_reasons` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `lateness_excused` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subject_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "month", + "columnName": "month", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceExcused", + "columnName": "absence_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceForSchoolReasons", + "columnName": "absence_for_school_reasons", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latenessExcused", + "columnName": "lateness_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Grades", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `entry` TEXT NOT NULL, `value` REAL NOT NULL, `modifier` REAL NOT NULL, `comment` TEXT NOT NULL, `color` TEXT NOT NULL, `grade_symbol` TEXT NOT NULL, `description` TEXT NOT NULL, `weight` TEXT NOT NULL, `weightValue` REAL NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entry", + "columnName": "entry", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "modifier", + "columnName": "modifier", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gradeSymbol", + "columnName": "grade_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weightValue", + "columnName": "weightValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `subject` TEXT NOT NULL, `predicted_grade` TEXT NOT NULL, `final_grade` TEXT NOT NULL, `proposed_points` TEXT NOT NULL, `final_points` TEXT NOT NULL, `points_sum` TEXT NOT NULL, `average` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_predicted_grade_notified` INTEGER NOT NULL, `is_final_grade_notified` INTEGER NOT NULL, `predicted_grade_last_change` INTEGER NOT NULL, `final_grade_last_change` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "predictedGrade", + "columnName": "predicted_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalGrade", + "columnName": "final_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "proposedPoints", + "columnName": "proposed_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalPoints", + "columnName": "final_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pointsSum", + "columnName": "points_sum", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "average", + "columnName": "average", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPredictedGradeNotified", + "columnName": "is_predicted_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFinalGradeNotified", + "columnName": "is_final_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "predictedGradeLastChange", + "columnName": "predicted_grade_last_change", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "finalGradeLastChange", + "columnName": "final_grade_last_change", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradePartialStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `class_average` TEXT NOT NULL, `student_average` TEXT NOT NULL, `class_amounts` TEXT NOT NULL, `student_amounts` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAverage", + "columnName": "class_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAverage", + "columnName": "student_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAmounts", + "columnName": "class_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAmounts", + "columnName": "student_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesPointsStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `others` REAL NOT NULL, `student` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "others", + "columnName": "others", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "student", + "columnName": "student", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradeSemesterStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `amounts` TEXT NOT NULL, `student_grade` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amounts", + "columnName": "amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentGrade", + "columnName": "student_grade", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `recipient_name` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `removed` INTEGER NOT NULL, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `unread_by` INTEGER NOT NULL, `read_by` INTEGER NOT NULL, `content` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipient", + "columnName": "recipient_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "removed", + "columnName": "removed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasAttachments", + "columnName": "has_attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `one_drive_id` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`real_id`))", + "fields": [ + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "oneDriveId", + "columnName": "one_drive_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "real_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `category` TEXT NOT NULL, `category_type` INTEGER NOT NULL, `is_points_show` INTEGER NOT NULL, `points` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryType", + "columnName": "category_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPointsShow", + "columnName": "is_points_show", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Homework", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `attachments` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_done` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "is_done", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Subjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "LuckyNumbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `lucky_number` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "luckyNumber", + "columnName": "lucky_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CompletedLesson", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `topic` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `substitution` TEXT NOT NULL, `absence` TEXT NOT NULL, `resources` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "topic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "substitution", + "columnName": "substitution", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "resources", + "columnName": "resources", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ReportingUnits", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `short` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `roles` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "senderName", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roles", + "columnName": "roles", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` TEXT NOT NULL, `name` TEXT NOT NULL, `real_name` TEXT NOT NULL, `login_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `role` INTEGER NOT NULL, `hash` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "realName", + "columnName": "real_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginId", + "columnName": "login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MobileDevices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Teachers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `short_name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "School", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `contact` TEXT NOT NULL, `headmaster` TEXT NOT NULL, `pedagogue` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contact", + "columnName": "contact", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headmaster", + "columnName": "headmaster", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pedagogue", + "columnName": "pedagogue", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Conferences", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `subject` TEXT NOT NULL, `agenda` TEXT NOT NULL, `present_on_conference` TEXT NOT NULL, `conference_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agenda", + "columnName": "agenda", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presentOnConference", + "columnName": "present_on_conference", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conference_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableAdditional", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StudentInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `full_name` TEXT NOT NULL, `first_name` TEXT NOT NULL, `second_name` TEXT NOT NULL, `surname` TEXT NOT NULL, `birth_date` INTEGER NOT NULL, `birth_place` TEXT NOT NULL, `gender` TEXT NOT NULL, `has_polish_citizenship` INTEGER NOT NULL, `family_name` TEXT NOT NULL, `parents_names` TEXT NOT NULL, `address` TEXT NOT NULL, `registered_address` TEXT NOT NULL, `correspondence_address` TEXT NOT NULL, `phone_number` TEXT NOT NULL, `cell_phone_number` TEXT NOT NULL, `email` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `first_guardian_full_name` TEXT, `first_guardian_kinship` TEXT, `first_guardian_address` TEXT, `first_guardian_phones` TEXT, `first_guardian_email` TEXT, `second_guardian_full_name` TEXT, `second_guardian_kinship` TEXT, `second_guardian_address` TEXT, `second_guardian_phones` TEXT, `second_guardian_email` TEXT)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "full_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondName", + "columnName": "second_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "surname", + "columnName": "surname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "birthDate", + "columnName": "birth_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthPlace", + "columnName": "birth_place", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gender", + "columnName": "gender", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPolishCitizenship", + "columnName": "has_polish_citizenship", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "familyName", + "columnName": "family_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentsNames", + "columnName": "parents_names", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "registeredAddress", + "columnName": "registered_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "correspondenceAddress", + "columnName": "correspondence_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cellPhoneNumber", + "columnName": "cell_phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstGuardian.fullName", + "columnName": "first_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.kinship", + "columnName": "first_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.address", + "columnName": "first_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.phones", + "columnName": "first_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.email", + "columnName": "first_guardian_email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.fullName", + "columnName": "second_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.kinship", + "columnName": "second_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.address", + "columnName": "second_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.phones", + "columnName": "second_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.email", + "columnName": "second_guardian_email", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableHeaders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SchoolAnnouncements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `type` TEXT NOT NULL, `date` INTEGER NOT NULL, `data` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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, 'e2fba6244951713b4e9b217adc5d1a23')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 77ce3506c..84c50523c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -45,6 +45,7 @@ tools:ignore="GoogleAppIndexingWarning,UnusedAttribute"> @@ -74,6 +75,7 @@ @@ -83,6 +85,7 @@ @@ -95,11 +98,20 @@ android:permission="android.permission.BIND_REMOTEVIEWS" /> + + + + + + diff --git a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt b/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt index a1c3cbbb7..f1b719e54 100644 --- a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt +++ b/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt @@ -8,8 +8,8 @@ import androidx.preference.PreferenceManager import com.chuckerteam.chucker.api.ChuckerCollector import com.chuckerteam.chucker.api.ChuckerInterceptor import com.chuckerteam.chucker.api.RetentionManager -import com.squareup.moshi.Moshi import com.fredporciuncula.flow.preferences.FlowSharedPreferences +import com.squareup.moshi.Moshi import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -202,4 +202,8 @@ internal class RepositoryModule { @Singleton @Provides fun provideSchoolAnnouncementDao(database: AppDatabase) = database.schoolAnnouncementDao + + @Singleton + @Provides + fun provideNotificationDao(database: AppDatabase) = database.notificationDao } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt index 0dca9aa18..09aa972f0 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt @@ -10,7 +10,6 @@ import io.github.wulkanowy.data.db.dao.AttendanceDao import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao import io.github.wulkanowy.data.db.dao.CompletedLessonsDao import io.github.wulkanowy.data.db.dao.ConferenceDao -import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao import io.github.wulkanowy.data.db.dao.ExamDao import io.github.wulkanowy.data.db.dao.GradeDao import io.github.wulkanowy.data.db.dao.GradePartialStatisticsDao @@ -23,8 +22,10 @@ import io.github.wulkanowy.data.db.dao.MessageAttachmentDao import io.github.wulkanowy.data.db.dao.MessagesDao import io.github.wulkanowy.data.db.dao.MobileDeviceDao import io.github.wulkanowy.data.db.dao.NoteDao +import io.github.wulkanowy.data.db.dao.NotificationDao import io.github.wulkanowy.data.db.dao.RecipientDao import io.github.wulkanowy.data.db.dao.ReportingUnitDao +import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao import io.github.wulkanowy.data.db.dao.SchoolDao import io.github.wulkanowy.data.db.dao.SemesterDao import io.github.wulkanowy.data.db.dao.StudentDao @@ -38,7 +39,6 @@ import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.AttendanceSummary import io.github.wulkanowy.data.db.entities.CompletedLesson import io.github.wulkanowy.data.db.entities.Conference -import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.GradePartialStatistics @@ -51,9 +51,11 @@ import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.MessageAttachment import io.github.wulkanowy.data.db.entities.MobileDevice import io.github.wulkanowy.data.db.entities.Note +import io.github.wulkanowy.data.db.entities.Notification import io.github.wulkanowy.data.db.entities.Recipient import io.github.wulkanowy.data.db.entities.ReportingUnit import io.github.wulkanowy.data.db.entities.School +import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentInfo @@ -95,6 +97,7 @@ import io.github.wulkanowy.data.db.migrations.Migration37 import io.github.wulkanowy.data.db.migrations.Migration38 import io.github.wulkanowy.data.db.migrations.Migration39 import io.github.wulkanowy.data.db.migrations.Migration4 +import io.github.wulkanowy.data.db.migrations.Migration40 import io.github.wulkanowy.data.db.migrations.Migration5 import io.github.wulkanowy.data.db.migrations.Migration6 import io.github.wulkanowy.data.db.migrations.Migration7 @@ -134,6 +137,7 @@ import javax.inject.Singleton StudentInfo::class, TimetableHeader::class, SchoolAnnouncement::class, + Notification::class ], version = AppDatabase.VERSION_SCHEMA, exportSchema = true @@ -142,7 +146,7 @@ import javax.inject.Singleton abstract class AppDatabase : RoomDatabase() { companion object { - const val VERSION_SCHEMA = 39 + const val VERSION_SCHEMA = 40 fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( Migration2(), @@ -183,6 +187,7 @@ abstract class AppDatabase : RoomDatabase() { Migration37(), Migration38(), Migration39(), + Migration40() ) fun newInstance( @@ -252,4 +257,6 @@ abstract class AppDatabase : RoomDatabase() { abstract val timetableHeaderDao: TimetableHeaderDao abstract val schoolAnnouncementDao: SchoolAnnouncementDao + + abstract val notificationDao: NotificationDao } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/NotificationDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/NotificationDao.kt new file mode 100644 index 000000000..c5ae21bc2 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/NotificationDao.kt @@ -0,0 +1,15 @@ +package io.github.wulkanowy.data.db.dao + +import androidx.room.Dao +import androidx.room.Query +import io.github.wulkanowy.data.db.entities.Notification +import kotlinx.coroutines.flow.Flow +import javax.inject.Singleton + +@Singleton +@Dao +interface NotificationDao : BaseDao { + + @Query("SELECT * FROM Notifications WHERE student_id = :studentId OR student_id = -1") + fun loadAll(studentId: Long): Flow> +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Notification.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Notification.kt new file mode 100644 index 000000000..740137f0f --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Notification.kt @@ -0,0 +1,27 @@ +package io.github.wulkanowy.data.db.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import io.github.wulkanowy.services.sync.notifications.NotificationType +import java.time.LocalDateTime + +@Entity(tableName = "Notifications") +data class Notification( + + @ColumnInfo(name = "student_id") + val studentId: Long, + + val title: String, + + val content: String, + + val type: NotificationType, + + val date: LocalDateTime, + + val data: String? = null +) { + @PrimaryKey(autoGenerate = true) + var id: Long = 0 +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration40.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration40.kt new file mode 100644 index 000000000..6d2795c7c --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration40.kt @@ -0,0 +1,23 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration40 : Migration(39, 40) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + """ + CREATE TABLE IF NOT EXISTS `Notifications` ( + `student_id` INTEGER NOT NULL, + `title` TEXT NOT NULL, + `content` TEXT NOT NULL, + `type` TEXT NOT NULL, + `date` INTEGER NOT NULL, + `data` TEXT, + `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL + ) + """ + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/Notification.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/NotificationData.kt similarity index 87% rename from app/src/main/java/io/github/wulkanowy/data/pojos/Notification.kt rename to app/src/main/java/io/github/wulkanowy/data/pojos/NotificationData.kt index ca2749374..0b4603ef7 100644 --- a/app/src/main/java/io/github/wulkanowy/data/pojos/Notification.kt +++ b/app/src/main/java/io/github/wulkanowy/data/pojos/NotificationData.kt @@ -6,7 +6,7 @@ import androidx.annotation.StringRes import io.github.wulkanowy.services.sync.notifications.NotificationType import io.github.wulkanowy.ui.modules.main.MainView -sealed interface Notification { +sealed interface NotificationData { val type: NotificationType val startMenu: MainView.Section val icon: Int @@ -14,7 +14,7 @@ sealed interface Notification { val contentStringRes: Int } -data class MultipleNotifications( +data class MultipleNotificationsData( override val type: NotificationType, override val startMenu: MainView.Section, @DrawableRes override val icon: Int, @@ -23,9 +23,9 @@ data class MultipleNotifications( @PluralsRes val summaryStringRes: Int, val lines: List, -) : Notification +) : NotificationData -data class OneNotification( +data class OneNotificationData( override val type: NotificationType, override val startMenu: MainView.Section, @DrawableRes override val icon: Int, @@ -33,4 +33,4 @@ data class OneNotification( @StringRes override val contentStringRes: Int, val contentValues: List, -) : Notification +) : NotificationData diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/NotificationRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/NotificationRepository.kt new file mode 100644 index 000000000..36bc7c25a --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/NotificationRepository.kt @@ -0,0 +1,15 @@ +package io.github.wulkanowy.data.repositories + +import io.github.wulkanowy.data.db.dao.NotificationDao +import io.github.wulkanowy.data.db.entities.Notification +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class NotificationRepository @Inject constructor(private val notificationDao: NotificationDao) { + + fun getNotifications(studentId: Long) = notificationDao.loadAll(studentId) + + suspend fun saveNotification(notification: Notification) = + notificationDao.insertAll(listOf(notification)) +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt new file mode 100644 index 000000000..69d0092c1 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt @@ -0,0 +1,145 @@ +package io.github.wulkanowy.services.sync.notifications + +import android.annotation.SuppressLint +import android.app.PendingIntent +import android.content.Context +import android.os.Build +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Notification +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.pojos.MultipleNotificationsData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.data.pojos.OneNotificationData +import io.github.wulkanowy.data.repositories.NotificationRepository +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.AppInfo +import io.github.wulkanowy.utils.getCompatBitmap +import io.github.wulkanowy.utils.getCompatColor +import io.github.wulkanowy.utils.nickOrName +import java.time.LocalDateTime +import javax.inject.Inject +import kotlin.random.Random + +class AppNotificationManager @Inject constructor( + private val notificationManager: NotificationManagerCompat, + @ApplicationContext private val context: Context, + private val appInfo: AppInfo, + private val notificationRepository: NotificationRepository +) { + + suspend fun sendNotification(notificationData: NotificationData, student: Student) = + when (notificationData) { + is OneNotificationData -> sendOneNotification(notificationData, student) + is MultipleNotificationsData -> sendMultipleNotifications(notificationData, student) + } + + private suspend fun sendOneNotification( + notificationData: OneNotificationData, + student: Student + ) { + val content = context.getString( + notificationData.contentStringRes, + *notificationData.contentValues.toTypedArray() + ) + + val title = context.getString(notificationData.titleStringRes) + + val notification = getDefaultNotificationBuilder(notificationData) + .setContentTitle(title) + .setContentText(content) + .setStyle( + NotificationCompat.BigTextStyle() + .setSummaryText(student.nickOrName) + .bigText(content) + ) + .build() + + notificationManager.notify(Random.nextInt(Int.MAX_VALUE), notification) + + saveNotification(title, content, notificationData, student) + } + + private suspend fun sendMultipleNotifications( + notificationData: MultipleNotificationsData, + student: Student + ) { + val groupType = notificationData.type.group ?: return + val group = "${groupType}_${student.id}" + val groupId = student.id * 100 + notificationData.type.ordinal + + notificationData.lines.forEach { item -> + val title = context.resources.getQuantityString(notificationData.titleStringRes, 1) + + val notification = getDefaultNotificationBuilder(notificationData) + .setContentTitle(title) + .setContentText(item) + .setStyle( + NotificationCompat.BigTextStyle() + .setSummaryText(student.nickOrName) + .bigText(item) + ) + .setGroup(group) + .build() + + notificationManager.notify(Random.nextInt(Int.MAX_VALUE), notification) + + saveNotification(title, item, notificationData, student) + } + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return + + val summaryNotification = getDefaultNotificationBuilder(notificationData) + .setSmallIcon(notificationData.icon) + .setGroup(group) + .setStyle(NotificationCompat.InboxStyle().setSummaryText(student.nickOrName)) + .setGroupSummary(true) + .build() + + notificationManager.notify(groupId.toInt(), summaryNotification) + } + + @SuppressLint("InlinedApi") + private fun getDefaultNotificationBuilder(notificationData: NotificationData): NotificationCompat.Builder { + val pendingIntentsFlags = if (appInfo.systemVersion >= Build.VERSION_CODES.M) { + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + + return NotificationCompat.Builder(context, notificationData.type.channel) + .setLargeIcon(context.getCompatBitmap(notificationData.icon, R.color.colorPrimary)) + .setSmallIcon(R.drawable.ic_stat_all) + .setAutoCancel(true) + .setDefaults(NotificationCompat.DEFAULT_ALL) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setColor(context.getCompatColor(R.color.colorPrimary)) + .setContentIntent( + PendingIntent.getActivity( + context, + notificationData.startMenu.id, + MainActivity.getStartIntent(context, notificationData.startMenu, true), + pendingIntentsFlags + ) + ) + } + + private suspend fun saveNotification( + title: String, + content: String, + notificationData: NotificationData, + student: Student + ) { + val notificationEntity = Notification( + studentId = student.id, + title = title, + content = content, + type = notificationData.type, + date = LocalDateTime.now() + ) + + notificationRepository.saveNotification(notificationEntity) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/BaseNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/BaseNotification.kt deleted file mode 100644 index 8c9cb471d..000000000 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/BaseNotification.kt +++ /dev/null @@ -1,102 +0,0 @@ -package io.github.wulkanowy.services.sync.notifications - -import android.app.PendingIntent -import android.content.Context -import android.os.Build -import androidx.annotation.PluralsRes -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import io.github.wulkanowy.R -import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotifications -import io.github.wulkanowy.data.pojos.Notification -import io.github.wulkanowy.data.pojos.OneNotification -import io.github.wulkanowy.ui.modules.main.MainActivity -import io.github.wulkanowy.utils.getCompatBitmap -import io.github.wulkanowy.utils.getCompatColor -import io.github.wulkanowy.utils.nickOrName -import kotlin.random.Random - -abstract class BaseNotification( - private val context: Context, - private val notificationManager: NotificationManagerCompat, -) { - - protected fun sendNotification(notification: Notification, student: Student) = - when (notification) { - is OneNotification -> sendOneNotification(notification, student) - is MultipleNotifications -> sendMultipleNotifications(notification, student) - } - - private fun sendOneNotification(notification: OneNotification, student: Student?) { - notificationManager.notify( - Random.nextInt(Int.MAX_VALUE), - getNotificationBuilder(notification).apply { - val content = context.getString( - notification.contentStringRes, - *notification.contentValues.toTypedArray() - ) - setContentTitle(context.getString(notification.titleStringRes)) - setContentText(content) - setStyle( - NotificationCompat.BigTextStyle() - .setSummaryText(student?.nickOrName) - .bigText(content) - ) - }.build() - ) - } - - private fun sendMultipleNotifications(notification: MultipleNotifications, student: Student) { - val group = notification.type.group + student.id - val groupId = student.id * 100 + notification.type.ordinal - - notification.lines.forEach { item -> - notificationManager.notify( - Random.nextInt(Int.MAX_VALUE), - getNotificationBuilder(notification).apply { - setContentTitle(getQuantityString(notification.titleStringRes, 1)) - setContentText(item) - setStyle( - NotificationCompat.BigTextStyle() - .setSummaryText(student.nickOrName) - .bigText(item) - ) - setGroup(group) - }.build() - ) - } - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return - - notificationManager.notify( - groupId.toInt(), - getNotificationBuilder(notification).apply { - setSmallIcon(notification.icon) - setGroup(group) - setStyle(NotificationCompat.InboxStyle().setSummaryText(student.nickOrName)) - setGroupSummary(true) - }.build() - ) - } - - private fun getNotificationBuilder(notification: Notification) = NotificationCompat - .Builder(context, notification.type.channel) - .setLargeIcon(context.getCompatBitmap(notification.icon, R.color.colorPrimary)) - .setSmallIcon(R.drawable.ic_stat_all) - .setAutoCancel(true) - .setDefaults(NotificationCompat.DEFAULT_ALL) - .setPriority(NotificationCompat.PRIORITY_HIGH) - .setColor(context.getCompatColor(R.color.colorPrimary)) - .setContentIntent( - PendingIntent.getActivity( - context, notification.startMenu.id, - MainActivity.getStartIntent(context, notification.startMenu, true), - PendingIntent.FLAG_UPDATE_CURRENT - ) - ) - - private fun getQuantityString(@PluralsRes id: Int, value: Int): String { - return context.resources.getQuantityString(id, value, value) - } -} diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt index fda2922f9..994cb8d4b 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt @@ -1,29 +1,25 @@ package io.github.wulkanowy.services.sync.notifications -import android.content.Context -import androidx.core.app.NotificationManagerCompat -import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotifications +import io.github.wulkanowy.data.pojos.MultipleNotificationsData import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDateTime import javax.inject.Inject class NewConferenceNotification @Inject constructor( - @ApplicationContext private val context: Context, - notificationManager: NotificationManagerCompat, -) : BaseNotification(context, notificationManager) { + private val appNotificationManager: AppNotificationManager +) { - fun notify(items: List, student: Student) { + suspend fun notify(items: List, student: Student) { val today = LocalDateTime.now() val lines = items.filter { !it.date.isBefore(today) }.map { "${it.date.toFormattedString("dd.MM")} - ${it.title}: ${it.subject}" }.ifEmpty { return } - val notification = MultipleNotifications( + val notification = MultipleNotificationsData( type = NotificationType.NEW_CONFERENCE, icon = R.drawable.ic_more_conferences, titleStringRes = R.plurals.conference_notify_new_item_title, @@ -33,6 +29,6 @@ class NewConferenceNotification @Inject constructor( lines = lines ) - sendNotification(notification, student) + appNotificationManager.sendNotification(notification, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt index d493c4d2e..f148fa34f 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt @@ -1,29 +1,25 @@ package io.github.wulkanowy.services.sync.notifications -import android.content.Context -import androidx.core.app.NotificationManagerCompat -import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotifications +import io.github.wulkanowy.data.pojos.MultipleNotificationsData import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDate import javax.inject.Inject class NewExamNotification @Inject constructor( - @ApplicationContext private val context: Context, - notificationManager: NotificationManagerCompat, -) : BaseNotification(context, notificationManager) { + private val appNotificationManager: AppNotificationManager +) { - fun notify(items: List, student: Student) { + suspend fun notify(items: List, student: Student) { val today = LocalDate.now() val lines = items.filter { !it.date.isBefore(today) }.map { "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.description}" }.ifEmpty { return } - val notification = MultipleNotifications( + val notification = MultipleNotificationsData( type = NotificationType.NEW_EXAM, icon = R.drawable.ic_main_exam, titleStringRes = R.plurals.exam_notify_new_item_title, @@ -33,6 +29,6 @@ class NewExamNotification @Inject constructor( lines = lines ) - sendNotification(notification, student) + appNotificationManager.sendNotification(notification, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt index 415ba3434..52bdff588 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt @@ -1,23 +1,19 @@ package io.github.wulkanowy.services.sync.notifications -import android.content.Context -import androidx.core.app.NotificationManagerCompat -import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotifications +import io.github.wulkanowy.data.pojos.MultipleNotificationsData import io.github.wulkanowy.ui.modules.main.MainView import javax.inject.Inject class NewGradeNotification @Inject constructor( - @ApplicationContext private val context: Context, - notificationManager: NotificationManagerCompat, -) : BaseNotification(context, notificationManager) { + private val appNotificationManager: AppNotificationManager +) { - fun notifyDetails(items: List, student: Student) { - val notification = MultipleNotifications( + suspend fun notifyDetails(items: List, student: Student) { + val notification = MultipleNotificationsData( type = NotificationType.NEW_GRADE_DETAILS, icon = R.drawable.ic_stat_grade, titleStringRes = R.plurals.grade_new_items, @@ -29,11 +25,11 @@ class NewGradeNotification @Inject constructor( } ) - sendNotification(notification, student) + appNotificationManager.sendNotification(notification, student) } - fun notifyPredicted(items: List, student: Student) { - val notification = MultipleNotifications( + suspend fun notifyPredicted(items: List, student: Student) { + val notification = MultipleNotificationsData( type = NotificationType.NEW_GRADE_PREDICTED, icon = R.drawable.ic_stat_grade, titleStringRes = R.plurals.grade_new_items_predicted, @@ -45,11 +41,11 @@ class NewGradeNotification @Inject constructor( } ) - sendNotification(notification, student) + appNotificationManager.sendNotification(notification, student) } - fun notifyFinal(items: List, student: Student) { - val notification = MultipleNotifications( + suspend fun notifyFinal(items: List, student: Student) { + val notification = MultipleNotificationsData( type = NotificationType.NEW_GRADE_FINAL, icon = R.drawable.ic_stat_grade, titleStringRes = R.plurals.grade_new_items_final, @@ -61,6 +57,6 @@ class NewGradeNotification @Inject constructor( } ) - sendNotification(notification, student) + appNotificationManager.sendNotification(notification, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt index fe973cade..4c34cb8ff 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt @@ -1,29 +1,25 @@ package io.github.wulkanowy.services.sync.notifications -import android.content.Context -import androidx.core.app.NotificationManagerCompat -import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Homework import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotifications +import io.github.wulkanowy.data.pojos.MultipleNotificationsData import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDate import javax.inject.Inject class NewHomeworkNotification @Inject constructor( - @ApplicationContext private val context: Context, - notificationManager: NotificationManagerCompat, -) : BaseNotification(context, notificationManager) { + private val appNotificationManager: AppNotificationManager +) { - fun notify(items: List, student: Student) { + suspend fun notify(items: List, student: Student) { val today = LocalDate.now() val lines = items.filter { !it.date.isBefore(today) }.map { "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.content}" }.ifEmpty { return } - val notification = MultipleNotifications( + val notification = MultipleNotificationsData( type = NotificationType.NEW_HOMEWORK, icon = R.drawable.ic_more_homework, titleStringRes = R.plurals.homework_notify_new_item_title, @@ -33,6 +29,6 @@ class NewHomeworkNotification @Inject constructor( lines = lines ) - sendNotification(notification, student) + appNotificationManager.sendNotification(notification, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt index 95156c451..08c985106 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt @@ -1,30 +1,26 @@ package io.github.wulkanowy.services.sync.notifications -import android.content.Context -import androidx.core.app.NotificationManagerCompat -import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.OneNotification +import io.github.wulkanowy.data.pojos.OneNotificationData import io.github.wulkanowy.ui.modules.main.MainView import javax.inject.Inject class NewLuckyNumberNotification @Inject constructor( - @ApplicationContext private val context: Context, - notificationManager: NotificationManagerCompat, -) : BaseNotification(context, notificationManager) { + private val appNotificationManager: AppNotificationManager +) { - fun notify(item: LuckyNumber, student: Student) { - val notification = OneNotification( - type = NotificationType.NEW_LUCKY_NUMBER, - icon = R.drawable.ic_stat_luckynumber, - titleStringRes = R.string.lucky_number_notify_new_item_title, - contentStringRes = R.string.lucky_number_notify_new_item, - startMenu = MainView.Section.LUCKY_NUMBER, - contentValues = listOf(item.luckyNumber.toString()) - ) + suspend fun notify(item: LuckyNumber, student: Student) { + val notification = OneNotificationData( + type = NotificationType.NEW_LUCKY_NUMBER, + icon = R.drawable.ic_stat_luckynumber, + titleStringRes = R.string.lucky_number_notify_new_item_title, + contentStringRes = R.string.lucky_number_notify_new_item, + startMenu = MainView.Section.LUCKY_NUMBER, + contentValues = listOf(item.luckyNumber.toString()) + ) - sendNotification(notification, student) + appNotificationManager.sendNotification(notification, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt index fc3641983..a6d503aa0 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt @@ -1,22 +1,18 @@ package io.github.wulkanowy.services.sync.notifications -import android.content.Context -import androidx.core.app.NotificationManagerCompat -import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotifications +import io.github.wulkanowy.data.pojos.MultipleNotificationsData import io.github.wulkanowy.ui.modules.main.MainView import javax.inject.Inject class NewMessageNotification @Inject constructor( - @ApplicationContext private val context: Context, - notificationManager: NotificationManagerCompat, -) : BaseNotification(context, notificationManager) { + private val appNotificationManager: AppNotificationManager +) { - fun notify(items: List, student: Student) { - val notification = MultipleNotifications( + suspend fun notify(items: List, student: Student) { + val notification = MultipleNotificationsData( type = NotificationType.NEW_MESSAGE, icon = R.drawable.ic_stat_message, titleStringRes = R.plurals.message_new_items, @@ -28,6 +24,6 @@ class NewMessageNotification @Inject constructor( } ) - sendNotification(notification, student) + appNotificationManager.sendNotification(notification, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt index f355341b7..ffa3cc9cb 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt @@ -1,23 +1,19 @@ package io.github.wulkanowy.services.sync.notifications -import android.content.Context -import androidx.core.app.NotificationManagerCompat -import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Note import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotifications +import io.github.wulkanowy.data.pojos.MultipleNotificationsData import io.github.wulkanowy.sdk.scrapper.notes.NoteCategory import io.github.wulkanowy.ui.modules.main.MainView import javax.inject.Inject class NewNoteNotification @Inject constructor( - @ApplicationContext private val context: Context, - notificationManager: NotificationManagerCompat, -) : BaseNotification(context, notificationManager) { + private val appNotificationManager: AppNotificationManager +) { - fun notify(items: List, student: Student) { - val notification = MultipleNotifications( + suspend fun notify(items: List, student: Student) { + val notification = MultipleNotificationsData( type = NotificationType.NEW_NOTE, icon = R.drawable.ic_stat_note, titleStringRes = when (NoteCategory.getByValue(items.first().categoryType)) { @@ -41,6 +37,6 @@ class NewNoteNotification @Inject constructor( } ) - sendNotification(notification, student) + appNotificationManager.sendNotification(notification, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt index b38e4f609..990a950b1 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt @@ -1,33 +1,29 @@ package io.github.wulkanowy.services.sync.notifications -import android.content.Context -import androidx.core.app.NotificationManagerCompat -import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotifications +import io.github.wulkanowy.data.pojos.MultipleNotificationsData import io.github.wulkanowy.ui.modules.main.MainView import javax.inject.Inject class NewSchoolAnnouncementNotification @Inject constructor( - @ApplicationContext private val context: Context, - notificationManager: NotificationManagerCompat, -) : BaseNotification(context, notificationManager) { + private val appNotificationManager: AppNotificationManager +) { - fun notify(items: List, student: Student) { - val notification = MultipleNotifications( - type = NotificationType.NEW_ANNOUNCEMENT, - icon = R.drawable.ic_all_about, - titleStringRes = R.plurals.school_announcement_notify_new_item_title, - contentStringRes = R.plurals.school_announcement_notify_new_items, - summaryStringRes = R.plurals.school_announcement_number_item, - startMenu = MainView.Section.SCHOOL_ANNOUNCEMENT, - lines = items.map { - "${it.subject}: ${it.content}" - } + suspend fun notify(items: List, student: Student) { + val notification = MultipleNotificationsData( + type = NotificationType.NEW_ANNOUNCEMENT, + icon = R.drawable.ic_all_about, + titleStringRes = R.plurals.school_announcement_notify_new_item_title, + contentStringRes = R.plurals.school_announcement_notify_new_items, + summaryStringRes = R.plurals.school_announcement_number_item, + startMenu = MainView.Section.SCHOOL_ANNOUNCEMENT, + lines = items.map { + "${it.subject}: ${it.content}" + } ) - sendNotification(notification, student) + appNotificationManager.sendNotification(notification, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt index c3df1960f..49cbcfe9e 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt @@ -8,8 +8,9 @@ import io.github.wulkanowy.services.sync.channels.NewHomeworkChannel import io.github.wulkanowy.services.sync.channels.NewMessagesChannel import io.github.wulkanowy.services.sync.channels.NewNotesChannel import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel +import io.github.wulkanowy.services.sync.channels.PushChannel -enum class NotificationType(val group: String, val channel: String) { +enum class NotificationType(val group: String?, val channel: String) { NEW_CONFERENCE("new_conferences_group", NewConferencesChannel.CHANNEL_ID), NEW_EXAM("new_exam_group", NewExamChannel.CHANNEL_ID), NEW_GRADE_DETAILS("new_grade_details_group", NewGradesChannel.CHANNEL_ID), @@ -20,4 +21,5 @@ enum class NotificationType(val group: String, val channel: String) { NEW_MESSAGE("new_message_group", NewMessagesChannel.CHANNEL_ID), NEW_NOTE("new_notes_group", NewNotesChannel.CHANNEL_ID), NEW_ANNOUNCEMENT("new_school_announcements_group", NewSchoolAnnouncementsChannel.CHANNEL_ID), + PUSH(null, PushChannel.CHANNEL_ID) } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt index 788e4ea2c..4823b2b5b 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt @@ -9,9 +9,12 @@ import io.github.wulkanowy.utils.waitForResult import java.time.LocalDate.now import javax.inject.Inject -class AttendanceWork @Inject constructor(private val attendanceRepository: AttendanceRepository) : Work { +class AttendanceWork @Inject constructor( + private val attendanceRepository: AttendanceRepository +) : Work { override suspend fun doWork(student: Student, semester: Semester) { - attendanceRepository.getAttendance(student, semester, now().monday, now().sunday, true).waitForResult() + attendanceRepository.getAttendance(student, semester, now().monday, now().sunday, true) + .waitForResult() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt index 59d9c8e4a..096c1b77a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt @@ -24,6 +24,7 @@ import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.message.MessageFragment +import io.github.wulkanowy.ui.modules.notificationscenter.NotificationsCenterFragment import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import io.github.wulkanowy.utils.capitalise @@ -120,6 +121,7 @@ class DashboardFragment : BaseFragment(R.layout.fragme override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.dashboard_menu_tiles -> presenter.onDashboardTileSettingsSelected() + R.id.dashboard_menu_notifaction_list -> presenter.onNotificationsCenterSelected() else -> false } } @@ -182,6 +184,10 @@ class DashboardFragment : BaseFragment(R.layout.fragme if (::presenter.isInitialized) presenter.onViewReselected() } + override fun openNotificationsCenterView() { + (requireActivity() as MainActivity).pushView(NotificationsCenterFragment.newInstance()) + } + override fun onDestroyView() { dashboardAdapter.clearTimers() presenter.onDetachView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt index 108a086b7..c513a9f6f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt @@ -209,6 +209,11 @@ class DashboardPresenter @Inject constructor( view?.showErrorDetailsDialog(lastError) } + fun onNotificationsCenterSelected(): Boolean { + view?.openNotificationsCenterView() + return true + } + fun onDashboardTileSettingsSelected(): Boolean { view?.showDashboardTileSettings(preferencesRepository.selectedDashboardTiles.toList()) return true diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt index d5c5e5a70..899eb3202 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt @@ -23,4 +23,6 @@ interface DashboardView : BaseView { fun resetView() fun popViewToRoot() + + fun openNotificationsCenterView() } \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt index 07468daa6..6103317d2 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt @@ -87,7 +87,7 @@ class NotificationDebugPresenter @Inject constructor( } } - private fun withStudent(block: (Student) -> Unit) { + private fun withStudent(block: suspend (Student) -> Unit) { launch { block(studentRepository.getCurrentStudent(false)) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterAdapter.kt new file mode 100644 index 000000000..7ee326f8f --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterAdapter.kt @@ -0,0 +1,62 @@ +package io.github.wulkanowy.ui.modules.notificationscenter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Notification +import io.github.wulkanowy.databinding.ItemNotificationsCenterBinding +import io.github.wulkanowy.services.sync.notifications.NotificationType +import io.github.wulkanowy.utils.toFormattedString +import javax.inject.Inject + +class NotificationsCenterAdapter @Inject constructor() : + ListAdapter(DiffUtilCallback()) { + + var onItemClickListener: (NotificationType) -> Unit = {} + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + ItemNotificationsCenterBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = getItem(position) + + with(holder.binding) { + notificationsCenterItemTitle.text = item.title + notificationsCenterItemContent.text = item.content + notificationsCenterItemDate.text = item.date.toFormattedString("HH:mm, d MMM") + notificationsCenterItemIcon.setImageResource(item.type.toDrawableResId()) + + root.setOnClickListener { onItemClickListener(item.type) } + } + } + + private fun NotificationType.toDrawableResId() = when (this) { + NotificationType.NEW_CONFERENCE -> R.drawable.ic_more_conferences + NotificationType.NEW_EXAM -> R.drawable.ic_main_exam + NotificationType.NEW_GRADE_DETAILS -> R.drawable.ic_stat_grade + NotificationType.NEW_GRADE_PREDICTED -> R.drawable.ic_stat_grade + NotificationType.NEW_GRADE_FINAL -> R.drawable.ic_stat_grade + NotificationType.NEW_HOMEWORK -> R.drawable.ic_more_homework + NotificationType.NEW_LUCKY_NUMBER -> R.drawable.ic_stat_luckynumber + NotificationType.NEW_MESSAGE -> R.drawable.ic_stat_message + NotificationType.NEW_NOTE -> R.drawable.ic_stat_note + NotificationType.NEW_ANNOUNCEMENT -> R.drawable.ic_all_about + NotificationType.PUSH -> R.drawable.ic_stat_all + } + + class ViewHolder(val binding: ItemNotificationsCenterBinding) : + RecyclerView.ViewHolder(binding.root) + + private class DiffUtilCallback : DiffUtil.ItemCallback() { + + override fun areContentsTheSame(oldItem: Notification, newItem: Notification) = + oldItem == newItem + + override fun areItemsTheSame(oldItem: Notification, newItem: Notification) = + oldItem.id == newItem.id + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterFragment.kt new file mode 100644 index 000000000..b9bfb447e --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterFragment.kt @@ -0,0 +1,108 @@ +package io.github.wulkanowy.ui.modules.notificationscenter + +import android.os.Bundle +import android.view.View +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Notification +import io.github.wulkanowy.databinding.FragmentNotificationsCenterBinding +import io.github.wulkanowy.services.sync.notifications.NotificationType +import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.conference.ConferenceFragment +import io.github.wulkanowy.ui.modules.exam.ExamFragment +import io.github.wulkanowy.ui.modules.grade.GradeFragment +import io.github.wulkanowy.ui.modules.homework.HomeworkFragment +import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.ui.modules.message.MessageFragment +import io.github.wulkanowy.ui.modules.note.NoteFragment +import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment +import javax.inject.Inject + +@AndroidEntryPoint +class NotificationsCenterFragment : + BaseFragment(R.layout.fragment_notifications_center), + NotificationsCenterView, MainView.TitledView { + + @Inject + lateinit var presenter: NotificationsCenterPresenter + + @Inject + lateinit var notificationsCenterAdapter: NotificationsCenterAdapter + + companion object { + + fun newInstance() = NotificationsCenterFragment() + } + + override val titleStringId: Int + get() = R.string.notifications_center_title + + override val isViewEmpty: Boolean + get() = notificationsCenterAdapter.itemCount == 0 + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentNotificationsCenterBinding.bind(view) + presenter.onAttachView(this) + } + + override fun initView() { + notificationsCenterAdapter.onItemClickListener = { notificationType -> + notificationType.toDestinationFragment() + ?.let { (requireActivity() as MainActivity).pushView(it) } + } + + with(binding.notificationsCenterRecycler) { + layoutManager = LinearLayoutManager(context) + adapter = notificationsCenterAdapter + } + } + + override fun updateData(data: List) { + notificationsCenterAdapter.submitList(data) + } + + override fun showEmpty(show: Boolean) { + binding.notificationsCenterEmpty.isVisible = show + } + + override fun showProgress(show: Boolean) { + binding.notificationsCenterProgress.isVisible = show + } + + override fun showContent(show: Boolean) { + binding.notificationsCenterRecycler.isVisible = show + } + + override fun showErrorView(show: Boolean) { + binding.notificationCenterError.isVisible = show + } + + override fun setErrorDetails(message: String) { + binding.notificationCenterErrorMessage.text = message + } + + override fun onDestroyView() { + presenter.onDetachView() + super.onDestroyView() + } + + private fun NotificationType.toDestinationFragment(): Fragment? = when (this) { + NotificationType.NEW_CONFERENCE -> ConferenceFragment.newInstance() + NotificationType.NEW_EXAM -> ExamFragment.newInstance() + NotificationType.NEW_GRADE_DETAILS -> GradeFragment.newInstance() + NotificationType.NEW_GRADE_PREDICTED -> GradeFragment.newInstance() + NotificationType.NEW_GRADE_FINAL -> GradeFragment.newInstance() + NotificationType.NEW_HOMEWORK -> HomeworkFragment.newInstance() + NotificationType.NEW_LUCKY_NUMBER -> LuckyNumberFragment.newInstance() + NotificationType.NEW_MESSAGE -> MessageFragment.newInstance() + NotificationType.NEW_NOTE -> NoteFragment.newInstance() + NotificationType.NEW_ANNOUNCEMENT -> SchoolAnnouncementFragment.newInstance() + NotificationType.PUSH -> null + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterPresenter.kt new file mode 100644 index 000000000..394c23101 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterPresenter.kt @@ -0,0 +1,83 @@ +package io.github.wulkanowy.ui.modules.notificationscenter + +import io.github.wulkanowy.data.repositories.NotificationRepository +import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.ui.base.ErrorHandler +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import timber.log.Timber +import javax.inject.Inject + +class NotificationsCenterPresenter @Inject constructor( + private val notificationRepository: NotificationRepository, + errorHandler: ErrorHandler, + studentRepository: StudentRepository +) : BasePresenter(errorHandler, studentRepository) { + + private lateinit var lastError: Throwable + + override fun onAttachView(view: NotificationsCenterView) { + super.onAttachView(view) + view.initView() + Timber.i("Notifications centre view was initialized") + errorHandler.showErrorMessage = ::showErrorViewOnError + loadData() + } + + fun onRetry() { + view?.run { + showErrorView(false) + showProgress(true) + } + loadData() + } + + fun onDetailsClick() { + view?.showErrorDetailsDialog(lastError) + } + + private fun loadData() { + Timber.i("Loading notifications data started") + + flow { + val studentId = studentRepository.getCurrentStudent(false).id + emitAll(notificationRepository.getNotifications(studentId)) + } + .map { notificationList -> notificationList.sortedByDescending { it.date } } + .catch { Timber.i("Loading notifications result: An exception occurred") } + .onEach { + Timber.i("Loading notifications result: Success") + + if (it.isEmpty()) { + view?.run { + showContent(false) + showProgress(false) + showEmpty(true) + } + } else { + view?.run { + showContent(true) + showProgress(false) + showEmpty(false) + updateData(it) + } + } + } + .launch() + } + + private fun showErrorViewOnError(message: String, error: Throwable) { + view?.run { + if (isViewEmpty) { + lastError = error + setErrorDetails(message) + showErrorView(true) + showEmpty(false) + } else showError(message, error) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterView.kt new file mode 100644 index 000000000..1bfbe75e1 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterView.kt @@ -0,0 +1,23 @@ +package io.github.wulkanowy.ui.modules.notificationscenter + +import io.github.wulkanowy.data.db.entities.Notification +import io.github.wulkanowy.ui.base.BaseView + +interface NotificationsCenterView : BaseView { + + val isViewEmpty: Boolean + + fun initView() + + fun updateData(data: List) + + fun showProgress(show: Boolean) + + fun showEmpty(show: Boolean) + + fun showContent(show: Boolean) + + fun showErrorView(show: Boolean) + + fun setErrorDetails(message: String) +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt index 207d587de..8470d1a5b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt @@ -184,6 +184,7 @@ class NotificationsFragment : PreferenceFragmentCompat(), .setNegativeButton(android.R.string.cancel) { _, _ -> setNotificationPiggybackPreferenceChecked(false) } + .setOnDismissListener { setNotificationPiggybackPreferenceChecked(false) } .show() } diff --git a/app/src/main/res/layout/fragment_notifications_center.xml b/app/src/main/res/layout/fragment_notifications_center.xml new file mode 100644 index 000000000..f59ce33c9 --- /dev/null +++ b/app/src/main/res/layout/fragment_notifications_center.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_notifications_center.xml b/app/src/main/res/layout/item_notifications_center.xml new file mode 100644 index 000000000..ae3f5586d --- /dev/null +++ b/app/src/main/res/layout/item_notifications_center.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/action_menu_dashboard.xml b/app/src/main/res/menu/action_menu_dashboard.xml index dbdd6e812..13565a196 100644 --- a/app/src/main/res/menu/action_menu_dashboard.xml +++ b/app/src/main/res/menu/action_menu_dashboard.xml @@ -1,10 +1,17 @@

+ diff --git a/app/src/main/res/menu/action_menu_main.xml b/app/src/main/res/menu/action_menu_main.xml index 219059391..f14d1f749 100644 --- a/app/src/main/res/menu/action_menu_main.xml +++ b/app/src/main/res/menu/action_menu_main.xml @@ -5,7 +5,7 @@ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ed2369322..ae11d493e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -24,6 +24,7 @@ Account details Student info Dashboard + Notifications center diff --git a/app/src/play/java/io/github/wulkanowy/services/messaging/AppMessagingService.kt b/app/src/play/java/io/github/wulkanowy/services/messaging/AppMessagingService.kt new file mode 100644 index 000000000..6e5c7e89c --- /dev/null +++ b/app/src/play/java/io/github/wulkanowy/services/messaging/AppMessagingService.kt @@ -0,0 +1,57 @@ +package io.github.wulkanowy.services.messaging + +import android.annotation.SuppressLint +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.data.db.entities.Notification +import io.github.wulkanowy.data.repositories.NotificationRepository +import io.github.wulkanowy.services.sync.notifications.NotificationType +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import timber.log.Timber +import java.time.LocalDateTime +import javax.inject.Inject + +@SuppressLint("MissingFirebaseInstanceTokenRefresh") +@AndroidEntryPoint +class AppMessagingService : FirebaseMessagingService() { + + @Inject + lateinit var notificationRepository: NotificationRepository + + private val job = Job() + + private val serviceScope = CoroutineScope(Dispatchers.Main + job) + + override fun onMessageReceived(remoteMessage: RemoteMessage) { + val remoteMessageData = remoteMessage.data + val title = remoteMessageData["title"] ?: return + val content = remoteMessageData["content"] ?: return + val customData = remoteMessageData["custom_data"] + + val notification = Notification( + title = title, + content = content, + data = customData, + date = LocalDateTime.now(), + type = NotificationType.PUSH, + studentId = -1 + ) + + serviceScope.launch { + try { + notificationRepository.saveNotification(notification) + } catch (e: Throwable) { + Timber.e(e) + } + } + } + + override fun onDestroy() { + job.cancel() + super.onDestroy() + } +} \ No newline at end of file From e10e530dee7972dbe8e58993e15c904c444259a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Mon, 27 Sep 2021 23:03:59 +0200 Subject: [PATCH 211/869] Remove seconds from timetable timer (#1539) --- .../ui/modules/timetable/TimetableAdapter.kt | 41 +++++++------------ 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt index 4a5a06995..2228aaf46 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt @@ -1,12 +1,13 @@ package io.github.wulkanowy.ui.modules.timetable import android.graphics.Paint +import android.os.Handler +import android.os.Looper import android.view.LayoutInflater import android.view.View.GONE import android.view.View.VISIBLE import android.view.ViewGroup import android.widget.TextView -import androidx.core.view.ViewCompat import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import io.github.wulkanowy.R @@ -151,8 +152,8 @@ class TimetableAdapter @Inject constructor() : RecyclerView.Adapter Date: Tue, 28 Sep 2021 11:48:25 +0200 Subject: [PATCH 212/869] Add option to make upcoming lesson notification not persistent (#1537) --- .../repositories/PreferencesRepository.kt | 8 ++ .../alarm/TimetableNotificationReceiver.kt | 77 +++++++++++++------ .../notifications/NotificationsPresenter.kt | 2 +- .../main/res/values/preferences_defaults.xml | 1 + app/src/main/res/values/preferences_keys.xml | 1 + app/src/main/res/values/strings.xml | 2 + .../xml/scheme_preferences_notifications.xml | 8 ++ 7 files changed, 74 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index f0f1c32dd..a08045f85 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -107,6 +107,14 @@ class PreferencesRepository @Inject constructor( R.bool.pref_default_notification_upcoming_lessons_enable ) + val isUpcomingLessonsNotificationsPersistentKey = + context.getString(R.string.pref_key_notifications_upcoming_lessons_persistent) + val isUpcomingLessonsNotificationsPersistent: Boolean + get() = getBoolean( + isUpcomingLessonsNotificationsPersistentKey, + R.bool.pref_default_notification_upcoming_lessons_persistent + ) + val isNotificationPiggybackEnabledKey = context.getString(R.string.pref_key_notifications_piggyback) val isNotificationPiggybackEnabled: Boolean diff --git a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt index 406d91f5f..5e4bad8cf 100644 --- a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt +++ b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt @@ -11,6 +11,7 @@ import androidx.core.app.NotificationManagerCompat import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.services.HiltBroadcastReceiver import io.github.wulkanowy.services.sync.channels.UpcomingLessonsChannel.Companion.CHANNEL_ID @@ -32,6 +33,9 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { @Inject lateinit var studentRepository: StudentRepository + @Inject + lateinit var preferencesRepository: PreferencesRepository + companion object { const val NOTIFICATION_TYPE_CURRENT = 1 const val NOTIFICATION_TYPE_UPCOMING = 2 @@ -68,6 +72,7 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { private fun prepareNotification(context: Context, intent: Intent) { val type = intent.getIntExtra(LESSON_TYPE, 0) val notificationId = intent.getIntExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id) + val isPersistent = preferencesRepository.isUpcomingLessonsNotificationsPersistent if (type == NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION) { return NotificationManagerCompat.from(context).cancel(notificationId) @@ -87,33 +92,57 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { Timber.d("TimetableNotification receive: type: $type, subject: $subject, start: ${start.toLocalDateTime()}, student: $studentId") - showNotification(context, notificationId, studentName, + showNotification(context, notificationId, isPersistent, studentName, if (type == NOTIFICATION_TYPE_CURRENT) end else start, end - start, - context.getString(if (type == NOTIFICATION_TYPE_CURRENT) R.string.timetable_now else R.string.timetable_next, "($room) $subject".removePrefix("()")), - nextSubject?.let { context.getString(R.string.timetable_later, "($nextRoom) $nextSubject".removePrefix("()")) } + context.getString( + if (type == NOTIFICATION_TYPE_CURRENT) R.string.timetable_now else R.string.timetable_next, + "($room) $subject".removePrefix("()") + ), + nextSubject?.let { + context.getString( + R.string.timetable_later, + "($nextRoom) $nextSubject".removePrefix("()") + ) + } ) } - private fun showNotification(context: Context, notificationId: Int, studentName: String?, countDown: Long, timeout: Long, title: String, next: String?) { - NotificationManagerCompat.from(context).notify(notificationId, NotificationCompat.Builder(context, CHANNEL_ID) - .setContentTitle(title) - .setContentText(next) - .setAutoCancel(false) - .setOngoing(true) - .setWhen(countDown) - .apply { - if (Build.VERSION.SDK_INT >= N) setUsesChronometer(true) - } - .setTimeoutAfter(timeout) - .setSmallIcon(R.drawable.ic_stat_timetable) - .setColor(context.getCompatColor(R.color.colorPrimary)) - .setStyle(NotificationCompat.InboxStyle().also { - it.setSummaryText(studentName) - it.addLine(next) - }) - .setContentIntent(PendingIntent.getActivity(context, MainView.Section.TIMETABLE.id, - MainActivity.getStartIntent(context, MainView.Section.TIMETABLE, true), FLAG_UPDATE_CURRENT)) - .build() - ) + private fun showNotification( + context: Context, + notificationId: Int, + isPersistent: Boolean, + studentName: String?, + countDown: Long, + timeout: Long, + title: String, + next: String? + ) { + NotificationManagerCompat.from(context) + .notify(notificationId, NotificationCompat.Builder(context, CHANNEL_ID) + .setContentTitle(title) + .setContentText(next) + .setAutoCancel(false) + .setWhen(countDown) + .setOngoing(isPersistent) + .apply { + if (Build.VERSION.SDK_INT >= N) setUsesChronometer(true) + } + .setTimeoutAfter(timeout) + .setSmallIcon(R.drawable.ic_stat_timetable) + .setColor(context.getCompatColor(R.color.colorPrimary)) + .setStyle(NotificationCompat.InboxStyle().also { + it.setSummaryText(studentName) + it.addLine(next) + }) + .setContentIntent( + PendingIntent.getActivity( + context, + MainView.Section.TIMETABLE.id, + MainActivity.getStartIntent(context, MainView.Section.TIMETABLE, true), + FLAG_UPDATE_CURRENT + ) + ) + .build() + ) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt index 19d2f5591..722ee96d1 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt @@ -42,7 +42,7 @@ class NotificationsPresenter @Inject constructor( preferencesRepository.apply { when (key) { - isUpcomingLessonsNotificationsEnableKey -> { + isUpcomingLessonsNotificationsEnableKey, isUpcomingLessonsNotificationsPersistentKey -> { if (!isUpcomingLessonsNotificationsEnable) { timetableNotificationHelper.cancelNotification() } diff --git a/app/src/main/res/values/preferences_defaults.xml b/app/src/main/res/values/preferences_defaults.xml index 9286052d8..df84d37d6 100644 --- a/app/src/main/res/values/preferences_defaults.xml +++ b/app/src/main/res/values/preferences_defaults.xml @@ -14,6 +14,7 @@ false true false + true false 0.33 0.33 diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index bae6a617b..c512a5f21 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -18,6 +18,7 @@ notifications_system_settings notifications_enable notifications_upcoming_lessons_enable + notifications_upcoming_lessons_persistent notification_debug grade_modifier_plus grade_modifier_minus diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ae11d493e..63d4fc423 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -621,6 +621,8 @@ Notifications Show notifications Show upcoming lesson notifications + Make upcoming lesson notification persistent + Turn off when notification is not showing in your watch/band Open system notification settings Fix synchronization & notifications issues Your device may have data synchronization issues and with notifications.\n\nTo fix them, you need to add Wulkanowy to the autostart and turn off battery optimization/saving in the phone settings. diff --git a/app/src/main/res/xml/scheme_preferences_notifications.xml b/app/src/main/res/xml/scheme_preferences_notifications.xml index 24b83169b..78e91cf06 100644 --- a/app/src/main/res/xml/scheme_preferences_notifications.xml +++ b/app/src/main/res/xml/scheme_preferences_notifications.xml @@ -14,6 +14,14 @@ app:key="@string/pref_key_notifications_upcoming_lessons_enable" app:singleLineTitle="false" app:title="@string/pref_notify_upcoming_lessons_switch" /> + Date: Tue, 28 Sep 2021 21:55:40 +0200 Subject: [PATCH 213/869] Update License (#1542) --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 5dd9cacf7..2fb96cee8 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2019 Wulkanowy + Copyright 2021 Wulkanowy Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From f8cb7599e6f82eaebf2d62470e9736307426b8ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Tue, 28 Sep 2021 22:40:43 +0200 Subject: [PATCH 214/869] Add missing auto refresh to recipients, subjects and teachers (#1540) --- app/build.gradle | 2 +- .../data/repositories/AttendanceRepository.kt | 30 ++++++++--- .../AttendanceSummaryRepository.kt | 6 +-- .../CompletedLessonsRepository.kt | 24 +++++++-- .../data/repositories/ConferenceRepository.kt | 6 +-- .../data/repositories/ExamRepository.kt | 6 +-- .../data/repositories/GradeRepository.kt | 14 +++-- .../repositories/GradeStatisticsRepository.kt | 54 +++++++++++++++---- .../data/repositories/HomeworkRepository.kt | 13 +++-- .../repositories/LuckyNumberRepository.kt | 20 ++++--- .../data/repositories/MessageRepository.kt | 25 ++++++--- .../repositories/MobileDeviceRepository.kt | 11 +++- .../data/repositories/NoteRepository.kt | 15 ++++-- .../repositories/NotificationRepository.kt | 6 ++- .../data/repositories/RecipientRepository.kt | 20 +++++-- .../data/repositories/RecoverRepository.kt | 6 +-- .../SchoolAnnouncementRepository.kt | 16 +++--- .../data/repositories/SchoolRepository.kt | 52 +++++++++++------- .../repositories/StudentInfoRepository.kt | 39 +++++++------- .../data/repositories/SubjectRepository.kt | 20 +++++-- .../data/repositories/TeacherRepository.kt | 20 +++++-- .../data/repositories/TimetableRepository.kt | 21 +++++--- .../io/github/wulkanowy/utils/RefreshUtils.kt | 2 +- app/src/main/res/values/api_hosts.xml | 2 +- .../repositories/AttendanceRepositoryTest.kt | 2 +- .../CompletedLessonsRepositoryTest.kt | 2 +- .../data/repositories/ExamRemoteTest.kt | 2 +- .../data/repositories/GradeRepositoryTest.kt | 2 +- .../GradeStatisticsRepositoryTest.kt | 2 +- .../repositories/MessageRepositoryTest.kt | 2 +- .../MobileDeviceRepositoryTest.kt | 2 +- .../data/repositories/RecipientLocalTest.kt | 8 ++- .../repositories/TimetableRepositoryTest.kt | 2 +- 33 files changed, 312 insertions(+), 142 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 8414ff5d0..e8680fe25 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -166,7 +166,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:49c2071d10" + implementation "io.github.wulkanowy:sdk:f62736adb0" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt index ffccb059e..d21ffb5fb 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt @@ -32,10 +32,23 @@ class AttendanceRepository @Inject constructor( private val cacheKey = "attendance" - fun getAttendance(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource( + fun getAttendance( + student: Student, + semester: Semester, + start: LocalDate, + end: LocalDate, + forceRefresh: Boolean, + ) = networkBoundResource( mutex = saveFetchResultMutex, - shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) }, - query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday) }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed( + key = getRefreshKey(cacheKey, semester, start, end) + ) + it.isEmpty() || forceRefresh || isExpired + }, + query = { + attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday) + }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) .getAttendance(start.monday, end.sunday, semester.semesterId) @@ -50,12 +63,17 @@ class AttendanceRepository @Inject constructor( filterResult = { it.filter { item -> item.date in start..end } } ) - suspend fun excuseForAbsence(student: Student, semester: Semester, absenceList: List, reason: String? = null) { - sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).excuseForAbsence(absenceList.map { attendance -> + suspend fun excuseForAbsence( + student: Student, semester: Semester, + absenceList: List, reason: String? = null + ) { + val items = absenceList.map { attendance -> Absent( date = LocalDateTime.of(attendance.date, LocalTime.of(0, 0)), timeId = attendance.timeId ) - }, reason) + } + sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) + .excuseForAbsence(items, reason) } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt index 58659914f..bc1fb2343 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt @@ -29,12 +29,12 @@ class AttendanceSummaryRepository @Inject constructor( student: Student, semester: Semester, subjectId: Int, - forceRefresh: Boolean + forceRefresh: Boolean, ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { - it.isEmpty() || forceRefresh - || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester)) + it.isEmpty() || forceRefresh || isExpired }, query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, subjectId) }, fetch = { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt index 99ef56f4b..c2e5a7217 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt @@ -28,10 +28,28 @@ class CompletedLessonsRepository @Inject constructor( private val cacheKey = "completed" - fun getCompletedLessons(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource( + fun getCompletedLessons( + student: Student, + semester: Semester, + start: LocalDate, + end: LocalDate, + forceRefresh: Boolean, + ) = networkBoundResource( mutex = saveFetchResultMutex, - shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) }, - query = { completedLessonsDb.loadAll(semester.studentId, semester.diaryId, start.monday, end.sunday) }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed( + key = getRefreshKey(cacheKey, semester, start, end) + ) + it.isEmpty() || forceRefresh || isExpired + }, + query = { + completedLessonsDb.loadAll( + studentId = semester.studentId, + diaryId = semester.diaryId, + from = start.monday, + end = end.sunday + ) + }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) .getCompletedLessons(start.monday, end.sunday) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt index 16d7c3c6c..e32271833 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt @@ -35,12 +35,12 @@ class ConferenceRepository @Inject constructor( semester: Semester, forceRefresh: Boolean, notify: Boolean = false, - startDate: LocalDateTime = LocalDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC) + startDate: LocalDateTime = LocalDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC), ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { - it.isEmpty() || forceRefresh - || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester)) + it.isEmpty() || forceRefresh || isExpired }, query = { conferenceDb.loadAll(semester.diaryId, student.studentId, startDate) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt index 93d5a47cb..9bdac0658 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt @@ -36,14 +36,14 @@ class ExamRepository @Inject constructor( start: LocalDate, end: LocalDate, forceRefresh: Boolean, - notify: Boolean = false + notify: Boolean = false, ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { - val isShouldBeRefreshed = refreshHelper.isShouldBeRefreshed( + val isExpired = refreshHelper.shouldBeRefreshed( key = getRefreshKey(cacheKey, semester, start, end) ) - it.isEmpty() || forceRefresh || isShouldBeRefreshed + it.isEmpty() || forceRefresh || isExpired }, query = { examDb.loadAll( diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt index d8417f8a9..6c574b48a 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt @@ -37,13 +37,12 @@ class GradeRepository @Inject constructor( student: Student, semester: Semester, forceRefresh: Boolean, - notify: Boolean = false + notify: Boolean = false, ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { (details, summaries) -> - val isShouldBeRefreshed = - refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) - details.isEmpty() || summaries.isEmpty() || forceRefresh || isShouldBeRefreshed + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester)) + details.isEmpty() || summaries.isEmpty() || forceRefresh || isExpired }, query = { val detailsFlow = gradeDb.loadAll(semester.semesterId, semester.studentId) @@ -71,8 +70,8 @@ class GradeRepository @Inject constructor( newDetails: List, notify: Boolean ) { - val notifyBreakDate = - oldGrades.maxByOrNull { it.date }?.date ?: student.registrationDate.toLocalDate() + val notifyBreakDate = oldGrades.maxByOrNull {it.date } + ?.date ?: student.registrationDate.toLocalDate() gradeDb.deleteAll(oldGrades uniqueSubtract newDetails) gradeDb.insertAll((newDetails uniqueSubtract oldGrades).onEach { if (it.date >= notifyBreakDate) it.apply { @@ -89,8 +88,7 @@ class GradeRepository @Inject constructor( ) { gradeSummaryDb.deleteAll(oldSummaries uniqueSubtract newSummary) gradeSummaryDb.insertAll((newSummary uniqueSubtract oldSummaries).onEach { summary -> - val oldSummary = - oldSummaries.find { oldSummary -> oldSummary.subject == summary.subject } + val oldSummary = oldSummaries.find { old -> old.subject == summary.subject } summary.isPredictedGradeNotified = when { summary.predictedGrade.isEmpty() -> true notify && oldSummary?.predictedGrade != summary.predictedGrade -> false diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt index 9cd8e711d..6c36f163b 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt @@ -39,9 +39,19 @@ class GradeStatisticsRepository @Inject constructor( private val semesterCacheKey = "grade_stats_semester" private val pointsCacheKey = "grade_stats_points" - fun getGradesPartialStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource( + fun getGradesPartialStatistics( + student: Student, + semester: Semester, + subjectName: String, + forceRefresh: Boolean, + ) = networkBoundResource( mutex = partialMutex, - shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(partialCacheKey, semester)) }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed( + key = getRefreshKey(partialCacheKey, semester) + ) + it.isEmpty() || forceRefresh || isExpired + }, query = { gradePartialStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) @@ -76,9 +86,19 @@ class GradeStatisticsRepository @Inject constructor( } ) - fun getGradesSemesterStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource( + fun getGradesSemesterStatistics( + student: Student, + semester: Semester, + subjectName: String, + forceRefresh: Boolean, + ) = networkBoundResource( mutex = semesterMutex, - shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(semesterCacheKey, semester)) }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed( + key = getRefreshKey(semesterCacheKey, semester) + ) + it.isEmpty() || forceRefresh || isExpired + }, query = { gradeSemesterStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) @@ -94,10 +114,12 @@ class GradeStatisticsRepository @Inject constructor( val itemsWithAverage = items.map { item -> item.copy().apply { val denominator = item.amounts.sum() - average = if (denominator == 0) "" else (item.amounts.mapIndexed { gradeValue, amount -> - (gradeValue + 1) * amount - }.sum().toDouble() / denominator).let { - "%.2f".format(Locale.FRANCE, it) + average = if (denominator == 0) "" else { + (item.amounts.mapIndexed { gradeValue, amount -> + (gradeValue + 1) * amount + }.sum().toDouble() / denominator).let { + "%.2f".format(Locale.FRANCE, it) + } } } } @@ -109,7 +131,9 @@ class GradeStatisticsRepository @Inject constructor( amounts = itemsWithAverage.map { it.amounts }.sumGradeAmounts(), studentGrade = 0 ).apply { - average = itemsWithAverage.mapNotNull { it.average.replace(",", ".").toDoubleOrNull() }.average().let { + average = itemsWithAverage.mapNotNull { + it.average.replace(",", ".").toDoubleOrNull() + }.average().let { "%.2f".format(Locale.FRANCE, it) } }).reversed() @@ -118,9 +142,17 @@ class GradeStatisticsRepository @Inject constructor( } ) - fun getGradesPointsStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource( + fun getGradesPointsStatistics( + student: Student, + semester: Semester, + subjectName: String, + forceRefresh: Boolean, + ) = networkBoundResource( mutex = pointsMutex, - shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(pointsCacheKey, semester)) }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(pointsCacheKey, semester)) + it.isEmpty() || forceRefresh || isExpired + }, query = { gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt index 23dd74c2c..a04085fb7 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt @@ -30,16 +30,19 @@ class HomeworkRepository @Inject constructor( private val cacheKey = "homework" fun getHomework( - student: Student, semester: Semester, - start: LocalDate, end: LocalDate, - forceRefresh: Boolean, notify: Boolean = false + student: Student, + semester: Semester, + start: LocalDate, + end: LocalDate, + forceRefresh: Boolean, + notify: Boolean = false, ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { - val isShouldBeRefreshed = refreshHelper.isShouldBeRefreshed( + val isExpired = refreshHelper.shouldBeRefreshed( key = getRefreshKey(cacheKey, semester, start, end) ) - it.isEmpty() || forceRefresh || isShouldBeRefreshed + it.isEmpty() || forceRefresh || isExpired }, query = { homeworkDb.loadAll( diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt index b904b7dba..41e824e57 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt @@ -23,11 +23,17 @@ class LuckyNumberRepository @Inject constructor( private val saveFetchResultMutex = Mutex() - fun getLuckyNumber(student: Student, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource( + fun getLuckyNumber( + student: Student, + forceRefresh: Boolean, + notify: Boolean = false, + ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { it == null || forceRefresh }, query = { luckyNumberDb.load(student.studentId, now()) }, - fetch = { sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student) }, + fetch = { + sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student) + }, saveFetchResult = { old, new -> if (new != old) { old?.let { luckyNumberDb.deleteAll(listOfNotNull(it)) } @@ -41,9 +47,11 @@ class LuckyNumberRepository @Inject constructor( fun getLuckyNumberHistory(student: Student, start: LocalDate, end: LocalDate) = luckyNumberDb.getAll(student.studentId, start, end) - suspend fun getNotNotifiedLuckyNumber(student: Student) = luckyNumberDb.load(student.studentId, now()).map { - if (it?.isNotified == false) it else null - }.first() + suspend fun getNotNotifiedLuckyNumber(student: Student) = + luckyNumberDb.load(student.studentId, now()).map { + if (it?.isNotified == false) it else null + }.first() - suspend fun updateLuckyNumber(luckyNumber: LuckyNumber?) = luckyNumberDb.updateAll(listOfNotNull(luckyNumber)) + suspend fun updateLuckyNumber(luckyNumber: LuckyNumber?) = + luckyNumberDb.updateAll(listOfNotNull(luckyNumber)) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt index 9977e1d57..ee5164957 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt @@ -51,14 +51,18 @@ class MessageRepository @Inject constructor( @Suppress("UNUSED_PARAMETER") fun getMessages( - student: Student, semester: Semester, - folder: MessageFolder, forceRefresh: Boolean, notify: Boolean = false + student: Student, + semester: Semester, + folder: MessageFolder, + forceRefresh: Boolean, + notify: Boolean = false, ): Flow>> = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { - it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed( - getRefreshKey(cacheKey, student, folder) + val isExpired = refreshHelper.shouldBeRefreshed( + key = getRefreshKey(cacheKey, student, folder) ) + it.isEmpty() || forceRefresh || isExpired }, query = { messagesDb.loadAll(student.id.toInt(), folder.id) }, fetch = { @@ -77,7 +81,8 @@ class MessageRepository @Inject constructor( ) private fun getMessagesWithReadByChange( - old: List, new: List, + old: List, + new: List, setNotified: Boolean ): List { val oldMeta = old.map { Triple(it, it.readBy, it.unreadBy) } @@ -96,7 +101,9 @@ class MessageRepository @Inject constructor( } fun getMessage( - student: Student, message: Message, markAsRead: Boolean = false + student: Student, + message: Message, + markAsRead: Boolean = false, ): Flow> = networkBoundResource( shouldFetch = { checkNotNull(it, { "This message no longer exist!" }) @@ -135,8 +142,10 @@ class MessageRepository @Inject constructor( } suspend fun sendMessage( - student: Student, subject: String, content: String, - recipients: List + student: Student, + subject: String, + content: String, + recipients: List, ): SentMessage = sdk.init(student).sendMessage( subject = subject, content = content, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt index 4b333bc6d..bf17cbbc5 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt @@ -28,9 +28,16 @@ class MobileDeviceRepository @Inject constructor( private val cacheKey = "devices" - fun getDevices(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource( + fun getDevices( + student: Student, + semester: Semester, + forceRefresh: Boolean, + ) = networkBoundResource( mutex = saveFetchResultMutex, - shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student)) }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student)) + it.isEmpty() || forceRefresh || isExpired + }, query = { mobileDb.loadAll(student.userLoginId.takeIf { it != 0 } ?: student.studentId) }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt index d43cdbc0c..c1738b36e 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt @@ -12,7 +12,6 @@ import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -28,9 +27,19 @@ class NoteRepository @Inject constructor( private val cacheKey = "note" - fun getNotes(student: Student, semester: Semester, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource( + fun getNotes( + student: Student, + semester: Semester, + forceRefresh: Boolean, + notify: Boolean = false, + ) = networkBoundResource( mutex = saveFetchResultMutex, - shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed( + getRefreshKey(cacheKey, semester) + ) + it.isEmpty() || forceRefresh || isExpired + }, query = { noteDb.loadAll(student.studentId) }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/NotificationRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/NotificationRepository.kt index 36bc7c25a..fca263782 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/NotificationRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/NotificationRepository.kt @@ -6,10 +6,12 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class NotificationRepository @Inject constructor(private val notificationDao: NotificationDao) { +class NotificationRepository @Inject constructor( + private val notificationDao: NotificationDao, +) { fun getNotifications(studentId: Long) = notificationDao.loadAll(studentId) suspend fun saveNotification(notification: Notification) = notificationDao.insertAll(listOf(notification)) -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/RecipientRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/RecipientRepository.kt index 975a30a20..60e6f248f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/RecipientRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/RecipientRepository.kt @@ -7,6 +7,8 @@ import io.github.wulkanowy.data.db.entities.ReportingUnit import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.utils.AutoRefreshHelper +import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.uniqueSubtract import javax.inject.Inject @@ -15,26 +17,34 @@ import javax.inject.Singleton @Singleton class RecipientRepository @Inject constructor( private val recipientDb: RecipientDao, - private val sdk: Sdk + private val sdk: Sdk, + private val refreshHelper: AutoRefreshHelper, ) { + private val cacheKey = "recipient" + suspend fun refreshRecipients(student: Student, unit: ReportingUnit, role: Int) { val new = sdk.init(student).getRecipients(unit.unitId, role).mapToEntities(unit.studentId) val old = recipientDb.loadAll(unit.studentId, unit.unitId, role) recipientDb.deleteAll(old uniqueSubtract new) recipientDb.insertAll(new uniqueSubtract old) + + refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student)) } suspend fun getRecipients(student: Student, unit: ReportingUnit, role: Int): List { - return recipientDb.loadAll(unit.studentId, unit.unitId, role).ifEmpty { - refreshRecipients(student, unit, role) + val cached = recipientDb.loadAll(unit.studentId, unit.unitId, role) + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student)) + return if (cached.isEmpty() || isExpired) { + refreshRecipients(student, unit, role) recipientDb.loadAll(unit.studentId, unit.unitId, role) - } + } else cached } suspend fun getMessageRecipients(student: Student, message: Message): List { - return sdk.init(student).getMessageRecipients(message.messageId, message.senderId).mapToEntities(student.studentId) + return sdk.init(student).getMessageRecipients(message.messageId, message.senderId) + .mapToEntities(student.studentId) } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/RecoverRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/RecoverRepository.kt index 5e1063558..5940f477b 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/RecoverRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/RecoverRepository.kt @@ -11,7 +11,7 @@ class RecoverRepository @Inject constructor(private val sdk: Sdk) { return sdk.getPasswordResetCaptchaCode(host, symbol) } - suspend fun sendRecoverRequest(url: String, symbol: String, email: String, reCaptchaResponse: String): String { - return sdk.sendPasswordResetRequest(url, symbol, email, reCaptchaResponse) - } + suspend fun sendRecoverRequest( + url: String, symbol: String, email: String, reCaptchaResponse: String + ): String = sdk.sendPasswordResetRequest(url, symbol, email, reCaptchaResponse) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt index 62d806ac2..b6724ed34 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolAnnouncementRepository.kt @@ -2,7 +2,6 @@ package io.github.wulkanowy.data.repositories import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao import io.github.wulkanowy.data.db.entities.SchoolAnnouncement -import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.sdk.Sdk @@ -12,7 +11,6 @@ import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -30,17 +28,15 @@ class SchoolAnnouncementRepository @Inject constructor( fun getSchoolAnnouncements( student: Student, - forceRefresh: Boolean, - notify: Boolean = false + forceRefresh: Boolean, notify: Boolean = false ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { - it.isEmpty() || forceRefresh - || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student)) + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student)) + it.isEmpty() || forceRefresh || isExpired }, query = { - schoolAnnouncementDb.loadAll( - student.studentId) + schoolAnnouncementDb.loadAll(student.studentId) }, fetch = { sdk.init(student) @@ -57,9 +53,11 @@ class SchoolAnnouncementRepository @Inject constructor( refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student)) } ) + fun getSchoolAnnouncementFromDatabase(student: Student): Flow> { return schoolAnnouncementDb.loadAll(student.studentId) } - suspend fun updateSchoolAnnouncement(schoolAnnouncement: List) = schoolAnnouncementDb.updateAll(schoolAnnouncement) + suspend fun updateSchoolAnnouncement(schoolAnnouncement: List) = + schoolAnnouncementDb.updateAll(schoolAnnouncement) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt index 8b59cb589..288a1fb67 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt @@ -5,6 +5,8 @@ import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntity import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.utils.AutoRefreshHelper +import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import kotlinx.coroutines.sync.Mutex @@ -14,29 +16,41 @@ import javax.inject.Singleton @Singleton class SchoolRepository @Inject constructor( private val schoolDb: SchoolDao, - private val sdk: Sdk + private val sdk: Sdk, + private val refreshHelper: AutoRefreshHelper, ) { private val saveFetchResultMutex = Mutex() - fun getSchoolInfo(student: Student, semester: Semester, forceRefresh: Boolean) = - networkBoundResource( - mutex = saveFetchResultMutex, - shouldFetch = { it == null || forceRefresh }, - query = { schoolDb.load(semester.studentId, semester.classId) }, - fetch = { - sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).getSchool() - .mapToEntity(semester) - }, - saveFetchResult = { old, new -> - if (old != null && new != old) { - with(schoolDb) { - deleteAll(listOf(old)) - insertAll(listOf(new)) - } - } else if (old == null) { - schoolDb.insertAll(listOf(new)) + private val cacheKey = "school_info" + + fun getSchoolInfo( + student: Student, + semester: Semester, + forceRefresh: Boolean, + ) = networkBoundResource( + mutex = saveFetchResultMutex, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed( + key = getRefreshKey(cacheKey, student) + ) + it == null || forceRefresh || isExpired + }, + query = { schoolDb.load(semester.studentId, semester.classId) }, + fetch = { + sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).getSchool() + .mapToEntity(semester) + }, + saveFetchResult = { old, new -> + if (old != null && new != old) { + with(schoolDb) { + deleteAll(listOf(old)) + insertAll(listOf(new)) } + } else if (old == null) { + schoolDb.insertAll(listOf(new)) } - ) + refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student)) + } + ) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt index de66ad20f..e98daedf2 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt @@ -19,24 +19,27 @@ class StudentInfoRepository @Inject constructor( private val saveFetchResultMutex = Mutex() - fun getStudentInfo(student: Student, semester: Semester, forceRefresh: Boolean) = - networkBoundResource( - mutex = saveFetchResultMutex, - shouldFetch = { it == null || forceRefresh }, - query = { studentInfoDao.loadStudentInfo(student.studentId) }, - fetch = { - sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) - .getStudentInfo().mapToEntity(semester) - }, - saveFetchResult = { old, new -> - if (old != null && new != old) { - with(studentInfoDao) { - deleteAll(listOf(old)) - insertAll(listOf(new)) - } - } else if (old == null) { - studentInfoDao.insertAll(listOf(new)) + fun getStudentInfo( + student: Student, + semester: Semester, + forceRefresh: Boolean, + ) = networkBoundResource( + mutex = saveFetchResultMutex, + shouldFetch = { it == null || forceRefresh }, + query = { studentInfoDao.loadStudentInfo(student.studentId) }, + fetch = { + sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) + .getStudentInfo().mapToEntity(semester) + }, + saveFetchResult = { old, new -> + if (old != null && new != old) { + with(studentInfoDao) { + deleteAll(listOf(old)) + insertAll(listOf(new)) } + } else if (old == null) { + studentInfoDao.insertAll(listOf(new)) } - ) + } + ) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt index b4bfef188..d81cb7c92 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt @@ -5,6 +5,8 @@ import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.utils.AutoRefreshHelper +import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract @@ -15,14 +17,24 @@ import javax.inject.Singleton @Singleton class SubjectRepository @Inject constructor( private val subjectDao: SubjectDao, - private val sdk: Sdk + private val sdk: Sdk, + private val refreshHelper: AutoRefreshHelper, ) { private val saveFetchResultMutex = Mutex() - fun getSubjects(student: Student, semester: Semester, forceRefresh: Boolean = false) = networkBoundResource( + private val cacheKey = "subjects" + + fun getSubjects( + student: Student, + semester: Semester, + forceRefresh: Boolean = false, + ) = networkBoundResource( mutex = saveFetchResultMutex, - shouldFetch = { it.isEmpty() || forceRefresh }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester)) + it.isEmpty() || forceRefresh || isExpired + }, query = { subjectDao.loadAll(semester.diaryId, semester.studentId) }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) @@ -31,6 +43,8 @@ class SubjectRepository @Inject constructor( saveFetchResult = { old, new -> subjectDao.deleteAll(old uniqueSubtract new) subjectDao.insertAll(new uniqueSubtract old) + + refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester)) } ) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt index 7135edbe9..029b2707a 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt @@ -5,6 +5,8 @@ import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.utils.AutoRefreshHelper +import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract @@ -15,14 +17,24 @@ import javax.inject.Singleton @Singleton class TeacherRepository @Inject constructor( private val teacherDb: TeacherDao, - private val sdk: Sdk + private val sdk: Sdk, + private val refreshHelper: AutoRefreshHelper, ) { private val saveFetchResultMutex = Mutex() - fun getTeachers(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource( + private val cacheKey = "teachers" + + fun getTeachers( + student: Student, + semester: Semester, + forceRefresh: Boolean, + ) = networkBoundResource( mutex = saveFetchResultMutex, - shouldFetch = { it.isEmpty() || forceRefresh }, + shouldFetch = { + val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester)) + it.isEmpty() || forceRefresh || isExpired + }, query = { teacherDb.loadAll(semester.studentId, semester.classId) }, fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) @@ -32,6 +44,8 @@ class TeacherRepository @Inject constructor( saveFetchResult = { old, new -> teacherDb.deleteAll(old uniqueSubtract new) teacherDb.insertAll(new uniqueSubtract old) + + refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester)) } ) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt index 5495d0778..1540d3cc7 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt @@ -41,18 +41,22 @@ class TimetableRepository @Inject constructor( private val cacheKey = "timetable" fun getTimetable( - student: Student, semester: Semester, start: LocalDate, end: LocalDate, - forceRefresh: Boolean, refreshAdditional: Boolean = false + student: Student, + semester: Semester, + start: LocalDate, + end: LocalDate, + forceRefresh: Boolean, + refreshAdditional: Boolean = false, ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { (timetable, additional, headers) -> val refreshKey = getRefreshKey(cacheKey, semester, start, end) - val isShouldRefresh = refreshHelper.isShouldBeRefreshed(refreshKey) + val isExpired = refreshHelper.shouldBeRefreshed(refreshKey) val isRefreshAdditional = additional.isEmpty() && refreshAdditional val isNoData = timetable.isEmpty() || isRefreshAdditional || headers.isEmpty() - isNoData || forceRefresh || isShouldRefresh + isNoData || forceRefresh || isExpired }, query = { getFullTimetableFromDatabase(student, semester, start, end) }, fetch = { @@ -79,8 +83,10 @@ class TimetableRepository @Inject constructor( ) private fun getFullTimetableFromDatabase( - student: Student, semester: Semester, - start: LocalDate, end: LocalDate + student: Student, + semester: Semester, + start: LocalDate, + end: LocalDate, ): Flow { val timetableFlow = timetableDb.loadAll( diaryId = semester.diaryId, @@ -113,7 +119,8 @@ class TimetableRepository @Inject constructor( private suspend fun refreshTimetable( student: Student, - lessonsOld: List, lessonsNew: List + lessonsOld: List, + lessonsNew: List, ) { val lessonsToRemove = lessonsOld uniqueSubtract lessonsNew val lessonsToAdd = (lessonsNew uniqueSubtract lessonsOld).map { new -> diff --git a/app/src/main/java/io/github/wulkanowy/utils/RefreshUtils.kt b/app/src/main/java/io/github/wulkanowy/utils/RefreshUtils.kt index cd59b8648..6bf97bae7 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/RefreshUtils.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/RefreshUtils.kt @@ -33,7 +33,7 @@ class AutoRefreshHelper @Inject constructor( private val sharedPref: SharedPrefProvider ) { - fun isShouldBeRefreshed(key: String): Boolean { + fun shouldBeRefreshed(key: String): Boolean { val timestamp = sharedPref.getLong(key, 0).toLocalDateTime() val servicesInterval = sharedPref.getString(context.getString(R.string.pref_key_services_interval), context.getString(R.string.pref_default_services_interval)).toLong() diff --git a/app/src/main/res/values/api_hosts.xml b/app/src/main/res/values/api_hosts.xml index 158490471..dac94c3ff 100644 --- a/app/src/main/res/values/api_hosts.xml +++ b/app/src/main/res/values/api_hosts.xml @@ -40,7 +40,7 @@ https://vulcan.net.pl/?login https://vulcan.net.pl/?login https://vulcan.net.pl/?login - http://fakelog.cf/?email + http://fakelog.tk/?email
Default diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt index 1c592c09f..f3c7fba77 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt @@ -53,7 +53,7 @@ class AttendanceRepositoryTest { @Before fun setUp() { MockKAnnotations.init(this) - every { refreshHelper.isShouldBeRefreshed(any()) } returns false + every { refreshHelper.shouldBeRefreshed(any()) } returns false attendanceRepository = AttendanceRepository(attendanceDb, sdk, refreshHelper) } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepositoryTest.kt index b116a623d..fa54522a8 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepositoryTest.kt @@ -53,7 +53,7 @@ class CompletedLessonsRepositoryTest { @Before fun initApi() { MockKAnnotations.init(this) - every { refreshHelper.isShouldBeRefreshed(any()) } returns false + every { refreshHelper.shouldBeRefreshed(any()) } returns false completedLessonRepository = CompletedLessonsRepository(completedLessonDb, sdk, refreshHelper) } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt index ead6dc5d1..8bf4deee3 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt @@ -54,7 +54,7 @@ class ExamRemoteTest { @Before fun setUp() { MockKAnnotations.init(this) - every { refreshHelper.isShouldBeRefreshed(any()) } returns false + every { refreshHelper.shouldBeRefreshed(any()) } returns false examRepository = ExamRepository(examDb, sdk, refreshHelper) } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt index 8a19d6337..6dd30a579 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt @@ -50,7 +50,7 @@ class GradeRepositoryTest { @Before fun initApi() { MockKAnnotations.init(this) - every { refreshHelper.isShouldBeRefreshed(any()) } returns false + every { refreshHelper.shouldBeRefreshed(any()) } returns false gradeRepository = GradeRepository(gradeDb, gradeSummaryDb, sdk, refreshHelper) diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepositoryTest.kt index 73dd4cfaa..cce3794de 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepositoryTest.kt @@ -56,7 +56,7 @@ class GradeStatisticsRepositoryTest { @Before fun setUp() { MockKAnnotations.init(this) - every { refreshHelper.isShouldBeRefreshed(any()) } returns false + every { refreshHelper.shouldBeRefreshed(any()) } returns false gradeStatisticsRepository = GradeStatisticsRepository(gradePartialStatisticsDb, gradePointsStatisticsDb, gradeSemesterStatisticsDb, sdk, refreshHelper) } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt index cadc4225a..25774d74c 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt @@ -69,7 +69,7 @@ class MessageRepositoryTest { @Before fun setUp() { MockKAnnotations.init(this) - every { refreshHelper.isShouldBeRefreshed(any()) } returns false + every { refreshHelper.shouldBeRefreshed(any()) } returns false repository = MessageRepository( messagesDb = messageDb, diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt index e5b3d1015..52a076d3c 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt @@ -48,7 +48,7 @@ class MobileDeviceRepositoryTest { @Before fun initTest() { MockKAnnotations.init(this) - every { refreshHelper.isShouldBeRefreshed(any()) } returns false + every { refreshHelper.shouldBeRefreshed(any()) } returns false mobileDeviceRepository = MobileDeviceRepository(mobileDeviceDb, sdk, refreshHelper) } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/RecipientLocalTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/RecipientLocalTest.kt index 82406ef46..980abac0a 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/RecipientLocalTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/RecipientLocalTest.kt @@ -5,10 +5,12 @@ import io.github.wulkanowy.data.db.entities.ReportingUnit import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.utils.AutoRefreshHelper import io.mockk.MockKAnnotations import io.mockk.Runs import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.SpyK import io.mockk.just @@ -26,6 +28,9 @@ class RecipientLocalTest { @MockK private lateinit var recipientDb: RecipientDao + @MockK(relaxUnitFun = true) + private lateinit var refreshHelper: AutoRefreshHelper + private val student = getStudentEntity() private lateinit var recipientRepository: RecipientRepository @@ -39,8 +44,9 @@ class RecipientLocalTest { @Before fun setUp() { MockKAnnotations.init(this) + every { refreshHelper.shouldBeRefreshed(any()) } returns false - recipientRepository = RecipientRepository(recipientDb, sdk) + recipientRepository = RecipientRepository(recipientDb, sdk, refreshHelper) } @Test diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt index 2c56a1b69..75c75a66c 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt @@ -62,7 +62,7 @@ class TimetableRepositoryTest { @Before fun initApi() { MockKAnnotations.init(this) - every { refreshHelper.isShouldBeRefreshed(any()) } returns false + every { refreshHelper.shouldBeRefreshed(any()) } returns false timetableRepository = TimetableRepository(timetableDb, timetableAdditionalDao, timetableHeaderDao, sdk, timetableNotificationSchedulerHelper, refreshHelper) } From 9711cc868c4583f1cc169b361e229f90631a3ca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Tue, 28 Sep 2021 22:42:06 +0200 Subject: [PATCH 215/869] New Crowdin updates (#1522) --- .../main/res/values-cs/preferences_values.xml | 4 +-- app/src/main/res/values-cs/strings.xml | 28 +++++++++------- .../main/res/values-de/preferences_values.xml | 2 +- app/src/main/res/values-de/strings.xml | 32 ++++++++++++------- .../main/res/values-pl/preferences_values.xml | 4 +-- app/src/main/res/values-pl/strings.xml | 24 +++++++------- .../main/res/values-ru/preferences_values.xml | 4 +-- app/src/main/res/values-ru/strings.xml | 30 ++++++++++------- .../main/res/values-sk/preferences_values.xml | 4 +-- app/src/main/res/values-sk/strings.xml | 28 +++++++++------- .../main/res/values-uk/preferences_values.xml | 4 +-- app/src/main/res/values-uk/strings.xml | 30 ++++++++++------- 12 files changed, 112 insertions(+), 82 deletions(-) diff --git a/app/src/main/res/values-cs/preferences_values.xml b/app/src/main/res/values-cs/preferences_values.xml index fb938f092..5e2f10fb8 100644 --- a/app/src/main/res/values-cs/preferences_values.xml +++ b/app/src/main/res/values-cs/preferences_values.xml @@ -41,8 +41,8 @@ Barvy známek v deníku - Průměrná známka od druhého semestru - Průměr známek z obou semestrů + Průměr známek pouze z vybraného semestru + Průměr z průměrů z obou semestrů Průměr známek z celého roku diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 0367efdd8..1557ecc14 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -12,7 +12,7 @@ O aplikaci Prohlížeč protokolů Ladění - Ladění oznámení + Ladění upozornění Tvůrci Licence Zprávy @@ -24,6 +24,7 @@ Podrobnosti účtu Informace o žáku Domů + Centrum upozornění Semestr %1$d, %2$d/%3$d @@ -43,7 +44,7 @@ Přihlásit Toto heslo je příliš krátké Přihlašovací údaje jsou nesprávné - %1$s. Ujistěte se, že je v poli níže vybrána správná variace deníku UONET+ + %1$s. Zkontrolujte, zda je níže vybrána správná variace deníku UONET+ Neplatný PIN Neplatný token Token vypršel @@ -91,6 +92,10 @@ Konečná známka Předpokládaná známka Vypočítaný průměr + Jak funguje vypočítaný průměr? + Vypočítaný průměr je aritmetický průměr vypočítaný z průměrů předmětů. Umožňuje vám to znát přibližný konečný průměr. Vypočítává se způsobem zvoleným uživatelem v nastavení aplikaci. Doporučuje se vybrat příslušnou možnost. Důvodem je rozdílný výpočet školních průměrů. Pokud vaše škola navíc uvádí průměr předmětů na stránce deníku Vulcan, aplikace si je stáhne a tyto průměry nepočítá. To lze změnit vynucením výpočtu průměru v nastavení aplikaci.\n\nPrůměr známek pouze z vybraného semestru:\n1. Výpočet váženého průměru pro každý předmět v daném semestru\n2. Sčítání vypočítaných průměrů\n3. Výpočet aritmetického průměru součtených průměrů\n\nPrůměr průměrů z obou semestrů:\n1. Výpočet váženého průměru pro každý předmět v semestru 1 a 2\n2. Výpočet aritmetického průměru vypočítaných průměrů za semestry 1 a 2 pro každý předmět.\n3. Sčítání vypočítaných průměrů\n4. Výpočet aritmetického průměru součtených průměrů\n\nPrůměr známek z celého roku:\n1. Výpočet váženého průměru za rok pro každý předmět. Konečný průměr v 1. semestru je nepodstatný.\n3. Sčítání vypočítaných průměrů\n4. Výpočet aritmetického průměru součtených průměrů + Jak funguje konečný průměr? + Konečný průměr je aritmetický průměr vypočítaný ze všech aktuálně dostupných konečných známek v daném semestru.\n\nSchéma výpočtu se skládá z následujících kroků:\n1. Sčítání konečných známek zadaných učiteli\n2. Děleno počtem předmětů, pro které už byly vydány známky Konečný průměr z %1$d z %2$d předmětů Shrnutí @@ -239,12 +244,7 @@ Pouze nepřečtené Pouze s přílohami Přečtena: %s - - Přečtena přes: %1$d z %2$d osob - Přečtena přes: %1$d z %2$d osob - Přečtena přes: %1$d z %2$d osob - Přečtena přes: %1$d z %2$d osob - + Přečtena přes: %1$d z %2$d osob %d zpráva %d zprávy @@ -403,7 +403,7 @@ Máte %1$d nových setkání Máte %1$d nových setkání - Present at conference + Přítomnost na setkání Agenda Školní oznámení @@ -593,7 +593,7 @@ Ano Ne Uložit - Title + Titul Žádné lekce Vybrat motiv @@ -603,7 +603,7 @@ Vzhled a chování aplikací Výchozí zobrazení - Výpočet koncoročního průměru + Možnosti vypočítaného průměru Vynutit průměrný výpočet podle aplikace Zobrazit přítomnost Motiv @@ -618,12 +618,18 @@ Upozornění Zobrazit upozornění Zobrazit upozornění o nadcházející lekci + Nastavit upozornění o nadcházející lekci jako trvalé + Vypnout, když upozornění není ve vašem hodinkách/náramku viditelné Otevřít systémová nastavení upozornění Opravte problémy se synchronizací a upozorněním Vaše zařízení může mít problémy se synchronizací dat as upozorněními.\n\nChcete-li je opravit, přidejte Wulkanového do funkce Autostart a vypněte optimalizaci/úsporu baterie v nastavení systému telefonu. Přejít do nastavení Zobrazit upozornění o ladění Synchronizace je vypnutá + Zachytit upozornění oficiální aplikací + Zachytit upozornění + S touto funkcí můžete získat náhradu push upozornění jako v oficiální aplikaci. Vše, co musíte udělat, je povolit Wulkanowému číst všechna vaše upozornění v nastaveních systému.\n\nJak to funguje?\nKdyž obdržíte oznámení v Deníčku VULCAN, Wulkanowy bude o tom informován (k tomu je to dodatečné povolení) a spustí synchronizaci, aby mohl zaslat vlastní upozornění.\n\nPOUZE PRO POKROČILÉ UŽIVATELE + Přejít do nastavení Synchronizace Automatická aktualizace Pozastaveno na dovolené diff --git a/app/src/main/res/values-de/preferences_values.xml b/app/src/main/res/values-de/preferences_values.xml index 1e0df8dea..699ca8240 100644 --- a/app/src/main/res/values-de/preferences_values.xml +++ b/app/src/main/res/values-de/preferences_values.xml @@ -41,8 +41,8 @@ Farben der Bewertungen im Logbuch - Durchschnittsnote für das 2. Semester Durchschnitt der Noten aus beiden Semestern + Durchschnittswert der Durchschnittswerte beider Semester Durchschnitt der Noten aus dem ganzen Jahr diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 8c30c7a99..de73e7828 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -24,6 +24,7 @@ Kontodetails Schülerinfo Übersicht + Benachrichtigungszentrum Semester %1$d, %2$d/%3$d @@ -43,7 +44,7 @@ Anmelden Passwort ist zu kurz Anmeldedaten sind falsch - %1$s. Stellen Sie sicher, dass die richtige UONET+ Registervariation im unteren Feld ausgewählt ist + %1$s. Stellen Sie sicher, dass die korrekte UONET+ Registervariation unten ausgewählt ist Ungültige PIN Ungültige token Token ist nicht mehr gültig @@ -91,6 +92,10 @@ Finaler Note Vorhergesagte Note Berechnender Durchschnitt + Wie funktioniert der berechnete Durchschnitt? + The Calculated Average is the arithmetic average calculated from the subjects averages. It allows you to know the approximate final average. It is calculated in a way selected by the user in the application settings. It is recommended that you choose the appropriate option. This is because the calculation of school averages differs. Additionally, if your school reports the average of the subjects on the Vulcan page, the application downloads them and does not calculate these averages. This can be changed by forcing the calculation of the average in the application settings.\n\nAverage of grades only from selected semester:\n1. Calculating the weighted average for each subject in a given semester\n2.Adding calculated averages\n3. Calculation of the arithmetic average of the summed averages\n\nAverage of averages from both semesters:\n1.Calculating the weighted average for each subject in semester 1 and 2\n2. Calculating the arithmetic average of the calculated averages for semesters 1 and 2 for each subject.\n3. Adding calculated averages\n4. Calculation of the arithmetic average of the summed averages\n\nAverage of grades from the whole year:\n1. Calculating weighted average over the year for each subject. The final average in the 1st semester is irrelevant.\n3. Adding calculated averages\n4. Calculating the arithmetic average of summed averages + Wie funktioniert der endgültige Durchschnitt? + The Final Average is the arithmetic average calculated from all currently available final grades in the given semester.\n\nThe calculation scheme consists of the following steps:\n1. Summing up the final grades given by teachers\n2. Divide by the number of subjects that have already been graded Finaler Durchschnitt aus %1$d von %2$d Schulfächern Zusammenfassung @@ -156,7 +161,7 @@ Zusätzliche Lektionen Zusätzliche Lektionen anzeigen - Keine Infos zu zusätzlichen Lektionen + Keine Informationen über zusätzlichen Lektionen Übersicht über die Schulbesuch Aus schulischen Gründen abwesend @@ -186,8 +191,8 @@ Neue prüfungen - Du hast %d neue Prüfung erhalten - Sie haben %d neue Prüfungen erhalten + Du hast %d neue Prüfung + Du hast %d neue Prüfungen %d prüfung @@ -213,16 +218,13 @@ Thema Inhalt Nachricht erfolgreich gesendet - Nachricht existiert nicht + Nachricht nicht vorhanden Sie müssen mindestens 1 Empfänger auswählen. Der Inhalt der Nachricht muss mindestens 3 Zeichen lang sein. Nur ungelesen Nur mit Anhängen Lesen: %s - - Lesen von: %1$d von %2$d Personen - Lesen von: %1$d von %2$d Personen - + Lesen von: %1$d von %2$d Personen %d nachricht %d nachrichten @@ -345,7 +347,7 @@ Sie haben %1$d neue konferenz Sie haben %1$d neue konferenzen - Present at conference + Teilnahme an einem Meeting Agenda Schulankündigungen @@ -515,7 +517,7 @@ Ja Nein Speichern - Title + Titel Keine Lektionen Thema wählen @@ -525,7 +527,7 @@ Aussehen & Verhalten Standard Ansicht - Berechnung des Jahresenddurchschnitts + Berechnete Durchschnittsoptionen Mittelwertberechnung durch App erzwingen Anwesendheit zeigen Thema @@ -540,12 +542,18 @@ Benachrichtigungen Benachrichtigungen anzeigen Benachrichtigungen über bevorstehende Lektionen anzeigen + Festlegen einer Benachrichtigung über die bevorstehende Lektion dauerhaft + Deaktivieren wenn die Benachrichtigung nicht in deiner Uhr/Band angezeigt wird Systembenachrichtigungseinstellungen öffnen Synchronisierungs- und Benachrichtigungsprobleme reparieren Ihr Gerät hat möglicherweise Probleme mit der Datensynchronisierung und Benachrichtigungen.\n\nUm diese zu reparieren, fügen Sie Wulkanowy zum Autostart hinzu und deaktivieren Sie die Batterieoptimierung in den Systemeinstellungen des Geräts. Gehe zu den Einstellungen Debug-Benachrichtigungen anzeigen Synchronisierung ist deaktiviert + Offizielle App-Benachrichtigungen erfassen + Benachrichtigungen erfassen + With this feature you can gain a substitute of push notifications like in the official app. All you need to do is allow Wulkanowy to receive all notifications in your system settings.\n\nHow it works?\nWhen you get a notification in Dziennik VULCAN, Wulkanowy will be notified (that\'s what these extra permissions are for) and will trigger a sync so that can send its own notification.\n\nFOR ADVANCED USERS ONLY + Gehe zu Einstellungen Synchronisierung Automatische Aktualisierung An Feiertagen suspendiert diff --git a/app/src/main/res/values-pl/preferences_values.xml b/app/src/main/res/values-pl/preferences_values.xml index 51e185f79..5b0a90c6d 100644 --- a/app/src/main/res/values-pl/preferences_values.xml +++ b/app/src/main/res/values-pl/preferences_values.xml @@ -41,8 +41,8 @@ Kolory ocen w dzienniku - Średnia ocen z wybranego semestru - Średnia średnich z obu semestrów + Średnia ocen tylko z wybranego semestru + Średnia ze średnich z obu semestrów Średnia wszystkich ocen z całego roku diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index d7a49c11d..0a231606e 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -24,6 +24,7 @@ Szczegóły konta Informacje o uczniu Start + Centrum powiadomień Semestr %1$d, %2$d/%3$d @@ -43,7 +44,7 @@ Zaloguj To hasło jest za krótkie Dane logowania są niepoprawne - %1$s. Upewnij się, że poniżej została wybrana odpowiednia odmiana dziennika UONET+ + %1$s. Upewnij się, że wybrano poprawną odmianę dziennika UONET+ poniżej Nieprawidłowy PIN Nieprawidłowy token Token stracił ważność @@ -91,11 +92,11 @@ Ocena końcowa Przewidywana ocena Obliczona średnia - Jak działa Obliczona średnia? - Obliczona średnia to średnia arytmetyczna wyliczona ze średnich przedmiotów. Pozwala ona na poznanie przybliżonej średniej końcowej. Jest obliczana w sposób wybrany przez użytkownika w ustawieniach aplikacji. Zalecane jest, aby wybrać odpowiednią opcję. Wynika to z tego, że sposoby obliczania średnich w szkołach różnią się. Dodatkowo jeśli twoja szkoła podaje średnią z przedmiotów na stronie dziennika aplikacja pobiera je i nie wylicza tych średnich. Można to zmienić wymuszając liczenie średniej w ustawieniach aplikacji.\n\nŚrednia ocen z aktualnie wybranego semestru:\n1. Obliczanie średniej ważonej dla każdego przedmiotu w danym semestrze\n2. Sumowanie obliczonych średnich\n3. Obliczenie średniej arytmetycznej z zsumowanych średnich\n\nŚrednia średnich z obu semestrów:\n1. Obliczanie średniej ważonej dla każdego przedmiotu w semestrze 1 i 2\n2. Obliczanie średniej arytmetycznej z obliczonych średnich semestrów 1 i 2 dla każdego przedmiotu.\n3. Sumowanie obliczonych średnich\n4. Obliczenie średniej arytmetycznej z zsumowanych średnich\n\nŚrednia wszystkich ocen z całego roku:\n1. Obliczanie średniej ważonej z całego roku dla każdego przedmiotu. Średnia końcowa w 1 semestrze jest bez znaczenia.\n3. Sumowanie obliczonych średnich\n4. Obliczenie średniej arytmetycznej z zsumowanych średnich + Jak działa obliczona średnia? + Obliczona średnia jest średnią arytmetyczną obliczoną ze średnich przedmiotów. Pozwala ona na poznanie przybliżonej średniej końcowej. Jest obliczana w sposób wybrany przez użytkownika w ustawieniach aplikacji. Zaleca się wybranie odpowiedniej opcji. Dzieje się tak dlatego, że obliczanie średnich w szkołach różni się. Dodatkowo, jeśli twoja szkoła ma włączone średnie przedmiotów na stronie dziennika Vulcan, aplikacja pobiera je i ich nie oblicza. Można to zmienić, wymuszając obliczanie średniej w ustawieniach aplikacji.\n\nŚrednia ocen tylko z wybranego semestru:\n1. Obliczanie średniej arytmetycznej każdego przedmiotu w danym semestrze\n2. Zsumowanie obliczonych średnich\n3. Obliczanie średniej arytmetycznej zsumowanych średnich\n\nŚrednia ze średnich z obu semestrów:\n1.Obliczanie średniej arytmetycznej każdego przedmiotu w semestrze 1 i 2\n2. Obliczanie średniej arytmetycznej obliczonych średnich w semetrze 1 i 2 każdego przedmiotu.\n3. Zsumowanie obliczonych średnich\n4. Obliczanie średniej arytmetycznej zsumowanych średnich\n\nŚrednia wszystkich ocen z całego roku:\n1. Obliczanie średniej arytmetycznej z każdego przedmiotu w ciągu całego roku. Końcowa ocena w 1 semestrze jest bez znaczenia.\n3. Zsumowanie obliczonych średnich\n4. Obliczanie średniej arytmetycznej z zsumowanych średnich + Jak działa końcowa średnia? + Średnią końcową jest średnia arytmetyczna obliczona na podstawie wszystkich obecnie dostępnych ocen końcowych w danym semestrze.\n\nSchemat obliczeń składa się z następujących kroków:\n1. Sumowanie końcowych ocen wpisanych przez nauczycieli\n2. Dzielenie przez liczbę przedmiotów, z których oceny zostały już wystawione Końcowa średnia - Jak działa Końcowa średnia? - Końcowa średnia to średnia arytmetyczna wyliczona ze wszystkich aktualnie dostępnych ocen końcowych w danym semestrze.\n\nSchemat obliczania składa się z następujących kroków:\n1. Zsumowanie ocen końcowych wystawionych przez nauczycieli\n2. Podzielenie przez liczbę przedmiotów, z których oceny zostały już wystawione z %1$d na %2$d przedmiotów Podsumowanie Klasa @@ -243,12 +244,7 @@ Tylko nieprzeczytane Tylko z załącznikami Przeczytana: %s - - Przeczytana przez: %1$d z %2$d osób - Przeczytana przez: %1$d z %2$d osób - Przeczytana przez: %1$d z %2$d osób - Przeczytana przez: %1$d z %2$d osób - + Przeczytana przez: %1$d z %2$d osób %d wiadomość %d wiadomości @@ -607,7 +603,7 @@ Wygląd i zachowanie aplikacji Domyślny widok - Opcje średniej obliczonej + Opcje obliczonej średniej Wymuś obliczanie średniej przez aplikację Pokazuj obecność Motyw @@ -622,6 +618,8 @@ Powiadomienia Pokazuj powiadomienia Pokazuj powiadomienia o nadchodzących lekcjach + Ustaw powiadomienie o nadchodzącej lekcji jako trwałe + Wyłącz, gdy powiadomienie nie jest widoczne na zegarku/opasce Otwórz systemowe ustawienia powiadomień Napraw problemy z synchronizacją i powiadomieniami Na twoim urządzeniu mogą występować problemy z synchronizacją danych i powiadomieniami.\n\nBy je naprawić, dodaj Wulkanowego do autostartu i wyłącz optymalizację/oszczędzanie baterii w ustawieniach systemowych telefonu. @@ -630,7 +628,7 @@ Synchronizacja jest wyłączona Przechwytywanie powiadomień oficjalnej aplikacji Przechwytywanie powiadomień - Dzięki tej funkcji możesz zyskać namiastkę powiadomień push takich, jak w oficjalnej aplikacji. Wystarczy że zezwolisz Wulkanowemu na odbieranie wszystkich powiadomień w ustawieniach systemowych.\n\nJak to działa?\nGdy dostaniesz powiadomienie w Dzienniczku VULCANa Wulkanowy zostanie o tym powiadomiony (po to te dodatkowe uprawnienia) i uruchomi synchronizację, dzięki czemu będzie mógł wysłać własne powiadomienie.\n\nTYLKO DLA ZAAWANSOWANYCH UŻYTKOWNIKÓW + Dzięki tej funkcji możesz uzyskać namiastkę powiadomień push, takich jak w oficjalnej aplikacji. Wszystko, co musisz zrobić, to zezwolić Wulkanowemu na odczytywanie wszystkich powiadomień w ustawieniach systemowych.\n\nJak to działa?\nKiedy otrzymasz powiadomienie w Dzienniczku VULCAN, Wulkanowy zostanie o tym powiadomiony (do tego jest to dodatkowe uprawnienie) i uruchomi synchronizację, aby mógł wysłać własne powiadomienie.\n\nWYŁĄCZNIE DLA ZAAWANSOWANYCH UŻYTKOWNIKÓW Przejdź do ustawień Synchronizacja Automatyczna aktualizacja diff --git a/app/src/main/res/values-ru/preferences_values.xml b/app/src/main/res/values-ru/preferences_values.xml index 7c4d14df6..4920068e7 100644 --- a/app/src/main/res/values-ru/preferences_values.xml +++ b/app/src/main/res/values-ru/preferences_values.xml @@ -41,8 +41,8 @@ Цвета оценок в дневнике - Средняя оценка со 2 семестра - Средняя оценка с двух семестров + Средние оценки только с выбранного семестра + Средние значения для обоих семестров Средняя оценок со всего года diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 54ae5e1ba..48d8a21a7 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -24,6 +24,7 @@ Данные аккаунта Информация о студенте Панель + Центр уведомлений %1$d семестр, %2$d/%3$d @@ -42,8 +43,8 @@ Symbol Войти Слишком короткий пароль - Данные для входа неверны - %1$s. Убедитесь, что в поле ниже выбран правильный вариант регистра UONET+ + Данные для входа указаны неверно + %1$s. Убедитесь, что ниже выбран правильный UONET+ вариант регистра Неправильный PIN Неверный token Token просрочен @@ -91,6 +92,10 @@ Итоговая оценка Ожидаемая оценка Рассчитанная средняя оценка + Как рассчитывается средняя работа? + Расчетное среднее - это среднее арифметическое, рассчитанное на основе средних значений испытуемых. Это позволяет узнать приблизительное итоговое среднее значение. Он рассчитывается способом, выбранным пользователем в настройках приложения. Рекомендуется выбрать подходящий вариант. Это потому, что расчет средних показателей школы отличается. Кроме того, если ваша школа сообщает среднее значение по предметам на странице Vulcan, приложение загружает их и не вычисляет эти средние значения. Это можно изменить, принудительно вычисляя среднее значение в настройках приложения. \ N \ n Среднее значение только за выбранный семестр : \ n1. Вычисление средневзвешенного значения по каждому предмету за семестр \ n2.Добавление вычисленных средних \ n3. Вычисление среднего арифметического суммарных средних \ n \ n Среднее из средних значений за оба семестра : \ n1.Расчет средневзвешенного значения для каждого предмета в семестрах 1 и 2 \ n2. Вычисление среднего арифметического рассчитанных средних значений для семестров 1 и 2 по каждому предмету. \ N3. Добавление вычисленных средних \ n4. Расчет среднего арифметического суммированных средних \ n \ n Среднее значение оценок за весь год: \ n1. Расчет средневзвешенного значения за год по каждому предмету. Итоговое среднее значение за 1 семестр не имеет значения. \ N3. Добавление вычисленных средних \ n4. Расчет среднего арифметического + Как работает окончательный средний показатель? + Среднее арифметическое - это среднее арифметическое, рассчитанное по всем имеющимся на данный момент итоговым классам данного семестра.\n\nСхема расчета состоит из следующих шагов:\n1. Суммирование итоговых классов преподавателей\n2. Деление по количеству уже оцененных предметов Итоговая средняя оценка от %1$d из %2$d субъектов Итоги @@ -239,12 +244,7 @@ Только непрочитанные Только с вложениями Чтение: %s - - Прочитано: %1$d из %2$d человек - Прочитано: %1$d из %2$d человек - Прочитано: %1$d из %2$d человек - Прочитано: %1$d из %2$d человек - + Прочитано: %1$d из %2$d человек %d сообщение %d сообщения @@ -403,8 +403,8 @@ У вас %1$d новая конференция У вас %1$d новых конференций - Present at conference - Agenda + Присутствует на конференции + Повестка дня Объявления школ Нет объявлений о школе @@ -593,7 +593,7 @@ Да Нет Сохранить - Title + Тема Нет уроков Выбрать тему @@ -603,7 +603,7 @@ Внешний вид приложения & поведение Окно по умолчанию - Способ определения средней годовой оценки + Рассчитанные средние параметры Принудительно высчитать среднюю оценку через приложение Показать присутствие Тема @@ -618,12 +618,18 @@ Уведомления Показывать уведомления Показывать уведомления о будущих уроках + Сделать уведомления о предстоящем уроке постоянным + Выключить, когда уведомление не отображается в чата/полосе Открыть настройки уведомлений системы Исправить проблемы с синхронизацией и уведомлениями На вашем устройстве могут быть проблемы с синхронизацией данных и уведомлениями.\n\nЧтобы их исправить, вам необходимо добавить Wulkanowy в авто-старт и выключить оптимизацию/экономию батареи в настройках устройства. Перейти в настройски Показывать дебаг-уведомления Синхронизация отключена + Записывать официальные уведомления + Показывать push-уведомления + С помощью этой функции вы можете получить замену push-уведомлений, как в официальном приложении. Все, что вам нужно сделать, это разрешить Wulkanowy получать все уведомления в настройках системы.\n\nКак это работает?\nКогда вы получаете уведомление в Dziennik VULCAN, Wulkanowy будет уведомлен (это требует дополнительных прав) и запустит синхронизацию, чтобы отправить свое уведомление.\n\nТОЛЬКО ДЛЯ ПОЛЬЗОВАТЕЛЯ + Перейти к настройкам Синхронизация Автоматическая синхронизация Приостановить синхронизации во время каникул diff --git a/app/src/main/res/values-sk/preferences_values.xml b/app/src/main/res/values-sk/preferences_values.xml index 108af555d..b8974f23e 100644 --- a/app/src/main/res/values-sk/preferences_values.xml +++ b/app/src/main/res/values-sk/preferences_values.xml @@ -41,8 +41,8 @@ Farby známok v denníku - Priemer známok až od druhého semestra - Priemer známok z oboch semestrov + Priemer známok iba z vybraného semestra + Priemer z priemerov z oboch semestrov Priemer známok z celého roka diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index b1fd78ae3..19e9735f9 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -12,7 +12,7 @@ O aplikácii Prehliadač protokolov Ladenie - Ladenie oznámení + Ladenie upozornení Prispievatelia Licencie Správy @@ -24,6 +24,7 @@ Podrobnosti účtu Informácie o žiakovi Domov + Centrum upozornení Semester %1$d, %2$d/%3$d @@ -43,7 +44,7 @@ Prihlásiť Toto heslo je príliš krátke Prihlasovacie údaje sú nesprávne - %1$s. Uistite sa, že je v poli nižšie vybraná správna variácie denníka UONET+ + %1$s. Skontrolujte, či je nižšie vybratá správna variácie denníka UONET+ Neplatný PIN Neplatný token Platnosť tokenu vypršala @@ -91,6 +92,10 @@ Konečná známka Predpokladaná známka Vypočítaný priemer + Ako funguje vypočítaný priemer? + Vypočítaný priemer je aritmetický priemer vypočítaný z priemerov predmetov. Umožňuje vám to poznať približný konečný priemer. Vypočítava sa spôsobom zvoleným užívateľom v nastaveniach aplikácii. Odporúča sa vybrať príslušnú možnosť. Dôvodom je rozdielny výpočet školských priemerov. Ak vaša škola navyše uvádza priemer predmetov na stránke denníka Vulcan, aplikácia si ich stiahne a tieto priemery nepočíta. To možno zmeniť vynútením výpočtu priemeru v nastavení aplikácii.\n\nPriemer známok iba z vybraného semestra:\n1. Výpočet váženého priemeru pre každý predmet v danom semestri\n2. Sčítanie vypočítaných priemerov\n3. Výpočet aritmetického priemeru součtených priemerov\n\nPriemer priemerov z oboch semestrov:\n1. Výpočet váženého priemeru pre každý predmet v semestri 1 a 2\n2. Výpočet aritmetického priemeru vypočítaných priemerov za semestre 1 a 2 pre každý predmet.\n3. Sčítanie vypočítaných priemerov\n4. Výpočet aritmetického priemeru součtených priemerov\n\nPriemer známok z celého roka:\n1. Výpočet váženého priemeru za rok pre každý predmet. Konečný priemer v 1. semestri je nepodstatný.\n3. Sčítanie vypočítaných priemerov\n4. Výpočet aritmetického priemeru součtených priemerov + Ako funguje konečný priemer? + Konečný priemer je aritmetický priemer vypočítaný zo všetkých aktuálne dostupných konečných známok v danom semestri.\n\nSchéma výpočtu sa skladá z nasledujúcich krokov:\n1. Sčítanie konečných známok zadaných učiteľmi\n2. Delené počtom predmetov, pre ktoré už boli vydané známky Konečný priemer z %1$d z %2$d predmetov Zhrnutie @@ -239,12 +244,7 @@ Iba neprečítané Iba s prílohami Prečítaná: %s - - Prečítaná cez: %1$d z %2$d osôb - Prečítaná cez: %1$d z %2$d osôb - Prečítaná cez: %1$d z %2$d osôb - Prečítaná cez: %1$d z %2$d osôb - + Prečítaná cez: %1$d z %2$d osôb %d správa %d správy @@ -403,7 +403,7 @@ Máte %1$d nových stretnutí Máte %1$d nových stretnutí - Present at conference + Prítomnosť na stretnutí Agenda Školské oznámenia @@ -593,7 +593,7 @@ Áno Nie Uložiť - Title + Titul Žiadne lekcie Vybrať motív @@ -603,7 +603,7 @@ Vzhľad a správanie aplikácií Predvolené zobrazenie - Výpočet koncoročního priemeru + Možnosti vypočítaného priemeru Vynútiť priemerný výpočet podľa aplikácie Zobraziť prítomnosť Motív @@ -618,12 +618,18 @@ Upozornenia Zobraziť upozornenia Zobraziť upozornenia o nadchádzajúcej lekciu + Nastaviť upozornenia o nadchádzajúcej lekcii ako trvalé + Vypnúť, keď upozornenia nie je vo vašom hodinkách/náramku viditeľné Otvoriť systémové nastavenia upozornení Opravte problémy so synchronizáciou a upozornením Vaše zariadenie môže mať problémy so synchronizáciou dát as upozorneniami.\n\nAk ich chcete opraviť, pridajte Wulkanového do funkcie Autostart a vypnite optimalizáciu/úsporu batérie v nastavení systému telefóne. Prejsť do nastavení Zobraziť upozornenia o ladení Synchronizácia je vypnutá + Zachytiť upozornenia oficiálnej aplikácie + Zachytiť upozornenia + S touto funkciou môžete získať náhradu push upozornení ako v oficiálnej aplikácii. Všetko, čo musíte urobiť, je povoliť Wulkanowému čítať všetky vaše upozornenia v nastaveniach systému.\n\nAko to funguje?\nKeď dostanete oznámenie v Deníčku VULCAN, Wulkanowy bude o tom informovaný (k tomu je to dodatočné povolenie) a spustí synchronizáciu, aby mohol zaslať vlastné upozornenie.\n\nLEN PRE POKROČILÝCH POUŽĺVATEĹOV + Prejsť do nastavení Synchronizácia Automatická aktualizácia Pozastavený počas dovolenky diff --git a/app/src/main/res/values-uk/preferences_values.xml b/app/src/main/res/values-uk/preferences_values.xml index f6f5b984f..f21ad819b 100644 --- a/app/src/main/res/values-uk/preferences_values.xml +++ b/app/src/main/res/values-uk/preferences_values.xml @@ -41,8 +41,8 @@ Кольори оцінок в щоденнику - Середня оцінка з 2 семестру - Середнє оцінювання за обидва семестри + Середні оцінки тільки від обраного семестру + Середнє значення для обох семестів Середнє оцінювання за весь рік diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 70f40b66c..58fed9881 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -24,6 +24,7 @@ Деталі облікового запису Інформація про учня Дошка + Журнал сповіщень %1$d семестр, %2$d/%3$d @@ -42,8 +43,8 @@ Symbol Увійти Занадто короткий пароль - Дані для входу неправильні - %1$s. Переконайтеся, що у полі нижче вказано правильний варіант реєстрації UONET+ + Вказані невірні дані + %1$s. Переконайтеся, що обрано правильну варіацію запису UONET+ Неправильний PIN Неправильний token Минув термін дії токену @@ -91,6 +92,10 @@ Підсумкова оцінка Очікувана оцінка Розрахована середня оцінка + Як розраховується середньо? + Розрахункове середнє - це середнє арифметичне, обчислене з середніх показників для випробуваних. Це дозволяє дізнатися приблизну кінцеву середню величину. Він розраховується способом, обраним користувачем у налаштуваннях програми. Рекомендується вибрати відповідний варіант. Це пояснюється тим, що розрахунок середніх показників за школою відрізняється. Крім того, якщо у вашій школі повідомляється середнє значення предметів на сторінці Вулкан, програма завантажує їх і не обчислює ці середні значення. Це можна змінити шляхом примусового розрахунку середнього значення в налаштуваннях програми. \ N \ n Середні оцінки лише за вибраний семестр : \ n1. Розрахунок середньозваженого для кожного предмета в даному семестрі \ n2.Додавання розрахункових середніх \ n3. Розрахунок середнього арифметичного підсумованих середніх значень \ n \ n Середнє значення середніх показників за обидва семестри : \ n1.Обчислення середньозваженого значення для кожного предмета у 1 та 2 семестрах \ n2. Обчислення середнього арифметичного розрахункових середніх показників за 1 та 2 семестри для кожного предмета. \ N3. Додавання розрахункових середніх \ n4. Розрахунок середнього арифметичного підсумованих середніх значень \ n \ n Середнє значення оцінок за весь рік: \ n1. Розрахунок середньозваженого показника за рік для кожного предмета. Остаточний середній показник у 1 -му семестрі не має значення. \ N3. Додавання розрахункових середніх \ n4. Обчислення середнього арифметичного середніх суммованих середніх + Як працює кінцевий середній показник? + Підсумкове середнє значення - це середнє арифметичне, обчислене з усіх наявних наразі підсумкових оцінок у даному семестрі. \ N \ nСхема обчислення складається з таких кроків: \ n1. Підбиття підсумкових оцінок викладачів \ n2. Поділіть на кількість предметів, які вже оцінені Підсумкова середня оцінка з %1$d із %2$d тем Підсумок @@ -239,12 +244,7 @@ Лише непрочитані Тільки з вкладеннями Читання: %s - - Прочитані: %1$d з %2$d осіб - Прочитані: %1$d з %2$d осіб - Прочитані: %1$d з %2$d осіб - Прочитані: %1$d з %2$d осіб - + Прочитанно:%1$d через %2$d людей %d повідомлення %d повідомлення @@ -403,8 +403,8 @@ У вас є %1$d нова конференція У вас є %1$d нових конференцій - Present at conference - Agenda + Присутність на конференції + Порядок денний Оголошення школи Жодних навчальних оголошень @@ -593,7 +593,7 @@ Так Ні Зберегти - Title + Титул Брак уроків Увібрати тему @@ -603,7 +603,7 @@ Поява додатка & amp; поведінки Вікно за замовчуванням - Спосіб облічування оцінки на кінець року + Розрахункові середні параметри Примусово розрахувати середню оцінку через додаток Показати присутність Тема @@ -618,12 +618,18 @@ Повідомлення Показувати повідомлення Показувати повідомлення о наступних уроках + Зробити сповіщення майбутнього уроку нестійкими + Вимкнути коли сповіщення не показуються у відстежувачі/темпі Відкрити налаштування сповіщень системи Виправити помилки з синхронізацією і повідомленнями На вашому пристрої можуть бути помилки з синхронізацією і повідомленнями\n\nЩоб виправити іх, вам необхідно додати Wulkanowy в авто-старт и вимкнути оптимізацію/экономію батареї в налаштуваннях пристрою. Перейти до налаштувань Показувати дебаг-повідомлення Синхронізація вимкнена + Захоплювати офіційні сповіщення програм + Показувати push-повідомлення + За допомогою цієї функції ви можете отримати заміну push -повідомлень, як у офіційному додатку. Все, що вам потрібно зробити, це дозволити Wulkanowy отримувати всі сповіщення у налаштуваннях вашої системи. \ N \ nЯк це працює? \ NКоли ви отримаєте сповіщення у Dziennik VULCAN, Wulkanowy отримає сповіщення (для цього призначені ці додаткові дозволи) і запустить синхронізація, яка може надсилати власне сповіщення. \ n \ n ТІЛЬКИ ДЛЯ РОЗШИРЕНИХ КОРИСТУВАЧІВ + Перейти до налаштувань Синхронізація Автоматична синхронізація Призупинено на час канікул From 0b83a66b85a16d5e590078825fee4f6cf461959f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Tue, 28 Sep 2021 23:10:11 +0200 Subject: [PATCH 216/869] Remove disappearing teachers workaround from timetable repository (#1545) --- .../data/repositories/TimetableRepository.kt | 12 +- .../repositories/TimetableRepositoryTest.kt | 129 +----------------- 2 files changed, 3 insertions(+), 138 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt index 1540d3cc7..769fa0f0d 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt @@ -123,17 +123,7 @@ class TimetableRepository @Inject constructor( lessonsNew: List, ) { val lessonsToRemove = lessonsOld uniqueSubtract lessonsNew - val lessonsToAdd = (lessonsNew uniqueSubtract lessonsOld).map { new -> - val matchingOld = lessonsOld.singleOrNull { new.start == it.start } - if (matchingOld != null) { - val useOldTeacher = new.teacher.isEmpty() && !new.changes && !matchingOld.changes - new.copy( - room = if (new.room.isEmpty()) matchingOld.room else new.room, - teacher = if (useOldTeacher) matchingOld.teacher - else new.teacher - ) - } else new - } + val lessonsToAdd = lessonsNew uniqueSubtract lessonsOld timetableDb.deleteAll(lessonsToRemove) timetableDb.insertAll(lessonsToAdd) diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt index 75c75a66c..edb3125eb 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt @@ -67,136 +67,11 @@ class TimetableRepositoryTest { timetableRepository = TimetableRepository(timetableDb, timetableAdditionalDao, timetableHeaderDao, sdk, timetableNotificationSchedulerHelper, refreshHelper) } - @Test - fun copyRoomToCompletedFromPrevious() { - // prepare - val remoteList = listOf( - createTimetableRemote(of(2021, 1, 4, 8, 0), 1, "", "Przyroda"), - createTimetableRemote(of(2021, 1, 4, 8, 50), 2, "", "Religia"), - createTimetableRemote(of(2021, 1, 4, 9, 40), 3, "", "W-F"), - createTimetableRemote(of(2021, 1, 4, 10, 30), 4, "", "W-F") - ) - coEvery { sdk.getTimetableFull(any(), any()) } returns TimetableFull(emptyList(), remoteList, emptyList()) - - val localList = listOf( - createTimetableRemote(of(2021, 1, 4, 8, 0), 1, "123", "Przyroda"), - createTimetableRemote(of(2021, 1, 4, 8, 50), 2, "321", "Religia"), - createTimetableRemote(of(2021, 1, 4, 9, 40), 3, "213", "W-F"), - createTimetableRemote(of(2021, 1, 4, 10, 30), 3, "213", "W-F", "Jan Kowalski") - ) - coEvery { timetableDb.loadAll(semester.diaryId, semester.studentId, startDate, endDate) } returns flowOf(localList.mapToEntities(semester)) - coEvery { timetableDb.insertAll(any()) } returns listOf(1, 2, 3) - coEvery { timetableDb.deleteAll(any()) } just Runs - - coEvery { timetableAdditionalDao.loadAll(1, 1, startDate, endDate) } returns flowOf(listOf()) - coEvery { timetableAdditionalDao.insertAll(emptyList()) } returns listOf(1, 2, 3) - coEvery { timetableAdditionalDao.deleteAll(emptyList()) } just Runs - - coEvery { timetableHeaderDao.loadAll(1, 1, startDate, endDate) } returns flowOf(listOf()) - coEvery { timetableHeaderDao.insertAll(emptyList()) } returns listOf(1, 2, 3) - coEvery { timetableHeaderDao.deleteAll(emptyList()) } just Runs - - // execute - val res = runBlocking { - timetableRepository.getTimetable(student, semester, startDate, endDate, true).toFirstResult() - } - - // verify - assertEquals(4, res.data?.lessons.orEmpty().size) - coVerify { - timetableDb.insertAll(withArg { - assertEquals(4, it.size) - assertEquals("123", it[0].room) - assertEquals("321", it[1].room) - assertEquals("213", it[2].room) - }) - } - coVerify { timetableDb.deleteAll(match { it.size == 4 }) } - } - - @Test - fun copyTeacherToCompletedFromPrevious() { - // prepare - val remoteList = listOf( - createTimetableRemote(of(2021, 1, 4, 8, 0), 1, "123", "Matematyka", "Paweł Poniedziałkowski", false), // skip - createTimetableRemote(of(2021, 1, 4, 8, 50), 2, "124", "Matematyka", "Jakub Wtorkowski", true), - createTimetableRemote(of(2021, 1, 4, 9, 40), 3, "125", "Język polski", "Joanna Poniedziałkowska", false), - createTimetableRemote(of(2021, 1, 4, 10, 40), 4, "126", "Język polski", "Joanna Wtorkowska", true), // skip - - createTimetableRemote(of(2021, 1, 5, 8, 0), 1, "123", "Język polski", "", false), - createTimetableRemote(of(2021, 1, 5, 8, 50), 2, "124", "Język polski", "", true), - createTimetableRemote(of(2021, 1, 5, 9, 40), 3, "125", "Język polski", "", false), - createTimetableRemote(of(2021, 1, 5, 10, 40), 4, "126", "Język polski", "", true), - - createTimetableRemote(of(2021, 1, 6, 8, 0), 1, "123", "Matematyka", "Paweł Środowski", false), - createTimetableRemote(of(2021, 1, 6, 8, 50), 2, "124", "Matematyka", "Paweł Czwartkowski", true), - createTimetableRemote(of(2021, 1, 6, 9, 40), 3, "125", "Matematyka", "Paweł Środowski", false), - createTimetableRemote(of(2021, 1, 6, 10, 40), 4, "126", "Matematyka", "Paweł Czwartkowski", true) - ) - coEvery { sdk.getTimetableFull(startDate, endDate) } returns TimetableFull(emptyList(), remoteList, emptyList()) - - val localList = listOf( - createTimetableRemote(of(2021, 1, 4, 8, 0), 1, "123", "Matematyka", "Paweł Poniedziałkowski", false), - createTimetableRemote(of(2021, 1, 4, 8, 50), 2, "124", "Matematyka", "Paweł Poniedziałkowski", false), - createTimetableRemote(of(2021, 1, 4, 9, 40), 3, "125", "Język polski", "Joanna Wtorkowska", true), - createTimetableRemote(of(2021, 1, 4, 10, 40), 4, "126", "Język polski", "Joanna Wtorkowska", true), - - createTimetableRemote(of(2021, 1, 5, 8, 0), 1, "123", "Język polski", "Joanna Wtorkowska", false), - createTimetableRemote(of(2021, 1, 5, 8, 50), 2, "124", "Język polski", "Joanna Wtorkowska", false), - createTimetableRemote(of(2021, 1, 5, 9, 40), 3, "125", "Język polski", "Joanna Środowska", true), - createTimetableRemote(of(2021, 1, 5, 10, 40), 4, "126", "Język polski", "Joanna Środowska", true), - - createTimetableRemote(of(2021, 1, 6, 8, 0), 1, "123", "Matematyka", "", false), - createTimetableRemote(of(2021, 1, 6, 8, 50), 2, "124", "Matematyka", "", false), - createTimetableRemote(of(2021, 1, 6, 9, 40), 3, "125", "Matematyka", "", true), - createTimetableRemote(of(2021, 1, 6, 10, 40), 4, "126", "Matematyka", "", true) - ) - coEvery { timetableDb.loadAll(semester.diaryId, semester.studentId, startDate, endDate) } returns flowOf(localList.mapToEntities(semester)) - coEvery { timetableDb.insertAll(any()) } returns listOf(1, 2, 3) - coEvery { timetableDb.deleteAll(any()) } just Runs - - coEvery { timetableAdditionalDao.loadAll(1, 1, startDate, endDate) } returns flowOf(listOf()) - coEvery { timetableAdditionalDao.insertAll(emptyList()) } returns listOf(1, 2, 3) - coEvery { timetableAdditionalDao.deleteAll(emptyList()) } just Runs - - coEvery { timetableHeaderDao.loadAll(1, 1, startDate, endDate) } returns flowOf(listOf()) - coEvery { timetableHeaderDao.insertAll(emptyList()) } returns listOf(1, 2, 3) - coEvery { timetableHeaderDao.deleteAll(emptyList()) } just Runs - - // execute - val res = runBlocking { timetableRepository.getTimetable(student, semester, startDate, endDate, true).toFirstResult() } - - // verify - assertEquals(null, res.error) - assertEquals(12, res.data!!.lessons.size) - - coVerify { - timetableDb.insertAll(withArg { - assertEquals(10, it.size) -// assertEquals("Paweł Poniedziałkowski", it[0].teacher) // skip - assertEquals("Jakub Wtorkowski", it[0].teacher) - assertEquals("Joanna Poniedziałkowska", it[1].teacher) -// assertEquals("Joanna Wtorkowska", it[3].teacher) // skip - - assertEquals("Joanna Wtorkowska", it[2].teacher) - assertEquals("", it[3].teacher) - assertEquals("", it[4].teacher) - assertEquals("", it[5].teacher) - - assertEquals("Paweł Środowski", it[6].teacher) - assertEquals("Paweł Czwartkowski", it[7].teacher) - assertEquals("Paweł Środowski", it[8].teacher) - assertEquals("Paweł Czwartkowski", it[9].teacher) - }) - } - coVerify { timetableDb.deleteAll(match { it.size == 10 }) } - } - @Test fun `force refresh without difference`() { val remoteList = listOf( - createTimetableRemote(of(2021, 1, 4, 8, 0), 1, "123", "Język polski", "", false), - createTimetableRemote(of(2021, 1, 4, 8, 50), 2, "124", "Język polski", "", true) + createTimetableRemote(of(2021, 1, 4, 8, 0), 1, "123", "Język polski", "Jan Kowalski", false), + createTimetableRemote(of(2021, 1, 4, 8, 50), 2, "124", "Język niemiecki", "Joanna Czarniecka", true) ) // prepare From 9c8bcbfdd38ae9c1289aa59efb218b126b6cde89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Tue, 28 Sep 2021 23:11:59 +0200 Subject: [PATCH 217/869] New Crowdin updates (#1544) --- app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values-uk/strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 48d8a21a7..92af176f2 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -93,7 +93,7 @@ Ожидаемая оценка Рассчитанная средняя оценка Как рассчитывается средняя работа? - Расчетное среднее - это среднее арифметическое, рассчитанное на основе средних значений испытуемых. Это позволяет узнать приблизительное итоговое среднее значение. Он рассчитывается способом, выбранным пользователем в настройках приложения. Рекомендуется выбрать подходящий вариант. Это потому, что расчет средних показателей школы отличается. Кроме того, если ваша школа сообщает среднее значение по предметам на странице Vulcan, приложение загружает их и не вычисляет эти средние значения. Это можно изменить, принудительно вычисляя среднее значение в настройках приложения. \ N \ n Среднее значение только за выбранный семестр : \ n1. Вычисление средневзвешенного значения по каждому предмету за семестр \ n2.Добавление вычисленных средних \ n3. Вычисление среднего арифметического суммарных средних \ n \ n Среднее из средних значений за оба семестра : \ n1.Расчет средневзвешенного значения для каждого предмета в семестрах 1 и 2 \ n2. Вычисление среднего арифметического рассчитанных средних значений для семестров 1 и 2 по каждому предмету. \ N3. Добавление вычисленных средних \ n4. Расчет среднего арифметического суммированных средних \ n \ n Среднее значение оценок за весь год: \ n1. Расчет средневзвешенного значения за год по каждому предмету. Итоговое среднее значение за 1 семестр не имеет значения. \ N3. Добавление вычисленных средних \ n4. Расчет среднего арифметического + Расчетное среднее - это среднее арифметическое, рассчитанное на основе средних значений испытуемых. Это позволяет узнать приблизительное итоговое среднее значение. Он рассчитывается способом, выбранным пользователем в настройках приложения. Рекомендуется выбрать подходящий вариант. Это потому, что расчет средних показателей школы отличается. Кроме того, если ваша школа сообщает среднее значение по предметам на странице Vulcan, приложение загружает их и не вычисляет эти средние значения. Это можно изменить, принудительно вычисляя среднее значение в настройках приложения.\n\nСреднее значение только за выбранный семестр :\n1. Вычисление средневзвешенного значения по каждому предмету за семестр\n2.Добавление вычисленных средних\n3. Вычисление среднего арифметического суммарных средних\n\nСреднее из средних значений за оба семестра:\n1.Расчет средневзвешенного значения для каждого предмета в семестрах 1 и 2\n2. Вычисление среднего арифметического рассчитанных средних значений для семестров 1 и 2 по каждому предмету.\n3. Добавление вычисленных средних\n4. Расчет среднего арифметического суммированных средних\n\nСреднее значение оценок за весь год: \n1. Расчет средневзвешенного значения за год по каждому предмету. Итоговое среднее значение за 1 семестр не имеет значения.\n3. Добавление вычисленных средних\n4. Расчет среднего арифметического Как работает окончательный средний показатель? Среднее арифметическое - это среднее арифметическое, рассчитанное по всем имеющимся на данный момент итоговым классам данного семестра.\n\nСхема расчета состоит из следующих шагов:\n1. Суммирование итоговых классов преподавателей\n2. Деление по количеству уже оцененных предметов Итоговая средняя оценка diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 58fed9881..741755486 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -93,7 +93,7 @@ Очікувана оцінка Розрахована середня оцінка Як розраховується середньо? - Розрахункове середнє - це середнє арифметичне, обчислене з середніх показників для випробуваних. Це дозволяє дізнатися приблизну кінцеву середню величину. Він розраховується способом, обраним користувачем у налаштуваннях програми. Рекомендується вибрати відповідний варіант. Це пояснюється тим, що розрахунок середніх показників за школою відрізняється. Крім того, якщо у вашій школі повідомляється середнє значення предметів на сторінці Вулкан, програма завантажує їх і не обчислює ці середні значення. Це можна змінити шляхом примусового розрахунку середнього значення в налаштуваннях програми. \ N \ n Середні оцінки лише за вибраний семестр : \ n1. Розрахунок середньозваженого для кожного предмета в даному семестрі \ n2.Додавання розрахункових середніх \ n3. Розрахунок середнього арифметичного підсумованих середніх значень \ n \ n Середнє значення середніх показників за обидва семестри : \ n1.Обчислення середньозваженого значення для кожного предмета у 1 та 2 семестрах \ n2. Обчислення середнього арифметичного розрахункових середніх показників за 1 та 2 семестри для кожного предмета. \ N3. Додавання розрахункових середніх \ n4. Розрахунок середнього арифметичного підсумованих середніх значень \ n \ n Середнє значення оцінок за весь рік: \ n1. Розрахунок середньозваженого показника за рік для кожного предмета. Остаточний середній показник у 1 -му семестрі не має значення. \ N3. Додавання розрахункових середніх \ n4. Обчислення середнього арифметичного середніх суммованих середніх + Розрахункове середнє - це середнє арифметичне, обчислене з середніх показників для випробуваних. Це дозволяє дізнатися приблизну кінцеву середню величину. Він розраховується способом, обраним користувачем у налаштуваннях програми. Рекомендується вибрати відповідний варіант. Це пояснюється тим, що розрахунок середніх показників за школою відрізняється. Крім того, якщо у вашій школі повідомляється середнє значення предметів на сторінці Вулкан, програма завантажує їх і не обчислює ці середні значення. Це можна змінити шляхом примусового розрахунку середнього значення в налаштуваннях програми.\n\nСередні оцінки лише за вибраний семестр :\n1. Розрахунок середньозваженого для кожного предмета в даному семестрі\n2.Додавання розрахункових середніх\n3. Розрахунок середнього арифметичного підсумованих середніх значень\n\nСереднє значення середніх показників за обидва семестри :\n1.Обчислення середньозваженого значення для кожного предмета у 1 та 2 семестрах\n2. Обчислення середнього арифметичного розрахункових середніх показників за 1 та 2 семестри для кожного предмета.\n3. Додавання розрахункових середніх\n4. Розрахунок середнього арифметичного підсумованих середніх значень\n\nСереднє значення оцінок за весь рік: \n1. Розрахунок середньозваженого показника за рік для кожного предмета. Остаточний середній показник у 1 -му семестрі не має значення.\n3. Додавання розрахункових середніх \n4. Обчислення середнього арифметичного середніх суммованих середніх Як працює кінцевий середній показник? Підсумкове середнє значення - це середнє арифметичне, обчислене з усіх наявних наразі підсумкових оцінок у даному семестрі. \ N \ nСхема обчислення складається з таких кроків: \ n1. Підбиття підсумкових оцінок викладачів \ n2. Поділіть на кількість предметів, які вже оцінені Підсумкова середня оцінка From 6cdcf927824380da83700e79cf88da5e903ecc97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Tue, 28 Sep 2021 23:26:10 +0200 Subject: [PATCH 218/869] Version 1.3.0 --- app/build.gradle | 6 +++--- app/src/main/play/release-notes/pl-PL/default.txt | 11 ++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index e8680fe25..1d6e56b11 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,8 +21,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 30 - versionCode 96 - versionName "1.2.3" + versionCode 97 + versionName "1.3.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true @@ -166,7 +166,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:f62736adb0" + implementation "io.github.wulkanowy:sdk:1.3.0" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index 7de10a26b..fc9fab88e 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,8 +1,9 @@ -Wersja 1.2.3 +Wersja 1.3.0 -- naprawiliśmy pomieszane imiona nauczycieli z salami w planie lekcji -- dodaliśmy brakujące okienka ze szczegółami na ekranie zebrań -- klikając w kafelek z lekcjami na jutro aplikacja teraz przekierowuje na ekran z planem na jutro -- naprawiliśmy błąd przy wylogowywaniu innego niż bieżący uczeń +- naprawiliśmy logowanie na platformę Opolskiej eSzkoły +- dodaliśmy centrum powiadomień i opcję odbierania pushy z oficjalnej aplikacji (dla zaawansowanych) +- dodaliśmy objaśnienie do informacji o obliczonych średnich w podsumowaniu +- poprawiliśmy wyświetlanie zmian w planie lekcji +- dokonaliśmy też kilka innych zmian i kosmetycznych poprawek poprawiających komfort używania aplikacji Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases From e02d93f9797c8ada9bb48372e9363d697e298140 Mon Sep 17 00:00:00 2001 From: Damian Czupryn <60961958+Daxxxis@users.noreply.github.com> Date: Sun, 3 Oct 2021 01:07:45 +0200 Subject: [PATCH 219/869] Add Czech and Slovak listings (#1555) --- .../main/play/listings/cs-CZ/full-description.txt | 14 ++++++++++++++ .../main/play/listings/cs-CZ/short-description.txt | 1 + app/src/main/play/listings/cs-CZ/title.txt | 1 + app/src/main/play/listings/sk/full-description.txt | 14 ++++++++++++++ .../main/play/listings/sk/short-description.txt | 1 + app/src/main/play/listings/sk/title.txt | 1 + 6 files changed, 32 insertions(+) create mode 100644 app/src/main/play/listings/cs-CZ/full-description.txt create mode 100644 app/src/main/play/listings/cs-CZ/short-description.txt create mode 100644 app/src/main/play/listings/cs-CZ/title.txt create mode 100644 app/src/main/play/listings/sk/full-description.txt create mode 100644 app/src/main/play/listings/sk/short-description.txt create mode 100644 app/src/main/play/listings/sk/title.txt diff --git a/app/src/main/play/listings/cs-CZ/full-description.txt b/app/src/main/play/listings/cs-CZ/full-description.txt new file mode 100644 index 000000000..1420f5d67 --- /dev/null +++ b/app/src/main/play/listings/cs-CZ/full-description.txt @@ -0,0 +1,14 @@ +Aplikace je určena pro uživatele deníku VULCAN UONET+. + +Zvýrazněné vlastnosti a funkce: +- výpočet váženého průměru, +- procentuální zobrazení docházky, +- šťastné číslo, +- náhled na další a dokončené lekce, +- tmavý motiv, +- žádné reklamy, +- offline režim, +- upozornění. + +GitHub: https://github.com/wulkanowy/wulkanowy +Discord: https://discord.gg/vccAQBr diff --git a/app/src/main/play/listings/cs-CZ/short-description.txt b/app/src/main/play/listings/cs-CZ/short-description.txt new file mode 100644 index 000000000..0f29ab1b5 --- /dev/null +++ b/app/src/main/play/listings/cs-CZ/short-description.txt @@ -0,0 +1 @@ +Neoficiální aplikace žáka a rodiče pro deníku VULCAN UONET+ diff --git a/app/src/main/play/listings/cs-CZ/title.txt b/app/src/main/play/listings/cs-CZ/title.txt new file mode 100644 index 000000000..b7f42a5b0 --- /dev/null +++ b/app/src/main/play/listings/cs-CZ/title.txt @@ -0,0 +1 @@ +Wulkanowy Deníček diff --git a/app/src/main/play/listings/sk/full-description.txt b/app/src/main/play/listings/sk/full-description.txt new file mode 100644 index 000000000..2a4787d2d --- /dev/null +++ b/app/src/main/play/listings/sk/full-description.txt @@ -0,0 +1,14 @@ +Aplikácia je určená pre užívateľov denníka VULCAN UONET+. + +Zvýraznené vlastnosti a funkcie: +- výpočet váženého priemeru, +- percentuálne zobrazenie dochádzky, +- šťastné číslo, +- náhľad na ďalšie a dokončené lekcie, +- tmavý motív, +- žiadne reklamy, +- offline režim, +- upozornenia. + +GitHub: https://github.com/wulkanowy/wulkanowy +Discord: https://discord.gg/vccAQBr diff --git a/app/src/main/play/listings/sk/short-description.txt b/app/src/main/play/listings/sk/short-description.txt new file mode 100644 index 000000000..645ebbb6a --- /dev/null +++ b/app/src/main/play/listings/sk/short-description.txt @@ -0,0 +1 @@ +Neoficiálna aplikácia žiaka a rodiča pre denníka VULCAN UONET+ diff --git a/app/src/main/play/listings/sk/title.txt b/app/src/main/play/listings/sk/title.txt new file mode 100644 index 000000000..aa50ce77a --- /dev/null +++ b/app/src/main/play/listings/sk/title.txt @@ -0,0 +1 @@ +Wulkanowy Denníček From 8e607d48f7388db76e359d44192e9d01118a5f32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sun, 3 Oct 2021 10:36:17 +0200 Subject: [PATCH 220/869] Add coroutines scope to presenter (#1554) --- .../github/wulkanowy/ui/base/BasePresenter.kt | 26 +++++++++---------- .../modules/dashboard/DashboardPresenter.kt | 2 +- .../NotificationDebugPresenter.kt | 2 +- .../ui/modules/message/MessagePresenter.kt | 2 +- .../message/send/SendMessagePresenter.kt | 4 +-- .../message/tab/MessageTabPresenter.kt | 4 +-- .../mobiledevice/MobileDevicePresenter.kt | 3 +-- .../ui/modules/note/NotePresenter.kt | 19 +++++++------- .../SchoolAndTeachersPresenter.kt | 2 +- 9 files changed, 31 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt index 6f363bfc4..5cd5d0109 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt @@ -6,29 +6,27 @@ import io.github.wulkanowy.utils.flowWithResource import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import timber.log.Timber -import kotlin.coroutines.CoroutineContext open class BasePresenter( protected val errorHandler: ErrorHandler, protected val studentRepository: StudentRepository -) : CoroutineScope { +) { + private val job = SupervisorJob() - private var job = Job() + protected val presenterScope = CoroutineScope(job + Dispatchers.Main) - private val jobs = mutableMapOf() - - override val coroutineContext: CoroutineContext - get() = Dispatchers.Main + job + private val childrenJobs = mutableMapOf() var view: T? = null open fun onAttachView(view: T) { - job = Job() this.view = view errorHandler.apply { showErrorMessage = view::showError @@ -64,22 +62,22 @@ open class BasePresenter( } fun Flow.launch(individualJobTag: String = "load"): Job { - jobs[individualJobTag]?.cancel() - val job = catch { errorHandler.dispatch(it) }.launchIn(this@BasePresenter) - jobs[individualJobTag] = job + childrenJobs[individualJobTag]?.cancel() + val job = catch { errorHandler.dispatch(it) }.launchIn(presenterScope) + childrenJobs[individualJobTag] = job Timber.d("Job $individualJobTag launched in ${this@BasePresenter.javaClass.simpleName}: $job") return job } fun cancelJobs(vararg names: String) { names.forEach { - jobs[it]?.cancel() + childrenJobs[it]?.cancel() } } open fun onDetachView() { - view = null - job.cancel() + job.cancelChildren() errorHandler.clear() + view = null } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt index c513a9f6f..d6d533e28 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt @@ -149,7 +149,7 @@ class DashboardPresenter @Inject constructor( tileList: List, forceRefresh: Boolean ) { - launch { + presenterScope.launch { Timber.i("Loading dashboard account data started") val student = runCatching { studentRepository.getCurrentStudent(true) } .onFailure { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt index 6103317d2..26eca18fd 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt @@ -88,7 +88,7 @@ class NotificationDebugPresenter @Inject constructor( } private fun withStudent(block: suspend (Student) -> Unit) { - launch { + presenterScope.launch { block(studentRepository.getCurrentStudent(false)) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt index 7b8c3d0f5..be6940520 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt @@ -15,7 +15,7 @@ class MessagePresenter @Inject constructor( override fun onAttachView(view: MessageView) { super.onAttachView(view) - launch { + presenterScope.launch { delay(150) view.initView() Timber.i("Message view was initialized") diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt index 60a23e58a..77fa82316 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt @@ -224,14 +224,14 @@ class SendMessagePresenter @Inject constructor( } fun onMessageContentChange() { - launch { + presenterScope.launch { messageUpdateChannel.send(Unit) } } @OptIn(FlowPreview::class) private fun initializeSubjectStream() { - launch { + presenterScope.launch { messageUpdateChannel.consumeAsFlow() .debounce(250) .catch { Timber.e(it) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt index a24f9b79f..4571390b5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt @@ -173,14 +173,14 @@ class MessageTabPresenter @Inject constructor( } fun onSearchQueryTextChange(query: String) { - launch { + presenterScope.launch { searchChannel.send(query) } } @OptIn(FlowPreview::class) private fun initializeSearchStream() { - launch { + presenterScope.launch { searchChannel.consumeAsFlow() .debounce(250) .map { query -> diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDevicePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDevicePresenter.kt index 9591867df..53049891a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDevicePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDevicePresenter.kt @@ -11,7 +11,6 @@ import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.flowWithResourceIn -import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -148,6 +147,6 @@ class MobileDevicePresenter @Inject constructor( errorHandler.dispatch(it.error!!) } } - }.launchIn(this) + }.launch("unregister") } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NotePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NotePresenter.kt index c441231e0..10a391820 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NotePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NotePresenter.kt @@ -11,7 +11,6 @@ import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.flowWithResourceIn -import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -123,15 +122,17 @@ class NotePresenter @Inject constructor( } private fun updateNote(note: Note) { - flowWithResource { noteRepository.updateNote(note) }.onEach { - when (it.status) { - Status.LOADING -> Timber.i("Attempt to update note ${note.id}") - Status.SUCCESS -> Timber.i("Update note result: Success") - Status.ERROR -> { - Timber.i("Update note result: An exception occurred") - errorHandler.dispatch(it.error!!) + flowWithResource { noteRepository.updateNote(note) } + .onEach { + when (it.status) { + Status.LOADING -> Timber.i("Attempt to update note ${note.id}") + Status.SUCCESS -> Timber.i("Update note result: Success") + Status.ERROR -> { + Timber.i("Update note result: An exception occurred") + errorHandler.dispatch(it.error!!) + } } } - }.launchIn(this) + .launch("update_note") } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt index 915cc4213..c14272b9d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt @@ -15,7 +15,7 @@ class SchoolAndTeachersPresenter @Inject constructor( override fun onAttachView(view: SchoolAndTeachersView) { super.onAttachView(view) - launch { + presenterScope.launch { delay(150) view.initView() Timber.i("Message view was initialized") From 60501fcd728bae993c110a4ed500b5dd66012ae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sun, 3 Oct 2021 14:13:42 +0200 Subject: [PATCH 221/869] Set buildTimestamp through manifest meta (#1556) --- app/build.gradle | 4 ++-- app/src/main/AndroidManifest.xml | 10 ++++++++-- .../java/io/github/wulkanowy/utils/AppInfo.kt | 16 +++++++++++++--- .../data/db/migrations/AbstractMigrationTest.kt | 5 +++-- .../data/db/migrations/Migration35Test.kt | 6 +++--- .../wulkanowy/data/repositories/StudentTest.kt | 14 +++++++------- 6 files changed, 36 insertions(+), 19 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 1d6e56b11..e8762ddae 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,10 +27,10 @@ android { vectorDrawables.useSupportLibrary = true resValue "string", "app_name", "Wulkanowy" - buildConfigField "long", "BUILD_TIMESTAMP", String.valueOf(System.currentTimeMillis()) manifestPlaceholders = [ - firebase_enabled: project.hasProperty("enableFirebase") + firebase_enabled: project.hasProperty("enableFirebase"), + buildTimestamp: String.valueOf(System.currentTimeMillis()) ] javaCompileOptions { annotationProcessorOptions { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 84c50523c..bebe33c62 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -106,7 +106,8 @@ + android:exported="false" + tools:ignore="MissingClass"> @@ -152,6 +153,10 @@ android:resource="@xml/provider_paths" /> + + @@ -162,7 +167,8 @@ android:name="com.google.firebase.provider.FirebaseInitProvider" android:authorities="${applicationId}.firebaseinitprovider" android:enabled="${firebase_enabled}" - android:exported="false" /> + android:exported="false" + tools:ignore="MissingClass" /> () + @get:Rule val helper: MigrationTestHelper = MigrationTestHelper( InstrumentationRegistry.getInstrumentation(), @@ -24,7 +26,6 @@ abstract class AbstractMigrationTest { ) fun getMigratedRoomDatabase(): AppDatabase { - val context = ApplicationProvider.getApplicationContext() val database = Room.databaseBuilder( ApplicationProvider.getApplicationContext(), AppDatabase::class.java, @@ -32,7 +33,7 @@ abstract class AbstractMigrationTest { ).addMigrations( *AppDatabase.getMigrations( SharedPrefProvider(PreferenceManager.getDefaultSharedPreferences(context)), - AppInfo() + AppInfo(context) ) ).build() // close the database and release any stream resources when the test finishes diff --git a/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt index 883cdb81c..9a38f4286 100644 --- a/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt +++ b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt @@ -29,15 +29,15 @@ class Migration35Test : AbstractMigrationTest() { close() } - helper.runMigrationsAndValidate(dbName, 35, true, Migration35(AppInfo())) + helper.runMigrationsAndValidate(dbName, 35, true, Migration35(AppInfo(context))) val db = getMigratedRoomDatabase() val students = runBlocking { db.studentDao.loadAll() } assertEquals(2, students.size) - assertTrue { students[0].avatarColor in AppInfo().defaultColorsForAvatar } - assertTrue { students[1].avatarColor in AppInfo().defaultColorsForAvatar } + assertTrue { students[0].avatarColor in AppInfo(context).defaultColorsForAvatar } + assertTrue { students[1].avatarColor in AppInfo(context).defaultColorsForAvatar } } private fun createStudent(db: SupportSQLiteDatabase, id: Long) { diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt index b34bbf1b2..39ebb4a19 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt @@ -32,13 +32,13 @@ class StudentTest { fun initApi() { MockKAnnotations.init(this) studentRepository = StudentRepository( - mockk(), - TestDispatchersProvider(), - studentDb, - semesterDb, - mockSdk, - AppInfo(), - mockk() + context = mockk(), + dispatchers = TestDispatchersProvider(), + studentDb = studentDb, + semesterDb = semesterDb, + sdk = mockSdk, + appInfo = AppInfo(mockk()), + appDatabase = mockk() ) } From 031a17ea502159fc33e1e8564abd503ba38ae08a Mon Sep 17 00:00:00 2001 From: Damian Czupryn <60961958+Daxxxis@users.noreply.github.com> Date: Mon, 4 Oct 2021 16:35:37 +0200 Subject: [PATCH 222/869] Change text to bold in notifications center (#1546) --- app/src/main/res/layout/item_notifications_center.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout/item_notifications_center.xml b/app/src/main/res/layout/item_notifications_center.xml index ae3f5586d..16a7ae0c0 100644 --- a/app/src/main/res/layout/item_notifications_center.xml +++ b/app/src/main/res/layout/item_notifications_center.xml @@ -33,6 +33,7 @@ android:layout_marginStart="16dp" android:layout_marginTop="6dp" android:layout_marginEnd="16dp" + android:fontFamily="sans-serif-medium" android:textSize="15sp" app:layout_constraintEnd_toStartOf="@id/notifications_center_item_icon" app:layout_constraintStart_toStartOf="parent" From 04393e60bb860566442712a08893400169eb160a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Oct 2021 14:43:46 +0000 Subject: [PATCH 223/869] Bump agconnect-crash from 1.6.0.300 to 1.6.1.200 (#1563) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index e8762ddae..9cfa670eb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -228,7 +228,7 @@ dependencies { playImplementation 'com.google.android.play:core-ktx:1.8.1' hmsImplementation 'com.huawei.hms:hianalytics:6.2.0.301' - hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.0.300' + hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.1.200' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 8315759c83b967ddbd82cfb5a9e39f1261e03039 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Oct 2021 14:44:15 +0000 Subject: [PATCH 224/869] Bump agcp from 1.6.0.300 to 1.6.1.200 (#1562) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 15d3c792b..370fe296c 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ buildscript { classpath 'com.android.tools.build:gradle:7.0.2' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.10' - classpath 'com.huawei.agconnect:agcp:1.6.0.300' + classpath 'com.huawei.agconnect:agcp:1.6.1.200' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.3.0" From 426379ec17f2bb6c74d583d2d2185f242f1b5b01 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Oct 2021 14:45:09 +0000 Subject: [PATCH 225/869] Bump about_libraries from 8.9.1 to 8.9.3 (#1560) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 370fe296c..fbd127192 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { kotlin_version = '1.5.31' - about_libraries = '8.9.1' + about_libraries = '8.9.3' hilt_version = "2.38.1" } repositories { From 2d84b0775a9586a9119bbed6133a254497e3a20f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Oct 2021 14:51:31 +0000 Subject: [PATCH 226/869] Bump hilt_version from 2.38.1 to 2.39.1 (#1561) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index fbd127192..118efd398 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { kotlin_version = '1.5.31' about_libraries = '8.9.3' - hilt_version = "2.38.1" + hilt_version = "2.39.1" } repositories { mavenCentral() From 1839d7cb8f4b4dd4f0bb99dcfecde86ed6d6d9bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Mon, 4 Oct 2021 17:13:31 +0200 Subject: [PATCH 227/869] Migrate from moshi to kotlinx serialization (#1557) --- app/build.gradle | 12 ++-- .../github/wulkanowy/data/RepositoryModule.kt | 6 +- .../io/github/wulkanowy/data/db/Converters.kt | 29 ++++---- .../data/db/adapters/PairAdapterFactory.kt | 68 ------------------- .../wulkanowy/data/db/entities/Recipient.kt | 3 +- .../wulkanowy/data/pojos/Contributor.kt | 4 +- .../wulkanowy/data/pojos/MessageDraft.kt | 4 +- .../data/repositories/AppCreatorRepository.kt | 15 ++-- .../data/repositories/MessageRepository.kt | 11 +-- .../repositories/PreferencesRepository.kt | 20 +++--- .../modules/message/send/RecipientChipItem.kt | 4 +- .../wulkanowy/data/db/ConvertersTest.kt | 4 +- .../repositories/MessageRepositoryTest.kt | 7 +- build.gradle | 1 + 14 files changed, 56 insertions(+), 132 deletions(-) delete mode 100644 app/src/main/java/io/github/wulkanowy/data/db/adapters/PairAdapterFactory.kt diff --git a/app/build.gradle b/app/build.gradle index 9cfa670eb..91ce5f24a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-kapt' apply plugin: 'dagger.hilt.android.plugin' @@ -162,7 +163,7 @@ ext { room = "2.3.0" chucker = "3.5.2" mockk = "1.12.0" - moshi = "1.12.0" + coroutines = "1.5.2" } dependencies { @@ -170,7 +171,8 @@ dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" implementation "androidx.core:core-ktx:1.6.0" implementation "androidx.activity:activity-ktx:1.3.1" @@ -207,10 +209,6 @@ dependencies { implementation 'com.github.ncapdevi:FragNav:3.3.0' implementation "com.github.YarikSOffice:lingver:1.3.0" - implementation "com.squareup.moshi:moshi:$moshi" - implementation "com.squareup.moshi:moshi-adapters:$moshi" - kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi" - implementation "com.jakewharton.timber:timber:5.0.1" implementation "at.favre.lib:slf4j-timber:1.0.1" implementation 'com.github.bastienpaulfr:Treessence:1.0.4' @@ -237,7 +235,7 @@ dependencies { testImplementation "junit:junit:4.13.2" testImplementation "io.mockk:mockk:$mockk" - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2' + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines" testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" testImplementation 'org.robolectric:robolectric:4.6.1' diff --git a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt b/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt index f1b719e54..863bb5950 100644 --- a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt +++ b/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt @@ -9,7 +9,6 @@ import com.chuckerteam.chucker.api.ChuckerCollector import com.chuckerteam.chucker.api.ChuckerInterceptor import com.chuckerteam.chucker.api.RetentionManager import com.fredporciuncula.flow.preferences.FlowSharedPreferences -import com.squareup.moshi.Moshi import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -21,6 +20,7 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AppInfo import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.serialization.json.Json import timber.log.Timber import javax.inject.Singleton @@ -88,7 +88,9 @@ internal class RepositoryModule { @Singleton @Provides - fun provideMoshi() = Moshi.Builder().build() + fun provideJson() = Json { + ignoreUnknownKeys = true + } @Singleton @Provides diff --git a/app/src/main/java/io/github/wulkanowy/data/db/Converters.kt b/app/src/main/java/io/github/wulkanowy/data/db/Converters.kt index def0b3715..1993c4338 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/Converters.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/Converters.kt @@ -1,9 +1,10 @@ package io.github.wulkanowy.data.db import androidx.room.TypeConverter -import com.squareup.moshi.Moshi -import com.squareup.moshi.Types -import io.github.wulkanowy.data.db.adapters.PairAdapterFactory +import kotlinx.serialization.SerializationException +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime @@ -13,15 +14,7 @@ import java.util.Date class Converters { - private val moshi by lazy { Moshi.Builder().add(PairAdapterFactory).build() } - - private val integerListAdapter by lazy { - moshi.adapter>(Types.newParameterizedType(List::class.java, Integer::class.java)) - } - - private val stringListPairAdapter by lazy { - moshi.adapter>>(Types.newParameterizedType(List::class.java, Pair::class.java, String::class.java, String::class.java)) - } + private val json = Json @TypeConverter fun timestampToDate(value: Long?): LocalDate? = value?.run { @@ -51,21 +44,25 @@ class Converters { @TypeConverter fun intListToJson(list: List): String { - return integerListAdapter.toJson(list) + return json.encodeToString(list) } @TypeConverter fun jsonToIntList(value: String): List { - return integerListAdapter.fromJson(value).orEmpty() + return json.decodeFromString(value) } @TypeConverter fun stringPairListToJson(list: List>): String { - return stringListPairAdapter.toJson(list) + return json.encodeToString(list) } @TypeConverter fun jsonToStringPairList(value: String): List> { - return stringListPairAdapter.fromJson(value).orEmpty() + return try { + json.decodeFromString(value) + } catch (e: SerializationException) { + emptyList() // handle errors from old gson Pair serialized data + } } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/adapters/PairAdapterFactory.kt b/app/src/main/java/io/github/wulkanowy/data/db/adapters/PairAdapterFactory.kt deleted file mode 100644 index 4a9b168dd..000000000 --- a/app/src/main/java/io/github/wulkanowy/data/db/adapters/PairAdapterFactory.kt +++ /dev/null @@ -1,68 +0,0 @@ -package io.github.wulkanowy.data.db.adapters - -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.JsonReader -import com.squareup.moshi.JsonWriter -import com.squareup.moshi.Moshi -import com.squareup.moshi.Types -import java.lang.reflect.ParameterizedType -import java.lang.reflect.Type - -object PairAdapterFactory : JsonAdapter.Factory { - - override fun create(type: Type, annotations: MutableSet, moshi: Moshi): JsonAdapter<*>? { - if (type !is ParameterizedType || List::class.java != type.rawType) return null - if (type.actualTypeArguments[0] != Pair::class.java) return null - - val listType = Types.newParameterizedType(List::class.java, Map::class.java, String::class.java) - val listAdapter = moshi.adapter>>(listType) - - val mapType = Types.newParameterizedType(MutableMap::class.java, String::class.java, String::class.java) - val mapAdapter = moshi.adapter>(mapType) - - return PairAdapter(listAdapter, mapAdapter) - } - - private class PairAdapter( - private val listAdapter: JsonAdapter>>, - private val mapAdapter: JsonAdapter>, - ) : JsonAdapter>>() { - - override fun toJson(writer: JsonWriter, value: List>?) { - writer.beginArray() - value?.forEach { - writer.beginObject() - writer.name("first").value(it.first) - writer.name("second").value(it.second) - writer.endObject() - } - writer.endArray() - } - - override fun fromJson(reader: JsonReader): List>? { - return if (reader.peek() == JsonReader.Token.BEGIN_OBJECT) deserializeMoshiMap(reader) - else deserializeGsonPair(reader) - } - - // for compatibility with 0.21.0 - private fun deserializeMoshiMap(reader: JsonReader): List>? { - val map = mapAdapter.fromJson(reader) ?: return null - - return map.entries.map { - it.key to it.value - } - } - - private fun deserializeGsonPair(reader: JsonReader): List>? { - val list = listAdapter.fromJson(reader) ?: return null - - return list.map { - require(it.size == 2) { - "pair with more or less than two elements: $list" - } - - it["first"].orEmpty() to it["second"].orEmpty() - } - } - } -} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Recipient.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Recipient.kt index 60e67d32f..223322705 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Recipient.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Recipient.kt @@ -3,10 +3,9 @@ package io.github.wulkanowy.data.db.entities import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey -import com.squareup.moshi.JsonClass import java.io.Serializable -@JsonClass(generateAdapter = true) +@kotlinx.serialization.Serializable @Entity(tableName = "Recipients") data class Recipient( diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/Contributor.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/Contributor.kt index d2338c281..4165b3f14 100644 --- a/app/src/main/java/io/github/wulkanowy/data/pojos/Contributor.kt +++ b/app/src/main/java/io/github/wulkanowy/data/pojos/Contributor.kt @@ -1,8 +1,8 @@ package io.github.wulkanowy.data.pojos -import com.squareup.moshi.JsonClass +import kotlinx.serialization.Serializable -@JsonClass(generateAdapter = true) +@Serializable class Contributor( val displayName: String, val githubUsername: String diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/MessageDraft.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/MessageDraft.kt index a79b70cd4..2e568e376 100644 --- a/app/src/main/java/io/github/wulkanowy/data/pojos/MessageDraft.kt +++ b/app/src/main/java/io/github/wulkanowy/data/pojos/MessageDraft.kt @@ -1,9 +1,9 @@ package io.github.wulkanowy.data.pojos -import com.squareup.moshi.JsonClass import io.github.wulkanowy.ui.modules.message.send.RecipientChipItem +import kotlinx.serialization.Serializable -@JsonClass(generateAdapter = true) +@Serializable data class MessageDraft( val recipients: List, val subject: String, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt index 71b7ea94e..f1fdbfed3 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt @@ -1,25 +1,26 @@ package io.github.wulkanowy.data.repositories import android.content.res.AssetManager -import com.squareup.moshi.Moshi -import com.squareup.moshi.Types import io.github.wulkanowy.data.pojos.Contributor import io.github.wulkanowy.utils.DispatchersProvider import kotlinx.coroutines.withContext +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream import javax.inject.Inject import javax.inject.Singleton @Singleton class AppCreatorRepository @Inject constructor( private val assets: AssetManager, - private val dispatchers: DispatchersProvider + private val dispatchers: DispatchersProvider, + private val json: Json, ) { + @OptIn(ExperimentalSerializationApi::class) @Suppress("BlockingMethodInNonBlockingContext") suspend fun getAppCreators() = withContext(dispatchers.backgroundThread) { - val moshi = Moshi.Builder().build() - val type = Types.newParameterizedType(List::class.java, Contributor::class.java) - val adapter = moshi.adapter>(type) - adapter.fromJson(assets.open("contributors.json").bufferedReader().use { it.readText() }) + val inputStream = assets.open("contributors.json").buffered() + json.decodeFromStream>(inputStream) } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt index ee5164957..224c69bd7 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt @@ -1,7 +1,6 @@ package io.github.wulkanowy.data.repositories import android.content.Context -import com.squareup.moshi.Moshi import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.Resource @@ -18,7 +17,6 @@ import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED import io.github.wulkanowy.data.mappers.mapFromEntities import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.pojos.MessageDraft -import io.github.wulkanowy.data.pojos.MessageDraftJsonAdapter import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.Folder import io.github.wulkanowy.sdk.pojo.SentMessage @@ -29,6 +27,9 @@ import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import timber.log.Timber import java.time.LocalDateTime.now import javax.inject.Inject @@ -42,7 +43,7 @@ class MessageRepository @Inject constructor( @ApplicationContext private val context: Context, private val refreshHelper: AutoRefreshHelper, private val sharedPrefProvider: SharedPrefProvider, - private val moshi: Moshi, + private val json: Json, ) { private val saveFetchResultMutex = Mutex() @@ -168,9 +169,9 @@ class MessageRepository @Inject constructor( var draftMessage: MessageDraft? get() = sharedPrefProvider.getString(context.getString(R.string.pref_key_message_send_draft)) - ?.let { MessageDraftJsonAdapter(moshi).fromJson(it) } + ?.let { json.decodeFromString(it) } set(value) = sharedPrefProvider.putString( context.getString(R.string.pref_key_message_send_draft), - value?.let { MessageDraftJsonAdapter(moshi).toJson(it) } + value?.let { json.encodeToString(it) } ) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index a08045f85..65edd53f0 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -5,9 +5,6 @@ import android.content.SharedPreferences import androidx.core.content.edit import com.fredporciuncula.flow.preferences.FlowSharedPreferences import com.fredporciuncula.flow.preferences.Preference -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.Moshi -import com.squareup.moshi.adapter import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.sdk.toLocalDate @@ -19,6 +16,9 @@ import io.github.wulkanowy.utils.toTimestamp import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import java.time.LocalDate import java.time.LocalDateTime import javax.inject.Inject @@ -27,16 +27,12 @@ import javax.inject.Singleton @OptIn(ExperimentalCoroutinesApi::class) @Singleton class PreferencesRepository @Inject constructor( + @ApplicationContext val context: Context, private val sharedPref: SharedPreferences, private val flowSharedPref: FlowSharedPreferences, - @ApplicationContext val context: Context, - moshi: Moshi + private val json: Json, ) { - @OptIn(ExperimentalStdlibApi::class) - private val dashboardItemsPositionAdapter: JsonAdapter> = - moshi.adapter() - val startMenuIndex: Int get() = getString(R.string.pref_key_start_menu, R.string.pref_default_startup).toInt() @@ -197,14 +193,14 @@ class PreferencesRepository @Inject constructor( var dashboardItemsPosition: Map? get() { - val json = sharedPref.getString(PREF_KEY_DASHBOARD_ITEMS_POSITION, null) ?: return null + val value = sharedPref.getString(PREF_KEY_DASHBOARD_ITEMS_POSITION, null) ?: return null - return dashboardItemsPositionAdapter.fromJson(json) + return json.decodeFromString(value) } set(value) = sharedPref.edit { putString( PREF_KEY_DASHBOARD_ITEMS_POSITION, - dashboardItemsPositionAdapter.toJson(value) + json.encodeToString(value) ) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/RecipientChipItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/RecipientChipItem.kt index 26ab7f488..bd14bc893 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/RecipientChipItem.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/RecipientChipItem.kt @@ -1,10 +1,10 @@ package io.github.wulkanowy.ui.modules.message.send -import com.squareup.moshi.JsonClass import io.github.wulkanowy.data.db.entities.Recipient import io.github.wulkanowy.materialchipsinput.ChipItem +import kotlinx.serialization.Serializable -@JsonClass(generateAdapter = true) +@Serializable data class RecipientChipItem( override val title: String, diff --git a/app/src/test/java/io/github/wulkanowy/data/db/ConvertersTest.kt b/app/src/test/java/io/github/wulkanowy/data/db/ConvertersTest.kt index 3b0d7c841..a044a4299 100644 --- a/app/src/test/java/io/github/wulkanowy/data/db/ConvertersTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/db/ConvertersTest.kt @@ -23,8 +23,8 @@ class ConvertersTest { @Test fun jsonToStringPairList_0210() { - assertEquals(Converters().jsonToStringPairList("{\"aaa\":\"bbb\",\"ccc\":\"ddd\"}"), listOf("aaa" to "bbb", "ccc" to "ddd")) - assertEquals(Converters().jsonToStringPairList("{\"aaa\":\"bbb\"}"), listOf("aaa" to "bbb")) + assertEquals(Converters().jsonToStringPairList("{\"aaa\":\"bbb\",\"ccc\":\"ddd\"}"), listOf>()) + assertEquals(Converters().jsonToStringPairList("{\"aaa\":\"bbb\"}"), listOf>()) assertEquals(Converters().jsonToStringPairList("{}"), listOf>()) } } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt index 25774d74c..052f08f00 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt @@ -1,7 +1,6 @@ package io.github.wulkanowy.data.repositories import android.content.Context -import com.squareup.moshi.Moshi import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.dao.MessageAttachmentDao @@ -30,6 +29,7 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -63,9 +63,6 @@ class MessageRepositoryTest { private lateinit var repository: MessageRepository - @MockK - private lateinit var moshi: Moshi - @Before fun setUp() { MockKAnnotations.init(this) @@ -78,7 +75,7 @@ class MessageRepositoryTest { context = context, refreshHelper = refreshHelper, sharedPrefProvider = sharedPrefProvider, - moshi = moshi, + json = Json, ) } diff --git a/build.gradle b/build.gradle index 118efd398..153088a6d 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,7 @@ buildscript { } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath 'com.android.tools.build:gradle:7.0.2' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.10' From e8075e30e4f3d6203d66e0dccd8921df97fd71a7 Mon Sep 17 00:00:00 2001 From: Mateusz Idziejczak Date: Fri, 8 Oct 2021 11:19:49 +0200 Subject: [PATCH 228/869] Add "add homework" feature (#1564) --- .../41.json | 2322 +++++++++++++++++ .../github/wulkanowy/data/db/AppDatabase.kt | 6 +- .../wulkanowy/data/db/entities/Homework.kt | 3 + .../data/db/migrations/Migration41.kt | 11 + .../data/repositories/HomeworkRepository.kt | 7 +- .../ui/modules/homework/HomeworkFragment.kt | 9 +- .../ui/modules/homework/HomeworkPresenter.kt | 6 +- .../ui/modules/homework/HomeworkView.kt | 4 +- .../modules/homework/add/HomeworkAddDialog.kt | 124 + .../homework/add/HomeworkAddPresenter.kt | 92 + .../modules/homework/add/HomeworkAddView.kt | 21 + .../details/HomeworkDetailsAdapter.kt | 28 +- .../homework/details/HomeworkDetailsDialog.kt | 8 + .../details/HomeworkDetailsPresenter.kt | 19 + .../homework/details/HomeworkDetailsView.kt | 4 + .../login/advanced/LoginAdvancedFragment.kt | 10 +- .../modules/login/form/LoginFormFragment.kt | 4 +- .../login/recover/LoginRecoverFragment.kt | 2 +- .../login/symbol/LoginSymbolFragment.kt | 2 +- app/src/main/res/layout/dialog_homework.xml | 17 +- .../main/res/layout/dialog_homework_add.xml | 134 + app/src/main/res/layout/fragment_homework.xml | 22 +- .../layout/item_homework_dialog_details.xml | 26 +- app/src/main/res/values/strings.xml | 16 +- 24 files changed, 2850 insertions(+), 47 deletions(-) create mode 100644 app/schemas/io.github.wulkanowy.data.db.AppDatabase/41.json create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddDialog.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddPresenter.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddView.kt create mode 100644 app/src/main/res/layout/dialog_homework_add.xml diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/41.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/41.json new file mode 100644 index 000000000..9d008060a --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/41.json @@ -0,0 +1,2322 @@ +{ + "formatVersion": 1, + "database": { + "version": 41, + "identityHash": "d9ce44a78495a358606612bd91603c0f", + "entities": [ + { + "tableName": "Students", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrapper_base_url` TEXT NOT NULL, `mobile_base_url` TEXT NOT NULL, `login_type` TEXT NOT NULL, `login_mode` TEXT NOT NULL, `certificate_key` TEXT NOT NULL, `private_key` TEXT NOT NULL, `is_parent` INTEGER NOT NULL, `email` TEXT NOT NULL, `password` TEXT NOT NULL, `symbol` TEXT NOT NULL, `student_id` INTEGER NOT NULL, `user_login_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `student_name` TEXT NOT NULL, `school_id` TEXT NOT NULL, `school_short` TEXT NOT NULL, `school_name` TEXT NOT NULL, `class_name` TEXT NOT NULL, `class_id` INTEGER NOT NULL, `is_current` INTEGER NOT NULL, `registration_date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `nick` TEXT NOT NULL, `avatar_color` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "scrapperBaseUrl", + "columnName": "scrapper_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mobileBaseUrl", + "columnName": "mobile_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginType", + "columnName": "login_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginMode", + "columnName": "login_mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificateKey", + "columnName": "certificate_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "private_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isParent", + "columnName": "is_parent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "user_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "student_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolSymbol", + "columnName": "school_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "school_short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolName", + "columnName": "school_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "className", + "columnName": "class_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCurrent", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registrationDate", + "columnName": "registration_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nick", + "columnName": "nick", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarColor", + "columnName": "avatar_color", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Students_email_symbol_student_id_school_id_class_id", + "unique": true, + "columnNames": [ + "email", + "symbol", + "student_id", + "school_id", + "class_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryName", + "columnName": "diary_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolYear", + "columnName": "school_year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterName", + "columnName": "semester_name", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "semester_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `semester_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Exams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Timetable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `number` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `subjectOld` TEXT NOT NULL, `group` TEXT NOT NULL, `room` TEXT NOT NULL, `roomOld` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacherOld` TEXT NOT NULL, `info` TEXT NOT NULL, `student_plan` INTEGER NOT NULL, `changes` INTEGER NOT NULL, `canceled` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectOld", + "columnName": "subjectOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "room", + "columnName": "room", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roomOld", + "columnName": "roomOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherOld", + "columnName": "teacherOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isStudentPlan", + "columnName": "student_plan", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "changes", + "columnName": "changes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canceled", + "columnName": "canceled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Attendance", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `time_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `excusable` INTEGER NOT NULL, `excuse_status` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeId", + "columnName": "time_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excusable", + "columnName": "excusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excuseStatus", + "columnName": "excuse_status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AttendanceSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `subject_id` INTEGER NOT NULL, `month` INTEGER NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `absence_excused` INTEGER NOT NULL, `absence_for_school_reasons` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `lateness_excused` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subject_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "month", + "columnName": "month", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceExcused", + "columnName": "absence_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceForSchoolReasons", + "columnName": "absence_for_school_reasons", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latenessExcused", + "columnName": "lateness_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Grades", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `entry` TEXT NOT NULL, `value` REAL NOT NULL, `modifier` REAL NOT NULL, `comment` TEXT NOT NULL, `color` TEXT NOT NULL, `grade_symbol` TEXT NOT NULL, `description` TEXT NOT NULL, `weight` TEXT NOT NULL, `weightValue` REAL NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entry", + "columnName": "entry", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "modifier", + "columnName": "modifier", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gradeSymbol", + "columnName": "grade_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weightValue", + "columnName": "weightValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `subject` TEXT NOT NULL, `predicted_grade` TEXT NOT NULL, `final_grade` TEXT NOT NULL, `proposed_points` TEXT NOT NULL, `final_points` TEXT NOT NULL, `points_sum` TEXT NOT NULL, `average` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_predicted_grade_notified` INTEGER NOT NULL, `is_final_grade_notified` INTEGER NOT NULL, `predicted_grade_last_change` INTEGER NOT NULL, `final_grade_last_change` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "predictedGrade", + "columnName": "predicted_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalGrade", + "columnName": "final_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "proposedPoints", + "columnName": "proposed_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalPoints", + "columnName": "final_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pointsSum", + "columnName": "points_sum", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "average", + "columnName": "average", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPredictedGradeNotified", + "columnName": "is_predicted_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFinalGradeNotified", + "columnName": "is_final_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "predictedGradeLastChange", + "columnName": "predicted_grade_last_change", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "finalGradeLastChange", + "columnName": "final_grade_last_change", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradePartialStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `class_average` TEXT NOT NULL, `student_average` TEXT NOT NULL, `class_amounts` TEXT NOT NULL, `student_amounts` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAverage", + "columnName": "class_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAverage", + "columnName": "student_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAmounts", + "columnName": "class_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAmounts", + "columnName": "student_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesPointsStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `others` REAL NOT NULL, `student` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "others", + "columnName": "others", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "student", + "columnName": "student", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradeSemesterStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `amounts` TEXT NOT NULL, `student_grade` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amounts", + "columnName": "amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentGrade", + "columnName": "student_grade", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `recipient_name` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `removed` INTEGER NOT NULL, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `unread_by` INTEGER NOT NULL, `read_by` INTEGER NOT NULL, `content` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipient", + "columnName": "recipient_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "removed", + "columnName": "removed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasAttachments", + "columnName": "has_attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `one_drive_id` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`real_id`))", + "fields": [ + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "oneDriveId", + "columnName": "one_drive_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "real_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `category` TEXT NOT NULL, `category_type` INTEGER NOT NULL, `is_points_show` INTEGER NOT NULL, `points` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryType", + "columnName": "category_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPointsShow", + "columnName": "is_points_show", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Homework", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `attachments` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_done` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL, `is_added_by_user` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "is_done", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Subjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "LuckyNumbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `lucky_number` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "luckyNumber", + "columnName": "lucky_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CompletedLesson", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `topic` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `substitution` TEXT NOT NULL, `absence` TEXT NOT NULL, `resources` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "topic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "substitution", + "columnName": "substitution", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "resources", + "columnName": "resources", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ReportingUnits", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `short` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `roles` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "senderName", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roles", + "columnName": "roles", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` TEXT NOT NULL, `name` TEXT NOT NULL, `real_name` TEXT NOT NULL, `login_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `role` INTEGER NOT NULL, `hash` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "realName", + "columnName": "real_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginId", + "columnName": "login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MobileDevices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Teachers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `short_name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "School", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `contact` TEXT NOT NULL, `headmaster` TEXT NOT NULL, `pedagogue` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contact", + "columnName": "contact", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headmaster", + "columnName": "headmaster", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pedagogue", + "columnName": "pedagogue", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Conferences", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `subject` TEXT NOT NULL, `agenda` TEXT NOT NULL, `present_on_conference` TEXT NOT NULL, `conference_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agenda", + "columnName": "agenda", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presentOnConference", + "columnName": "present_on_conference", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conference_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableAdditional", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StudentInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `full_name` TEXT NOT NULL, `first_name` TEXT NOT NULL, `second_name` TEXT NOT NULL, `surname` TEXT NOT NULL, `birth_date` INTEGER NOT NULL, `birth_place` TEXT NOT NULL, `gender` TEXT NOT NULL, `has_polish_citizenship` INTEGER NOT NULL, `family_name` TEXT NOT NULL, `parents_names` TEXT NOT NULL, `address` TEXT NOT NULL, `registered_address` TEXT NOT NULL, `correspondence_address` TEXT NOT NULL, `phone_number` TEXT NOT NULL, `cell_phone_number` TEXT NOT NULL, `email` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `first_guardian_full_name` TEXT, `first_guardian_kinship` TEXT, `first_guardian_address` TEXT, `first_guardian_phones` TEXT, `first_guardian_email` TEXT, `second_guardian_full_name` TEXT, `second_guardian_kinship` TEXT, `second_guardian_address` TEXT, `second_guardian_phones` TEXT, `second_guardian_email` TEXT)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "full_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondName", + "columnName": "second_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "surname", + "columnName": "surname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "birthDate", + "columnName": "birth_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthPlace", + "columnName": "birth_place", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gender", + "columnName": "gender", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPolishCitizenship", + "columnName": "has_polish_citizenship", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "familyName", + "columnName": "family_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentsNames", + "columnName": "parents_names", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "registeredAddress", + "columnName": "registered_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "correspondenceAddress", + "columnName": "correspondence_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cellPhoneNumber", + "columnName": "cell_phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstGuardian.fullName", + "columnName": "first_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.kinship", + "columnName": "first_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.address", + "columnName": "first_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.phones", + "columnName": "first_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.email", + "columnName": "first_guardian_email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.fullName", + "columnName": "second_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.kinship", + "columnName": "second_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.address", + "columnName": "second_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.phones", + "columnName": "second_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.email", + "columnName": "second_guardian_email", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableHeaders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SchoolAnnouncements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `type` TEXT NOT NULL, `date` INTEGER NOT NULL, `data` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "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, 'd9ce44a78495a358606612bd91603c0f')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt index 09aa972f0..b93d00881 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt @@ -97,6 +97,7 @@ import io.github.wulkanowy.data.db.migrations.Migration37 import io.github.wulkanowy.data.db.migrations.Migration38 import io.github.wulkanowy.data.db.migrations.Migration39 import io.github.wulkanowy.data.db.migrations.Migration4 +import io.github.wulkanowy.data.db.migrations.Migration41 import io.github.wulkanowy.data.db.migrations.Migration40 import io.github.wulkanowy.data.db.migrations.Migration5 import io.github.wulkanowy.data.db.migrations.Migration6 @@ -146,7 +147,7 @@ import javax.inject.Singleton abstract class AppDatabase : RoomDatabase() { companion object { - const val VERSION_SCHEMA = 40 + const val VERSION_SCHEMA = 41 fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( Migration2(), @@ -187,7 +188,8 @@ abstract class AppDatabase : RoomDatabase() { Migration37(), Migration38(), Migration39(), - Migration40() + Migration40(), + Migration41() ) fun newInstance( diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Homework.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Homework.kt index 04ee1e8c6..4538cf31f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Homework.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Homework.kt @@ -40,4 +40,7 @@ data class Homework( @ColumnInfo(name = "is_notified") var isNotified: Boolean = true + + @ColumnInfo(name = "is_added_by_user") + var isAddedByUser: Boolean = false } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt new file mode 100644 index 000000000..17773efdb --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt @@ -0,0 +1,11 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration41 : Migration(40, 41) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE Homework ADD COLUMN is_added_by_user INTEGER NOT NULL DEFAULT 0") + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt index a04085fb7..95a375a45 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt @@ -61,8 +61,9 @@ class HomeworkRepository @Inject constructor( val homeWorkToSave = (new uniqueSubtract old).onEach { if (notify) it.isNotified = false } + val filteredOld = old.filterNot { it.isAddedByUser } - homeworkDb.deleteAll(old uniqueSubtract new) + homeworkDb.deleteAll(filteredOld uniqueSubtract new) homeworkDb.insertAll(homeWorkToSave) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end)) @@ -79,4 +80,8 @@ class HomeworkRepository @Inject constructor( homeworkDb.loadAll(semester.semesterId, semester.studentId, start.monday, end.sunday) suspend fun updateHomework(homework: List) = homeworkDb.updateAll(homework) + + suspend fun saveHomework(homework: Homework) = homeworkDb.insertAll(listOf(homework)) + + suspend fun deleteHomework(homework: Homework) = homeworkDb.deleteAll(listOf(homework)) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt index 1d9434dc7..a723b0483 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt @@ -10,6 +10,7 @@ import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Homework import io.github.wulkanowy.databinding.FragmentHomeworkBinding import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.homework.add.HomeworkAddDialog import io.github.wulkanowy.ui.modules.homework.details.HomeworkDetailsDialog import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView @@ -64,6 +65,8 @@ class HomeworkFragment : BaseFragment(R.layout.fragment homeworkPreviousButton.setOnClickListener { presenter.onPreviousDay() } homeworkNextButton.setOnClickListener { presenter.onNextDay() } + openAddHomeworkButton.setOnClickListener { presenter.onHomeworkAddButtonClicked() } + homeworkNavContainer.setElevationCompat(requireContext().dpToPx(8f)) } } @@ -122,10 +125,14 @@ class HomeworkFragment : BaseFragment(R.layout.fragment binding.homeworkNextButton.visibility = if (show) VISIBLE else View.INVISIBLE } - override fun showTimetableDialog(homework: Homework) { + override fun showHomeworkDialog(homework: Homework) { (activity as? MainActivity)?.showDialogFragment(HomeworkDetailsDialog.newInstance(homework)) } + override fun showAddHomeworkDialog() { + (activity as? MainActivity)?.showDialogFragment(HomeworkAddDialog()) + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay()) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkPresenter.kt index 11c54dc24..d7d5d7cb9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkPresenter.kt @@ -78,7 +78,11 @@ class HomeworkPresenter @Inject constructor( fun onHomeworkItemSelected(homework: Homework) { Timber.i("Select homework item ${homework.id}") - view?.showTimetableDialog(homework) + view?.showHomeworkDialog(homework) + } + + fun onHomeworkAddButtonClicked() { + view?.showAddHomeworkDialog() } private fun setBaseDateOnHolidays() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt index a1d6a04a9..7c05ab865 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt @@ -33,5 +33,7 @@ interface HomeworkView : BaseView { fun showNextButton(show: Boolean) - fun showTimetableDialog(homework: Homework) + fun showHomeworkDialog(homework: Homework) + + fun showAddHomeworkDialog() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddDialog.kt new file mode 100644 index 000000000..12168f142 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddDialog.kt @@ -0,0 +1,124 @@ +package io.github.wulkanowy.ui.modules.homework.add + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.widget.doOnTextChanged +import com.google.android.material.datepicker.CalendarConstraints +import com.google.android.material.datepicker.MaterialDatePicker +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +import io.github.wulkanowy.databinding.DialogHomeworkAddBinding +import io.github.wulkanowy.ui.base.BaseDialogFragment +import io.github.wulkanowy.utils.toFormattedString +import io.github.wulkanowy.utils.toLocalDateTime +import io.github.wulkanowy.utils.toTimestamp +import java.time.LocalDate +import javax.inject.Inject + +@AndroidEntryPoint +class HomeworkAddDialog : BaseDialogFragment(), HomeworkAddView { + + @Inject + lateinit var presenter: HomeworkAddPresenter + + private var date: LocalDate? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NO_TITLE, 0) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = DialogHomeworkAddBinding.inflate(inflater).apply { binding = this }.root + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + presenter.onAttachView(this) + } + + override fun initView() { + with(binding) { + homeworkDialogSubjectEdit.doOnTextChanged { _, _, _, _ -> + homeworkDialogSubject.error = null + homeworkDialogSubject.isErrorEnabled = false + } + homeworkDialogDateEdit.doOnTextChanged { _, _, _, _ -> + homeworkDialogDate.error = null + homeworkDialogDate.isErrorEnabled = false + } + homeworkDialogContentEdit.doOnTextChanged { _, _, _, _ -> + homeworkDialogContent.error = null + homeworkDialogContent.isErrorEnabled = false + } + homeworkDialogClose.setOnClickListener { dismiss() } + homeworkDialogDateEdit.setOnClickListener { presenter.showDatePicker(date) } + homeworkDialogAdd.setOnClickListener { + presenter.onAddHomeworkClicked( + subject = homeworkDialogSubjectEdit.text?.toString(), + teacher = homeworkDialogTeacherEdit.text?.toString(), + date = homeworkDialogDateEdit.text?.toString(), + content = homeworkDialogContentEdit.text?.toString() + ) + } + } + } + + override fun showSuccessMessage() { + showMessage(getString(R.string.homework_add_success)) + } + + override fun setErrorSubjectRequired() { + with(binding.homeworkDialogSubject) { + isErrorEnabled = true + error = getString(R.string.error_field_required) + } + } + + override fun setErrorDateRequired() { + with(binding.homeworkDialogDate) { + isErrorEnabled = true + error = getString(R.string.error_field_required) + } + } + + override fun setErrorContentRequired() { + with(binding.homeworkDialogContent) { + isErrorEnabled = true + error = getString(R.string.error_field_required) + } + } + + override fun closeDialog() { + dismiss() + } + + override fun showDatePickerDialog(currentDate: LocalDate) { + val constraintsBuilder = CalendarConstraints.Builder().apply { + setStart(LocalDate.now().toEpochDay()) + } + val datePicker = + MaterialDatePicker.Builder.datePicker() + .setCalendarConstraints(constraintsBuilder.build()) + .setSelection(currentDate.toTimestamp()) + .build() + + datePicker.addOnPositiveButtonClickListener { + date = it.toLocalDateTime().toLocalDate() + binding.homeworkDialogDate.editText?.setText(date!!.toFormattedString()) + } + + if (!parentFragmentManager.isStateSaved) { + datePicker.show(this.parentFragmentManager, null) + } + } + + override fun onDestroyView() { + presenter.onDetachView() + super.onDestroyView() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddPresenter.kt new file mode 100644 index 000000000..3639c2fef --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddPresenter.kt @@ -0,0 +1,92 @@ +package io.github.wulkanowy.ui.modules.homework.add + +import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.db.entities.Homework +import io.github.wulkanowy.data.repositories.HomeworkRepository +import io.github.wulkanowy.data.repositories.SemesterRepository +import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.ui.base.ErrorHandler +import io.github.wulkanowy.utils.flowWithResource +import io.github.wulkanowy.utils.toLocalDate +import kotlinx.coroutines.flow.onEach +import timber.log.Timber +import java.time.LocalDate +import javax.inject.Inject + +class HomeworkAddPresenter @Inject constructor( + errorHandler: ErrorHandler, + studentRepository: StudentRepository, + private val homeworkRepository: HomeworkRepository, + private val semesterRepository: SemesterRepository +) : BasePresenter(errorHandler, studentRepository) { + + override fun onAttachView(view: HomeworkAddView) { + super.onAttachView(view) + view.initView() + Timber.i("Homework details view was initialized") + } + + fun showDatePicker(date: LocalDate?) { + view?.showDatePickerDialog(date ?: LocalDate.now()) + } + + fun onAddHomeworkClicked(subject: String?, teacher: String?, date: String?, content: String?) { + var isError = false + + if (subject.isNullOrBlank()) { + view?.setErrorSubjectRequired() + isError = true + } + + if (date.isNullOrBlank()) { + view?.setErrorDateRequired() + isError = true + } + + if (content.isNullOrBlank()) { + view?.setErrorContentRequired() + isError = true + } + + if (!isError) { + saveHomework(subject!!, teacher.orEmpty(), date!!.toLocalDate(), content!!) + } + } + + private fun saveHomework(subject: String, teacher: String, date: LocalDate, content: String) { + flowWithResource { + val student = studentRepository.getCurrentStudent() + val semester = semesterRepository.getCurrentSemester(student) + val entryDate = LocalDate.now() + homeworkRepository.saveHomework( + Homework( + semesterId = semester.semesterId, + studentId = student.studentId, + date = date, + entryDate = entryDate, + subject = subject, + content = content, + teacher = teacher, + teacherSymbol = "", + attachments = emptyList(), + ).apply { isAddedByUser = true } + ) + }.onEach { + when (it.status) { + Status.LOADING -> Timber.i("Homework insert start") + Status.SUCCESS -> { + Timber.i("Homework insert: Success") + view?.run { + showSuccessMessage() + closeDialog() + } + } + Status.ERROR -> { + Timber.i("Homework insert result: An exception occurred") + errorHandler.dispatch(it.error!!) + } + } + }.launch("add_homework") + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddView.kt new file mode 100644 index 000000000..3bb304d9c --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddView.kt @@ -0,0 +1,21 @@ +package io.github.wulkanowy.ui.modules.homework.add + +import io.github.wulkanowy.ui.base.BaseView +import java.time.LocalDate + +interface HomeworkAddView : BaseView { + + fun initView() + + fun showSuccessMessage() + + fun setErrorSubjectRequired() + + fun setErrorDateRequired() + + fun setErrorContentRequired() + + fun closeDialog() + + fun showDatePickerDialog(currentDate: LocalDate) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt index cd9a7e851..e03707a5c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt @@ -5,10 +5,12 @@ import android.view.View.GONE import android.view.View.VISIBLE import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Homework import io.github.wulkanowy.databinding.ItemHomeworkDialogAttachmentBinding import io.github.wulkanowy.databinding.ItemHomeworkDialogAttachmentsHeaderBinding import io.github.wulkanowy.databinding.ItemHomeworkDialogDetailsBinding +import io.github.wulkanowy.utils.ifNullOrBlank import io.github.wulkanowy.utils.toFormattedString import javax.inject.Inject @@ -37,6 +39,8 @@ class HomeworkDetailsAdapter @Inject constructor() : var onFullScreenExitClickListener = {} + var onDeleteClickListener: (homework: Homework) -> Unit = {} + override fun getItemCount() = 1 + if (attachments.isNotEmpty()) attachments.size + 1 else 0 override fun getItemViewType(position: Int) = when (position) { @@ -49,9 +53,15 @@ class HomeworkDetailsAdapter @Inject constructor() : val inflater = LayoutInflater.from(parent.context) return when (viewType) { - ViewType.ATTACHMENTS_HEADER.id -> AttachmentsHeaderViewHolder(ItemHomeworkDialogAttachmentsHeaderBinding.inflate(inflater, parent, false)) - ViewType.ATTACHMENT.id -> AttachmentViewHolder(ItemHomeworkDialogAttachmentBinding.inflate(inflater, parent, false)) - else -> DetailsViewHolder(ItemHomeworkDialogDetailsBinding.inflate(inflater, parent, false)) + ViewType.ATTACHMENTS_HEADER.id -> AttachmentsHeaderViewHolder( + ItemHomeworkDialogAttachmentsHeaderBinding.inflate(inflater, parent, false) + ) + ViewType.ATTACHMENT.id -> AttachmentViewHolder( + ItemHomeworkDialogAttachmentBinding.inflate(inflater, parent, false) + ) + else -> DetailsViewHolder( + ItemHomeworkDialogDetailsBinding.inflate(inflater, parent, false) + ) } } @@ -63,12 +73,15 @@ class HomeworkDetailsAdapter @Inject constructor() : } private fun bindDetailsViewHolder(holder: DetailsViewHolder) { + val noDataString = holder.binding.root.context.getString(R.string.all_no_data) + with(holder.binding) { homeworkDialogDate.text = homework?.date?.toFormattedString() homeworkDialogEntryDate.text = homework?.entryDate?.toFormattedString() - homeworkDialogSubject.text = homework?.subject - homeworkDialogTeacher.text = homework?.teacher - homeworkDialogContent.text = homework?.content + homeworkDialogSubject.text = homework?.subject.ifNullOrBlank { noDataString } + homeworkDialogTeacher.text = homework?.teacher.ifNullOrBlank { noDataString } + homeworkDialogContent.text = homework?.content.ifNullOrBlank { noDataString } + homeworkDialogDelete.visibility = if (homework?.isAddedByUser == true) VISIBLE else GONE homeworkDialogFullScreen.visibility = if (isHomeworkFullscreen) GONE else VISIBLE homeworkDialogFullScreenExit.visibility = if (isHomeworkFullscreen) VISIBLE else GONE homeworkDialogFullScreen.setOnClickListener { @@ -81,6 +94,9 @@ class HomeworkDetailsAdapter @Inject constructor() : homeworkDialogFullScreenExit.visibility = GONE onFullScreenExitClickListener() } + homeworkDialogDelete.setOnClickListener { + onDeleteClickListener(homework!!) + } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt index 93045a481..f9d463510 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt @@ -25,6 +25,9 @@ class HomeworkDetailsDialog : BaseDialogFragment(), Homew @Inject lateinit var detailsAdapter: HomeworkDetailsAdapter + override val homeworkDeleteSuccess: String + get() = getString(R.string.homework_delete_success) + private lateinit var homework: Homework companion object { @@ -82,12 +85,17 @@ class HomeworkDetailsDialog : BaseDialogFragment(), Homew dialog?.window?.setLayout(WRAP_CONTENT, WRAP_CONTENT) presenter.isHomeworkFullscreen = false } + onDeleteClickListener = { homework -> presenter.deleteHomework(homework) } isHomeworkFullscreen = presenter.isHomeworkFullscreen homework = this@HomeworkDetailsDialog.homework } } } + override fun closeDialog() { + dismiss() + } + override fun updateMarkAsDoneLabel(isDone: Boolean) { binding.homeworkDialogRead.text = view?.context?.getString(if (isDone) R.string.homework_mark_as_undone else R.string.homework_mark_as_done) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsPresenter.kt index ca6fc71ee..ea9b47a05 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsPresenter.kt @@ -33,6 +33,25 @@ class HomeworkDetailsPresenter @Inject constructor( Timber.i("Homework details view was initialized") } + fun deleteHomework(homework: Homework) { + flowWithResource { homeworkRepository.deleteHomework(homework) }.onEach { + when (it.status) { + Status.LOADING -> Timber.i("Homework delete start") + Status.SUCCESS -> { + Timber.i("Homework delete: Success") + view?.run { + showMessage(homeworkDeleteSuccess) + closeDialog() + } + } + Status.ERROR -> { + Timber.i("Homework delete result: An exception occurred") + errorHandler.dispatch(it.error!!) + } + } + }.launch("delete") + } + fun toggleDone(homework: Homework) { flowWithResource { homeworkRepository.toggleDone(homework) }.onEach { when (it.status) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsView.kt index 697f22335..4a47de43b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsView.kt @@ -4,7 +4,11 @@ import io.github.wulkanowy.ui.base.BaseView interface HomeworkDetailsView : BaseView { + val homeworkDeleteSuccess: String + fun initView() + fun closeDialog() + fun updateMarkAsDoneLabel(isDone: Boolean) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt index 0672d75fa..4a93d4ca8 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt @@ -170,7 +170,7 @@ class LoginAdvancedFragment : override fun setErrorUsernameRequired() { with(binding.loginFormUsernameLayout) { requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } @@ -191,7 +191,7 @@ class LoginAdvancedFragment : override fun setErrorPassRequired(focus: Boolean) { with(binding.loginFormPassLayout) { if (focus) requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } @@ -212,7 +212,7 @@ class LoginAdvancedFragment : override fun setErrorPinRequired() { with(binding.loginFormPinLayout) { requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } @@ -226,7 +226,7 @@ class LoginAdvancedFragment : override fun setErrorSymbolRequired() { with(binding.loginFormSymbolLayout) { requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } @@ -240,7 +240,7 @@ class LoginAdvancedFragment : override fun setErrorTokenRequired() { with(binding.loginFormTokenLayout) { requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt index 6e0294a4e..78cc6db09 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt @@ -123,7 +123,7 @@ class LoginFormFragment : BaseFragment(R.layout.fragme override fun setErrorUsernameRequired() { with(binding.loginFormUsernameLayout) { - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } @@ -141,7 +141,7 @@ class LoginFormFragment : BaseFragment(R.layout.fragme override fun setErrorPassRequired(focus: Boolean) { with(binding.loginFormPassLayout) { - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverFragment.kt index 2e2f9f5ce..a91dfb618 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverFragment.kt @@ -99,7 +99,7 @@ class LoginRecoverFragment : override fun setErrorNameRequired() { with(bindingLocal.loginRecoverNameLayout) { requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt index e2c37db61..ae0f75a5e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt @@ -77,7 +77,7 @@ class LoginSymbolFragment : override fun setErrorSymbolRequire() { binding.loginSymbolNameLayout.apply { requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } diff --git a/app/src/main/res/layout/dialog_homework.xml b/app/src/main/res/layout/dialog_homework.xml index 22a03cb21..341cec544 100644 --- a/app/src/main/res/layout/dialog_homework.xml +++ b/app/src/main/res/layout/dialog_homework.xml @@ -4,6 +4,7 @@ android:id="@+id/parent" android:layout_width="match_parent" android:layout_height="match_parent" + android:minWidth="300dp" android:orientation="vertical"> + android:layout_height="1dp" + android:background="@drawable/ic_all_divider" /> @@ -52,6 +53,9 @@ style="@style/Widget.MaterialComponents.Button.TextButton.Dialog" android:layout_width="wrap_content" android:layout_height="36dp" + android:layout_alignParentEnd="true" + android:layout_alignParentBottom="true" + android:layout_gravity="center_vertical" android:layout_marginTop="8dp" android:layout_marginEnd="8dp" android:layout_marginBottom="8dp" @@ -60,9 +64,6 @@ android:insetRight="0dp" android:insetBottom="0dp" android:minWidth="88dp" - android:layout_gravity="center_vertical" - android:layout_alignParentEnd="true" - android:layout_alignParentBottom="true" android:text="@string/all_close" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" /> diff --git a/app/src/main/res/layout/dialog_homework_add.xml b/app/src/main/res/layout/dialog_homework_add.xml new file mode 100644 index 000000000..b9b8d1a2e --- /dev/null +++ b/app/src/main/res/layout/dialog_homework_add.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_homework.xml b/app/src/main/res/layout/fragment_homework.xml index c0b5698d5..e1f0a16be 100644 --- a/app/src/main/res/layout/fragment_homework.xml +++ b/app/src/main/res/layout/fragment_homework.xml @@ -26,6 +26,8 @@ android:id="@+id/homeworkRecycler" android:layout_width="match_parent" android:layout_height="match_parent" + android:clipToPadding="false" + android:paddingBottom="70dp" tools:listitem="@layout/item_homework" /> @@ -105,6 +107,18 @@ android:text="@string/all_retry" /> + + + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/item_homework_dialog_details.xml b/app/src/main/res/layout/item_homework_dialog_details.xml index abc371da8..9d560ba51 100644 --- a/app/src/main/res/layout/item_homework_dialog_details.xml +++ b/app/src/main/res/layout/item_homework_dialog_details.xml @@ -10,26 +10,37 @@ + android:orientation="horizontal"> + + - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 63d4fc423..b662d4ad5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,6 +17,7 @@ Licenses Messages New message + New homework Notes and achievements Homework Accounts manager @@ -57,7 +58,6 @@ Use the assigned login or email in @%1$s Invalid symbol Student not found. Validate the symbol and the chosen variation of the UONET+ register - This field is required Selected student is already logged in The symbol can be found on the register page in Uczeń → Dostęp Mobilny → Zarejestruj urządzenie mobilne.\n\nMake sure that you have set the appropriate register variant in the UONET+ register variant field on the previous screen. Wulkanowy does not detect pre-school students at the moment Select students to log in to the application @@ -316,6 +316,9 @@ No info about homework Mark as done Mark as undone + Add homework + Homework added successfully + Homework deleted successfully Attachments New homework @@ -592,6 +595,10 @@ No Save Title + Add + Copied + Undo + Change @@ -697,12 +704,6 @@ No color - - Copied - Undo - Change - - Download of updates has started… An update has just been downloaded. @@ -721,4 +722,5 @@ An unexpected error occurred Feature disabled by your school Feature not available. Login in a mode other than Mobile API + This field is required From ebf9e741c2cc19d0cb8287878e9d789cf480575e Mon Sep 17 00:00:00 2001 From: Damian Czupryn <60961958+Daxxxis@users.noreply.github.com> Date: Sat, 9 Oct 2021 01:04:04 +0200 Subject: [PATCH 229/869] Fix homework last item padding (#1568) --- app/src/main/res/layout/fragment_homework.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/fragment_homework.xml b/app/src/main/res/layout/fragment_homework.xml index e1f0a16be..ae8270ab1 100644 --- a/app/src/main/res/layout/fragment_homework.xml +++ b/app/src/main/res/layout/fragment_homework.xml @@ -27,7 +27,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:clipToPadding="false" - android:paddingBottom="70dp" + android:paddingBottom="64dp" tools:listitem="@layout/item_homework" /> From 2ab0a57a4168b172f21a5f686c12feeb95756eb2 Mon Sep 17 00:00:00 2001 From: Damian Czupryn <60961958+Daxxxis@users.noreply.github.com> Date: Sat, 9 Oct 2021 01:55:00 +0200 Subject: [PATCH 230/869] Separate calculated average settings (#1558) --- app/src/main/res/values/strings.xml | 1 + .../main/res/xml/scheme_preferences_advanced.xml | 16 ++++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b662d4ad5..d8674705a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -668,6 +668,7 @@ Attendance Timetable Grades + Calculated average Messages Appearance & Behavior diff --git a/app/src/main/res/xml/scheme_preferences_advanced.xml b/app/src/main/res/xml/scheme_preferences_advanced.xml index 461037871..95f6f383a 100644 --- a/app/src/main/res/xml/scheme_preferences_advanced.xml +++ b/app/src/main/res/xml/scheme_preferences_advanced.xml @@ -19,18 +19,16 @@ app:key="@string/pref_key_grade_modifier_minus" app:title="@string/pref_other_grade_modifier_minus" app:useSimpleSummaryProvider="true" /> + + - + Date: Sat, 9 Oct 2021 17:36:22 +0200 Subject: [PATCH 231/869] Change text when there are no lessons today and tomorrow in dashboard (#1571) --- .../ui/modules/dashboard/DashboardAdapter.kt | 9 +++++++-- app/src/main/res/layout/item_dashboard_lessons.xml | 13 +++++++++++++ app/src/main/res/values/strings.xml | 1 + 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt index 11b575c1c..7bd6ed0ca 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt @@ -301,26 +301,31 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter { dateToNavigate = tomorrowDate updateLessonView(item, tomorrowTimetable, binding) binding.dashboardLessonsItemTitleTomorrow.isVisible = true + binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false } currentDayHeader != null && currentDayHeader.content.isNotBlank() -> { dateToNavigate = currentDate updateLessonView(item, emptyList(), binding, currentDayHeader) binding.dashboardLessonsItemTitleTomorrow.isVisible = false + binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false } tomorrowDayHeader != null && tomorrowDayHeader.content.isNotBlank() -> { dateToNavigate = tomorrowDate updateLessonView(item, emptyList(), binding, tomorrowDayHeader) binding.dashboardLessonsItemTitleTomorrow.isVisible = true + binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false } else -> { - dateToNavigate = tomorrowDate + dateToNavigate = currentDate updateLessonView(item, emptyList(), binding) - binding.dashboardLessonsItemTitleTomorrow.isVisible = + binding.dashboardLessonsItemTitleTomorrow.isVisible = false + binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = !(item.isLoading && item.error == null) } } diff --git a/app/src/main/res/layout/item_dashboard_lessons.xml b/app/src/main/res/layout/item_dashboard_lessons.xml index a2a92c54c..39bd317a7 100644 --- a/app/src/main/res/layout/item_dashboard_lessons.xml +++ b/app/src/main/res/layout/item_dashboard_lessons.xml @@ -42,6 +42,19 @@ app:layout_constraintBottom_toBottomOf="@id/dashboard_lessons_item_title" app:layout_constraintStart_toEndOf="@id/dashboard_lessons_item_title" /> + + Lessons (Tomorrow) + (Today and tomorrow) In a moment: Soon: First: From a240fd5d5f1cbe1bdf90f39c6bebdeeb0a05a1c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sat, 9 Oct 2021 18:37:27 +0200 Subject: [PATCH 232/869] Add build timestamp as build config field (#1567) --- app/build.gradle | 11 +++++++---- app/src/main/AndroidManifest.xml | 12 ++++-------- .../java/io/github/wulkanowy/utils/AppInfo.kt | 16 +++------------- .../data/db/migrations/AbstractMigrationTest.kt | 4 ++-- .../data/db/migrations/Migration35Test.kt | 6 +++--- .../wulkanowy/data/repositories/StudentTest.kt | 2 +- 6 files changed, 20 insertions(+), 31 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 91ce5f24a..031385feb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,10 +29,7 @@ android { resValue "string", "app_name", "Wulkanowy" - manifestPlaceholders = [ - firebase_enabled: project.hasProperty("enableFirebase"), - buildTimestamp: String.valueOf(System.currentTimeMillis()) - ] + manifestPlaceholders = [firebase_enabled: project.hasProperty("enableFirebase")] javaCompileOptions { annotationProcessorOptions { arguments += [ @@ -41,6 +38,12 @@ android { ] } } + + if (System.env.SET_BUILD_TIMESTAMP) { + buildConfigField "long", "BUILD_TIMESTAMP", String.valueOf(System.currentTimeMillis()) + } else { + buildConfigField "long", "BUILD_TIMESTAMP", "1486235849000" + } } sourceSets { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bebe33c62..8c7eb8527 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -153,14 +153,6 @@ android:resource="@xml/provider_paths" /> - - - - + + diff --git a/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt b/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt index 4875425f1..a3961aed8 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt @@ -1,12 +1,10 @@ package io.github.wulkanowy.utils -import android.content.Context -import android.content.pm.PackageManager import android.content.res.Resources import android.os.Build.MANUFACTURER import android.os.Build.MODEL import android.os.Build.VERSION.SDK_INT -import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.BuildConfig.BUILD_TIMESTAMP import io.github.wulkanowy.BuildConfig.DEBUG import io.github.wulkanowy.BuildConfig.FLAVOR import io.github.wulkanowy.BuildConfig.VERSION_CODE @@ -15,21 +13,13 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -open class AppInfo @Inject constructor( - @ApplicationContext private val context: Context, -) { +open class AppInfo @Inject constructor() { open val isDebug get() = DEBUG open val versionCode get() = VERSION_CODE - open val buildTimestamp: Long - get() { - val info = context.packageManager.getApplicationInfo( - context.packageName, PackageManager.GET_META_DATA, - ) - return info.metaData?.getFloat("buildTimestamp")?.toLong() ?: 0 - } + open val buildTimestamp get() = BUILD_TIMESTAMP open val buildFlavor get() = FLAVOR diff --git a/app/src/test/java/io/github/wulkanowy/data/db/migrations/AbstractMigrationTest.kt b/app/src/test/java/io/github/wulkanowy/data/db/migrations/AbstractMigrationTest.kt index 5897af738..cc31d8933 100644 --- a/app/src/test/java/io/github/wulkanowy/data/db/migrations/AbstractMigrationTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/db/migrations/AbstractMigrationTest.kt @@ -16,7 +16,7 @@ abstract class AbstractMigrationTest { val dbName = "migration-test" - val context: Context get() = ApplicationProvider.getApplicationContext() + val context: Context get() = ApplicationProvider.getApplicationContext() @get:Rule val helper: MigrationTestHelper = MigrationTestHelper( @@ -33,7 +33,7 @@ abstract class AbstractMigrationTest { ).addMigrations( *AppDatabase.getMigrations( SharedPrefProvider(PreferenceManager.getDefaultSharedPreferences(context)), - AppInfo(context) + AppInfo() ) ).build() // close the database and release any stream resources when the test finishes diff --git a/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt index 9a38f4286..883cdb81c 100644 --- a/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt +++ b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt @@ -29,15 +29,15 @@ class Migration35Test : AbstractMigrationTest() { close() } - helper.runMigrationsAndValidate(dbName, 35, true, Migration35(AppInfo(context))) + helper.runMigrationsAndValidate(dbName, 35, true, Migration35(AppInfo())) val db = getMigratedRoomDatabase() val students = runBlocking { db.studentDao.loadAll() } assertEquals(2, students.size) - assertTrue { students[0].avatarColor in AppInfo(context).defaultColorsForAvatar } - assertTrue { students[1].avatarColor in AppInfo(context).defaultColorsForAvatar } + assertTrue { students[0].avatarColor in AppInfo().defaultColorsForAvatar } + assertTrue { students[1].avatarColor in AppInfo().defaultColorsForAvatar } } private fun createStudent(db: SupportSQLiteDatabase, id: Long) { diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt index 39ebb4a19..9d3d7a2ec 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt @@ -37,7 +37,7 @@ class StudentTest { studentDb = studentDb, semesterDb = semesterDb, sdk = mockSdk, - appInfo = AppInfo(mockk()), + appInfo = AppInfo(), appDatabase = mockk() ) } From 539be586ce402d74bdfc2ce195a175c02726007d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Mon, 11 Oct 2021 14:39:54 +0200 Subject: [PATCH 233/869] Fix text color of time left indicator on dashboard timetable card (#1572) --- app/src/main/res/layout/item_dashboard_lessons.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/layout/item_dashboard_lessons.xml b/app/src/main/res/layout/item_dashboard_lessons.xml index 39bd317a7..9156c1a2f 100644 --- a/app/src/main/res/layout/item_dashboard_lessons.xml +++ b/app/src/main/res/layout/item_dashboard_lessons.xml @@ -50,10 +50,10 @@ android:text="@string/dashboard_timetable_title_today_and_tomorrow" android:textColor="?colorOnSurface" android:textSize="14sp" - tools:visibility="invisible" app:layout_constraintBaseline_toBaselineOf="@id/dashboard_lessons_item_title" app:layout_constraintBottom_toBottomOf="@id/dashboard_lessons_item_title" - app:layout_constraintStart_toEndOf="@id/dashboard_lessons_item_title" /> + app:layout_constraintStart_toEndOf="@id/dashboard_lessons_item_title" + tools:visibility="invisible" /> Date: Mon, 11 Oct 2021 15:43:29 +0000 Subject: [PATCH 234/869] Bump hianalytics from 6.2.0.301 to 6.3.0.300 (#1576) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 031385feb..fc7d50d51 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -228,7 +228,7 @@ dependencies { playImplementation 'com.google.android.play:core:1.10.2' playImplementation 'com.google.android.play:core-ktx:1.8.1' - hmsImplementation 'com.huawei.hms:hianalytics:6.2.0.301' + hmsImplementation 'com.huawei.hms:hianalytics:6.3.0.300' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.1.200' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 3d0cd11ba45042b612d3c04f146f364852cac373 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 15:44:03 +0000 Subject: [PATCH 235/869] Bump Treessence from 1.0.4 to 1.0.5 (#1575) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index fc7d50d51..ff40c7c85 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -214,7 +214,7 @@ dependencies { implementation "com.jakewharton.timber:timber:5.0.1" implementation "at.favre.lib:slf4j-timber:1.0.1" - implementation 'com.github.bastienpaulfr:Treessence:1.0.4' + implementation 'com.github.bastienpaulfr:Treessence:1.0.5' implementation "com.mikepenz:aboutlibraries-core:$about_libraries" implementation "io.coil-kt:coil:1.3.2" implementation "io.github.wulkanowy:AppKillerManager:3.0.0" From 9d0366d010433994a296b69bb05ea2e3e644396d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 15:44:19 +0000 Subject: [PATCH 236/869] Bump firebase-bom from 28.4.1 to 28.4.2 (#1574) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index ff40c7c85..ce6416069 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -221,7 +221,7 @@ dependencies { implementation 'me.xdrop:fuzzywuzzy:1.3.1' implementation 'com.fredporciuncula:flow-preferences:1.5.0' - playImplementation platform('com.google.firebase:firebase-bom:28.4.1') + playImplementation platform('com.google.firebase:firebase-bom:28.4.2') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' From d6918077bff00d91f51d7cdd5c6a5bd201a1a5e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Jelnicki?= Date: Wed, 13 Oct 2021 21:47:03 +0200 Subject: [PATCH 237/869] Set upcoming lesson notification visibility to public (#1581) --- .../wulkanowy/services/alarm/TimetableNotificationReceiver.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt index 5e4bad8cf..9ce96ef3f 100644 --- a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt +++ b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt @@ -124,6 +124,7 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { .setAutoCancel(false) .setWhen(countDown) .setOngoing(isPersistent) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .apply { if (Build.VERSION.SDK_INT >= N) setUsesChronometer(true) } From e3122127c0912fe0854c21a8fc140becec01982b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Wed, 13 Oct 2021 23:58:24 +0200 Subject: [PATCH 238/869] Add admin messages (#1553) --- app/build.gradle | 10 +- .../42.json | 2396 +++++++++++++++++ .../{RepositoryModule.kt => DataModule.kt} | 88 +- .../wulkanowy/data/api/AdminMessageService.kt | 12 + .../github/wulkanowy/data/db/AppDatabase.kt | 15 +- .../wulkanowy/data/db/dao/AdminMessageDao.kt | 25 + .../data/db/entities/AdminMessage.kt | 37 + .../data/db/migrations/Migration42.kt | 24 + .../repositories/AdminMessageRepository.kt | 53 + .../data/repositories/AppCreatorRepository.kt | 7 +- .../repositories/PreferencesRepository.kt | 2 + .../github/wulkanowy/ui/base/ErrorHandler.kt | 7 +- .../ui/modules/dashboard/DashboardAdapter.kt | 40 + .../ui/modules/dashboard/DashboardFragment.kt | 13 + .../ui/modules/dashboard/DashboardItem.kt | 13 + .../dashboard/DashboardItemMoveCallback.kt | 7 +- .../modules/dashboard/DashboardPresenter.kt | 59 +- .../ui/modules/dashboard/DashboardView.kt | 2 + .../ui/modules/login/LoginErrorHandler.kt | 7 +- .../login/recover/RecoverErrorHandler.kt | 9 +- .../completed/CompletedLessonsErrorHandler.kt | 6 +- .../java/io/github/wulkanowy/utils/AppInfo.kt | 28 +- .../res/drawable/ic_dashboard_warning.xml | 10 + .../layout/item_dashboard_admin_message.xml | 60 + app/src/main/res/values-night/styles.xml | 1 + app/src/main/res/values/api_hosts.xml | 2 +- app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/colors.xml | 3 + app/src/main/res/values/styles.xml | 1 + 29 files changed, 2868 insertions(+), 70 deletions(-) create mode 100644 app/schemas/io.github.wulkanowy.data.db.AppDatabase/42.json rename app/src/main/java/io/github/wulkanowy/data/{RepositoryModule.kt => DataModule.kt} (72%) create mode 100644 app/src/main/java/io/github/wulkanowy/data/api/AdminMessageService.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration42.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt create mode 100644 app/src/main/res/drawable/ic_dashboard_warning.xml create mode 100644 app/src/main/res/layout/item_dashboard_admin_message.xml diff --git a/app/build.gradle b/app/build.gradle index ce6416069..227423e4d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -66,12 +66,14 @@ android { shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' signingConfig signingConfigs.release + buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\"" } debug { resValue "string", "app_name", "Wulkanowy DEV " + defaultConfig.versionCode applicationIdSuffix ".dev" versionNameSuffix "-dev" ext.enableCrashlytics = project.hasProperty("enableFirebase") + buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\"" } } @@ -170,7 +172,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:1.3.0" + implementation "io.github.wulkanowy:sdk:4efd64264b" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' @@ -212,6 +214,10 @@ dependencies { implementation 'com.github.ncapdevi:FragNav:3.3.0' implementation "com.github.YarikSOffice:lingver:1.3.0" + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0" + implementation "com.squareup.okhttp3:logging-interceptor:4.9.2" + implementation "com.jakewharton.timber:timber:5.0.1" implementation "at.favre.lib:slf4j-timber:1.0.1" implementation 'com.github.bastienpaulfr:Treessence:1.0.5' @@ -234,7 +240,7 @@ dependencies { releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" debugImplementation "com.github.ChuckerTeam.Chucker:library:$chucker" - debugImplementation 'com.github.amitshekhariitbhu.Android-Debug-Database:debug-db:v1.0.6' + debugImplementation 'com.github.amitshekhariitbhu.Android-Debug-Database:debug-db:1.0.6' testImplementation "junit:junit:4.13.2" testImplementation "io.mockk:mockk:$mockk" diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/42.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/42.json new file mode 100644 index 000000000..a5faa57b7 --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/42.json @@ -0,0 +1,2396 @@ +{ + "formatVersion": 1, + "database": { + "version": 42, + "identityHash": "5c8b7f9409294ecdebf9f74a44f8e883", + "entities": [ + { + "tableName": "Students", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrapper_base_url` TEXT NOT NULL, `mobile_base_url` TEXT NOT NULL, `login_type` TEXT NOT NULL, `login_mode` TEXT NOT NULL, `certificate_key` TEXT NOT NULL, `private_key` TEXT NOT NULL, `is_parent` INTEGER NOT NULL, `email` TEXT NOT NULL, `password` TEXT NOT NULL, `symbol` TEXT NOT NULL, `student_id` INTEGER NOT NULL, `user_login_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `student_name` TEXT NOT NULL, `school_id` TEXT NOT NULL, `school_short` TEXT NOT NULL, `school_name` TEXT NOT NULL, `class_name` TEXT NOT NULL, `class_id` INTEGER NOT NULL, `is_current` INTEGER NOT NULL, `registration_date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `nick` TEXT NOT NULL, `avatar_color` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "scrapperBaseUrl", + "columnName": "scrapper_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mobileBaseUrl", + "columnName": "mobile_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginType", + "columnName": "login_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginMode", + "columnName": "login_mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificateKey", + "columnName": "certificate_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "private_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isParent", + "columnName": "is_parent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "user_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "student_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolSymbol", + "columnName": "school_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "school_short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolName", + "columnName": "school_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "className", + "columnName": "class_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCurrent", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registrationDate", + "columnName": "registration_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nick", + "columnName": "nick", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarColor", + "columnName": "avatar_color", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Students_email_symbol_student_id_school_id_class_id", + "unique": true, + "columnNames": [ + "email", + "symbol", + "student_id", + "school_id", + "class_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryName", + "columnName": "diary_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolYear", + "columnName": "school_year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterName", + "columnName": "semester_name", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "semester_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `semester_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Exams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Timetable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `number` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `subjectOld` TEXT NOT NULL, `group` TEXT NOT NULL, `room` TEXT NOT NULL, `roomOld` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacherOld` TEXT NOT NULL, `info` TEXT NOT NULL, `student_plan` INTEGER NOT NULL, `changes` INTEGER NOT NULL, `canceled` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectOld", + "columnName": "subjectOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "room", + "columnName": "room", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roomOld", + "columnName": "roomOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherOld", + "columnName": "teacherOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isStudentPlan", + "columnName": "student_plan", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "changes", + "columnName": "changes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canceled", + "columnName": "canceled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Attendance", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `time_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `excusable` INTEGER NOT NULL, `excuse_status` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeId", + "columnName": "time_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excusable", + "columnName": "excusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excuseStatus", + "columnName": "excuse_status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AttendanceSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `subject_id` INTEGER NOT NULL, `month` INTEGER NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `absence_excused` INTEGER NOT NULL, `absence_for_school_reasons` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `lateness_excused` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subject_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "month", + "columnName": "month", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceExcused", + "columnName": "absence_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceForSchoolReasons", + "columnName": "absence_for_school_reasons", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latenessExcused", + "columnName": "lateness_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Grades", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `entry` TEXT NOT NULL, `value` REAL NOT NULL, `modifier` REAL NOT NULL, `comment` TEXT NOT NULL, `color` TEXT NOT NULL, `grade_symbol` TEXT NOT NULL, `description` TEXT NOT NULL, `weight` TEXT NOT NULL, `weightValue` REAL NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entry", + "columnName": "entry", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "modifier", + "columnName": "modifier", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gradeSymbol", + "columnName": "grade_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weightValue", + "columnName": "weightValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `subject` TEXT NOT NULL, `predicted_grade` TEXT NOT NULL, `final_grade` TEXT NOT NULL, `proposed_points` TEXT NOT NULL, `final_points` TEXT NOT NULL, `points_sum` TEXT NOT NULL, `average` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_predicted_grade_notified` INTEGER NOT NULL, `is_final_grade_notified` INTEGER NOT NULL, `predicted_grade_last_change` INTEGER NOT NULL, `final_grade_last_change` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "predictedGrade", + "columnName": "predicted_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalGrade", + "columnName": "final_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "proposedPoints", + "columnName": "proposed_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalPoints", + "columnName": "final_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pointsSum", + "columnName": "points_sum", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "average", + "columnName": "average", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPredictedGradeNotified", + "columnName": "is_predicted_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFinalGradeNotified", + "columnName": "is_final_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "predictedGradeLastChange", + "columnName": "predicted_grade_last_change", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "finalGradeLastChange", + "columnName": "final_grade_last_change", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradePartialStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `class_average` TEXT NOT NULL, `student_average` TEXT NOT NULL, `class_amounts` TEXT NOT NULL, `student_amounts` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAverage", + "columnName": "class_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAverage", + "columnName": "student_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAmounts", + "columnName": "class_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAmounts", + "columnName": "student_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesPointsStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `others` REAL NOT NULL, `student` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "others", + "columnName": "others", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "student", + "columnName": "student", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradeSemesterStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `amounts` TEXT NOT NULL, `student_grade` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amounts", + "columnName": "amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentGrade", + "columnName": "student_grade", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `recipient_name` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `removed` INTEGER NOT NULL, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `unread_by` INTEGER NOT NULL, `read_by` INTEGER NOT NULL, `content` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipient", + "columnName": "recipient_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "removed", + "columnName": "removed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasAttachments", + "columnName": "has_attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `one_drive_id` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`real_id`))", + "fields": [ + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "oneDriveId", + "columnName": "one_drive_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "real_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `category` TEXT NOT NULL, `category_type` INTEGER NOT NULL, `is_points_show` INTEGER NOT NULL, `points` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryType", + "columnName": "category_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPointsShow", + "columnName": "is_points_show", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Homework", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `attachments` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_done` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL, `is_added_by_user` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "is_done", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Subjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "LuckyNumbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `lucky_number` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "luckyNumber", + "columnName": "lucky_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CompletedLesson", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `topic` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `substitution` TEXT NOT NULL, `absence` TEXT NOT NULL, `resources` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "topic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "substitution", + "columnName": "substitution", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "resources", + "columnName": "resources", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ReportingUnits", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `short` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `roles` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "senderName", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roles", + "columnName": "roles", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` TEXT NOT NULL, `name` TEXT NOT NULL, `real_name` TEXT NOT NULL, `login_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `role` INTEGER NOT NULL, `hash` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "realName", + "columnName": "real_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginId", + "columnName": "login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MobileDevices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Teachers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `short_name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "School", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `contact` TEXT NOT NULL, `headmaster` TEXT NOT NULL, `pedagogue` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contact", + "columnName": "contact", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headmaster", + "columnName": "headmaster", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pedagogue", + "columnName": "pedagogue", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Conferences", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `subject` TEXT NOT NULL, `agenda` TEXT NOT NULL, `present_on_conference` TEXT NOT NULL, `conference_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agenda", + "columnName": "agenda", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presentOnConference", + "columnName": "present_on_conference", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conference_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableAdditional", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StudentInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `full_name` TEXT NOT NULL, `first_name` TEXT NOT NULL, `second_name` TEXT NOT NULL, `surname` TEXT NOT NULL, `birth_date` INTEGER NOT NULL, `birth_place` TEXT NOT NULL, `gender` TEXT NOT NULL, `has_polish_citizenship` INTEGER NOT NULL, `family_name` TEXT NOT NULL, `parents_names` TEXT NOT NULL, `address` TEXT NOT NULL, `registered_address` TEXT NOT NULL, `correspondence_address` TEXT NOT NULL, `phone_number` TEXT NOT NULL, `cell_phone_number` TEXT NOT NULL, `email` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `first_guardian_full_name` TEXT, `first_guardian_kinship` TEXT, `first_guardian_address` TEXT, `first_guardian_phones` TEXT, `first_guardian_email` TEXT, `second_guardian_full_name` TEXT, `second_guardian_kinship` TEXT, `second_guardian_address` TEXT, `second_guardian_phones` TEXT, `second_guardian_email` TEXT)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "full_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondName", + "columnName": "second_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "surname", + "columnName": "surname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "birthDate", + "columnName": "birth_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthPlace", + "columnName": "birth_place", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gender", + "columnName": "gender", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPolishCitizenship", + "columnName": "has_polish_citizenship", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "familyName", + "columnName": "family_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentsNames", + "columnName": "parents_names", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "registeredAddress", + "columnName": "registered_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "correspondenceAddress", + "columnName": "correspondence_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cellPhoneNumber", + "columnName": "cell_phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstGuardian.fullName", + "columnName": "first_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.kinship", + "columnName": "first_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.address", + "columnName": "first_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.phones", + "columnName": "first_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.email", + "columnName": "first_guardian_email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.fullName", + "columnName": "second_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.kinship", + "columnName": "second_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.address", + "columnName": "second_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.phones", + "columnName": "second_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.email", + "columnName": "second_guardian_email", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableHeaders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SchoolAnnouncements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `type` TEXT NOT NULL, `date` INTEGER NOT NULL, `data` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AdminMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `version_name` INTEGER, `version_max` INTEGER, `target_register_host` TEXT, `target_flavor` TEXT, `destination_url` TEXT, `priority` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "versionMin", + "columnName": "version_name", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMax", + "columnName": "version_max", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "targetRegisterHost", + "columnName": "target_register_host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "targetFlavor", + "columnName": "target_flavor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "destinationUrl", + "columnName": "destination_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "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, '5c8b7f9409294ecdebf9f74a44f8e883')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt b/app/src/main/java/io/github/wulkanowy/data/DataModule.kt similarity index 72% rename from app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt rename to app/src/main/java/io/github/wulkanowy/data/DataModule.kt index 863bb5950..cac3ffc23 100644 --- a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt +++ b/app/src/main/java/io/github/wulkanowy/data/DataModule.kt @@ -2,62 +2,100 @@ package io.github.wulkanowy.data import android.content.Context import android.content.SharedPreferences -import android.content.res.AssetManager -import android.content.res.Resources import androidx.preference.PreferenceManager import com.chuckerteam.chucker.api.ChuckerCollector import com.chuckerteam.chucker.api.ChuckerInterceptor import com.chuckerteam.chucker.api.RetentionManager import com.fredporciuncula.flow.preferences.FlowSharedPreferences +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import io.github.wulkanowy.data.api.AdminMessageService import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AppInfo import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.create import timber.log.Timber +import java.util.concurrent.TimeUnit import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal class RepositoryModule { +internal class DataModule { @Singleton @Provides - fun provideSdk(chuckerCollector: ChuckerCollector, @ApplicationContext context: Context): Sdk { - return Sdk().apply { + fun provideSdk(chuckerInterceptor: ChuckerInterceptor) = + Sdk().apply { androidVersion = android.os.Build.VERSION.RELEASE buildTag = android.os.Build.MODEL setSimpleHttpLogger { Timber.d(it) } // for debug only - addInterceptor( - ChuckerInterceptor.Builder(context) - .collector(chuckerCollector) - .alwaysReadResponseBody(true) - .build(), network = true - ) + addInterceptor(chuckerInterceptor, network = true) } - } @Singleton @Provides fun provideChuckerCollector( @ApplicationContext context: Context, prefRepository: PreferencesRepository - ): ChuckerCollector { - return ChuckerCollector( - context = context, - showNotification = prefRepository.isDebugNotificationEnable, - retentionPeriod = RetentionManager.Period.ONE_HOUR - ) - } + ) = ChuckerCollector( + context = context, + showNotification = prefRepository.isDebugNotificationEnable, + retentionPeriod = RetentionManager.Period.ONE_HOUR + ) + + @Singleton + @Provides + fun provideChuckerInterceptor( + @ApplicationContext context: Context, + chuckerCollector: ChuckerCollector + ) = ChuckerInterceptor.Builder(context) + .collector(chuckerCollector) + .alwaysReadResponseBody(true) + .build() + + @Singleton + @Provides + fun provideOkHttpClient(chuckerInterceptor: ChuckerInterceptor): OkHttpClient = + OkHttpClient.Builder() + .addNetworkInterceptor(chuckerInterceptor) + .addInterceptor(HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BASIC + }) + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build() + + @OptIn(ExperimentalSerializationApi::class) + @Singleton + @Provides + fun provideRetrofit( + okHttpClient: OkHttpClient, + json: Json, + appInfo: AppInfo + ): Retrofit = Retrofit.Builder() + .baseUrl(appInfo.messagesBaseUrl) + .client(okHttpClient) + .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) + .build() + + @Singleton + @Provides + fun provideAdminMessageService(retrofit: Retrofit): AdminMessageService = retrofit.create() @Singleton @Provides @@ -67,14 +105,6 @@ internal class RepositoryModule { appInfo: AppInfo ) = AppDatabase.newInstance(context, sharedPrefProvider, appInfo) - @Singleton - @Provides - fun provideResources(@ApplicationContext context: Context): Resources = context.resources - - @Singleton - @Provides - fun provideAssets(@ApplicationContext context: Context): AssetManager = context.assets - @Singleton @Provides fun provideSharedPref(@ApplicationContext context: Context): SharedPreferences = @@ -208,4 +238,8 @@ internal class RepositoryModule { @Singleton @Provides fun provideNotificationDao(database: AppDatabase) = database.notificationDao + + @Singleton + @Provides + fun provideAdminMessageDao(database: AppDatabase) = database.adminMessagesDao } diff --git a/app/src/main/java/io/github/wulkanowy/data/api/AdminMessageService.kt b/app/src/main/java/io/github/wulkanowy/data/api/AdminMessageService.kt new file mode 100644 index 000000000..23f5af24a --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/api/AdminMessageService.kt @@ -0,0 +1,12 @@ +package io.github.wulkanowy.data.api + +import io.github.wulkanowy.data.db.entities.AdminMessage +import retrofit2.http.GET +import javax.inject.Singleton + +@Singleton +interface AdminMessageService { + + @GET("/v1.json") + suspend fun getAdminMessages(): List +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt index b93d00881..b8831acd8 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt @@ -6,6 +6,7 @@ import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.RoomDatabase.JournalMode.TRUNCATE import androidx.room.TypeConverters +import io.github.wulkanowy.data.db.dao.AdminMessageDao import io.github.wulkanowy.data.db.dao.AttendanceDao import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao import io.github.wulkanowy.data.db.dao.CompletedLessonsDao @@ -35,6 +36,7 @@ import io.github.wulkanowy.data.db.dao.TeacherDao import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao import io.github.wulkanowy.data.db.dao.TimetableDao import io.github.wulkanowy.data.db.dao.TimetableHeaderDao +import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.AttendanceSummary import io.github.wulkanowy.data.db.entities.CompletedLesson @@ -97,8 +99,9 @@ import io.github.wulkanowy.data.db.migrations.Migration37 import io.github.wulkanowy.data.db.migrations.Migration38 import io.github.wulkanowy.data.db.migrations.Migration39 import io.github.wulkanowy.data.db.migrations.Migration4 -import io.github.wulkanowy.data.db.migrations.Migration41 import io.github.wulkanowy.data.db.migrations.Migration40 +import io.github.wulkanowy.data.db.migrations.Migration41 +import io.github.wulkanowy.data.db.migrations.Migration42 import io.github.wulkanowy.data.db.migrations.Migration5 import io.github.wulkanowy.data.db.migrations.Migration6 import io.github.wulkanowy.data.db.migrations.Migration7 @@ -138,7 +141,8 @@ import javax.inject.Singleton StudentInfo::class, TimetableHeader::class, SchoolAnnouncement::class, - Notification::class + Notification::class, + AdminMessage::class ], version = AppDatabase.VERSION_SCHEMA, exportSchema = true @@ -147,7 +151,7 @@ import javax.inject.Singleton abstract class AppDatabase : RoomDatabase() { companion object { - const val VERSION_SCHEMA = 41 + const val VERSION_SCHEMA = 42 fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( Migration2(), @@ -189,7 +193,8 @@ abstract class AppDatabase : RoomDatabase() { Migration38(), Migration39(), Migration40(), - Migration41() + Migration41(), + Migration42() ) fun newInstance( @@ -261,4 +266,6 @@ abstract class AppDatabase : RoomDatabase() { abstract val schoolAnnouncementDao: SchoolAnnouncementDao abstract val notificationDao: NotificationDao + + abstract val adminMessagesDao: AdminMessageDao } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt new file mode 100644 index 000000000..87f4812da --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt @@ -0,0 +1,25 @@ +package io.github.wulkanowy.data.db.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Transaction +import io.github.wulkanowy.data.db.entities.AdminMessage +import kotlinx.coroutines.flow.Flow +import javax.inject.Singleton + +@Singleton +@Dao +abstract class AdminMessageDao : BaseDao { + + @Query("SELECT * FROM AdminMessages") + abstract fun loadAll(): Flow> + + @Transaction + open suspend fun removeOldAndSaveNew( + oldMessages: List, + newMessages: List + ) { + deleteAll(oldMessages) + insertAll(newMessages) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt new file mode 100644 index 000000000..b7b0cf58a --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt @@ -0,0 +1,37 @@ +package io.github.wulkanowy.data.db.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.serialization.Serializable + +@Serializable +@Entity(tableName = "AdminMessages") +data class AdminMessage( + + @PrimaryKey + val id: Int, + + val title: String, + + val content: String, + + @ColumnInfo(name = "version_name") + val versionMin: Int? = null, + + @ColumnInfo(name = "version_max") + val versionMax: Int? = null, + + @ColumnInfo(name = "target_register_host") + val targetRegisterHost: String? = null, + + @ColumnInfo(name = "target_flavor") + val targetFlavor: String? = null, + + @ColumnInfo(name = "destination_url") + val destinationUrl: String? = null, + + val priority: String, + + val type: String +) diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration42.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration42.kt new file mode 100644 index 000000000..3d66f301b --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration42.kt @@ -0,0 +1,24 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration42 : Migration(41, 42) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + """CREATE TABLE IF NOT EXISTS `AdminMessages` ( + `id` INTEGER NOT NULL, + `title` TEXT NOT NULL, + `content` TEXT NOT NULL, + `version_name` INTEGER, + `version_max` INTEGER, + `target_register_host` TEXT, + `target_flavor` TEXT, + `destination_url` TEXT, + `priority` TEXT NOT NULL, + `type` TEXT NOT NULL, + PRIMARY KEY(`id`))""" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt new file mode 100644 index 000000000..a620caade --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt @@ -0,0 +1,53 @@ +package io.github.wulkanowy.data.repositories + +import io.github.wulkanowy.data.api.AdminMessageService +import io.github.wulkanowy.data.db.dao.AdminMessageDao +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.utils.AppInfo +import io.github.wulkanowy.utils.AutoRefreshHelper +import io.github.wulkanowy.utils.getRefreshKey +import io.github.wulkanowy.utils.networkBoundResource +import kotlinx.coroutines.sync.Mutex +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class AdminMessageRepository @Inject constructor( + private val adminMessageService: AdminMessageService, + private val adminMessageDao: AdminMessageDao, + private val appInfo: AppInfo, + private val refreshHelper: AutoRefreshHelper, +) { + private val saveFetchResultMutex = Mutex() + + private val cacheKey = "admin_messages" + + suspend fun getAdminMessages(student: Student, forceRefresh: Boolean) = networkBoundResource( + mutex = saveFetchResultMutex, + query = { adminMessageDao.loadAll() }, + fetch = { adminMessageService.getAdminMessages() }, + shouldFetch = { + refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student)) || forceRefresh + }, + saveFetchResult = { oldItems, newItems -> + adminMessageDao.removeOldAndSaveNew(oldItems, newItems) + refreshHelper.updateLastRefreshTimestamp(cacheKey) + }, + showSavedOnLoading = false, + mapResult = { adminMessages -> + adminMessages.filter { adminMessage -> + val isCorrectRegister = adminMessage.targetRegisterHost?.let { + student.scrapperBaseUrl.contains(it, true) + } ?: true + val isCorrectFlavor = + adminMessage.targetFlavor?.equals(appInfo.buildFlavor, true) ?: true + val isCorrectMaxVersion = + adminMessage.versionMax?.let { it >= appInfo.versionCode } ?: true + val isCorrectMinVersion = + adminMessage.versionMin?.let { it <= appInfo.versionCode } ?: true + + isCorrectRegister && isCorrectFlavor && isCorrectMaxVersion && isCorrectMinVersion + }.maxByOrNull { it.id } + } + ) +} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt index f1fdbfed3..3d83f7181 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.data.repositories -import android.content.res.AssetManager +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.data.pojos.Contributor import io.github.wulkanowy.utils.DispatchersProvider import kotlinx.coroutines.withContext @@ -12,7 +13,7 @@ import javax.inject.Singleton @Singleton class AppCreatorRepository @Inject constructor( - private val assets: AssetManager, + @ApplicationContext private val context: Context, private val dispatchers: DispatchersProvider, private val json: Json, ) { @@ -20,7 +21,7 @@ class AppCreatorRepository @Inject constructor( @OptIn(ExperimentalSerializationApi::class) @Suppress("BlockingMethodInNonBlockingContext") suspend fun getAppCreators() = withContext(dispatchers.backgroundThread) { - val inputStream = assets.open("contributors.json").buffered() + val inputStream = context.assets.open("contributors.json").buffered() json.decodeFromStream>(inputStream) } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index 65edd53f0..b0991bbcb 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -209,6 +209,7 @@ class PreferencesRepository @Inject constructor( .map { set -> set.map { DashboardItem.Tile.valueOf(it) } .plus(DashboardItem.Tile.ACCOUNT) + .plus(DashboardItem.Tile.ADMIN_MESSAGE) .toSet() } @@ -216,6 +217,7 @@ class PreferencesRepository @Inject constructor( get() = selectedDashboardTilesPreference.get() .map { DashboardItem.Tile.valueOf(it) } .plus(DashboardItem.Tile.ACCOUNT) + .plus(DashboardItem.Tile.ADMIN_MESSAGE) .toSet() set(value) { val filteredValue = value.filterNot { it == DashboardItem.Tile.ACCOUNT } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt index 7c32ef184..fbc994e2c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.ui.base -import android.content.res.Resources +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException @@ -9,7 +10,7 @@ import io.github.wulkanowy.utils.security.ScramblerException import timber.log.Timber import javax.inject.Inject -open class ErrorHandler @Inject constructor(protected val resources: Resources) { +open class ErrorHandler @Inject constructor(@ApplicationContext protected val context: Context) { var showErrorMessage: (String, Throwable) -> Unit = { _, _ -> } @@ -25,7 +26,7 @@ open class ErrorHandler @Inject constructor(protected val resources: Resources) } protected open fun proceed(error: Throwable) { - showErrorMessage(resources.getString(error), error) + showErrorMessage(context.resources.getString(error), error) when (error) { is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl) is ScramblerException, is BadCredentialsException -> onSessionExpired() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt index 7bd6ed0ca..405bfbc5a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt @@ -1,6 +1,8 @@ package io.github.wulkanowy.ui.modules.dashboard import android.annotation.SuppressLint +import android.content.res.ColorStateList +import android.graphics.Color import android.graphics.Typeface import android.os.Handler import android.os.Looper @@ -18,6 +20,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.db.entities.TimetableHeader import io.github.wulkanowy.databinding.ItemDashboardAccountBinding +import io.github.wulkanowy.databinding.ItemDashboardAdminMessageBinding import io.github.wulkanowy.databinding.ItemDashboardAnnouncementsBinding import io.github.wulkanowy.databinding.ItemDashboardConferencesBinding import io.github.wulkanowy.databinding.ItemDashboardExamsBinding @@ -63,6 +66,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter Unit = {} + var onAdminMessageClickListener: (String?) -> Unit = {} + val items = mutableListOf() fun submitList(newItems: List) { @@ -109,6 +114,9 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter ConferencesViewHolder( ItemDashboardConferencesBinding.inflate(inflater, parent, false) ) + DashboardItem.Type.ADMIN_MESSAGE.ordinal -> AdminMessageViewHolder( + ItemDashboardAdminMessageBinding.inflate(inflater, parent, false) + ) else -> throw IllegalArgumentException() } } @@ -123,6 +131,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter bindAnnouncementsViewHolder(holder, position) is ExamsViewHolder -> bindExamsViewHolder(holder, position) is ConferencesViewHolder -> bindConferencesViewHolder(holder, position) + is AdminMessageViewHolder -> bindAdminMessage(holder, position) } } @@ -697,6 +706,34 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter { + context.getThemeAttrColor(R.attr.colorPrimary) to + context.getThemeAttrColor(R.attr.colorOnPrimary) + } + "MEDIUM" -> { + context.getThemeAttrColor(R.attr.colorMessageMedium) to Color.BLACK + } + else -> null to context.getThemeAttrColor(R.attr.colorOnSurface) + } + + with(adminMessageViewHolder.binding) { + dashboardAdminMessageItemTitle.text = item.title + dashboardAdminMessageItemTitle.setTextColor(textColor) + dashboardAdminMessageItemDescription.text = item.content + dashboardAdminMessageItemDescription.setTextColor(textColor) + dashboardAdminMessageItemIcon.setColorFilter(textColor) + + root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) }) + item.destinationUrl?.let { url -> + root.setOnClickListener { onAdminMessageClickListener(url) } + } + } + } + class AccountViewHolder(val binding: ItemDashboardAccountBinding) : RecyclerView.ViewHolder(binding.root) @@ -736,6 +773,9 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter, private val oldList: List diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt index 096c1b77a..775b7b557 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt @@ -10,6 +10,7 @@ import androidx.core.view.isVisible import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.databinding.FragmentDashboardBinding @@ -29,6 +30,7 @@ import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragm import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import io.github.wulkanowy.utils.capitalise import io.github.wulkanowy.utils.getThemeAttrColor +import io.github.wulkanowy.utils.openInternetBrowser import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDate import javax.inject.Inject @@ -97,6 +99,13 @@ class DashboardFragment : BaseFragment(R.layout.fragme onConferencesTileClickListener = { mainActivity.pushView(ConferenceFragment.newInstance()) } + onAdminMessageClickListener = presenter::onAdminMessageSelected + + registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + binding.dashboardRecycler.scrollToPosition(0) + } + }) } with(binding) { @@ -188,6 +197,10 @@ class DashboardFragment : BaseFragment(R.layout.fragme (requireActivity() as MainActivity).pushView(NotificationsCenterFragment.newInstance()) } + override fun openInternetBrowser(url: String) { + requireContext().openInternetBrowser(url) + } + override fun onDestroyView() { dashboardAdapter.clearTimers() presenter.onDetachView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt index cf99f0c9a..92665857b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.ui.modules.dashboard +import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.db.entities.Grade @@ -16,6 +17,15 @@ sealed class DashboardItem(val type: Type) { abstract val isDataLoaded: Boolean + data class AdminMessages( + val adminMessage: AdminMessage? = null, + override val error: Throwable? = null, + override val isLoading: Boolean = false + ) : DashboardItem(Type.ADMIN_MESSAGE) { + + override val isDataLoaded get() = adminMessage != null + } + data class Account( val student: Student? = null, override val error: Throwable? = null, @@ -96,6 +106,7 @@ sealed class DashboardItem(val type: Type) { } enum class Type { + ADMIN_MESSAGE, ACCOUNT, HORIZONTAL_GROUP, LESSONS, @@ -108,6 +119,7 @@ sealed class DashboardItem(val type: Type) { } enum class Tile { + ADMIN_MESSAGE, ACCOUNT, LUCKY_NUMBER, MESSAGES, @@ -123,6 +135,7 @@ sealed class DashboardItem(val type: Type) { } fun DashboardItem.Tile.toDashboardItemType() = when (this) { + DashboardItem.Tile.ADMIN_MESSAGE -> DashboardItem.Type.ADMIN_MESSAGE DashboardItem.Tile.ACCOUNT -> DashboardItem.Type.ACCOUNT DashboardItem.Tile.LUCKY_NUMBER -> DashboardItem.Type.HORIZONTAL_GROUP DashboardItem.Tile.MESSAGES -> DashboardItem.Type.HORIZONTAL_GROUP diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt index cf4097a4a..b9625570f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt @@ -21,7 +21,7 @@ class DashboardItemMoveCallback( recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder ): Int { - val dragFlags = if (viewHolder.bindingAdapterPosition != 0) { + val dragFlags = if (!viewHolder.isAdminMessageOrAccountItem) { ItemTouchHelper.UP or ItemTouchHelper.DOWN } else 0 @@ -32,7 +32,7 @@ class DashboardItemMoveCallback( recyclerView: RecyclerView, current: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder - ) = target.bindingAdapterPosition != 0 + ) = !target.isAdminMessageOrAccountItem override fun onMove( recyclerView: RecyclerView, @@ -52,4 +52,7 @@ class DashboardItemMoveCallback( onUserInteractionEndListener(dashboardAdapter.items.toList()) } + + private val RecyclerView.ViewHolder.isAdminMessageOrAccountItem: Boolean + get() = this is DashboardAdapter.AdminMessageViewHolder || this is DashboardAdapter.AccountViewHolder } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt index d6d533e28..5ff7f2ce8 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt @@ -5,6 +5,7 @@ import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.enums.MessageFolder +import io.github.wulkanowy.data.repositories.AdminMessageRepository import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository import io.github.wulkanowy.data.repositories.ConferenceRepository import io.github.wulkanowy.data.repositories.ExamRepository @@ -50,7 +51,8 @@ class DashboardPresenter @Inject constructor( private val examRepository: ExamRepository, private val conferenceRepository: ConferenceRepository, private val preferencesRepository: PreferencesRepository, - private val schoolAnnouncementRepository: SchoolAnnouncementRepository + private val schoolAnnouncementRepository: SchoolAnnouncementRepository, + private val adminMessageRepository: AdminMessageRepository ) : BasePresenter(errorHandler, studentRepository) { private val dashboardItemLoadedList = mutableListOf() @@ -179,6 +181,7 @@ class DashboardPresenter @Inject constructor( loadConferences(student, forceRefresh) } DashboardItem.Type.ADS -> TODO() + DashboardItem.Type.ADMIN_MESSAGE -> loadAdminMessage(student, forceRefresh) } } } @@ -225,6 +228,10 @@ class DashboardPresenter @Inject constructor( }.toSet() } + fun onAdminMessageSelected(url: String?) { + url?.let { view?.openInternetBrowser(it) } + } + private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) { flow { val semester = semesterRepository.getCurrentSemester(student) @@ -567,6 +574,38 @@ class DashboardPresenter @Inject constructor( }.launch("dashboard_conferences") } + private fun loadAdminMessage(student: Student, forceRefresh: Boolean) { + flowWithResourceIn { adminMessageRepository.getAdminMessages(student, forceRefresh) } + .onEach { + when (it.status) { + Status.LOADING -> { + Timber.i("Loading dashboard admin message data started") + if (forceRefresh) return@onEach + updateData(DashboardItem.AdminMessages(), forceRefresh) + } + Status.SUCCESS -> { + Timber.i("Loading dashboard admin message result: Success") + updateData( + dashboardItem = DashboardItem.AdminMessages(adminMessage = it.data), + forceRefresh = forceRefresh + ) + } + Status.ERROR -> { + Timber.i("Loading dashboard admin message result: An exception occurred") + errorHandler.dispatch(it.error!!) + updateData( + dashboardItem = DashboardItem.AdminMessages( + adminMessage = it.data, + error = it.error + ), + forceRefresh = forceRefresh + ) + } + } + } + .launch("dashboard_admin_messages") + } + private fun updateData(dashboardItem: DashboardItem, forceRefresh: Boolean) { val isForceRefreshError = forceRefresh && dashboardItem.error != null val isFirstRunDataLoadedError = @@ -579,6 +618,11 @@ class DashboardPresenter @Inject constructor( sortDashboardItems() + if (dashboardItem is DashboardItem.AdminMessages && !dashboardItem.isDataLoaded) { + dashboardItemsToLoad = dashboardItemsToLoad - DashboardItem.Type.ADMIN_MESSAGE + dashboardItemLoadedList.removeAll { it.type == DashboardItem.Type.ADMIN_MESSAGE } + } + if (forceRefresh) { updateForceRefreshData(dashboardItem) } else { @@ -644,7 +688,9 @@ class DashboardPresenter @Inject constructor( itemsLoadedList: List, forceRefresh: Boolean ) { - val filteredItems = itemsLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT } + val filteredItems = itemsLoadedList.filterNot { + it.type == DashboardItem.Type.ACCOUNT || it.type == DashboardItem.Type.ADMIN_MESSAGE + } val isAccountItemError = itemsLoadedList.find { it.type == DashboardItem.Type.ACCOUNT }?.error != null val isGeneralError = @@ -676,10 +722,13 @@ class DashboardPresenter @Inject constructor( val dashboardItemsPosition = preferencesRepository.dashboardItemsPosition dashboardItemLoadedList.sortBy { tile -> - dashboardItemsPosition?.getOrDefault( - tile.type, + val defaultPosition = if (tile is DashboardItem.AdminMessages) { + -1 + } else { tile.type.ordinal + 100 - ) ?: tile.type.ordinal + } + + dashboardItemsPosition?.getOrDefault(tile.type, defaultPosition) ?: tile.type.ordinal } } } \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt index 899eb3202..730e19a35 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt @@ -25,4 +25,6 @@ interface DashboardView : BaseView { fun popViewToRoot() fun openNotificationsCenterView() + + fun openInternetBrowser(url: String) } \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt index 2f76cd51e..ea7215cea 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt @@ -1,7 +1,8 @@ package io.github.wulkanowy.ui.modules.login -import android.content.res.Resources +import android.content.Context import android.database.sqlite.SQLiteConstraintException +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.sdk.mobile.exception.InvalidPinException import io.github.wulkanowy.sdk.mobile.exception.InvalidSymbolException @@ -11,7 +12,8 @@ import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException import io.github.wulkanowy.ui.base.ErrorHandler import javax.inject.Inject -class LoginErrorHandler @Inject constructor(resources: Resources) : ErrorHandler(resources) { +class LoginErrorHandler @Inject constructor(@ApplicationContext context: Context) : + ErrorHandler(context) { var onBadCredentials: (String?) -> Unit = {} @@ -24,6 +26,7 @@ class LoginErrorHandler @Inject constructor(resources: Resources) : ErrorHandler var onStudentDuplicate: (String) -> Unit = {} override fun proceed(error: Throwable) { + val resources = context.resources when (error) { is BadCredentialsException -> onBadCredentials(error.message) is SQLiteConstraintException -> onStudentDuplicate(resources.getString(R.string.login_duplicate_student)) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/RecoverErrorHandler.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/RecoverErrorHandler.kt index 8619369dd..ac4c03130 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/RecoverErrorHandler.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/RecoverErrorHandler.kt @@ -1,13 +1,15 @@ package io.github.wulkanowy.ui.modules.login.recover -import android.content.res.Resources +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.sdk.scrapper.exception.InvalidCaptchaException import io.github.wulkanowy.sdk.scrapper.exception.InvalidEmailException import io.github.wulkanowy.sdk.scrapper.exception.NoAccountFoundException import io.github.wulkanowy.ui.base.ErrorHandler import javax.inject.Inject -class RecoverErrorHandler @Inject constructor(resources: Resources) : ErrorHandler(resources) { +class RecoverErrorHandler @Inject constructor(@ApplicationContext context: Context) : + ErrorHandler(context) { var onInvalidUsername: (String) -> Unit = {} @@ -15,7 +17,8 @@ class RecoverErrorHandler @Inject constructor(resources: Resources) : ErrorHandl override fun proceed(error: Throwable) { when (error) { - is InvalidEmailException, is NoAccountFoundException -> onInvalidUsername(error.localizedMessage.orEmpty()) + is InvalidEmailException, + is NoAccountFoundException -> onInvalidUsername(error.localizedMessage.orEmpty()) is InvalidCaptchaException -> onInvalidCaptcha(error.localizedMessage.orEmpty(), error) else -> super.proceed(error) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsErrorHandler.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsErrorHandler.kt index 00ba0bad8..36e38fb96 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsErrorHandler.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsErrorHandler.kt @@ -1,11 +1,13 @@ package io.github.wulkanowy.ui.modules.timetable.completed -import android.content.res.Resources +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException import io.github.wulkanowy.ui.base.ErrorHandler import javax.inject.Inject -class CompletedLessonsErrorHandler @Inject constructor(resources: Resources) : ErrorHandler(resources) { +class CompletedLessonsErrorHandler @Inject constructor(@ApplicationContext context: Context) : + ErrorHandler(context) { var onFeatureDisabled: () -> Unit = {} diff --git a/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt b/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt index a3961aed8..962e5b208 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt @@ -1,35 +1,31 @@ package io.github.wulkanowy.utils import android.content.res.Resources -import android.os.Build.MANUFACTURER -import android.os.Build.MODEL -import android.os.Build.VERSION.SDK_INT -import io.github.wulkanowy.BuildConfig.BUILD_TIMESTAMP -import io.github.wulkanowy.BuildConfig.DEBUG -import io.github.wulkanowy.BuildConfig.FLAVOR -import io.github.wulkanowy.BuildConfig.VERSION_CODE -import io.github.wulkanowy.BuildConfig.VERSION_NAME +import android.os.Build +import io.github.wulkanowy.BuildConfig import javax.inject.Inject import javax.inject.Singleton @Singleton open class AppInfo @Inject constructor() { - open val isDebug get() = DEBUG + open val isDebug get() = BuildConfig.DEBUG - open val versionCode get() = VERSION_CODE + open val versionCode get() = BuildConfig.VERSION_CODE - open val buildTimestamp get() = BUILD_TIMESTAMP + open val buildTimestamp get() = BuildConfig.BUILD_TIMESTAMP - open val buildFlavor get() = FLAVOR + open val buildFlavor get() = BuildConfig.FLAVOR - open val versionName get() = VERSION_NAME + open val versionName get() = BuildConfig.VERSION_NAME - open val systemVersion get() = SDK_INT + open val systemVersion get() = Build.VERSION.SDK_INT - open val systemManufacturer: String get() = MANUFACTURER + open val systemManufacturer: String get() = Build.MANUFACTURER - open val systemModel: String get() = MODEL + open val systemModel: String get() = Build.MODEL + + open val messagesBaseUrl = BuildConfig.MESSAGES_BASE_URL @Suppress("DEPRECATION") open val systemLanguage: String diff --git a/app/src/main/res/drawable/ic_dashboard_warning.xml b/app/src/main/res/drawable/ic_dashboard_warning.xml new file mode 100644 index 000000000..e7a5dc5a4 --- /dev/null +++ b/app/src/main/res/drawable/ic_dashboard_warning.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/item_dashboard_admin_message.xml b/app/src/main/res/layout/item_dashboard_admin_message.xml new file mode 100644 index 000000000..265cc14e0 --- /dev/null +++ b/app/src/main/res/layout/item_dashboard_admin_message.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml index 5eca4680e..881d5bd4f 100644 --- a/app/src/main/res/values-night/styles.xml +++ b/app/src/main/res/values-night/styles.xml @@ -8,6 +8,7 @@ @color/colorErrorLight @color/colorDividerInverse @color/colorSwipeRefreshDark + @color/dashboard_message_medium_light ?colorSurface ?android:textColorPrimary @color/colorNavigationBarLight diff --git a/app/src/main/res/values/api_hosts.xml b/app/src/main/res/values/api_hosts.xml index dac94c3ff..158490471 100644 --- a/app/src/main/res/values/api_hosts.xml +++ b/app/src/main/res/values/api_hosts.xml @@ -40,7 +40,7 @@ https://vulcan.net.pl/?login https://vulcan.net.pl/?login https://vulcan.net.pl/?login - http://fakelog.tk/?email + http://fakelog.cf/?email Default diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 49ef39ab3..d4ed6e971 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -3,4 +3,5 @@ + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index a68e2710f..f3112b101 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -12,6 +12,9 @@ #1C1C1C #0D0D0D + #FFD980 + #ffd54f + #ffd54f #ff8f00 diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 628aa2974..45382e6d2 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -11,6 +11,7 @@ @color/colorError @color/colorDivider @color/colorSwipeRefresh + @color/dashboard_message_medium_dark @android:color/darker_gray ?android:textColorPrimary @style/PreferenceThemeOverlay From ac86737050fbeb95d593b8747279cc593eb025ad Mon Sep 17 00:00:00 2001 From: Michael <5672750+mibac138@users.noreply.github.com> Date: Thu, 14 Oct 2021 01:44:34 +0200 Subject: [PATCH 239/869] Fix NPE in SyncPresenter (#1582) --- .../java/io/github/wulkanowy/services/sync/SyncManager.kt | 2 +- .../wulkanowy/ui/modules/settings/sync/SyncPresenter.kt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt b/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt index 02d8b964f..32ca20afc 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt @@ -74,7 +74,7 @@ class SyncManager @Inject constructor( } } - fun startOneTimeSyncWorker(): Flow { + fun startOneTimeSyncWorker(): Flow { val work = OneTimeWorkRequestBuilder() .setInputData( Data.Builder() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncPresenter.kt index 63e86a475..0d404a138 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncPresenter.kt @@ -46,7 +46,7 @@ class SyncPresenter @Inject constructor( fun onSyncNowClicked() { view?.run { syncManager.startOneTimeSyncWorker().onEach { workInfo -> - when (workInfo.state) { + when (workInfo?.state) { WorkInfo.State.ENQUEUED -> { setSyncInProgress(true) Timber.i("Setting sync now started") @@ -63,9 +63,9 @@ class SyncPresenter @Inject constructor( ) analytics.logEvent("sync_now", "status" to "failed") } - else -> Timber.d("Sync now state: ${workInfo.state}") + else -> Timber.d("Sync now state: ${workInfo?.state}") } - if (workInfo.state.isFinished) { + if (workInfo?.state?.isFinished == true) { setSyncInProgress(false) setSyncDateInView() } From e7550f7a43a74b02df366501481eb679be7a9979 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Oct 2021 00:10:22 +0000 Subject: [PATCH 240/869] Bump gradle from 7.0.2 to 7.0.3 (#1586) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 153088a6d..ad27aba07 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ buildscript { dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" - classpath 'com.android.tools.build:gradle:7.0.2' + classpath 'com.android.tools.build:gradle:7.0.3' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.10' classpath 'com.huawei.agconnect:agcp:1.6.1.200' From 4c8d9c8f7fa2fb347d467ccc08a0f864ed5b07c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 14 Oct 2021 16:25:09 +0200 Subject: [PATCH 241/869] Fix infinite refresh when admin messages list is empty (#1587) --- .../wulkanowy/ui/modules/dashboard/DashboardPresenter.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt index 5ff7f2ce8..f791fa0d0 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt @@ -620,6 +620,8 @@ class DashboardPresenter @Inject constructor( if (dashboardItem is DashboardItem.AdminMessages && !dashboardItem.isDataLoaded) { dashboardItemsToLoad = dashboardItemsToLoad - DashboardItem.Type.ADMIN_MESSAGE + dashboardTileLoadedList = dashboardTileLoadedList - DashboardItem.Tile.ADMIN_MESSAGE + dashboardItemLoadedList.removeAll { it.type == DashboardItem.Type.ADMIN_MESSAGE } } @@ -654,9 +656,12 @@ class DashboardPresenter @Inject constructor( } private fun updateForceRefreshData(dashboardItem: DashboardItem) { + val isNotLoadedAdminMessage = + dashboardItem is DashboardItem.AdminMessages && !dashboardItem.isDataLoaded + with(dashboardItemRefreshLoadedList) { removeAll { it.type == dashboardItem.type } - add(dashboardItem) + if (!isNotLoadedAdminMessage) add(dashboardItem) } val isRefreshItemLoaded = From 54e9ea6478226f40363c279dddc993406aae7220 Mon Sep 17 00:00:00 2001 From: Michael <5672750+mibac138@users.noreply.github.com> Date: Sat, 16 Oct 2021 11:17:00 +0200 Subject: [PATCH 242/869] Use FloatingActionButton.{show,hide} instead of using setVisibility (#1591) --- .../wulkanowy/ui/modules/attendance/AttendanceFragment.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt index 3fbdaec5a..092c9d0d8 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt @@ -218,7 +218,13 @@ class AttendanceFragment : BaseFragment(R.layout.frag } override fun showExcuseButton(show: Boolean) { - binding.attendanceExcuseButton.visibility = if (show) VISIBLE else GONE + with(binding.attendanceExcuseButton) { + if (show) { + show() + } else { + hide() + } + } } override fun showAttendanceDialog(lesson: Attendance) { From 84e4167dbdaeb7a419693f1ba0978601b247a0bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Mon, 18 Oct 2021 00:11:46 +0200 Subject: [PATCH 243/869] Mi Band notification improvements (#1579) --- .../notifications/AppNotificationManager.kt | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt index 69d0092c1..ddad9bf27 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Context import android.os.Build +import androidx.annotation.PluralsRes import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import dagger.hilt.android.qualifiers.ApplicationContext @@ -68,7 +69,8 @@ class AppNotificationManager @Inject constructor( ) { val groupType = notificationData.type.group ?: return val group = "${groupType}_${student.id}" - val groupId = student.id * 100 + notificationData.type.ordinal + + notificationData.sendSummaryNotification(group, student) notificationData.lines.forEach { item -> val title = context.resources.getQuantityString(notificationData.titleStringRes, 1) @@ -88,16 +90,26 @@ class AppNotificationManager @Inject constructor( saveNotification(title, item, notificationData, student) } + } + private fun MultipleNotificationsData.sendSummaryNotification(group: String, student: Student) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return - val summaryNotification = getDefaultNotificationBuilder(notificationData) - .setSmallIcon(notificationData.icon) + val summaryNotification = getDefaultNotificationBuilder(this) + .setSmallIcon(icon) + .setContentTitle(getQuantityString(titleStringRes, lines.size)) + .setContentText(getQuantityString(contentStringRes, lines.size)) + .setStyle( + NotificationCompat.InboxStyle() + .setSummaryText(student.nickOrName) + .also { builder -> lines.forEach { builder.addLine(it) } } + ) + .setLocalOnly(true) .setGroup(group) - .setStyle(NotificationCompat.InboxStyle().setSummaryText(student.nickOrName)) .setGroupSummary(true) .build() + val groupId = student.id * 100 + type.ordinal notificationManager.notify(groupId.toInt(), summaryNotification) } @@ -116,6 +128,7 @@ class AppNotificationManager @Inject constructor( .setDefaults(NotificationCompat.DEFAULT_ALL) .setPriority(NotificationCompat.PRIORITY_HIGH) .setColor(context.getCompatColor(R.color.colorPrimary)) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) .setContentIntent( PendingIntent.getActivity( context, @@ -142,4 +155,8 @@ class AppNotificationManager @Inject constructor( notificationRepository.saveNotification(notificationEntity) } -} \ No newline at end of file + + private fun getQuantityString(@PluralsRes res: Int, arg: Int): String { + return context.resources.getQuantityString(res, arg, arg) + } +} From 58ea2c530e55991843e47ecbb9824e46678921a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Oct 2021 06:36:46 +0000 Subject: [PATCH 244/869] Bump hianalytics from 6.3.0.300 to 6.3.0.301 (#1593) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 227423e4d..90b4cdb94 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -234,7 +234,7 @@ dependencies { playImplementation 'com.google.android.play:core:1.10.2' playImplementation 'com.google.android.play:core-ktx:1.8.1' - hmsImplementation 'com.huawei.hms:hianalytics:6.3.0.300' + hmsImplementation 'com.huawei.hms:hianalytics:6.3.0.301' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.1.200' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 09a134d442c26d3e2a3e242ef5765b348b8a2c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 21 Oct 2021 10:47:37 +0200 Subject: [PATCH 245/869] Fix last sync date to save only successful sync (#1595) --- .../data/repositories/AppCreatorRepository.kt | 2 +- .../data/repositories/LoggerRepository.kt | 23 +++++++++--------- .../data/repositories/SemesterRepository.kt | 4 ++-- .../data/repositories/StudentRepository.kt | 8 +++---- .../TimetableNotificationSchedulerHelper.kt | 4 ++-- .../wulkanowy/services/sync/SyncWorker.kt | 24 +++++++++++-------- .../modules/about/license/LicensePresenter.kt | 2 +- .../wulkanowy/utils/DispatchersProvider.kt | 4 +--- .../wulkanowy/TestDispatchersProvider.kt | 4 +--- 9 files changed, 37 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt index 3d83f7181..cbaa12bd3 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt @@ -20,7 +20,7 @@ class AppCreatorRepository @Inject constructor( @OptIn(ExperimentalSerializationApi::class) @Suppress("BlockingMethodInNonBlockingContext") - suspend fun getAppCreators() = withContext(dispatchers.backgroundThread) { + suspend fun getAppCreators() = withContext(dispatchers.io) { val inputStream = context.assets.open("contributors.json").buffered() json.decodeFromStream>(inputStream) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/LoggerRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/LoggerRepository.kt index 6d509b026..1a8cd6ea3 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/LoggerRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/LoggerRepository.kt @@ -15,24 +15,23 @@ class LoggerRepository @Inject constructor( suspend fun getLastLogLines() = getLastModified().readText().split("\n") - suspend fun getLogFiles() = withContext(dispatchers.backgroundThread) { - File(context.filesDir.absolutePath).listFiles(File::isFile)?.filter { - it.name.endsWith(".log") - }!! + suspend fun getLogFiles() = withContext(dispatchers.io) { + File(context.filesDir.absolutePath).listFiles(File::isFile) + ?.filter { it.name.endsWith(".log") }!! } - private suspend fun getLastModified(): File { - return withContext(dispatchers.backgroundThread) { - var lastModifiedTime = Long.MIN_VALUE - var chosenFile: File? = null - File(context.filesDir.absolutePath).listFiles(File::isFile)?.forEach { file -> + private suspend fun getLastModified() = withContext(dispatchers.io) { + var lastModifiedTime = Long.MIN_VALUE + var chosenFile: File? = null + + File(context.filesDir.absolutePath).listFiles(File::isFile) + ?.forEach { file -> if (file.lastModified() > lastModifiedTime) { lastModifiedTime = file.lastModified() chosenFile = file } } - if (chosenFile == null) throw FileNotFoundException("Log file not found") - chosenFile!! - } + + chosenFile ?: throw FileNotFoundException("Log file not found") } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt index 4336877a5..cc954558f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt @@ -26,7 +26,7 @@ class SemesterRepository @Inject constructor( student: Student, forceRefresh: Boolean = false, refreshOnNoCurrent: Boolean = false - ) = withContext(dispatchers.backgroundThread) { + ) = withContext(dispatchers.io) { val semesters = semesterDb.loadAll(student.studentId, student.classId) if (isShouldFetch(student, semesters, forceRefresh, refreshOnNoCurrent)) { @@ -64,7 +64,7 @@ class SemesterRepository @Inject constructor( } suspend fun getCurrentSemester(student: Student, forceRefresh: Boolean = false) = - withContext(dispatchers.backgroundThread) { + withContext(dispatchers.io) { getSemesters(student, forceRefresh).getCurrentOrLast() } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt index 2ac892d01..9e4a1aabc 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt @@ -66,7 +66,7 @@ class StudentRepository @Inject constructor( .map { it.apply { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { - student.password = withContext(dispatchers.backgroundThread) { + student.password = withContext(dispatchers.io) { decrypt(student.password) } } @@ -77,7 +77,7 @@ class StudentRepository @Inject constructor( val student = studentDb.loadById(id) ?: throw NoCurrentStudentException() if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { - student.password = withContext(dispatchers.backgroundThread) { + student.password = withContext(dispatchers.io) { decrypt(student.password) } } @@ -88,7 +88,7 @@ class StudentRepository @Inject constructor( val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException() if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { - student.password = withContext(dispatchers.backgroundThread) { + student.password = withContext(dispatchers.io) { decrypt(student.password) } } @@ -101,7 +101,7 @@ class StudentRepository @Inject constructor( .map { it.apply { if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.API) { - password = withContext(dispatchers.backgroundThread) { + password = withContext(dispatchers.io) { encrypt(password, context) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt index a42a0ab4b..2ec4e527b 100644 --- a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt +++ b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt @@ -54,7 +54,7 @@ class TimetableNotificationSchedulerHelper @Inject constructor( suspend fun cancelScheduled(lessons: List, student: Student) { val studentId = student.studentId - withContext(dispatchersProvider.backgroundThread) { + withContext(dispatchersProvider.io) { lessons.sortedBy { it.start }.forEachIndexed { index, lesson -> val upcomingTime = getUpcomingLessonTime(index, lessons, lesson) cancelScheduledTo( @@ -91,7 +91,7 @@ class TimetableNotificationSchedulerHelper @Inject constructor( return } - withContext(dispatchersProvider.backgroundThread) { + withContext(dispatchersProvider.io) { lessons.groupBy { it.date } .map { it.value.sortedBy { lesson -> lesson.start } } .map { it.filter { lesson -> lesson.isStudentPlan } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/SyncWorker.kt b/app/src/main/java/io/github/wulkanowy/services/sync/SyncWorker.kt index ea1f79cbf..52979e635 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/SyncWorker.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/SyncWorker.kt @@ -19,11 +19,11 @@ import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException import io.github.wulkanowy.services.sync.channels.DebugChannel import io.github.wulkanowy.services.sync.works.Work +import io.github.wulkanowy.utils.DispatchersProvider import io.github.wulkanowy.utils.getCompatColor -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext import timber.log.Timber import java.time.LocalDateTime -import java.time.ZoneId import kotlin.random.Random @HiltWorker @@ -34,13 +34,14 @@ class SyncWorker @AssistedInject constructor( private val semesterRepository: SemesterRepository, private val works: Set<@JvmSuppressWildcards Work>, private val preferencesRepository: PreferencesRepository, - private val notificationManager: NotificationManagerCompat + private val notificationManager: NotificationManagerCompat, + private val dispatchersProvider: DispatchersProvider ) : CoroutineWorker(appContext, workerParameters) { - override suspend fun doWork() = coroutineScope { + override suspend fun doWork() = withContext(dispatchersProvider.io) { Timber.i("SyncWorker is starting") - if (!studentRepository.isCurrentStudentSet()) return@coroutineScope Result.failure() + if (!studentRepository.isCurrentStudentSet()) return@withContext Result.failure() val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student, true) @@ -50,12 +51,12 @@ class SyncWorker @AssistedInject constructor( Timber.i("${work::class.java.simpleName} is starting") work.doWork(student, semester) Timber.i("${work::class.java.simpleName} result: Success") - preferencesRepository.lasSyncDate = LocalDateTime.now(ZoneId.systemDefault()) null } catch (e: Throwable) { Timber.w("${work::class.java.simpleName} result: An exception ${e.message} occurred") - if (e is FeatureDisabledException || e is FeatureNotAvailableException) null - else { + if (e is FeatureDisabledException || e is FeatureNotAvailableException) { + null + } else { Timber.e(e) e } @@ -70,13 +71,16 @@ class SyncWorker @AssistedInject constructor( ) } exceptions.isNotEmpty() -> Result.retry() - else -> Result.success() + else -> { + preferencesRepository.lasSyncDate = LocalDateTime.now() + Result.success() + } } if (preferencesRepository.isDebugNotificationEnable) notify(result) Timber.i("SyncWorker result: $result") - result + return@withContext result } private fun notify(result: Result) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicensePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicensePresenter.kt index cc430fc2c..5368cc19d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicensePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicensePresenter.kt @@ -31,7 +31,7 @@ class LicensePresenter @Inject constructor( private fun loadData() { flowWithResource { - withContext(dispatchers.backgroundThread) { + withContext(dispatchers.io) { view?.appLibraries.orEmpty() } }.onEach { diff --git a/app/src/main/java/io/github/wulkanowy/utils/DispatchersProvider.kt b/app/src/main/java/io/github/wulkanowy/utils/DispatchersProvider.kt index ecc8e05eb..8aaa57f4f 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/DispatchersProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/DispatchersProvider.kt @@ -1,10 +1,8 @@ package io.github.wulkanowy.utils -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers open class DispatchersProvider { - open val backgroundThread: CoroutineDispatcher - get() = Dispatchers.IO + open val io get() = Dispatchers.IO } diff --git a/app/src/test/java/io/github/wulkanowy/TestDispatchersProvider.kt b/app/src/test/java/io/github/wulkanowy/TestDispatchersProvider.kt index e60b1d7a2..d4cba7b5e 100644 --- a/app/src/test/java/io/github/wulkanowy/TestDispatchersProvider.kt +++ b/app/src/test/java/io/github/wulkanowy/TestDispatchersProvider.kt @@ -1,11 +1,9 @@ package io.github.wulkanowy import io.github.wulkanowy.utils.DispatchersProvider -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers class TestDispatchersProvider : DispatchersProvider() { - override val backgroundThread: CoroutineDispatcher - get() = Dispatchers.Unconfined + override val io get() = Dispatchers.Unconfined } From 94fd303f8ea6edcdd0f8e528cf623615c8aa3619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 21 Oct 2021 10:51:00 +0200 Subject: [PATCH 246/869] Add single support advert (#1484) --- app/build.gradle | 20 ++-- app/src/main/AndroidManifest.xml | 13 ++- .../ui/modules/about/AboutPresenter.kt | 26 ++--- .../ui/modules/dashboard/DashboardAdapter.kt | 3 +- .../settings/advanced/AdvancedFragment.kt | 4 - .../settings/appearance/AppearanceFragment.kt | 4 - .../notifications/NotificationsFragment.kt | 4 - .../ui/modules/settings/sync/SyncFragment.kt | 4 - app/src/main/res/drawable/ic_settings_ads.xml | 22 +++++ app/src/main/res/values/preferences_keys.xml | 1 + app/src/main/res/values/strings.xml | 10 ++ app/src/main/res/xml/scheme_preferences.xml | 30 +++--- .../ui/modules/settings/ads/AdsFragment.kt | 94 +++++++++++++++++++ .../ui/modules/settings/ads/AdsPresenter.kt | 41 ++++++++ .../ui/modules/settings/ads/AdsView.kt | 17 ++++ .../io/github/wulkanowy/utils/AdsHelper.kt | 39 ++++++++ app/src/play/res/xml/scheme_preferences.xml | 39 ++++++++ .../play/res/xml/scheme_preferences_ads.xml | 12 +++ 18 files changed, 324 insertions(+), 59 deletions(-) create mode 100644 app/src/main/res/drawable/ic_settings_ads.xml create mode 100644 app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt create mode 100644 app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsPresenter.kt create mode 100644 app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsView.kt create mode 100644 app/src/play/java/io/github/wulkanowy/utils/AdsHelper.kt create mode 100644 app/src/play/res/xml/scheme_preferences.xml create mode 100644 app/src/play/res/xml/scheme_preferences_ads.xml diff --git a/app/build.gradle b/app/build.gradle index 90b4cdb94..421774062 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,7 +29,10 @@ android { resValue "string", "app_name", "Wulkanowy" - manifestPlaceholders = [firebase_enabled: project.hasProperty("enableFirebase")] + manifestPlaceholders = [ + firebase_enabled: project.hasProperty("enableFirebase"), + admob_project_id: "" + ] javaCompileOptions { annotationProcessorOptions { arguments += [ @@ -39,6 +42,8 @@ android { } } + buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null" + if (System.env.SET_BUILD_TIMESTAMP) { buildConfigField "long", "BUILD_TIMESTAMP", String.valueOf(System.currentTimeMillis()) } else { @@ -82,23 +87,21 @@ android { productFlavors { hms { dimension "platform" - manifestPlaceholders = [ - install_channel: "AppGallery" - ] + manifestPlaceholders = [install_channel: "AppGallery"] } play { dimension "platform" manifestPlaceholders = [ - install_channel: "Google Play" + install_channel : "Google Play", + admob_project_id: System.getenv("ADMOB_PROJECT_ID") ?: "ca-app-pub-3940256099942544~3347511713" ] + buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "\"${System.getenv("SINGLE_SUPPORT_AD_ID") ?: "ca-app-pub-3940256099942544/5354046379"}\"" } fdroid { dimension "platform" - manifestPlaceholders = [ - install_channel: "F-Droid" - ] + manifestPlaceholders = [install_channel: "F-Droid"] } } @@ -233,6 +236,7 @@ dependencies { playImplementation 'com.google.firebase:firebase-crashlytics:' playImplementation 'com.google.android.play:core:1.10.2' playImplementation 'com.google.android.play:core-ktx:1.8.1' + playImplementation 'com.google.android.gms:play-services-ads:20.4.0' hmsImplementation 'com.huawei.hms:hianalytics:6.3.0.301' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.1.200' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8c7eb8527..810d469f4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -165,33 +165,32 @@ - - - - - - - + + diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutPresenter.kt index 6bcf5f77b..552749349 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutPresenter.kt @@ -82,18 +82,20 @@ class AboutPresenter @Inject constructor( private fun loadData() { view?.run { - updateData(listOfNotNull( - versionRes, - creatorsRes, - feedbackRes, - faqRes, - discordRes, - facebookRes, - twitterRes, - homepageRes, - licensesRes, - privacyRes - )) + updateData( + listOfNotNull( + versionRes, + creatorsRes, + feedbackRes, + faqRes, + discordRes, + facebookRes, + twitterRes, + homepageRes, + licensesRes, + privacyRes + ) + ) } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt index 405bfbc5a..440bbd5d4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt @@ -299,7 +299,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter + + + + + + diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index c512a5f21..1aba7d856 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -34,4 +34,5 @@ message_send_recipients last_sync_date notifications_piggyback + single_ad_support diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 48c7712dc..13030366a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -658,10 +658,19 @@ Reply with message history Show arithmetic average when no weights provided + Support + Watch single ad to support project + Consent to data processing + To view an advertisement you must agree to the data processing terms of our Privacy Policy + Agree + Privacy policy + Ad is loading + Advanced Appearance & Behavior Notifications Synchronization + Advertisements Grades Dashboard @@ -681,6 +690,7 @@ Plus and minus values, average calculation Advanced App version, contributors, social portals, licenses + Displaying advertisements, project support diff --git a/app/src/main/res/xml/scheme_preferences.xml b/app/src/main/res/xml/scheme_preferences.xml index 086214923..5bf7ad8aa 100644 --- a/app/src/main/res/xml/scheme_preferences.xml +++ b/app/src/main/res/xml/scheme_preferences.xml @@ -1,33 +1,33 @@ + app:title="@string/pref_appearance_category" /> + app:title="@string/pref_notifications_category" /> + app:title="@string/pref_sync_category" /> + app:title="@string/pref_advanced_category" /> + app:title="@string/about_title" /> diff --git a/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt new file mode 100644 index 000000000..960a54b8c --- /dev/null +++ b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt @@ -0,0 +1,94 @@ +package io.github.wulkanowy.ui.modules.settings.ads + +import android.os.Bundle +import android.view.View +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAd +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +import io.github.wulkanowy.ui.base.BaseActivity +import io.github.wulkanowy.ui.base.ErrorDialog +import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.utils.openInternetBrowser +import javax.inject.Inject + +@AndroidEntryPoint +class AdsFragment : PreferenceFragmentCompat(), MainView.TitledView, AdsView { + + @Inject + lateinit var presenter: AdsPresenter + + override val titleStringId = R.string.pref_settings_ads_title + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.scheme_preferences_ads, rootKey) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + presenter.onAttachView(this) + } + + override fun initView() { + findPreference(getString(R.string.pref_key_ads_single_support))?.setOnPreferenceClickListener { + presenter.onWatchSingleAdSelected() + true + } + } + + override fun showAd(ad: RewardedInterstitialAd) { + if (isVisible) { + ad.show(requireActivity()) {} + } + } + + override fun showPrivacyPolicyDialog() { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.pref_ads_privacy_title)) + .setMessage(getString(R.string.pref_ads_privacy_description)) + .setPositiveButton(getString(R.string.pref_ads_privacy_agree)) { _, _ -> presenter.onAgreedPrivacy() } + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .setNeutralButton(getString(R.string.pref_ads_privacy_link)) { _, _ -> presenter.onPrivacySelected() } + .show() + } + + override fun openPrivacyPolicy() { + requireContext().openInternetBrowser( + "https://wulkanowy.github.io/polityka-prywatnosci.html", + ::showMessage + ) + } + + override fun showLoadingSupportAd(show: Boolean) { + findPreference(getString(R.string.pref_key_ads_single_support))?.run { + isEnabled = !show + summary = if (show) getString(R.string.pref_ads_loading) else null + } + } + + override fun showError(text: String, error: Throwable) { + (activity as? BaseActivity<*, *>)?.showError(text, error) + } + + override fun showMessage(text: String) { + (activity as? BaseActivity<*, *>)?.showMessage(text) + } + + override fun showExpiredDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredDialog() + } + + override fun showChangePasswordSnackbar(redirectUrl: String) { + (activity as? BaseActivity<*, *>)?.showChangePasswordSnackbar(redirectUrl) + } + + override fun openClearLoginView() { + (activity as? BaseActivity<*, *>)?.openClearLoginView() + } + + override fun showErrorDetailsDialog(error: Throwable) { + ErrorDialog.newInstance(error).show(childFragmentManager, error.toString()) + } +} \ No newline at end of file diff --git a/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsPresenter.kt b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsPresenter.kt new file mode 100644 index 000000000..fd5cc9b6f --- /dev/null +++ b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsPresenter.kt @@ -0,0 +1,41 @@ +package io.github.wulkanowy.ui.modules.settings.ads + +import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.ui.base.ErrorHandler +import io.github.wulkanowy.utils.AdsHelper +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +class AdsPresenter @Inject constructor( + errorHandler: ErrorHandler, + studentRepository: StudentRepository, + private val adsHelper: AdsHelper +) : BasePresenter(errorHandler, studentRepository) { + + override fun onAttachView(view: AdsView) { + super.onAttachView(view) + view.initView() + Timber.i("Settings ads view was initialized") + } + + fun onWatchSingleAdSelected() { + view?.showPrivacyPolicyDialog() + } + + fun onPrivacySelected() { + view?.openPrivacyPolicy() + } + + fun onAgreedPrivacy() { + view?.showLoadingSupportAd(true) + presenterScope.launch { + runCatching { adsHelper.getSupportAd() } + .onFailure(errorHandler::dispatch) + .onSuccess { it?.let { view?.showAd(it) } } + + view?.showLoadingSupportAd(false) + } + } +} \ No newline at end of file diff --git a/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsView.kt b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsView.kt new file mode 100644 index 000000000..25eeaaeca --- /dev/null +++ b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsView.kt @@ -0,0 +1,17 @@ +package io.github.wulkanowy.ui.modules.settings.ads + +import com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAd +import io.github.wulkanowy.ui.base.BaseView + +interface AdsView : BaseView { + + fun initView() + + fun showAd(ad: RewardedInterstitialAd) + + fun showPrivacyPolicyDialog() + + fun openPrivacyPolicy() + + fun showLoadingSupportAd(show: Boolean) +} \ No newline at end of file diff --git a/app/src/play/java/io/github/wulkanowy/utils/AdsHelper.kt b/app/src/play/java/io/github/wulkanowy/utils/AdsHelper.kt new file mode 100644 index 000000000..f363c13f9 --- /dev/null +++ b/app/src/play/java/io/github/wulkanowy/utils/AdsHelper.kt @@ -0,0 +1,39 @@ +package io.github.wulkanowy.utils + +import android.content.Context +import com.google.android.gms.ads.AdRequest +import com.google.android.gms.ads.LoadAdError +import com.google.android.gms.ads.MobileAds +import com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAd +import com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAdLoadCallback +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.BuildConfig +import javax.inject.Inject +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +class AdsHelper @Inject constructor(@ApplicationContext private val context: Context) { + + suspend fun getSupportAd(): RewardedInterstitialAd? { + MobileAds.initialize(context) + + val adRequest = AdRequest.Builder().build() + + return suspendCoroutine { + RewardedInterstitialAd.load( + context, + BuildConfig.SINGLE_SUPPORT_AD_ID, + adRequest, + object : RewardedInterstitialAdLoadCallback() { + override fun onAdLoaded(rewardedInterstitialAd: RewardedInterstitialAd) { + it.resume(rewardedInterstitialAd) + } + + override fun onAdFailedToLoad(loadAdError: LoadAdError) { + it.resumeWithException(IllegalArgumentException(loadAdError.message)) + } + }) + } + } +} \ No newline at end of file diff --git a/app/src/play/res/xml/scheme_preferences.xml b/app/src/play/res/xml/scheme_preferences.xml new file mode 100644 index 000000000..05b0bf644 --- /dev/null +++ b/app/src/play/res/xml/scheme_preferences.xml @@ -0,0 +1,39 @@ + + + + + + + + + diff --git a/app/src/play/res/xml/scheme_preferences_ads.xml b/app/src/play/res/xml/scheme_preferences_ads.xml new file mode 100644 index 000000000..6b3625cae --- /dev/null +++ b/app/src/play/res/xml/scheme_preferences_ads.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file From 0f800b61f63f8b7ae303316354ae371eafc9799d Mon Sep 17 00:00:00 2001 From: Michael <5672750+mibac138@users.noreply.github.com> Date: Sun, 24 Oct 2021 01:23:36 +0200 Subject: [PATCH 247/869] Allow expanding multiple subject' grades at once (#1584) --- .../github/wulkanowy/data/db/AppDatabase.kt | 2 +- .../data/db/migrations/Migration41.kt | 12 +- .../repositories/PreferencesRepository.kt | 15 +- .../ui/modules/grade/GradeExpandMode.kt | 9 ++ .../grade/details/GradeDetailsAdapter.kt | 146 +++++++++++++----- .../grade/details/GradeDetailsFragment.kt | 5 +- .../grade/details/GradeDetailsPresenter.kt | 33 ++-- .../modules/grade/details/GradeDetailsView.kt | 3 +- .../main/res/values/preferences_defaults.xml | 2 +- app/src/main/res/values/preferences_keys.xml | 3 +- .../main/res/values/preferences_values.xml | 11 ++ .../res/xml/scheme_preferences_appearance.xml | 11 +- 12 files changed, 187 insertions(+), 65 deletions(-) create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeExpandMode.kt diff --git a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt index b8831acd8..d96856068 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt @@ -193,7 +193,7 @@ abstract class AppDatabase : RoomDatabase() { Migration38(), Migration39(), Migration40(), - Migration41(), + Migration41(sharedPrefProvider), Migration42() ) diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt index 17773efdb..0080e057c 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt @@ -2,10 +2,20 @@ package io.github.wulkanowy.data.db.migrations import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import io.github.wulkanowy.data.db.SharedPrefProvider +import io.github.wulkanowy.ui.modules.grade.GradeExpandMode -class Migration41 : Migration(40, 41) { +class Migration41(private val sharedPrefProvider: SharedPrefProvider) : Migration(40, 41) { override fun migrate(database: SupportSQLiteDatabase) { + migrateSharedPreferences() database.execSQL("ALTER TABLE Homework ADD COLUMN is_added_by_user INTEGER NOT NULL DEFAULT 0") } + + private fun migrateSharedPreferences() { + if (sharedPrefProvider.getBoolean("pref_key_expand_grade", false)) { + sharedPrefProvider.putString("pref_key_expand_grade_mode", GradeExpandMode.ALWAYS_EXPANDED.value) + } + sharedPrefProvider.delete("pref_key_expand_grade") + } } \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index b0991bbcb..696ffd63e 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -10,6 +10,7 @@ import io.github.wulkanowy.R import io.github.wulkanowy.sdk.toLocalDate import io.github.wulkanowy.ui.modules.dashboard.DashboardItem import io.github.wulkanowy.ui.modules.grade.GradeAverageMode +import io.github.wulkanowy.ui.modules.grade.GradeExpandMode import io.github.wulkanowy.ui.modules.grade.GradeSortingMode import io.github.wulkanowy.utils.toLocalDateTime import io.github.wulkanowy.utils.toTimestamp @@ -19,6 +20,8 @@ import kotlinx.coroutines.flow.map import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import java.lang.ClassCastException +import java.lang.IllegalStateException import java.time.LocalDate import java.time.LocalDateTime import javax.inject.Inject @@ -56,8 +59,13 @@ class PreferencesRepository @Inject constructor( R.bool.pref_default_grade_average_force_calc ) - val isGradeExpandable: Boolean - get() = !getBoolean(R.string.pref_key_expand_grade, R.bool.pref_default_expand_grade) + val gradeExpandMode: GradeExpandMode + get() = GradeExpandMode.getByValue( + getString( + R.string.pref_key_expand_grade_mode, + R.string.pref_default_expand_grade_mode + ) + ) val showAllSubjectsOnStatisticsList: Boolean get() = getBoolean( @@ -265,6 +273,9 @@ class PreferencesRepository @Inject constructor( private fun getBoolean(id: String, default: Int) = sharedPref.getBoolean(id, context.resources.getBoolean(default)) + private fun getBoolean(id: Int, default: Boolean) = + sharedPref.getBoolean(context.getString(id), default) + private companion object { private const val PREF_KEY_DASHBOARD_ITEMS_POSITION = "dashboard_items_position" diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeExpandMode.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeExpandMode.kt new file mode 100644 index 000000000..722e986ee --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeExpandMode.kt @@ -0,0 +1,9 @@ +package io.github.wulkanowy.ui.modules.grade + +enum class GradeExpandMode(val value: String) { + ONE("one"), UNLIMITED("any"), ALWAYS_EXPANDED("always"); + + companion object { + fun getByValue(value: String) = values().firstOrNull { it.value == value } ?: ONE + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt index 01631140c..d96ac0928 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt @@ -5,6 +5,7 @@ import android.content.res.Resources import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.NO_POSITION @@ -13,9 +14,11 @@ import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.databinding.HeaderGradeDetailsBinding import io.github.wulkanowy.databinding.ItemGradeDetailsBinding import io.github.wulkanowy.ui.base.BaseExpandableAdapter +import io.github.wulkanowy.ui.modules.grade.GradeExpandMode import io.github.wulkanowy.utils.getBackgroundColor import io.github.wulkanowy.utils.toFormattedString import timber.log.Timber +import java.util.BitSet import javax.inject.Inject class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter() { @@ -24,19 +27,20 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter() - private var expandedPosition = NO_POSITION + private val expandedPositions = BitSet(items.size) - private var isExpandable = false + private var expandMode = GradeExpandMode.ONE var onClickListener: (Grade, position: Int) -> Unit = { _, _ -> } var colorTheme = "" - fun setDataItems(data: List, isExpanded: Boolean = isExpandable) { + fun setDataItems(data: List, expandMode: GradeExpandMode = this.expandMode) { headers = data.filter { it.viewType == ViewType.HEADER }.toMutableList() - items = if (isExpanded) headers else data.toMutableList() - isExpandable = isExpanded - expandedPosition = NO_POSITION + items = + (if (expandMode != GradeExpandMode.ALWAYS_EXPANDED) headers else data).toMutableList() + this.expandMode = expandMode + expandedPositions.clear() } fun updateDetailsItem(position: Int, grade: Grade) { @@ -48,7 +52,7 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter 1) { - Timber.e("Header with subject $subject found ${candidates.size} times! Expanded: $expandedPosition. Items: $candidates") + Timber.e("Header with subject $subject found ${candidates.size} times! Expanded: $expandedPositions. Items: $candidates") } return candidates.first() @@ -64,9 +68,9 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter HeaderViewHolder(HeaderGradeDetailsBinding.inflate(inflater, parent, false)) - ViewType.ITEM.id -> ItemViewHolder(ItemGradeDetailsBinding.inflate(inflater, parent, false)) + ViewType.HEADER.id -> HeaderViewHolder( + HeaderGradeDetailsBinding.inflate(inflater, parent, false) + ) + ViewType.ITEM.id -> ItemViewHolder( + ItemGradeDetailsBinding.inflate(inflater, parent, false) + ) else -> throw IllegalStateException() } } @@ -106,46 +114,91 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter 0) View.VISIBLE else View.GONE - if (header.newGrades > 0) gradeHeaderNote.text = header.newGrades.toString(10) + gradeHeaderPointsSum.text = + context.getString(R.string.grade_points_sum, header.pointsSum) + gradeHeaderPointsSum.isVisible = !header.pointsSum.isNullOrEmpty() + gradeHeaderNumber.text = context.resources.getQuantityString( + R.plurals.grade_number_item, + header.grades.size, + header.grades.size + ) + gradeHeaderNote.isVisible = header.newGrades > 0 - gradeHeaderContainer.isEnabled = isExpandable + if (header.newGrades > 0) { + gradeHeaderNote.text = header.newGrades.toString() + } + + gradeHeaderContainer.isEnabled = expandMode != GradeExpandMode.ALWAYS_EXPANDED gradeHeaderContainer.setOnClickListener { - expandedPosition = if (expandedPosition == adapterPosition) -1 else adapterPosition - - if (expandedPosition != NO_POSITION) { - refreshList(headers.toMutableList().apply { - addAll(headerPosition + 1, header.grades) - }) - scrollToHeaderWithSubItems(headerPosition, header.grades.size) - } else { - refreshList(headers) - } + expandGradeHeader(headerPosition, header, holder) } } } - private fun formatAverage(average: Double?, resources: Resources): String { - return if (average == null || average == .0) resources.getString(R.string.grade_no_average) - else resources.getString(R.string.grade_average, average) + private fun expandGradeHeader( + headerPosition: Int, + header: GradeDetailsHeader, + holder: HeaderViewHolder + ) { + if (expandMode == GradeExpandMode.ONE) { + val isHeaderExpanded = expandedPositions[headerPosition] + + expandedPositions.clear() + + if (!isHeaderExpanded) { + val updatedItemList = headers.toMutableList() + .apply { addAll(headerPosition + 1, header.grades) } + + expandedPositions.set(headerPosition) + refreshList(updatedItemList) + scrollToHeaderWithSubItems(headerPosition, header.grades.size) + } else { + refreshList(headers.toMutableList()) + } + } else if (expandMode == GradeExpandMode.UNLIMITED) { + val headerAdapterPosition = holder.bindingAdapterPosition + val isHeaderExpanded = expandedPositions[headerPosition] + + expandedPositions.flip(headerPosition) + + if (!isHeaderExpanded) { + val updatedList = items.toMutableList() + .apply { addAll(headerAdapterPosition + 1, header.grades) } + + refreshList(updatedList) + scrollToHeaderWithSubItems(headerAdapterPosition, header.grades.size) + } else { + val startPosition = headerAdapterPosition + 1 + val updatedList = items.toMutableList() + .apply { + subList(startPosition, startPosition + header.grades.size).clear() + } + + refreshList(updatedList) + } + } } @SuppressLint("SetTextI18n") private fun bindItemViewHolder(holder: ItemViewHolder, grade: Grade) { + val context = holder.binding.root.context + with(holder.binding) { gradeItemValue.run { text = grade.entry @@ -154,26 +207,37 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter grade.description grade.gradeSymbol.isNotBlank() -> grade.gradeSymbol - else -> root.context.getString(R.string.all_no_description) + else -> context.getString(R.string.all_no_description) } gradeItemDate.text = grade.date.toFormattedString() - gradeItemWeight.text = "${root.context.getString(R.string.grade_weight)}: ${grade.weight}" + gradeItemWeight.text = "${context.getString(R.string.grade_weight)}: ${grade.weight}" gradeItemNote.visibility = if (!grade.isRead) View.VISIBLE else View.GONE root.setOnClickListener { - holder.bindingAdapterPosition.let { if (it != NO_POSITION) onClickListener(grade, it) } + holder.bindingAdapterPosition.let { + if (it != NO_POSITION) onClickListener(grade, it) + } } } } + private fun formatAverage(average: Double?, resources: Resources) = + if (average == null || average == .0) { + resources.getString(R.string.grade_no_average) + } else { + resources.getString(R.string.grade_average, average) + } + private class HeaderViewHolder(val binding: HeaderGradeDetailsBinding) : RecyclerView.ViewHolder(binding.root) private class ItemViewHolder(val binding: ItemGradeDetailsBinding) : RecyclerView.ViewHolder(binding.root) - class GradeDetailsDiffUtil(private val old: List, private val new: List) : - DiffUtil.Callback() { + private class GradeDetailsDiffUtil( + private val old: List, + private val new: List + ) : DiffUtil.Callback() { override fun getOldListSize() = old.size diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsFragment.kt index 9d4da767d..c93600d49 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsFragment.kt @@ -12,6 +12,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.ui.modules.grade.GradeExpandMode import io.github.wulkanowy.databinding.FragmentGradeDetailsBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment @@ -79,10 +80,10 @@ class GradeDetailsFragment : else false } - override fun updateData(data: List, isGradeExpandable: Boolean, gradeColorTheme: String) { + override fun updateData(data: List, expandMode: GradeExpandMode, gradeColorTheme: String) { with(gradeDetailsAdapter) { colorTheme = gradeColorTheme - setDataItems(data, isGradeExpandable) + setDataItems(data, expandMode) notifyDataSetChanged() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt index 7544d2aa1..9b9651e1c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt @@ -9,6 +9,7 @@ import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider +import io.github.wulkanowy.ui.modules.grade.GradeExpandMode import io.github.wulkanowy.ui.modules.grade.GradeSortingMode.ALPHABETIC import io.github.wulkanowy.ui.modules.grade.GradeSortingMode.DATE import io.github.wulkanowy.ui.modules.grade.GradeSubject @@ -113,7 +114,7 @@ class GradeDetailsPresenter @Inject constructor( fun onParentViewReselected() { view?.run { if (!isViewEmpty) { - if (preferencesRepository.isGradeExpandable) collapseAllItems() + if (preferencesRepository.gradeExpandMode != GradeExpandMode.ALWAYS_EXPANDED) collapseAllItems() scrollToStart() } } @@ -157,7 +158,7 @@ class GradeDetailsPresenter @Inject constructor( showContent(true) updateData( data = items, - isGradeExpandable = preferencesRepository.isGradeExpandable, + expandMode = preferencesRepository.gradeExpandMode, gradeColorTheme = preferencesRepository.gradeColorTheme ) notifyParentDataLoaded(semesterId) @@ -175,7 +176,7 @@ class GradeDetailsPresenter @Inject constructor( showContent(items.isNotEmpty()) updateData( data = items, - isGradeExpandable = preferencesRepository.isGradeExpandable, + expandMode = preferencesRepository.gradeExpandMode, gradeColorTheme = preferencesRepository.gradeColorTheme ) } @@ -235,14 +236,24 @@ class GradeDetailsPresenter @Inject constructor( .sortedByDescending { it.date } .map { GradeDetailsItem(it, ViewType.ITEM) } - listOf(GradeDetailsItem(GradeDetailsHeader( - subject = subject, - average = average, - pointsSum = points, - grades = subItems - ).apply { - newGrades = grades.filter { grade -> !grade.isRead }.size - }, ViewType.HEADER)) + if (preferencesRepository.isGradeExpandable) emptyList() else subItems + val gradeDetailsItems = listOf( + GradeDetailsItem( + GradeDetailsHeader( + subject = subject, + average = average, + pointsSum = points, + grades = subItems + ).apply { + newGrades = grades.filter { grade -> !grade.isRead }.size + }, ViewType.HEADER + ) + ) + + if (preferencesRepository.gradeExpandMode == GradeExpandMode.ALWAYS_EXPANDED) { + gradeDetailsItems + subItems + } else { + gradeDetailsItems + } }.flatten() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsView.kt index e71fcc3c8..556332290 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsView.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.ui.modules.grade.details import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.ui.modules.grade.GradeExpandMode import io.github.wulkanowy.ui.base.BaseView interface GradeDetailsView : BaseView { @@ -9,7 +10,7 @@ interface GradeDetailsView : BaseView { fun initView() - fun updateData(data: List, isGradeExpandable: Boolean, gradeColorTheme: String) + fun updateData(data: List, expandMode: GradeExpandMode, gradeColorTheme: String) fun updateItem(item: Grade, position: Int) diff --git a/app/src/main/res/values/preferences_defaults.xml b/app/src/main/res/values/preferences_defaults.xml index df84d37d6..7fb3d5c05 100644 --- a/app/src/main/res/values/preferences_defaults.xml +++ b/app/src/main/res/values/preferences_defaults.xml @@ -4,7 +4,7 @@ true only_one_semester false - false + one false light vulcan diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index 1aba7d856..fef062dd1 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -5,7 +5,8 @@ app_theme dashboard_tiles grade_color_scheme - expand_grade + expand_grade + expand_grade_mode grade_average_mode grade_average_always_calc grade_statistics_list diff --git a/app/src/main/res/values/preferences_values.xml b/app/src/main/res/values/preferences_values.xml index bd3e8b47d..1d777bdb6 100644 --- a/app/src/main/res/values/preferences_values.xml +++ b/app/src/main/res/values/preferences_values.xml @@ -99,6 +99,17 @@ grade_color + + Up to 1 at once + Always expanded + Unlimited expansions + + + one + always + any + + Average of grades only from selected semester Average of averages from both semesters diff --git a/app/src/main/res/xml/scheme_preferences_appearance.xml b/app/src/main/res/xml/scheme_preferences_appearance.xml index b34fd417d..b2da0287f 100644 --- a/app/src/main/res/xml/scheme_preferences_appearance.xml +++ b/app/src/main/res/xml/scheme_preferences_appearance.xml @@ -50,11 +50,14 @@ app:iconSpaceReserved="false" app:key="@string/pref_key_grade_color_scheme" app:title="@string/pref_view_grade_color_scheme" /> - + app:key="@string/pref_key_expand_grade_mode" + app:title="@string/pref_view_expand_grade" + app:useSimpleSummaryProvider="true" /> Date: Tue, 26 Oct 2021 09:51:36 +0000 Subject: [PATCH 248/869] Bump about_libraries from 8.9.3 to 8.9.4 (#1604) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ad27aba07..6a563d2e6 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { kotlin_version = '1.5.31' - about_libraries = '8.9.3' + about_libraries = '8.9.4' hilt_version = "2.39.1" } repositories { From 621db49fbf860bad00910f906226e68fe6f02f32 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Oct 2021 09:52:55 +0000 Subject: [PATCH 249/869] Bump agcp from 1.6.1.200 to 1.6.1.300 (#1606) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6a563d2e6..c345e4c7d 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ buildscript { classpath 'com.android.tools.build:gradle:7.0.3' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.10' - classpath 'com.huawei.agconnect:agcp:1.6.1.200' + classpath 'com.huawei.agconnect:agcp:1.6.1.300' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.3.0" From de11644e9b6b599c45d204dd30a777ab40bc47ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Oct 2021 09:53:16 +0000 Subject: [PATCH 250/869] Bump agconnect-crash from 1.6.1.200 to 1.6.1.300 (#1605) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 421774062..94dfc1666 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -239,7 +239,7 @@ dependencies { playImplementation 'com.google.android.gms:play-services-ads:20.4.0' hmsImplementation 'com.huawei.hms:hianalytics:6.3.0.301' - hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.1.200' + hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.1.300' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 1d910f8d661ba218ab674447528d764071fa7ae1 Mon Sep 17 00:00:00 2001 From: Michael <5672750+mibac138@users.noreply.github.com> Date: Wed, 27 Oct 2021 10:07:04 +0200 Subject: [PATCH 251/869] Fix excuse button showing up despite no lessons available to excuse (#1607) This was happening when there was an unexcused lesson that you excused until the teacher sent a response (accepted or denied it) --- .../wulkanowy/ui/modules/attendance/AttendanceAdapter.kt | 2 +- .../wulkanowy/ui/modules/attendance/AttendancePresenter.kt | 5 ++--- .../java/io/github/wulkanowy/utils/AttendanceExtension.kt | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt index 6cee23963..bb4f70224 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt @@ -46,7 +46,7 @@ class AttendanceAdapter @Inject constructor() : onExcuseCheckboxSelect(item, checked) } - when (if (item.excuseStatus != null) SentExcuseStatus.valueOf(item.excuseStatus) else null) { + when (item.excuseStatus?.let { SentExcuseStatus.valueOf(it)}) { SentExcuseStatus.WAITING -> { attendanceItemExcuseInfo.setImageResource(R.drawable.ic_excuse_waiting) attendanceItemExcuseInfo.visibility = View.VISIBLE diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt index 03545b25b..5ed14f67e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt @@ -259,9 +259,8 @@ class AttendancePresenter @Inject constructor( showEmpty(filteredAttendance.isEmpty()) showErrorView(false) showContent(filteredAttendance.isNotEmpty()) - showExcuseButton(filteredAttendance.any { item -> - (!isParent && isVulcanExcusedFunctionEnabled) || (isParent && item.isExcusableOrNotExcused) - }) + val anyExcusables = filteredAttendance.any { it.isExcusableOrNotExcused } + showExcuseButton(anyExcusables && (isParent || isVulcanExcusedFunctionEnabled)) } analytics.logEvent( "load_data", diff --git a/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt index 479cc5188..b89ad57d3 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt @@ -17,7 +17,7 @@ private inline val AttendanceSummary.allAbsences: Double get() = absence.toDouble() + absenceExcused inline val Attendance.isExcusableOrNotExcused: Boolean - get() = excusable || ((absence || lateness) && !excused) + get() = (excusable || ((absence || lateness) && !excused)) && excuseStatus == null fun AttendanceSummary.calculatePercentage() = calculatePercentage(allPresences, allAbsences) From 26c749c219b8d35e302fd12cd82641015d1dba9a Mon Sep 17 00:00:00 2001 From: Michael <5672750+mibac138@users.noreply.github.com> Date: Fri, 29 Oct 2021 20:30:27 +0200 Subject: [PATCH 252/869] Fix about header layout to support long app names (for DEV builds) (#1602) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mikołaj Pich --- app/build.gradle | 2 +- app/src/main/res/layout/scrollable_header_about.xml | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 94dfc1666..1d5411955 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -74,7 +74,7 @@ android { buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\"" } debug { - resValue "string", "app_name", "Wulkanowy DEV " + defaultConfig.versionCode + resValue "string", "app_name", "Wulkanowy DEV" applicationIdSuffix ".dev" versionNameSuffix "-dev" ext.enableCrashlytics = project.hasProperty("enableFirebase") diff --git a/app/src/main/res/layout/scrollable_header_about.xml b/app/src/main/res/layout/scrollable_header_about.xml index e203d98d4..5a7669fdf 100644 --- a/app/src/main/res/layout/scrollable_header_about.xml +++ b/app/src/main/res/layout/scrollable_header_about.xml @@ -6,6 +6,7 @@ android:layout_height="wrap_content" android:minHeight="104dp" android:orientation="vertical" + android:paddingHorizontal="20dp" tools:context=".ui.modules.about.AboutAdapter"> + app:layout_constraintTop_toTopOf="@id/aboutScrollableHeaderIcon" + app:layout_constraintWidth_max="wrap" /> From 8ed8b5a33cdf10431ecb582f319e59c9f7d4ab66 Mon Sep 17 00:00:00 2001 From: Damian Czupryn <60961958+Daxxxis@users.noreply.github.com> Date: Sun, 31 Oct 2021 20:28:01 +0100 Subject: [PATCH 253/869] Error messages content wrap in error dialog (#1577) --- .../io/github/wulkanowy/ui/base/ErrorDialog.kt | 17 ++++++++--------- app/src/main/res/layout/dialog_error.xml | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorDialog.kt index 4ce977709..4c279d816 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorDialog.kt @@ -11,6 +11,7 @@ import android.widget.Toast import android.widget.Toast.LENGTH_LONG import androidx.appcompat.app.AlertDialog import androidx.core.content.getSystemService +import androidx.core.view.isGone import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.databinding.DialogErrorBinding @@ -24,8 +25,6 @@ import io.github.wulkanowy.utils.openEmailClient import io.github.wulkanowy.utils.openInternetBrowser import okhttp3.internal.http2.StreamResetException import java.io.InterruptedIOException -import java.io.PrintWriter -import java.io.StringWriter import java.net.ConnectException import java.net.SocketTimeoutException import java.net.UnknownHostException @@ -64,26 +63,26 @@ class ErrorDialog : BaseDialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val stringWriter = StringWriter().apply { - error.printStackTrace(PrintWriter(this)) - } + val errorStacktrace = error.stackTraceToString() with(binding) { - errorDialogContent.text = stringWriter.toString() + errorDialogContent.text = errorStacktrace.replace(": ${error.localizedMessage}", "") with(errorDialogHorizontalScroll) { post { fullScroll(HorizontalScrollView.FOCUS_LEFT) } } errorDialogCopy.setOnClickListener { - val clip = ClipData.newPlainText("wulkanowy", stringWriter.toString()) + val clip = ClipData.newPlainText("Error details", errorStacktrace) activity?.getSystemService()?.setPrimaryClip(clip) Toast.makeText(context, R.string.all_copied, LENGTH_LONG).show() } errorDialogCancel.setOnClickListener { dismiss() } errorDialogReport.setOnClickListener { - openConfirmDialog { openEmailClient(stringWriter.toString()) } + openConfirmDialog { openEmailClient(errorStacktrace) } } - errorDialogMessage.text = resources.getString(error) + errorDialogHumanizedMessage.text = resources.getString(error) + errorDialogErrorMessage.text = error.localizedMessage + errorDialogErrorMessage.isGone = error.localizedMessage.isNullOrBlank() errorDialogReport.isEnabled = when (error) { is UnknownHostException, is InterruptedIOException, diff --git a/app/src/main/res/layout/dialog_error.xml b/app/src/main/res/layout/dialog_error.xml index a78790bce..75abcf0fd 100644 --- a/app/src/main/res/layout/dialog_error.xml +++ b/app/src/main/res/layout/dialog_error.xml @@ -4,7 +4,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:minWidth="300dp" - android:orientation="vertical"> + android:orientation="vertical" + tools:context=".ui.base.ErrorDialog"> + + From 36a570eeb0120a8211e62a6ab9a39a04bfbc6e49 Mon Sep 17 00:00:00 2001 From: Damian Czupryn <60961958+Daxxxis@users.noreply.github.com> Date: Mon, 1 Nov 2021 01:46:23 +0100 Subject: [PATCH 254/869] Allow selecting text in error dialog (#1615) --- app/src/main/res/layout/dialog_error.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout/dialog_error.xml b/app/src/main/res/layout/dialog_error.xml index 75abcf0fd..b52c99aca 100644 --- a/app/src/main/res/layout/dialog_error.xml +++ b/app/src/main/res/layout/dialog_error.xml @@ -32,6 +32,7 @@ android:layout_marginBottom="5dp" android:paddingHorizontal="20dp" android:paddingTop="10dp" + android:textIsSelectable="true" android:textColor="?android:textColorSecondary" tools:text="@tools:sample/lorem" /> From a62ed54d07728c4949464e7619c3eddf83b0e398 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 16:54:10 +0000 Subject: [PATCH 255/869] Bump hianalytics from 6.3.0.301 to 6.3.0.302 (#1621) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 1d5411955..b5eb455c1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -238,7 +238,7 @@ dependencies { playImplementation 'com.google.android.play:core-ktx:1.8.1' playImplementation 'com.google.android.gms:play-services-ads:20.4.0' - hmsImplementation 'com.huawei.hms:hianalytics:6.3.0.301' + hmsImplementation 'com.huawei.hms:hianalytics:6.3.0.302' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.1.300' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 1a3d58011650afefd16e73633457120a4a293287 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 16:54:43 +0000 Subject: [PATCH 256/869] Bump firebase-bom from 28.4.2 to 29.0.0 (#1619) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index b5eb455c1..53814dae3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -230,7 +230,7 @@ dependencies { implementation 'me.xdrop:fuzzywuzzy:1.3.1' implementation 'com.fredporciuncula:flow-preferences:1.5.0' - playImplementation platform('com.google.firebase:firebase-bom:28.4.2') + playImplementation platform('com.google.firebase:firebase-bom:29.0.0') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' From 8be605629a4ace0411b5598eb4f098f650771d72 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 16:56:18 +0000 Subject: [PATCH 257/869] Bump firebase-crashlytics-gradle from 2.7.1 to 2.8.0 (#1623) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c345e4c7d..575293aa2 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.10' classpath 'com.huawei.agconnect:agcp:1.6.1.300' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.0' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.3.0" classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3" From b7134221cb5d1d7b862c57512025685b93ea9b67 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 17:05:57 +0000 Subject: [PATCH 258/869] Bump hilt_version from 2.39.1 to 2.40 (#1617) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 575293aa2..99c4820e8 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { kotlin_version = '1.5.31' about_libraries = '8.9.4' - hilt_version = "2.39.1" + hilt_version = "2.40" } repositories { mavenCentral() From eea20ced57e2d06b79cc04c4a24711866b084c4a Mon Sep 17 00:00:00 2001 From: Damian Czupryn <60961958+Daxxxis@users.noreply.github.com> Date: Thu, 4 Nov 2021 03:06:54 +0100 Subject: [PATCH 259/869] Notifications settings reorganize and strings update (#1616) --- app/src/main/res/values/strings.xml | 7 ++++--- .../res/xml/scheme_preferences_notifications.xml | 16 ++++++++++------ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 13030366a..1e88ee1ad 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -611,13 +611,13 @@ - App appearance & behavior + App Default view Calculated average options Force average calculation by app Show presence Theme - Expand grades + Grades expanding Mark current lesson Show groups next to subjects Show chart list in class grades @@ -627,6 +627,7 @@ Language Notifications + Other Show notifications Show upcoming lesson notifications Make upcoming lesson notification persistent @@ -689,7 +690,7 @@ Automatic update, synchronization interval Plus and minus values, average calculation Advanced - App version, contributors, social portals, licenses + App version, contributors, social portals Displaying advertisements, project support diff --git a/app/src/main/res/xml/scheme_preferences_notifications.xml b/app/src/main/res/xml/scheme_preferences_notifications.xml index 78e91cf06..442581bfd 100644 --- a/app/src/main/res/xml/scheme_preferences_notifications.xml +++ b/app/src/main/res/xml/scheme_preferences_notifications.xml @@ -22,18 +22,22 @@ app:singleLineTitle="false" app:summary="@string/pref_notify_upcoming_lessons_persistent_summary" app:title="@string/pref_notify_upcoming_lessons_persistent_switch" /> - + + + Date: Thu, 4 Nov 2021 03:09:47 +0100 Subject: [PATCH 260/869] Add support for user ca in debug flavor(#1624) --- app/src/main/AndroidManifest.xml | 1 + app/src/main/res/xml/network_security_config.xml | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 app/src/main/res/xml/network_security_config.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 810d469f4..e43a0ec08 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -38,6 +38,7 @@ android:allowBackup="false" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:networkSecurityConfig="@xml/network_security_config" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="false" android:theme="@style/WulkanowyTheme" diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 000000000..84ff05a04 --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,9 @@ + + + + + + + + + From 4401df6203bb408e1aa8cb63cf0976fe4a6f2d01 Mon Sep 17 00:00:00 2001 From: Michael <5672750+mibac138@users.noreply.github.com> Date: Sat, 6 Nov 2021 19:07:26 +0100 Subject: [PATCH 261/869] Migrate from ViewPager to ViewPager2 (#1601) --- .../ui/base/BaseFragmentPagerAdapter.kt | 37 ++++++++-------- .../ui/modules/grade/GradeFragment.kt | 42 ++++++++++++------ .../ui/modules/grade/GradePresenter.kt | 1 - .../ui/modules/login/LoginActivity.kt | 42 +++++++++++------- .../ui/modules/message/MessageFragment.kt | 40 ++++++++++++----- .../ui/modules/message/MessagePresenter.kt | 1 - .../SchoolAndTeachersFragment.kt | 44 ++++++++++++++----- .../SchoolAndTeachersPresenter.kt | 1 - .../ui/widgets/SwipeDisabledViewPager.kt | 19 -------- .../wulkanowy/utils/ViewPagerExtension.kt | 8 ++-- app/src/main/res/layout/activity_login.xml | 2 +- app/src/main/res/layout/fragment_grade.xml | 2 +- app/src/main/res/layout/fragment_message.xml | 2 +- .../res/layout/fragment_schoolandteachers.xml | 2 +- 14 files changed, 142 insertions(+), 101 deletions(-) delete mode 100644 app/src/main/java/io/github/wulkanowy/ui/widgets/SwipeDisabledViewPager.kt diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragmentPagerAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragmentPagerAdapter.kt index bd735535d..6bca87f15 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragmentPagerAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragmentPagerAdapter.kt @@ -2,32 +2,33 @@ package io.github.wulkanowy.ui.base import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentPagerAdapter +import androidx.lifecycle.Lifecycle +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.google.android.material.tabs.TabLayout +import com.google.android.material.tabs.TabLayoutMediator -//TODO Use ViewPager2 -class BaseFragmentPagerAdapter(private val fragmentManager: FragmentManager) : - FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { +class BaseFragmentPagerAdapter( + private val fragmentManager: FragmentManager, + private val pagesCount: Int, + lifecycle: Lifecycle, +) : FragmentStateAdapter(fragmentManager, lifecycle), TabLayoutMediator.TabConfigurationStrategy { - private val pages = mutableMapOf() + lateinit var itemFactory: (position: Int) -> Fragment + + var titleFactory: (position: Int) -> String? = { "" } var containerId = 0 fun getFragmentInstance(position: Int): Fragment? { require(containerId != 0) { "Container id is 0" } - return fragmentManager.findFragmentByTag("android:switcher:$containerId:$position") + return fragmentManager.findFragmentByTag("f$position") } - fun addFragments(fragments: List) { - fragments.forEach { pages[it] = null } + override fun createFragment(position: Int): Fragment = itemFactory(position) + + override fun getItemCount() = pagesCount + + override fun onConfigureTab(tab: TabLayout.Tab, position: Int) { + tab.text = titleFactory(position) } - - fun addFragmentsWithTitle(pages: Map) { - this.pages.putAll(pages) - } - - override fun getItem(position: Int) = pages.keys.elementAt(position) - - override fun getCount() = pages.size - - override fun getPageTitle(position: Int) = pages.values.elementAt(position) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt index b3ef3037a..f9ecb681b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt @@ -8,6 +8,7 @@ import android.view.View import android.view.View.INVISIBLE import android.view.View.VISIBLE import androidx.appcompat.app.AlertDialog +import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Semester @@ -29,7 +30,13 @@ class GradeFragment : BaseFragment(R.layout.fragment_grade @Inject lateinit var presenter: GradePresenter - private val pagerAdapter by lazy { BaseFragmentPagerAdapter(childFragmentManager) } + private val pagerAdapter by lazy { + BaseFragmentPagerAdapter( + fragmentManager = childFragmentManager, + pagesCount = 3, + lifecycle = lifecycle, + ) + } private var semesterSwitchMenu: MenuItem? = null @@ -62,25 +69,34 @@ class GradeFragment : BaseFragment(R.layout.fragment_grade } override fun initView() { - with(pagerAdapter) { - containerId = binding.gradeViewPager.id - addFragmentsWithTitle( - mapOf( - GradeDetailsFragment.newInstance() to getString(R.string.all_details), - GradeSummaryFragment.newInstance() to getString(R.string.grade_menu_summary), - GradeStatisticsFragment.newInstance() to getString(R.string.grade_menu_statistics) - ) - ) - } - with(binding.gradeViewPager) { adapter = pagerAdapter offscreenPageLimit = 3 setOnSelectPageListener(presenter::onPageSelected) } + with(pagerAdapter) { + containerId = binding.gradeViewPager.id + titleFactory = { + when (it) { + 0 -> getString(R.string.all_details) + 1 -> getString(R.string.grade_menu_summary) + 2 -> getString(R.string.grade_menu_statistics) + else -> throw IllegalStateException() + } + } + itemFactory = { + when (it) { + 0 -> GradeDetailsFragment.newInstance() + 1 -> GradeSummaryFragment.newInstance() + 2 -> GradeStatisticsFragment.newInstance() + else -> throw IllegalStateException() + } + } + TabLayoutMediator(binding.gradeTabLayout, binding.gradeViewPager, this).attach() + } + with(binding.gradeTabLayout) { - setupWithViewPager(binding.gradeViewPager) setElevationCompat(context.dpToPx(4f)) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradePresenter.kt index 504c730de..76e88bcdb 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradePresenter.kt @@ -101,7 +101,6 @@ class GradePresenter @Inject constructor( private fun loadData() { flowWithResource { val student = studentRepository.getCurrentStudent() - delay(200) semesterRepository.getSemesters(student, refreshOnNoCurrent = true) }.onEach { when (it.status) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt index 10f6c0737..f7bab526c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt @@ -24,7 +24,13 @@ class LoginActivity : BaseActivity(), Logi @Inject override lateinit var presenter: LoginPresenter - private val loginAdapter = BaseFragmentPagerAdapter(supportFragmentManager) + private val pagerAdapter by lazy { + BaseFragmentPagerAdapter( + fragmentManager = supportFragmentManager, + pagesCount = 5, + lifecycle = lifecycle, + ) + } @Inject lateinit var updateHelper: UpdateHelper @@ -65,24 +71,26 @@ class LoginActivity : BaseActivity(), Logi setDisplayShowTitleEnabled(false) } - with(loginAdapter) { - containerId = binding.loginViewpager.id - addFragments( - listOf( - LoginFormFragment.newInstance(), - LoginSymbolFragment.newInstance(), - LoginStudentSelectFragment.newInstance(), - LoginAdvancedFragment.newInstance(), - LoginRecoverFragment.newInstance() - ) - ) - } - with(binding.loginViewpager) { offscreenPageLimit = 2 - adapter = loginAdapter + adapter = pagerAdapter + isUserInputEnabled = false setOnSelectPageListener(presenter::onViewSelected) } + + with(pagerAdapter) { + containerId = binding.loginViewpager.id + itemFactory = { + when (it) { + 0 -> LoginFormFragment.newInstance() + 1 -> LoginSymbolFragment.newInstance() + 2 -> LoginStudentSelectFragment.newInstance() + 3 -> LoginAdvancedFragment.newInstance() + 4 -> LoginRecoverFragment.newInstance() + else -> throw IllegalStateException() + } + } + } } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -103,12 +111,12 @@ class LoginActivity : BaseActivity(), Logi } override fun notifyInitSymbolFragment(loginData: Triple) { - (loginAdapter.getFragmentInstance(1) as? LoginSymbolFragment) + (pagerAdapter.getFragmentInstance(1) as? LoginSymbolFragment) ?.onParentInitSymbolFragment(loginData) } override fun notifyInitStudentSelectFragment(studentsWithSemesters: List) { - (loginAdapter.getFragmentInstance(2) as? LoginStudentSelectFragment) + (pagerAdapter.getFragmentInstance(2) as? LoginStudentSelectFragment) ?.onParentInitStudentSelectFragment(studentsWithSemesters) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt index 72fc627f8..51076c649 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.View import android.view.View.INVISIBLE import android.view.View.VISIBLE +import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED @@ -26,7 +27,13 @@ class MessageFragment : BaseFragment(R.layout.fragment_m @Inject lateinit var presenter: MessagePresenter - private val pagerAdapter by lazy { BaseFragmentPagerAdapter(childFragmentManager) } + private val pagerAdapter by lazy { + BaseFragmentPagerAdapter( + fragmentManager = childFragmentManager, + pagesCount = 3, + lifecycle = lifecycle, + ) + } companion object { fun newInstance() = MessageFragment() @@ -43,23 +50,34 @@ class MessageFragment : BaseFragment(R.layout.fragment_m } override fun initView() { - with(pagerAdapter) { - containerId = binding.messageViewPager.id - addFragmentsWithTitle(mapOf( - MessageTabFragment.newInstance(RECEIVED) to getString(R.string.message_inbox), - MessageTabFragment.newInstance(SENT) to getString(R.string.message_sent), - MessageTabFragment.newInstance(TRASHED) to getString(R.string.message_trash) - )) - } - with(binding.messageViewPager) { adapter = pagerAdapter offscreenPageLimit = 2 setOnSelectPageListener(presenter::onPageSelected) } + with(pagerAdapter) { + containerId = binding.messageViewPager.id + titleFactory = { + when (it) { + 0 -> getString(R.string.message_inbox) + 1 -> getString(R.string.message_sent) + 2 -> getString(R.string.message_trash) + else -> throw IllegalStateException() + } + } + itemFactory = { + when (it) { + 0 -> MessageTabFragment.newInstance(RECEIVED) + 1 -> MessageTabFragment.newInstance(SENT) + 2 -> MessageTabFragment.newInstance(TRASHED) + else -> throw IllegalStateException() + } + } + TabLayoutMediator(binding.messageTabLayout, binding.messageViewPager, this).attach() + } + with(binding.messageTabLayout) { - setupWithViewPager(binding.messageViewPager) setElevationCompat(context.dpToPx(4f)) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt index be6940520..9e19517bd 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt @@ -16,7 +16,6 @@ class MessagePresenter @Inject constructor( override fun onAttachView(view: MessageView) { super.onAttachView(view) presenterScope.launch { - delay(150) view.initView() Timber.i("Message view was initialized") loadData() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt index c1c569611..9892b77ad 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.View import android.view.View.INVISIBLE import android.view.View.VISIBLE +import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.databinding.FragmentSchoolandteachersBinding @@ -24,7 +25,13 @@ class SchoolAndTeachersFragment : @Inject lateinit var presenter: SchoolAndTeachersPresenter - private val pagerAdapter by lazy { BaseFragmentPagerAdapter(childFragmentManager) } + private val pagerAdapter by lazy { + BaseFragmentPagerAdapter( + fragmentManager = childFragmentManager, + pagesCount = 2, + lifecycle = lifecycle, + ) + } companion object { fun newInstance() = SchoolAndTeachersFragment() @@ -41,22 +48,36 @@ class SchoolAndTeachersFragment : } override fun initView() { - with(pagerAdapter) { - containerId = binding.schoolandteachersViewPager.id - addFragmentsWithTitle(mapOf( - SchoolFragment.newInstance() to getString(R.string.school_title), - TeacherFragment.newInstance() to getString(R.string.teachers_title) - )) - } - with(binding.schoolandteachersViewPager) { adapter = pagerAdapter offscreenPageLimit = 2 setOnSelectPageListener(presenter::onPageSelected) } + with(pagerAdapter) { + containerId = binding.schoolandteachersViewPager.id + titleFactory = { + when (it) { + 0 -> getString(R.string.school_title) + 1 -> getString(R.string.teachers_title) + else -> throw IllegalStateException() + } + } + itemFactory = { + when (it) { + 0 -> SchoolFragment.newInstance() + 1 -> TeacherFragment.newInstance() + else -> throw IllegalStateException() + } + } + TabLayoutMediator( + binding.schoolandteachersTabLayout, + binding.schoolandteachersViewPager, + this + ).attach() + } + with(binding.schoolandteachersTabLayout) { - setupWithViewPager(binding.schoolandteachersViewPager) setElevationCompat(context.dpToPx(4f)) } } @@ -77,7 +98,8 @@ class SchoolAndTeachersFragment : } override fun notifyChildLoadData(index: Int, forceRefresh: Boolean) { - (pagerAdapter.getFragmentInstance(index) as? SchoolAndTeachersChildView)?.onParentLoadData(forceRefresh) + (pagerAdapter.getFragmentInstance(index) as? SchoolAndTeachersChildView) + ?.onParentLoadData(forceRefresh) } override fun onDestroyView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt index c14272b9d..43823d6b4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt @@ -16,7 +16,6 @@ class SchoolAndTeachersPresenter @Inject constructor( override fun onAttachView(view: SchoolAndTeachersView) { super.onAttachView(view) presenterScope.launch { - delay(150) view.initView() Timber.i("Message view was initialized") loadData() diff --git a/app/src/main/java/io/github/wulkanowy/ui/widgets/SwipeDisabledViewPager.kt b/app/src/main/java/io/github/wulkanowy/ui/widgets/SwipeDisabledViewPager.kt deleted file mode 100644 index eb5cae4f3..000000000 --- a/app/src/main/java/io/github/wulkanowy/ui/widgets/SwipeDisabledViewPager.kt +++ /dev/null @@ -1,19 +0,0 @@ -package io.github.wulkanowy.ui.widgets - -import android.annotation.SuppressLint -import android.content.Context -import android.util.AttributeSet -import android.view.MotionEvent -import androidx.viewpager.widget.ViewPager - -class SwipeDisabledViewPager : ViewPager { - - constructor(context: Context) : super(context) - - constructor(context: Context, attr: AttributeSet) : super(context, attr) - - @SuppressLint("ClickableViewAccessibility") - override fun onTouchEvent(ev: MotionEvent) = false - - override fun onInterceptTouchEvent(ev: MotionEvent) = false -} diff --git a/app/src/main/java/io/github/wulkanowy/utils/ViewPagerExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/ViewPagerExtension.kt index 6a5ad880a..700ac2f1d 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/ViewPagerExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/ViewPagerExtension.kt @@ -1,13 +1,11 @@ package io.github.wulkanowy.utils -import androidx.viewpager.widget.ViewPager +import androidx.viewpager2.widget.ViewPager2 -inline fun ViewPager.setOnSelectPageListener(crossinline selectListener: (position: Int) -> Unit) { - addOnPageChangeListener(object : ViewPager.OnPageChangeListener { +inline fun ViewPager2.setOnSelectPageListener(crossinline selectListener: (position: Int) -> Unit) { + registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { selectListener(position) } - override fun onPageScrollStateChanged(state: Int) {} - override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {} }) } diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index e55ea8b95..1d5b5280d 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -10,7 +10,7 @@ android:layout_height="wrap_content" android:background="@android:color/transparent" /> - diff --git a/app/src/main/res/layout/fragment_grade.xml b/app/src/main/res/layout/fragment_grade.xml index ed0447fb5..989929d4c 100644 --- a/app/src/main/res/layout/fragment_grade.xml +++ b/app/src/main/res/layout/fragment_grade.xml @@ -17,7 +17,7 @@ tools:ignore="UnusedAttribute" tools:visibility="visible" /> - - - Date: Sat, 6 Nov 2021 22:21:34 +0100 Subject: [PATCH 262/869] Add timetable changes, attendance notifications and refactor notification deeplinks (#1547) --- .../43.json | 2408 +++++++++++++++++ .../github/wulkanowy/data/db/AppDatabase.kt | 6 +- .../wulkanowy/data/db/dao/AttendanceDao.kt | 9 +- .../wulkanowy/data/db/entities/Attendance.kt | 3 + .../wulkanowy/data/db/entities/Timetable.kt | 3 + .../data/db/migrations/Migration43.kt | 12 + .../wulkanowy/data/pojos/NotificationData.kt | 43 +- .../data/repositories/AttendanceRepository.kt | 19 +- .../data/repositories/TimetableRepository.kt | 20 +- .../wulkanowy/services/ServicesModule.kt | 10 + .../alarm/TimetableNotificationReceiver.kt | 17 +- .../TimetableNotificationSchedulerHelper.kt | 4 +- .../services/shortcuts/ShortcutsHelper.kt | 90 + .../sync/channels/NewAttendanceChannel.kt | 36 + .../sync/channels/TimetableChangeChannel.kt | 36 + .../notifications/AppNotificationManager.kt | 220 +- .../ChangeTimetableNotification.kt | 125 + .../NewAttendanceNotification.kt | 55 + .../NewConferenceNotification.kt | 48 +- .../sync/notifications/NewExamNotification.kt | 48 +- .../notifications/NewGradeNotification.kt | 98 +- .../notifications/NewHomeworkNotification.kt | 46 +- .../NewLuckyNumberNotification.kt | 34 +- .../notifications/NewMessageNotification.kt | 38 +- .../sync/notifications/NewNoteNotification.kt | 48 +- .../NewSchoolAnnouncementNotification.kt | 57 +- .../sync/notifications/NotificationType.kt | 85 +- .../services/sync/works/AttendanceWork.kt | 30 +- .../services/sync/works/TimetableWork.kt | 31 +- .../wulkanowy/ui/modules/Destination.kt | 132 + .../modules/attendance/AttendanceAdapter.kt | 4 +- .../ui/modules/attendance/AttendanceDialog.kt | 4 +- .../NotificationDebugPresenter.kt | 12 + .../debug/notification/mock/attendance.kt | 35 + .../debug/notification/mock/timetable.kt | 39 + .../LoginStudentSelectFragment.kt | 9 +- .../LuckyNumberWidgetProvider.kt | 59 +- .../wulkanowy/ui/modules/main/MainActivity.kt | 191 +- .../ui/modules/main/MainPresenter.kt | 61 +- .../wulkanowy/ui/modules/main/MainView.kt | 32 +- .../NotificationsCenterAdapter.kt | 19 +- .../NotificationsCenterFragment.kt | 6 +- .../ui/modules/settings/SettingsFragment.kt | 14 +- .../ui/modules/settings/SettingsView.kt | 5 + .../TimetableWidgetProvider.kt | 8 +- .../wulkanowy/utils/AttendanceExtension.kt | 2 +- .../wulkanowy/utils/ContextExtension.kt | 4 + .../utils/FragNavControlerExtension.kt | 13 +- .../wulkanowy/utils/FragmentExtension.kt | 44 - app/src/main/res/values/strings.xml | 34 + .../ui/modules/main/MainPresenterTest.kt | 14 +- 51 files changed, 3819 insertions(+), 601 deletions(-) create mode 100644 app/schemas/io.github.wulkanowy.data.db.AppDatabase/43.json create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration43.kt create mode 100644 app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt create mode 100644 app/src/main/java/io/github/wulkanowy/services/sync/channels/NewAttendanceChannel.kt create mode 100644 app/src/main/java/io/github/wulkanowy/services/sync/channels/TimetableChangeChannel.kt create mode 100644 app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt create mode 100644 app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewAttendanceNotification.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/attendance.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/timetable.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsView.kt delete mode 100644 app/src/main/java/io/github/wulkanowy/utils/FragmentExtension.kt diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/43.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/43.json new file mode 100644 index 000000000..22c0d8125 --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/43.json @@ -0,0 +1,2408 @@ +{ + "formatVersion": 1, + "database": { + "version": 43, + "identityHash": "66946510bb620ae82686a5a1a31aba18", + "entities": [ + { + "tableName": "Students", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrapper_base_url` TEXT NOT NULL, `mobile_base_url` TEXT NOT NULL, `login_type` TEXT NOT NULL, `login_mode` TEXT NOT NULL, `certificate_key` TEXT NOT NULL, `private_key` TEXT NOT NULL, `is_parent` INTEGER NOT NULL, `email` TEXT NOT NULL, `password` TEXT NOT NULL, `symbol` TEXT NOT NULL, `student_id` INTEGER NOT NULL, `user_login_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `student_name` TEXT NOT NULL, `school_id` TEXT NOT NULL, `school_short` TEXT NOT NULL, `school_name` TEXT NOT NULL, `class_name` TEXT NOT NULL, `class_id` INTEGER NOT NULL, `is_current` INTEGER NOT NULL, `registration_date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `nick` TEXT NOT NULL, `avatar_color` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "scrapperBaseUrl", + "columnName": "scrapper_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mobileBaseUrl", + "columnName": "mobile_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginType", + "columnName": "login_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginMode", + "columnName": "login_mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificateKey", + "columnName": "certificate_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "private_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isParent", + "columnName": "is_parent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "user_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "student_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolSymbol", + "columnName": "school_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "school_short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolName", + "columnName": "school_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "className", + "columnName": "class_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCurrent", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registrationDate", + "columnName": "registration_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nick", + "columnName": "nick", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarColor", + "columnName": "avatar_color", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Students_email_symbol_student_id_school_id_class_id", + "unique": true, + "columnNames": [ + "email", + "symbol", + "student_id", + "school_id", + "class_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryName", + "columnName": "diary_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolYear", + "columnName": "school_year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterName", + "columnName": "semester_name", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "semester_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `semester_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Exams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Timetable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `number` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `subjectOld` TEXT NOT NULL, `group` TEXT NOT NULL, `room` TEXT NOT NULL, `roomOld` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacherOld` TEXT NOT NULL, `info` TEXT NOT NULL, `student_plan` INTEGER NOT NULL, `changes` INTEGER NOT NULL, `canceled` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectOld", + "columnName": "subjectOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "room", + "columnName": "room", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roomOld", + "columnName": "roomOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherOld", + "columnName": "teacherOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isStudentPlan", + "columnName": "student_plan", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "changes", + "columnName": "changes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canceled", + "columnName": "canceled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Attendance", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `time_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `excusable` INTEGER NOT NULL, `excuse_status` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeId", + "columnName": "time_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excusable", + "columnName": "excusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excuseStatus", + "columnName": "excuse_status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AttendanceSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `subject_id` INTEGER NOT NULL, `month` INTEGER NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `absence_excused` INTEGER NOT NULL, `absence_for_school_reasons` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `lateness_excused` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subject_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "month", + "columnName": "month", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceExcused", + "columnName": "absence_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceForSchoolReasons", + "columnName": "absence_for_school_reasons", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latenessExcused", + "columnName": "lateness_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Grades", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `entry` TEXT NOT NULL, `value` REAL NOT NULL, `modifier` REAL NOT NULL, `comment` TEXT NOT NULL, `color` TEXT NOT NULL, `grade_symbol` TEXT NOT NULL, `description` TEXT NOT NULL, `weight` TEXT NOT NULL, `weightValue` REAL NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entry", + "columnName": "entry", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "modifier", + "columnName": "modifier", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gradeSymbol", + "columnName": "grade_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weightValue", + "columnName": "weightValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `subject` TEXT NOT NULL, `predicted_grade` TEXT NOT NULL, `final_grade` TEXT NOT NULL, `proposed_points` TEXT NOT NULL, `final_points` TEXT NOT NULL, `points_sum` TEXT NOT NULL, `average` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_predicted_grade_notified` INTEGER NOT NULL, `is_final_grade_notified` INTEGER NOT NULL, `predicted_grade_last_change` INTEGER NOT NULL, `final_grade_last_change` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "predictedGrade", + "columnName": "predicted_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalGrade", + "columnName": "final_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "proposedPoints", + "columnName": "proposed_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalPoints", + "columnName": "final_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pointsSum", + "columnName": "points_sum", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "average", + "columnName": "average", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPredictedGradeNotified", + "columnName": "is_predicted_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFinalGradeNotified", + "columnName": "is_final_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "predictedGradeLastChange", + "columnName": "predicted_grade_last_change", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "finalGradeLastChange", + "columnName": "final_grade_last_change", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradePartialStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `class_average` TEXT NOT NULL, `student_average` TEXT NOT NULL, `class_amounts` TEXT NOT NULL, `student_amounts` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAverage", + "columnName": "class_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAverage", + "columnName": "student_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAmounts", + "columnName": "class_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAmounts", + "columnName": "student_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesPointsStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `others` REAL NOT NULL, `student` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "others", + "columnName": "others", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "student", + "columnName": "student", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradeSemesterStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `amounts` TEXT NOT NULL, `student_grade` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amounts", + "columnName": "amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentGrade", + "columnName": "student_grade", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `recipient_name` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `removed` INTEGER NOT NULL, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `unread_by` INTEGER NOT NULL, `read_by` INTEGER NOT NULL, `content` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipient", + "columnName": "recipient_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "removed", + "columnName": "removed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasAttachments", + "columnName": "has_attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `one_drive_id` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`real_id`))", + "fields": [ + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "oneDriveId", + "columnName": "one_drive_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "real_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `category` TEXT NOT NULL, `category_type` INTEGER NOT NULL, `is_points_show` INTEGER NOT NULL, `points` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryType", + "columnName": "category_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPointsShow", + "columnName": "is_points_show", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Homework", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `attachments` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_done` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL, `is_added_by_user` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "is_done", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Subjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "LuckyNumbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `lucky_number` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "luckyNumber", + "columnName": "lucky_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CompletedLesson", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `topic` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `substitution` TEXT NOT NULL, `absence` TEXT NOT NULL, `resources` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "topic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "substitution", + "columnName": "substitution", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "resources", + "columnName": "resources", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ReportingUnits", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `short` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `roles` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "senderName", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roles", + "columnName": "roles", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` TEXT NOT NULL, `name` TEXT NOT NULL, `real_name` TEXT NOT NULL, `login_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `role` INTEGER NOT NULL, `hash` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "realName", + "columnName": "real_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginId", + "columnName": "login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MobileDevices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Teachers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `short_name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "School", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `contact` TEXT NOT NULL, `headmaster` TEXT NOT NULL, `pedagogue` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contact", + "columnName": "contact", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headmaster", + "columnName": "headmaster", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pedagogue", + "columnName": "pedagogue", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Conferences", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `subject` TEXT NOT NULL, `agenda` TEXT NOT NULL, `present_on_conference` TEXT NOT NULL, `conference_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agenda", + "columnName": "agenda", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presentOnConference", + "columnName": "present_on_conference", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conference_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableAdditional", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StudentInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `full_name` TEXT NOT NULL, `first_name` TEXT NOT NULL, `second_name` TEXT NOT NULL, `surname` TEXT NOT NULL, `birth_date` INTEGER NOT NULL, `birth_place` TEXT NOT NULL, `gender` TEXT NOT NULL, `has_polish_citizenship` INTEGER NOT NULL, `family_name` TEXT NOT NULL, `parents_names` TEXT NOT NULL, `address` TEXT NOT NULL, `registered_address` TEXT NOT NULL, `correspondence_address` TEXT NOT NULL, `phone_number` TEXT NOT NULL, `cell_phone_number` TEXT NOT NULL, `email` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `first_guardian_full_name` TEXT, `first_guardian_kinship` TEXT, `first_guardian_address` TEXT, `first_guardian_phones` TEXT, `first_guardian_email` TEXT, `second_guardian_full_name` TEXT, `second_guardian_kinship` TEXT, `second_guardian_address` TEXT, `second_guardian_phones` TEXT, `second_guardian_email` TEXT)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "full_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondName", + "columnName": "second_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "surname", + "columnName": "surname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "birthDate", + "columnName": "birth_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthPlace", + "columnName": "birth_place", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gender", + "columnName": "gender", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPolishCitizenship", + "columnName": "has_polish_citizenship", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "familyName", + "columnName": "family_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentsNames", + "columnName": "parents_names", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "registeredAddress", + "columnName": "registered_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "correspondenceAddress", + "columnName": "correspondence_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cellPhoneNumber", + "columnName": "cell_phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstGuardian.fullName", + "columnName": "first_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.kinship", + "columnName": "first_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.address", + "columnName": "first_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.phones", + "columnName": "first_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.email", + "columnName": "first_guardian_email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.fullName", + "columnName": "second_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.kinship", + "columnName": "second_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.address", + "columnName": "second_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.phones", + "columnName": "second_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.email", + "columnName": "second_guardian_email", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableHeaders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SchoolAnnouncements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `type` TEXT NOT NULL, `date` INTEGER NOT NULL, `data` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AdminMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `version_name` INTEGER, `version_max` INTEGER, `target_register_host` TEXT, `target_flavor` TEXT, `destination_url` TEXT, `priority` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "versionMin", + "columnName": "version_name", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMax", + "columnName": "version_max", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "targetRegisterHost", + "columnName": "target_register_host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "targetFlavor", + "columnName": "target_flavor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "destinationUrl", + "columnName": "destination_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "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, '66946510bb620ae82686a5a1a31aba18')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt index d96856068..7e6d609f7 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt @@ -102,6 +102,7 @@ import io.github.wulkanowy.data.db.migrations.Migration4 import io.github.wulkanowy.data.db.migrations.Migration40 import io.github.wulkanowy.data.db.migrations.Migration41 import io.github.wulkanowy.data.db.migrations.Migration42 +import io.github.wulkanowy.data.db.migrations.Migration43 import io.github.wulkanowy.data.db.migrations.Migration5 import io.github.wulkanowy.data.db.migrations.Migration6 import io.github.wulkanowy.data.db.migrations.Migration7 @@ -151,7 +152,7 @@ import javax.inject.Singleton abstract class AppDatabase : RoomDatabase() { companion object { - const val VERSION_SCHEMA = 42 + const val VERSION_SCHEMA = 43 fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( Migration2(), @@ -194,7 +195,8 @@ abstract class AppDatabase : RoomDatabase() { Migration39(), Migration40(), Migration41(sharedPrefProvider), - Migration42() + Migration42(), + Migration43() ) fun newInstance( diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/AttendanceDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/AttendanceDao.kt index 8ef3fd446..c6c255a1f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/AttendanceDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/AttendanceDao.kt @@ -11,6 +11,11 @@ import javax.inject.Singleton @Dao interface AttendanceDao : BaseDao { - @Query("SELECT * FROM Attendance WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :from AND date <= :end") - fun loadAll(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): Flow> + @Query("SELECT * FROM Attendance WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :start AND date <= :end") + fun loadAll( + diaryId: Int, + studentId: Int, + start: LocalDate, + end: LocalDate + ): Flow> } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Attendance.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Attendance.kt index f141d5d52..b40dd52e5 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Attendance.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Attendance.kt @@ -47,4 +47,7 @@ data class Attendance( @PrimaryKey(autoGenerate = true) var id: Long = 0 + + @ColumnInfo(name = "is_notified") + var isNotified: Boolean = true } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Timetable.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Timetable.kt index 1bf159efd..29b3737bc 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Timetable.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Timetable.kt @@ -50,4 +50,7 @@ data class Timetable( @PrimaryKey(autoGenerate = true) var id: Long = 0 + + @ColumnInfo(name = "is_notified") + var isNotified: Boolean = true } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration43.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration43.kt new file mode 100644 index 000000000..68c2834d6 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration43.kt @@ -0,0 +1,12 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration43 : Migration(42, 43) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE Timetable ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1") + database.execSQL("ALTER TABLE Attendance ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1") + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/NotificationData.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/NotificationData.kt index 0b4603ef7..0748ba647 100644 --- a/app/src/main/java/io/github/wulkanowy/data/pojos/NotificationData.kt +++ b/app/src/main/java/io/github/wulkanowy/data/pojos/NotificationData.kt @@ -1,36 +1,19 @@ package io.github.wulkanowy.data.pojos -import androidx.annotation.DrawableRes -import androidx.annotation.PluralsRes -import androidx.annotation.StringRes +import android.content.Intent import io.github.wulkanowy.services.sync.notifications.NotificationType -import io.github.wulkanowy.ui.modules.main.MainView -sealed interface NotificationData { +data class NotificationData( + val intentToStart: Intent, + val title: String, + val content: String +) + +data class GroupNotificationData( + val notificationDataList: List, + val title: String, + val content: String, + val intentToStart: Intent, val type: NotificationType - val startMenu: MainView.Section - val icon: Int - val titleStringRes: Int - val contentStringRes: Int -} +) -data class MultipleNotificationsData( - override val type: NotificationType, - override val startMenu: MainView.Section, - @DrawableRes override val icon: Int, - @PluralsRes override val titleStringRes: Int, - @PluralsRes override val contentStringRes: Int, - - @PluralsRes val summaryStringRes: Int, - val lines: List, -) : NotificationData - -data class OneNotificationData( - override val type: NotificationType, - override val startMenu: MainView.Section, - @DrawableRes override val icon: Int, - @StringRes override val titleStringRes: Int, - @StringRes override val contentStringRes: Int, - - val contentValues: List, -) : NotificationData diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt index d21ffb5fb..ec9198175 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt @@ -14,6 +14,7 @@ import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.uniqueSubtract +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex import java.time.LocalDate import java.time.LocalDateTime @@ -38,6 +39,7 @@ class AttendanceRepository @Inject constructor( start: LocalDate, end: LocalDate, forceRefresh: Boolean, + notify: Boolean = false, ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { @@ -56,13 +58,28 @@ class AttendanceRepository @Inject constructor( }, saveFetchResult = { old, new -> attendanceDb.deleteAll(old uniqueSubtract new) - attendanceDb.insertAll(new uniqueSubtract old) + val attendanceToAdd = (new uniqueSubtract old).map { newAttendance -> + newAttendance.apply { if (notify) isNotified = false } + } + attendanceDb.insertAll(attendanceToAdd) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end)) }, filterResult = { it.filter { item -> item.date in start..end } } ) + fun getAttendanceFromDatabase( + semester: Semester, + start: LocalDate, + end: LocalDate + ): Flow> { + return attendanceDb.loadAll(semester.diaryId, semester.studentId, start, end) + } + + suspend fun updateTimetable(timetable: List) { + return attendanceDb.updateAll(timetable) + } + suspend fun excuseForAbsence( student: Student, semester: Semester, absenceList: List, reason: String? = null diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt index 769fa0f0d..8be621122 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt @@ -47,6 +47,7 @@ class TimetableRepository @Inject constructor( end: LocalDate, forceRefresh: Boolean, refreshAdditional: Boolean = false, + notify: Boolean = false ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { (timetable, additional, headers) -> @@ -67,7 +68,7 @@ class TimetableRepository @Inject constructor( timetableFull.mapToEntities(semester) }, saveFetchResult = { timetableOld, timetableNew -> - refreshTimetable(student, timetableOld.lessons, timetableNew.lessons) + refreshTimetable(student, timetableOld.lessons, timetableNew.lessons, notify) refreshAdditional(timetableOld.additional, timetableNew.additional) refreshDayHeaders(timetableOld.headers, timetableNew.headers) @@ -117,13 +118,28 @@ class TimetableRepository @Inject constructor( } } + fun getTimetableFromDatabase( + semester: Semester, + from: LocalDate, + end: LocalDate + ): Flow> { + return timetableDb.loadAll(semester.diaryId, semester.studentId, from, end) + } + + suspend fun updateTimetable(timetable: List) { + return timetableDb.updateAll(timetable) + } + private suspend fun refreshTimetable( student: Student, lessonsOld: List, lessonsNew: List, + notify: Boolean ) { val lessonsToRemove = lessonsOld uniqueSubtract lessonsNew - val lessonsToAdd = lessonsNew uniqueSubtract lessonsOld + val lessonsToAdd = (lessonsNew uniqueSubtract lessonsOld).map { new -> + new.apply { if (notify) isNotified = false } + } timetableDb.deleteAll(lessonsToRemove) timetableDb.insertAll(lessonsToAdd) diff --git a/app/src/main/java/io/github/wulkanowy/services/ServicesModule.kt b/app/src/main/java/io/github/wulkanowy/services/ServicesModule.kt index cdf0c26a9..1729f1006 100644 --- a/app/src/main/java/io/github/wulkanowy/services/ServicesModule.kt +++ b/app/src/main/java/io/github/wulkanowy/services/ServicesModule.kt @@ -15,6 +15,7 @@ import dagger.multibindings.IntoSet import io.github.wulkanowy.services.sync.channels.Channel import io.github.wulkanowy.services.sync.channels.DebugChannel import io.github.wulkanowy.services.sync.channels.LuckyNumberChannel +import io.github.wulkanowy.services.sync.channels.NewAttendanceChannel import io.github.wulkanowy.services.sync.channels.NewConferencesChannel import io.github.wulkanowy.services.sync.channels.NewExamChannel import io.github.wulkanowy.services.sync.channels.NewGradesChannel @@ -23,6 +24,7 @@ import io.github.wulkanowy.services.sync.channels.NewMessagesChannel import io.github.wulkanowy.services.sync.channels.NewNotesChannel import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel import io.github.wulkanowy.services.sync.channels.PushChannel +import io.github.wulkanowy.services.sync.channels.TimetableChangeChannel import io.github.wulkanowy.services.sync.channels.UpcomingLessonsChannel import io.github.wulkanowy.services.sync.works.AttendanceSummaryWork import io.github.wulkanowy.services.sync.works.AttendanceWork @@ -167,4 +169,12 @@ abstract class ServicesModule { @Binds @IntoSet abstract fun provideUpcomingLessonsChannel(channel: UpcomingLessonsChannel): Channel + + @Binds + @IntoSet + abstract fun provideChangeTimetableChannel(channel: TimetableChangeChannel): Channel + + @Binds + @IntoSet + abstract fun provideNewAttendanceChannel(channel: NewAttendanceChannel): Channel } diff --git a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt index 9ce96ef3f..38ae7884c 100644 --- a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt +++ b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt @@ -15,8 +15,8 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.services.HiltBroadcastReceiver import io.github.wulkanowy.services.sync.channels.UpcomingLessonsChannel.Companion.CHANNEL_ID +import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.main.MainActivity -import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.getCompatColor import io.github.wulkanowy.utils.toLocalDateTime @@ -41,7 +41,7 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { const val NOTIFICATION_TYPE_UPCOMING = 2 const val NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION = 3 - const val NOTIFICATION_ID = "id" + const val NOTIFICATION_ID = 2137 const val STUDENT_NAME = "student_name" const val STUDENT_ID = "student_id" @@ -71,11 +71,10 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { private fun prepareNotification(context: Context, intent: Intent) { val type = intent.getIntExtra(LESSON_TYPE, 0) - val notificationId = intent.getIntExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id) val isPersistent = preferencesRepository.isUpcomingLessonsNotificationsPersistent if (type == NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION) { - return NotificationManagerCompat.from(context).cancel(notificationId) + return NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID) } val studentId = intent.getIntExtra(STUDENT_ID, 0) @@ -92,7 +91,8 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { Timber.d("TimetableNotification receive: type: $type, subject: $subject, start: ${start.toLocalDateTime()}, student: $studentId") - showNotification(context, notificationId, isPersistent, studentName, + showNotification( + context, isPersistent, studentName, if (type == NOTIFICATION_TYPE_CURRENT) end else start, end - start, context.getString( if (type == NOTIFICATION_TYPE_CURRENT) R.string.timetable_now else R.string.timetable_next, @@ -109,7 +109,6 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { private fun showNotification( context: Context, - notificationId: Int, isPersistent: Boolean, studentName: String?, countDown: Long, @@ -118,7 +117,7 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { next: String? ) { NotificationManagerCompat.from(context) - .notify(notificationId, NotificationCompat.Builder(context, CHANNEL_ID) + .notify(NOTIFICATION_ID, NotificationCompat.Builder(context, CHANNEL_ID) .setContentTitle(title) .setContentText(next) .setAutoCancel(false) @@ -138,8 +137,8 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { .setContentIntent( PendingIntent.getActivity( context, - MainView.Section.TIMETABLE.id, - MainActivity.getStartIntent(context, MainView.Section.TIMETABLE, true), + NOTIFICATION_ID, + MainActivity.getStartIntent(context, Destination.Timetable(), true), FLAG_UPDATE_CURRENT ) ) diff --git a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt index 2ec4e527b..28a9b3737 100644 --- a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt +++ b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt @@ -25,7 +25,6 @@ import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companio import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.NOTIFICATION_TYPE_UPCOMING import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.STUDENT_ID import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.STUDENT_NAME -import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.DispatchersProvider import io.github.wulkanowy.utils.nickOrName import io.github.wulkanowy.utils.toTimestamp @@ -79,7 +78,7 @@ class TimetableNotificationSchedulerHelper @Inject constructor( } fun cancelNotification() = - NotificationManagerCompat.from(context).cancel(MainView.Section.TIMETABLE.id) + NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID) suspend fun scheduleNotifications(lessons: List, student: Student) { if (!preferencesRepository.isUpcomingLessonsNotificationsEnable) { @@ -156,7 +155,6 @@ class TimetableNotificationSchedulerHelper @Inject constructor( AlarmManagerCompat.setExactAndAllowWhileIdle( alarmManager, RTC_WAKEUP, time.toTimestamp(), PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also { - it.putExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id) it.putExtra(LESSON_TYPE, notificationType) }, FLAG_UPDATE_CURRENT) ) diff --git a/app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt b/app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt new file mode 100644 index 000000000..d1c215f26 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt @@ -0,0 +1,90 @@ +package io.github.wulkanowy.services.shortcuts + +import android.content.Context +import android.content.Intent +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.R +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ShortcutsHelper @Inject constructor(@ApplicationContext private val context: Context) { + + private val destinations = mapOf( + "grade" to Destination.Grade, + "attendance" to Destination.Attendance, + "exam" to Destination.Exam, + "timetable" to Destination.Timetable() + ) + + init { + initializeShortcuts() + } + + fun getDestination(intent: Intent) = + destinations[intent.getStringExtra(EXTRA_SHORTCUT_DESTINATION_ID)] + + private fun initializeShortcuts() { + val shortcutsInfo = listOf( + ShortcutInfoCompat.Builder(context, "grade_shortcut") + .setShortLabel(context.getString(R.string.grade_title)) + .setLongLabel(context.getString(R.string.grade_title)) + .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_grade)) + .setIntent(MainActivity.getStartIntent(context, startNewTask = true) + .apply { + action = Intent.ACTION_VIEW + putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "grade") + } + ) + .build(), + + ShortcutInfoCompat.Builder(context, "attendance_shortcut") + .setShortLabel(context.getString(R.string.attendance_title)) + .setLongLabel(context.getString(R.string.attendance_title)) + .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_attendance)) + .setIntent(MainActivity.getStartIntent(context, startNewTask = true) + .apply { + action = Intent.ACTION_VIEW + putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "attendance") + } + ) + .build(), + + ShortcutInfoCompat.Builder(context, "exam_shortcut") + .setShortLabel(context.getString(R.string.exam_title)) + .setLongLabel(context.getString(R.string.exam_title)) + .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_exam)) + .setIntent(MainActivity.getStartIntent(context, startNewTask = true) + .apply { + action = Intent.ACTION_VIEW + putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "exam") + } + ) + .build(), + + ShortcutInfoCompat.Builder(context, "timetable_shortcut") + .setShortLabel(context.getString(R.string.timetable_title)) + .setLongLabel(context.getString(R.string.timetable_title)) + .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_timetable)) + .setIntent(MainActivity.getStartIntent(context, startNewTask = true) + .apply { + action = Intent.ACTION_VIEW + putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "timetable") + } + ) + .build() + ) + + shortcutsInfo.forEach { ShortcutManagerCompat.pushDynamicShortcut(context, it) } + } + + private companion object { + + private const val EXTRA_SHORTCUT_DESTINATION_ID = "shortcut_destination_id" + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/channels/NewAttendanceChannel.kt b/app/src/main/java/io/github/wulkanowy/services/sync/channels/NewAttendanceChannel.kt new file mode 100644 index 000000000..3110099e5 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/sync/channels/NewAttendanceChannel.kt @@ -0,0 +1,36 @@ +package io.github.wulkanowy.services.sync.channels + +import android.annotation.TargetApi +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import androidx.core.app.NotificationManagerCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.R +import javax.inject.Inject + +@TargetApi(26) +class NewAttendanceChannel @Inject constructor( + private val notificationManager: NotificationManagerCompat, + @ApplicationContext private val context: Context +) : Channel { + + companion object { + const val CHANNEL_ID = "new_attendance_channel" + } + + override fun create() { + notificationManager.createNotificationChannel( + NotificationChannel( + CHANNEL_ID, + context.getString(R.string.channel_new_attendance), + NotificationManager.IMPORTANCE_HIGH + ) + .apply { + enableLights(true) + enableVibration(true) + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + }) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/channels/TimetableChangeChannel.kt b/app/src/main/java/io/github/wulkanowy/services/sync/channels/TimetableChangeChannel.kt new file mode 100644 index 000000000..10dd3e004 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/sync/channels/TimetableChangeChannel.kt @@ -0,0 +1,36 @@ +package io.github.wulkanowy.services.sync.channels + +import android.annotation.TargetApi +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import androidx.core.app.NotificationManagerCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.R +import javax.inject.Inject + +@TargetApi(26) +class TimetableChangeChannel @Inject constructor( + private val notificationManager: NotificationManagerCompat, + @ApplicationContext private val context: Context +) : Channel { + + companion object { + const val CHANNEL_ID = "change_timetable_channel" + } + + override fun create() { + notificationManager.createNotificationChannel( + NotificationChannel( + CHANNEL_ID, + context.getString(R.string.channel_change_timetable), + NotificationManager.IMPORTANCE_HIGH + ) + .apply { + enableLights(true) + enableVibration(true) + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + }) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt index ddad9bf27..542b9346b 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt @@ -4,19 +4,15 @@ import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Context import android.os.Build -import androidx.annotation.PluralsRes import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Notification import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotificationsData +import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData -import io.github.wulkanowy.data.pojos.OneNotificationData import io.github.wulkanowy.data.repositories.NotificationRepository -import io.github.wulkanowy.ui.modules.main.MainActivity -import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.getCompatBitmap import io.github.wulkanowy.utils.getCompatColor import io.github.wulkanowy.utils.nickOrName @@ -27,102 +23,17 @@ import kotlin.random.Random class AppNotificationManager @Inject constructor( private val notificationManager: NotificationManagerCompat, @ApplicationContext private val context: Context, - private val appInfo: AppInfo, private val notificationRepository: NotificationRepository ) { - suspend fun sendNotification(notificationData: NotificationData, student: Student) = - when (notificationData) { - is OneNotificationData -> sendOneNotification(notificationData, student) - is MultipleNotificationsData -> sendMultipleNotifications(notificationData, student) - } - - private suspend fun sendOneNotification( - notificationData: OneNotificationData, - student: Student - ) { - val content = context.getString( - notificationData.contentStringRes, - *notificationData.contentValues.toTypedArray() - ) - - val title = context.getString(notificationData.titleStringRes) - - val notification = getDefaultNotificationBuilder(notificationData) - .setContentTitle(title) - .setContentText(content) - .setStyle( - NotificationCompat.BigTextStyle() - .setSummaryText(student.nickOrName) - .bigText(content) - ) - .build() - - notificationManager.notify(Random.nextInt(Int.MAX_VALUE), notification) - - saveNotification(title, content, notificationData, student) - } - - private suspend fun sendMultipleNotifications( - notificationData: MultipleNotificationsData, - student: Student - ) { - val groupType = notificationData.type.group ?: return - val group = "${groupType}_${student.id}" - - notificationData.sendSummaryNotification(group, student) - - notificationData.lines.forEach { item -> - val title = context.resources.getQuantityString(notificationData.titleStringRes, 1) - - val notification = getDefaultNotificationBuilder(notificationData) - .setContentTitle(title) - .setContentText(item) - .setStyle( - NotificationCompat.BigTextStyle() - .setSummaryText(student.nickOrName) - .bigText(item) - ) - .setGroup(group) - .build() - - notificationManager.notify(Random.nextInt(Int.MAX_VALUE), notification) - - saveNotification(title, item, notificationData, student) - } - } - - private fun MultipleNotificationsData.sendSummaryNotification(group: String, student: Student) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return - - val summaryNotification = getDefaultNotificationBuilder(this) - .setSmallIcon(icon) - .setContentTitle(getQuantityString(titleStringRes, lines.size)) - .setContentText(getQuantityString(contentStringRes, lines.size)) - .setStyle( - NotificationCompat.InboxStyle() - .setSummaryText(student.nickOrName) - .also { builder -> lines.forEach { builder.addLine(it) } } - ) - .setLocalOnly(true) - .setGroup(group) - .setGroupSummary(true) - .build() - - val groupId = student.id * 100 + type.ordinal - notificationManager.notify(groupId.toInt(), summaryNotification) - } - @SuppressLint("InlinedApi") - private fun getDefaultNotificationBuilder(notificationData: NotificationData): NotificationCompat.Builder { - val pendingIntentsFlags = if (appInfo.systemVersion >= Build.VERSION_CODES.M) { - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - } else { - PendingIntent.FLAG_UPDATE_CURRENT - } - - return NotificationCompat.Builder(context, notificationData.type.channel) - .setLargeIcon(context.getCompatBitmap(notificationData.icon, R.color.colorPrimary)) + suspend fun sendSingleNotification( + notificationData: NotificationData, + notificationType: NotificationType, + student: Student + ) { + val notification = NotificationCompat.Builder(context, notificationType.channel) + .setLargeIcon(context.getCompatBitmap(notificationType.icon, R.color.colorPrimary)) .setSmallIcon(R.drawable.ic_stat_all) .setAutoCancel(true) .setDefaults(NotificationCompat.DEFAULT_ALL) @@ -132,31 +43,122 @@ class AppNotificationManager @Inject constructor( .setContentIntent( PendingIntent.getActivity( context, - notificationData.startMenu.id, - MainActivity.getStartIntent(context, notificationData.startMenu, true), - pendingIntentsFlags + Random.nextInt(), + notificationData.intentToStart, + PendingIntent.FLAG_UPDATE_CURRENT ) ) + .setContentTitle(notificationData.title) + .setContentText(notificationData.content) + .setStyle( + NotificationCompat.BigTextStyle() + .setSummaryText(student.nickOrName) + .bigText(notificationData.content) + ) + .build() + + notificationManager.notify(Random.nextInt(), notification) + saveNotification(notificationData, notificationType, student) + } + + @SuppressLint("InlinedApi") + suspend fun sendMultipleNotifications( + groupNotificationData: GroupNotificationData, + student: Student + ) { + val notificationType = groupNotificationData.type + val groupType = notificationType.group ?: return + val group = "${groupType}_${student.id}" + + sendSummaryNotification(groupNotificationData, group, student) + + groupNotificationData.notificationDataList.forEach { notificationData -> + val notification = NotificationCompat.Builder(context, notificationType.channel) + .setLargeIcon(context.getCompatBitmap(notificationType.icon, R.color.colorPrimary)) + .setSmallIcon(R.drawable.ic_stat_all) + .setAutoCancel(true) + .setDefaults(NotificationCompat.DEFAULT_ALL) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setColor(context.getCompatColor(R.color.colorPrimary)) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) + .setContentIntent( + PendingIntent.getActivity( + context, + Random.nextInt(), + notificationData.intentToStart, + PendingIntent.FLAG_UPDATE_CURRENT + ) + ) + .setContentTitle(notificationData.title) + .setContentText(notificationData.content) + .setStyle( + NotificationCompat.BigTextStyle() + .setSummaryText(student.nickOrName) + .bigText(notificationData.content) + ) + .setGroup(group) + .build() + + notificationManager.notify(Random.nextInt(), notification) + saveNotification(notificationData, groupNotificationData.type, student) + } + } + + private fun sendSummaryNotification( + groupNotificationData: GroupNotificationData, + group: String, + student: Student + ) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return + + val summaryNotification = + NotificationCompat.Builder(context, groupNotificationData.type.channel) + .setContentTitle(groupNotificationData.title) + .setContentText(groupNotificationData.content) + .setSmallIcon(groupNotificationData.type.icon) + .setAutoCancel(true) + .setDefaults(NotificationCompat.DEFAULT_ALL) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setColor(context.getCompatColor(R.color.colorPrimary)) + .setStyle( + NotificationCompat.InboxStyle() + .setSummaryText(student.nickOrName) + .also { builder -> + groupNotificationData.notificationDataList.forEach { + builder.addLine(it.content) + } + } + ) + .setContentIntent( + PendingIntent.getActivity( + context, + Random.nextInt(), + groupNotificationData.intentToStart, + PendingIntent.FLAG_UPDATE_CURRENT + ) + ) + .setLocalOnly(true) + .setGroup(group) + .setGroupSummary(true) + .build() + + val groupId = student.id * 100 + groupNotificationData.type.ordinal + notificationManager.notify(groupId.toInt(), summaryNotification) } private suspend fun saveNotification( - title: String, - content: String, notificationData: NotificationData, + notificationType: NotificationType, student: Student ) { val notificationEntity = Notification( studentId = student.id, - title = title, - content = content, - type = notificationData.type, + title = notificationData.title, + content = notificationData.content, + type = notificationType, date = LocalDateTime.now() ) notificationRepository.saveNotification(notificationEntity) } - - private fun getQuantityString(@PluralsRes res: Int, arg: Int): String { - return context.resources.getQuantityString(res, arg, arg) - } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt new file mode 100644 index 000000000..6d2d3a59f --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt @@ -0,0 +1,125 @@ +package io.github.wulkanowy.services.sync.notifications + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.Timetable +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.getPlural +import io.github.wulkanowy.utils.toFormattedString +import java.time.LocalDate +import java.time.LocalDateTime +import javax.inject.Inject + +class ChangeTimetableNotification @Inject constructor( + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context, +) { + + suspend fun notify(items: List, student: Student) { + val currentTime = LocalDateTime.now() + val changedLessons = items.filter { (it.canceled || it.changes) && it.start > currentTime } + val notificationDataList = changedLessons.groupBy { it.date } + .map { (date, lessons) -> + getNotificationContents(date, lessons).map { + NotificationData( + title = context.getPlural( + R.plurals.timetable_notify_new_items_title, + 1 + ), + content = it, + intentToStart = MainActivity.getStartIntent( + context = context, + destination = Destination.Timetable(date), + startNewTask = true + ) + ) + } + } + .flatten() + .ifEmpty { return } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural( + R.plurals.timetable_notify_new_items_title, + changedLessons.size + ), + content = context.getPlural( + R.plurals.timetable_notify_new_items_group, + changedLessons.size, + changedLessons.size + ), + intentToStart = MainActivity.getStartIntent(context, Destination.Timetable(), true), + type = NotificationType.CHANGE_TIMETABLE + ) + + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) + } + + private fun getNotificationContents(date: LocalDate, lessons: List): List { + val formattedDate = date.toFormattedString("EEE dd.MM") + + return if (lessons.size > 2) { + listOf( + context.getPlural( + R.plurals.timetable_notify_new_items, + lessons.size, + formattedDate, + lessons.size, + ) + ) + } else { + lessons.map { + buildString { + append( + context.getString( + R.string.timetable_notify_lesson, + formattedDate, + it.number, + it.subject + ) + ) + if (it.roomOld.isNotBlank()) { + appendLine() + append( + context.getString( + R.string.timetable_notify_change_room, + it.roomOld, + it.room + ) + ) + } + if (it.teacherOld.isNotBlank() && it.teacher != it.teacherOld) { + appendLine() + append( + context.getString( + R.string.timetable_notify_change_teacher, + it.teacherOld, + it.teacher + ) + ) + } + if (it.subjectOld.isNotBlank()) { + appendLine() + append( + context.getString( + R.string.timetable_notify_change_subject, + it.subjectOld, + it.subject + ) + ) + } + if (it.info.isNotBlank()) { + appendLine() + append(it.info) + } + } + } + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewAttendanceNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewAttendanceNotification.kt new file mode 100644 index 000000000..695552721 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewAttendanceNotification.kt @@ -0,0 +1,55 @@ +package io.github.wulkanowy.services.sync.notifications + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Attendance +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.descriptionRes +import io.github.wulkanowy.utils.getPlural +import io.github.wulkanowy.utils.toFormattedString +import javax.inject.Inject + +class NewAttendanceNotification @Inject constructor( + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context +) { + + suspend fun notify(items: List, student: Student) { + val lines = items.filterNot { it.presence || it.name == "UNKNOWN" } + .map { + val description = context.getString(it.descriptionRes) + "${it.date.toFormattedString("dd.MM")} - ${it.subject}: $description" + } + .ifEmpty { return } + + val notificationDataList = lines.map { + NotificationData( + title = context.getPlural(R.plurals.attendance_notify_new_items_title, 1), + content = it, + intentToStart = MainActivity.getStartIntent(context, Destination.Attendance, true) + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural( + R.plurals.attendance_notify_new_items_title, + notificationDataList.size + ), + content = context.getPlural( + R.plurals.attendance_notify_new_items, + notificationDataList.size, + notificationDataList.size + ), + intentToStart = MainActivity.getStartIntent(context, Destination.Attendance, true), + type = NotificationType.NEW_ATTENDANCE + ) + + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt index 994cb8d4b..97b1332d8 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt @@ -1,34 +1,52 @@ package io.github.wulkanowy.services.sync.notifications +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotificationsData -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDateTime import javax.inject.Inject class NewConferenceNotification @Inject constructor( - private val appNotificationManager: AppNotificationManager + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context ) { suspend fun notify(items: List, student: Student) { val today = LocalDateTime.now() - val lines = items.filter { !it.date.isBefore(today) }.map { - "${it.date.toFormattedString("dd.MM")} - ${it.title}: ${it.subject}" - }.ifEmpty { return } + val lines = items.filter { !it.date.isBefore(today) } + .map { + "${it.date.toFormattedString("dd.MM")} - ${it.title}: ${it.subject}" + } + .ifEmpty { return } - val notification = MultipleNotificationsData( - type = NotificationType.NEW_CONFERENCE, - icon = R.drawable.ic_more_conferences, - titleStringRes = R.plurals.conference_notify_new_item_title, - contentStringRes = R.plurals.conference_notify_new_items, - summaryStringRes = R.plurals.conference_number_item, - startMenu = MainView.Section.CONFERENCE, - lines = lines + val notificationDataList = lines.map { + NotificationData( + title = context.getPlural(R.plurals.conference_notify_new_item_title, 1), + content = it, + intentToStart = MainActivity.getStartIntent(context, Destination.Conference, true) + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural(R.plurals.conference_notify_new_item_title, lines.size), + content = context.getPlural( + R.plurals.conference_notify_new_items, + lines.size, + lines.size + ), + intentToStart = MainActivity.getStartIntent(context, Destination.Conference, true), + type = NotificationType.NEW_CONFERENCE ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt index f148fa34f..6f8ed8961 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt @@ -1,34 +1,52 @@ package io.github.wulkanowy.services.sync.notifications +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotificationsData -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDate import javax.inject.Inject class NewExamNotification @Inject constructor( - private val appNotificationManager: AppNotificationManager + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context ) { suspend fun notify(items: List, student: Student) { val today = LocalDate.now() - val lines = items.filter { !it.date.isBefore(today) }.map { - "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.description}" - }.ifEmpty { return } + val lines = items.filter { !it.date.isBefore(today) } + .map { + "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.description}" + } + .ifEmpty { return } - val notification = MultipleNotificationsData( - type = NotificationType.NEW_EXAM, - icon = R.drawable.ic_main_exam, - titleStringRes = R.plurals.exam_notify_new_item_title, - contentStringRes = R.plurals.exam_notify_new_item_content, - summaryStringRes = R.plurals.exam_number_item, - startMenu = MainView.Section.EXAM, - lines = lines + val notificationDataList = lines.map { + NotificationData( + title = context.getPlural(R.plurals.exam_notify_new_item_title, 1), + content = it, + intentToStart = MainActivity.getStartIntent(context, Destination.Exam, true), + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural(R.plurals.exam_notify_new_item_title, lines.size), + content = context.getPlural( + R.plurals.exam_notify_new_item_content, + lines.size, + lines.size + ), + intentToStart = MainActivity.getStartIntent(context, Destination.Exam, true), + type = NotificationType.NEW_EXAM ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt index 52bdff588..09692fb7c 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt @@ -1,62 +1,88 @@ package io.github.wulkanowy.services.sync.notifications +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotificationsData -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.getPlural import javax.inject.Inject class NewGradeNotification @Inject constructor( - private val appNotificationManager: AppNotificationManager + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context ) { suspend fun notifyDetails(items: List, student: Student) { - val notification = MultipleNotificationsData( - type = NotificationType.NEW_GRADE_DETAILS, - icon = R.drawable.ic_stat_grade, - titleStringRes = R.plurals.grade_new_items, - contentStringRes = R.plurals.grade_notify_new_items, - summaryStringRes = R.plurals.grade_number_item, - startMenu = MainView.Section.GRADE, - lines = items.map { - "${it.subject}: ${it.entry}" - } + val notificationDataList = items.map { + NotificationData( + title = context.getPlural(R.plurals.grade_new_items, 1), + content = "${it.subject}: ${it.entry}", + intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural(R.plurals.grade_new_items, items.size), + content = context.getPlural(R.plurals.grade_notify_new_items, items.size, items.size), + intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + type = NotificationType.NEW_GRADE_DETAILS ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } suspend fun notifyPredicted(items: List, student: Student) { - val notification = MultipleNotificationsData( - type = NotificationType.NEW_GRADE_PREDICTED, - icon = R.drawable.ic_stat_grade, - titleStringRes = R.plurals.grade_new_items_predicted, - contentStringRes = R.plurals.grade_notify_new_items_predicted, - summaryStringRes = R.plurals.grade_number_item, - startMenu = MainView.Section.GRADE, - lines = items.map { - "${it.subject}: ${it.predictedGrade}" - } + val notificationDataList = items.map { + NotificationData( + title = context.getPlural(R.plurals.grade_new_items_predicted, 1), + content = "${it.subject}: ${it.predictedGrade}", + intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural(R.plurals.grade_new_items_predicted, items.size), + content = context.getPlural( + R.plurals.grade_notify_new_items_predicted, + items.size, + items.size + ), + intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + type = NotificationType.NEW_GRADE_PREDICTED ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } suspend fun notifyFinal(items: List, student: Student) { - val notification = MultipleNotificationsData( - type = NotificationType.NEW_GRADE_FINAL, - icon = R.drawable.ic_stat_grade, - titleStringRes = R.plurals.grade_new_items_final, - contentStringRes = R.plurals.grade_notify_new_items_final, - summaryStringRes = R.plurals.grade_number_item, - startMenu = MainView.Section.GRADE, - lines = items.map { - "${it.subject}: ${it.finalGrade}" - } + val notificationDataList = items.map { + NotificationData( + title = context.getPlural(R.plurals.grade_new_items_final, 1), + content = "${it.subject}: ${it.finalGrade}", + intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural(R.plurals.grade_new_items_final, items.size), + content = context.getPlural( + R.plurals.grade_notify_new_items_final, + items.size, + items.size + ), + intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + type = NotificationType.NEW_GRADE_FINAL ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt index 4c34cb8ff..cdada844e 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt @@ -1,34 +1,52 @@ package io.github.wulkanowy.services.sync.notifications +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Homework import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotificationsData -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDate import javax.inject.Inject class NewHomeworkNotification @Inject constructor( - private val appNotificationManager: AppNotificationManager + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context ) { suspend fun notify(items: List, student: Student) { val today = LocalDate.now() - val lines = items.filter { !it.date.isBefore(today) }.map { - "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.content}" - }.ifEmpty { return } + val lines = items.filter { !it.date.isBefore(today) } + .map { + "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.content}" + } + .ifEmpty { return } - val notification = MultipleNotificationsData( + val notificationDataList = lines.map { + NotificationData( + title = context.getPlural(R.plurals.homework_notify_new_item_title, 1), + content = it, + intentToStart = MainActivity.getStartIntent(context, Destination.Homework, true), + ) + } + + val groupNotificationData = GroupNotificationData( + title = context.getPlural(R.plurals.homework_notify_new_item_title, lines.size), + content = context.getPlural( + R.plurals.homework_notify_new_item_content, + lines.size, + lines.size + ), + intentToStart = MainActivity.getStartIntent(context, Destination.Homework, true), type = NotificationType.NEW_HOMEWORK, - icon = R.drawable.ic_more_homework, - titleStringRes = R.plurals.homework_notify_new_item_title, - contentStringRes = R.plurals.homework_notify_new_item_content, - summaryStringRes = R.plurals.homework_number_item, - startMenu = MainView.Section.HOMEWORK, - lines = lines + notificationDataList = notificationDataList ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt index 08c985106..d9f138b5c 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt @@ -1,26 +1,34 @@ package io.github.wulkanowy.services.sync.notifications +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.OneNotificationData -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity import javax.inject.Inject class NewLuckyNumberNotification @Inject constructor( - private val appNotificationManager: AppNotificationManager + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context ) { - suspend fun notify(item: LuckyNumber, student: Student) { - val notification = OneNotificationData( - type = NotificationType.NEW_LUCKY_NUMBER, - icon = R.drawable.ic_stat_luckynumber, - titleStringRes = R.string.lucky_number_notify_new_item_title, - contentStringRes = R.string.lucky_number_notify_new_item, - startMenu = MainView.Section.LUCKY_NUMBER, - contentValues = listOf(item.luckyNumber.toString()) - ) + suspend fun notify(item: LuckyNumber, student: Student) { + val notificationData = NotificationData( + title = context.getString(R.string.lucky_number_notify_new_item_title), + content = context.getString( + R.string.lucky_number_notify_new_item, + item.luckyNumber.toString() + ), + intentToStart = MainActivity.getStartIntent(context, Destination.LuckyNumber, true) + ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendSingleNotification( + notificationData = notificationData, + notificationType = NotificationType.NEW_LUCKY_NUMBER, + student = student + ) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt index a6d503aa0..cdb5ab9c8 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt @@ -1,29 +1,39 @@ package io.github.wulkanowy.services.sync.notifications +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotificationsData -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.getPlural import javax.inject.Inject class NewMessageNotification @Inject constructor( - private val appNotificationManager: AppNotificationManager + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context ) { suspend fun notify(items: List, student: Student) { - val notification = MultipleNotificationsData( - type = NotificationType.NEW_MESSAGE, - icon = R.drawable.ic_stat_message, - titleStringRes = R.plurals.message_new_items, - contentStringRes = R.plurals.message_notify_new_items, - summaryStringRes = R.plurals.message_number_item, - startMenu = MainView.Section.MESSAGE, - lines = items.map { - "${it.sender}: ${it.subject}" - } + val notificationDataList = items.map { + NotificationData( + title = context.getPlural(R.plurals.message_new_items, 1), + content = "${it.sender}: ${it.subject}", + intentToStart = MainActivity.getStartIntent(context, Destination.Message, true), + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural(R.plurals.message_new_items, items.size), + content = context.getPlural(R.plurals.message_notify_new_items, items.size, items.size), + intentToStart = MainActivity.getStartIntent(context, Destination.Message, true), + type = NotificationType.NEW_MESSAGE ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt index ffa3cc9cb..16be1ca50 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt @@ -1,42 +1,46 @@ package io.github.wulkanowy.services.sync.notifications +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Note import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotificationsData +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.sdk.scrapper.notes.NoteCategory -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.getPlural import javax.inject.Inject class NewNoteNotification @Inject constructor( - private val appNotificationManager: AppNotificationManager + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context ) { suspend fun notify(items: List, student: Student) { - val notification = MultipleNotificationsData( - type = NotificationType.NEW_NOTE, - icon = R.drawable.ic_stat_note, - titleStringRes = when (NoteCategory.getByValue(items.first().categoryType)) { + val notificationDataList = items.map { + val titleRes = when (NoteCategory.getByValue(it.categoryType)) { NoteCategory.POSITIVE -> R.plurals.praise_new_items NoteCategory.NEUTRAL -> R.plurals.neutral_note_new_items else -> R.plurals.note_new_items - }, - contentStringRes = when (NoteCategory.getByValue(items.first().categoryType)) { - NoteCategory.POSITIVE -> R.plurals.praise_notify_new_items - NoteCategory.NEUTRAL -> R.plurals.neutral_note_notify_new_items - else -> R.plurals.note_notify_new_items - }, - summaryStringRes = when (NoteCategory.getByValue(items.first().categoryType)) { - NoteCategory.POSITIVE -> R.plurals.praise_number_item - NoteCategory.NEUTRAL -> R.plurals.neutral_note_number_item - else -> R.plurals.note_number_item - }, - startMenu = MainView.Section.NOTE, - lines = items.map { - "${it.teacher}: ${it.category}" } + + NotificationData( + title = context.getPlural(titleRes, 1), + content = "${it.teacher}: ${it.category}", + intentToStart = MainActivity.getStartIntent(context, Destination.Note, true), + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + intentToStart = MainActivity.getStartIntent(context, Destination.Note, true), + title = context.getPlural(R.plurals.note_new_items, items.size), + content = context.getPlural(R.plurals.note_notify_new_items, items.size, items.size), + type = NotificationType.NEW_NOTE ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt index 990a950b1..1f603624b 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt @@ -1,29 +1,56 @@ package io.github.wulkanowy.services.sync.notifications +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotificationsData -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.getPlural import javax.inject.Inject class NewSchoolAnnouncementNotification @Inject constructor( - private val appNotificationManager: AppNotificationManager + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context ) { - suspend fun notify(items: List, student: Student) { - val notification = MultipleNotificationsData( - type = NotificationType.NEW_ANNOUNCEMENT, - icon = R.drawable.ic_all_about, - titleStringRes = R.plurals.school_announcement_notify_new_item_title, - contentStringRes = R.plurals.school_announcement_notify_new_items, - summaryStringRes = R.plurals.school_announcement_number_item, - startMenu = MainView.Section.SCHOOL_ANNOUNCEMENT, - lines = items.map { - "${it.subject}: ${it.content}" - } + suspend fun notify(items: List, student: Student) { + val notificationDataList = items.map { + NotificationData( + intentToStart = MainActivity.getStartIntent( + context = context, + destination = Destination.SchoolAnnouncement, + startNewTask = true + ), + title = context.getPlural( + R.plurals.school_announcement_notify_new_item_title, + 1 + ), + content = "${it.subject}: ${it.content}" + ) + } + val groupNotificationData = GroupNotificationData( + type = NotificationType.NEW_ANNOUNCEMENT, + intentToStart = MainActivity.getStartIntent( + context = context, + destination = Destination.SchoolAnnouncement, + startNewTask = true + ), + title = context.getPlural( + R.plurals.school_announcement_notify_new_item_title, + items.size + ), + content = context.getPlural( + R.plurals.school_announcement_notify_new_items, + items.size, + items.size + ), + notificationDataList = notificationDataList ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt index 49cbcfe9e..af79fcd2a 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt @@ -1,6 +1,8 @@ package io.github.wulkanowy.services.sync.notifications +import io.github.wulkanowy.R import io.github.wulkanowy.services.sync.channels.LuckyNumberChannel +import io.github.wulkanowy.services.sync.channels.NewAttendanceChannel import io.github.wulkanowy.services.sync.channels.NewConferencesChannel import io.github.wulkanowy.services.sync.channels.NewExamChannel import io.github.wulkanowy.services.sync.channels.NewGradesChannel @@ -9,17 +11,76 @@ import io.github.wulkanowy.services.sync.channels.NewMessagesChannel import io.github.wulkanowy.services.sync.channels.NewNotesChannel import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel import io.github.wulkanowy.services.sync.channels.PushChannel +import io.github.wulkanowy.services.sync.channels.TimetableChangeChannel -enum class NotificationType(val group: String?, val channel: String) { - NEW_CONFERENCE("new_conferences_group", NewConferencesChannel.CHANNEL_ID), - NEW_EXAM("new_exam_group", NewExamChannel.CHANNEL_ID), - NEW_GRADE_DETAILS("new_grade_details_group", NewGradesChannel.CHANNEL_ID), - NEW_GRADE_PREDICTED("new_grade_predicted_group", NewGradesChannel.CHANNEL_ID), - NEW_GRADE_FINAL("new_grade_final_group", NewGradesChannel.CHANNEL_ID), - NEW_HOMEWORK("new_homework_group", NewHomeworkChannel.CHANNEL_ID), - NEW_LUCKY_NUMBER("lucky_number_group", LuckyNumberChannel.CHANNEL_ID), - NEW_MESSAGE("new_message_group", NewMessagesChannel.CHANNEL_ID), - NEW_NOTE("new_notes_group", NewNotesChannel.CHANNEL_ID), - NEW_ANNOUNCEMENT("new_school_announcements_group", NewSchoolAnnouncementsChannel.CHANNEL_ID), - PUSH(null, PushChannel.CHANNEL_ID) +enum class NotificationType( + val group: String?, + val channel: String, + val icon: Int +) { + NEW_CONFERENCE( + group = "new_conferences_group", + channel = NewConferencesChannel.CHANNEL_ID, + icon = R.drawable.ic_more_conferences, + ), + NEW_EXAM( + group = "new_exam_group", + channel = NewExamChannel.CHANNEL_ID, + icon = R.drawable.ic_main_exam + ), + NEW_GRADE_DETAILS( + group = "new_grade_details_group", + channel = NewGradesChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_grade, + ), + NEW_GRADE_PREDICTED( + group = "new_grade_predicted_group", + channel = NewGradesChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_grade, + ), + NEW_GRADE_FINAL( + group = "new_grade_final_group", + channel = NewGradesChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_grade, + ), + NEW_HOMEWORK( + group = "new_homework_group", + channel = NewHomeworkChannel.CHANNEL_ID, + icon = R.drawable.ic_more_homework, + ), + NEW_LUCKY_NUMBER( + group = null, + channel = LuckyNumberChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_luckynumber, + ), + NEW_MESSAGE( + group = "new_message_group", + channel = NewMessagesChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_message, + ), + NEW_NOTE( + group = "new_notes_group", + channel = NewNotesChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_note + ), + NEW_ANNOUNCEMENT( + group = "new_school_announcements_group", + channel = NewSchoolAnnouncementsChannel.CHANNEL_ID, + icon = R.drawable.ic_all_about + ), + CHANGE_TIMETABLE( + group = "change_timetable_group", + channel = TimetableChangeChannel.CHANNEL_ID, + icon = R.drawable.ic_main_timetable + ), + NEW_ATTENDANCE( + group = "new_attendance_group", + channel = NewAttendanceChannel.CHANNEL_ID, + icon = R.drawable.ic_main_attendance + ), + PUSH( + group = null, + channel = PushChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_all + ) } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt index 4823b2b5b..f7b680e31 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt @@ -3,18 +3,40 @@ package io.github.wulkanowy.services.sync.works import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.AttendanceRepository -import io.github.wulkanowy.utils.monday -import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.services.sync.notifications.NewAttendanceNotification +import io.github.wulkanowy.utils.previousOrSameSchoolDay import io.github.wulkanowy.utils.waitForResult +import kotlinx.coroutines.flow.first import java.time.LocalDate.now import javax.inject.Inject class AttendanceWork @Inject constructor( - private val attendanceRepository: AttendanceRepository + private val attendanceRepository: AttendanceRepository, + private val newAttendanceNotification: NewAttendanceNotification, + private val preferencesRepository: PreferencesRepository ) : Work { override suspend fun doWork(student: Student, semester: Semester) { - attendanceRepository.getAttendance(student, semester, now().monday, now().sunday, true) + attendanceRepository.getAttendance( + student = student, + semester = semester, + start = now().previousOrSameSchoolDay, + end = now().previousOrSameSchoolDay, + forceRefresh = true, + notify = preferencesRepository.isNotificationsEnable + ) .waitForResult() + + attendanceRepository.getAttendanceFromDatabase(semester, now().minusDays(7), now()) + .first() + .filterNot { it.isNotified } + .let { + if (it.isNotEmpty()) newAttendanceNotification.notify(it, student) + + attendanceRepository.updateTimetable(it.onEach { attendance -> + attendance.isNotified = true + }) + } } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/TimetableWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/TimetableWork.kt index 2df2c9dcb..fcc330638 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/TimetableWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/TimetableWork.kt @@ -2,18 +2,41 @@ package io.github.wulkanowy.services.sync.works import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.TimetableRepository -import io.github.wulkanowy.utils.monday -import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.services.sync.notifications.ChangeTimetableNotification +import io.github.wulkanowy.utils.nextOrSameSchoolDay import io.github.wulkanowy.utils.waitForResult +import kotlinx.coroutines.flow.first import java.time.LocalDate.now import javax.inject.Inject class TimetableWork @Inject constructor( - private val timetableRepository: TimetableRepository + private val timetableRepository: TimetableRepository, + private val changeTimetableNotification: ChangeTimetableNotification, + private val preferencesRepository: PreferencesRepository ) : Work { override suspend fun doWork(student: Student, semester: Semester) { - timetableRepository.getTimetable(student, semester, now().monday, now().sunday, true).waitForResult() + timetableRepository.getTimetable( + student = student, + semester = semester, + start = now().nextOrSameSchoolDay, + end = now().nextOrSameSchoolDay, + forceRefresh = true, + notify = preferencesRepository.isNotificationsEnable + ) + .waitForResult() + + timetableRepository.getTimetableFromDatabase(semester, now(), now().plusDays(7)) + .first() + .filterNot { it.isNotified } + .let { + if (it.isNotEmpty()) changeTimetableNotification.notify(it, student) + + timetableRepository.updateTimetable(it.onEach { timetable -> + timetable.isNotified = true + }) + } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt new file mode 100644 index 000000000..0406afa41 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt @@ -0,0 +1,132 @@ +package io.github.wulkanowy.ui.modules + +import androidx.fragment.app.Fragment +import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment +import io.github.wulkanowy.ui.modules.conference.ConferenceFragment +import io.github.wulkanowy.ui.modules.dashboard.DashboardFragment +import io.github.wulkanowy.ui.modules.exam.ExamFragment +import io.github.wulkanowy.ui.modules.grade.GradeFragment +import io.github.wulkanowy.ui.modules.homework.HomeworkFragment +import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment +import io.github.wulkanowy.ui.modules.message.MessageFragment +import io.github.wulkanowy.ui.modules.more.MoreFragment +import io.github.wulkanowy.ui.modules.note.NoteFragment +import io.github.wulkanowy.ui.modules.schoolandteachers.school.SchoolFragment +import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment +import io.github.wulkanowy.ui.modules.timetable.TimetableFragment +import java.io.Serializable +import java.time.LocalDate + +sealed interface Destination : Serializable { + + val type: Type + + val fragment: Fragment + + enum class Type(val defaultDestination: Destination) { + DASHBOARD(Dashboard), + GRADE(Grade), + ATTENDANCE(Attendance), + EXAM(Exam), + TIMETABLE(Timetable()), + HOMEWORK(Homework), + NOTE(Note), + CONFERENCE(Conference), + SCHOOL_ANNOUNCEMENT(SchoolAnnouncement), + SCHOOL(School), + LUCKY_NUMBER(More), + MORE(More), + MESSAGE(Message); + } + + object Dashboard : Destination { + + override val type = Type.DASHBOARD + + override val fragment get() = DashboardFragment.newInstance() + } + + object Grade : Destination { + + override val type = Type.GRADE + + override val fragment get() = GradeFragment.newInstance() + } + + object Attendance : Destination { + + override val type = Type.ATTENDANCE + + override val fragment get() = AttendanceFragment.newInstance() + } + + object Exam : Destination { + + override val type = Type.EXAM + + override val fragment get() = ExamFragment.newInstance() + } + + data class Timetable(val date: LocalDate? = null) : Destination { + + override val type = Type.TIMETABLE + + override val fragment get() = TimetableFragment.newInstance(date) + } + + object Homework : Destination { + + override val type = Type.HOMEWORK + + override val fragment get() = HomeworkFragment.newInstance() + } + + object Note : Destination { + + override val type = Type.NOTE + + override val fragment get() = NoteFragment.newInstance() + } + + object Conference : Destination { + + override val type = Type.CONFERENCE + + override val fragment get() = ConferenceFragment.newInstance() + } + + object SchoolAnnouncement : Destination { + + override val type = Type.SCHOOL_ANNOUNCEMENT + + override val fragment get() = SchoolAnnouncementFragment.newInstance() + } + + object School : Destination { + + override val type = Type.SCHOOL + + override val fragment get() = SchoolFragment.newInstance() + } + + object LuckyNumber : Destination { + + override val type = Type.LUCKY_NUMBER + + override val fragment get() = LuckyNumberFragment.newInstance() + } + + object More : Destination { + + override val type = Type.MORE + + override val fragment get() = MoreFragment.newInstance() + } + + object Message : Destination { + + override val type = Type.MESSAGE + + override val fragment get() = MessageFragment.newInstance() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt index bb4f70224..5d5ed504c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt @@ -9,7 +9,7 @@ import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.enums.SentExcuseStatus import io.github.wulkanowy.databinding.ItemAttendanceBinding -import io.github.wulkanowy.utils.description +import io.github.wulkanowy.utils.descriptionRes import io.github.wulkanowy.utils.isExcusableOrNotExcused import javax.inject.Inject @@ -36,7 +36,7 @@ class AttendanceAdapter @Inject constructor() : with(holder.binding) { attendanceItemNumber.text = item.number.toString() attendanceItemSubject.text = item.subject - attendanceItemDescription.setText(item.description) + attendanceItemDescription.setText(item.descriptionRes) attendanceItemAlert.visibility = item.run { if (absence && !excused) View.VISIBLE else View.INVISIBLE } attendanceItemNumber.visibility = View.GONE attendanceItemExcuseInfo.visibility = View.GONE diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt index d816d8f00..9b5c63e4c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt @@ -7,7 +7,7 @@ import android.view.ViewGroup import androidx.fragment.app.DialogFragment import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.databinding.DialogAttendanceBinding -import io.github.wulkanowy.utils.description +import io.github.wulkanowy.utils.descriptionRes import io.github.wulkanowy.utils.lifecycleAwareVariable import io.github.wulkanowy.utils.toFormattedString @@ -45,7 +45,7 @@ class AttendanceDialog : DialogFragment() { with(binding) { attendanceDialogSubjectValue.text = attendance.subject - attendanceDialogDescriptionValue.setText(attendance.description) + attendanceDialogDescriptionValue.setText(attendance.descriptionRes) attendanceDialogDateValue.text = attendance.date.toFormattedString() attendanceDialogNumberValue.text = attendance.number.toString() attendanceDialogClose.setOnClickListener { dismiss() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt index 26eca18fd..d0dfcd696 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt @@ -3,6 +3,8 @@ package io.github.wulkanowy.ui.modules.debug.notification import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.services.sync.notifications.ChangeTimetableNotification +import io.github.wulkanowy.services.sync.notifications.NewAttendanceNotification import io.github.wulkanowy.services.sync.notifications.NewConferenceNotification import io.github.wulkanowy.services.sync.notifications.NewExamNotification import io.github.wulkanowy.services.sync.notifications.NewGradeNotification @@ -13,6 +15,7 @@ import io.github.wulkanowy.services.sync.notifications.NewNoteNotification import io.github.wulkanowy.services.sync.notifications.NewSchoolAnnouncementNotification import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler +import io.github.wulkanowy.ui.modules.debug.notification.mock.debugAttendanceItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugConferenceItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugExamItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugGradeDetailsItems @@ -22,6 +25,7 @@ import io.github.wulkanowy.ui.modules.debug.notification.mock.debugLuckyNumber import io.github.wulkanowy.ui.modules.debug.notification.mock.debugMessageItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugNoteItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugSchoolAnnouncementItems +import io.github.wulkanowy.ui.modules.debug.notification.mock.debugTimetableItems import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -37,6 +41,8 @@ class NotificationDebugPresenter @Inject constructor( private val newNoteNotification: NewNoteNotification, private val newSchoolAnnouncementNotification: NewSchoolAnnouncementNotification, private val newLuckyNumberNotification: NewLuckyNumberNotification, + private val changeTimetableNotification: ChangeTimetableNotification, + private val newAttendanceNotification: NewAttendanceNotification, ) : BasePresenter(errorHandler, studentRepository) { private val items = listOf( @@ -64,6 +70,12 @@ class NotificationDebugPresenter @Inject constructor( NotificationDebugItem(R.string.note_title) { n -> withStudent { newNoteNotification.notify(debugNoteItems.take(n), it) } }, + NotificationDebugItem(R.string.attendance_title) { n -> + withStudent { newAttendanceNotification.notify(debugAttendanceItems.take(n), it) } + }, + NotificationDebugItem(R.string.timetable_title) { n -> + withStudent { changeTimetableNotification.notify(debugTimetableItems.take(n), it) } + }, NotificationDebugItem(R.string.school_announcement_title) { n -> withStudent { newSchoolAnnouncementNotification.notify(debugSchoolAnnouncementItems.take(n), it) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/attendance.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/attendance.kt new file mode 100644 index 000000000..042cf07e7 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/attendance.kt @@ -0,0 +1,35 @@ +package io.github.wulkanowy.ui.modules.debug.notification.mock + +import io.github.wulkanowy.data.db.entities.Attendance +import java.time.LocalDate + +val debugAttendanceItems = listOf( + generateAttendance("Matematyka", "PRESENCE"), + generateAttendance("Język angielski", "UNEXCUSED_LATENESS"), + generateAttendance("Geografia", "ABSENCE_UNEXCUSED"), + generateAttendance("Sieci komputerowe", "ABSENCE_EXCUSED"), + generateAttendance("Systemy operacyjne", "EXCUSED_LATENESS"), + generateAttendance("Język niemiecki", "ABSENCE_UNEXCUSED"), + generateAttendance("Biologia", "ABSENCE_UNEXCUSED"), + generateAttendance("Chemia", "ABSENCE_EXCUSED"), + generateAttendance("Fizyka", "ABSENCE_UNEXCUSED"), + generateAttendance("Matematyka", "ABSENCE_EXCUSED"), +) + +private fun generateAttendance(subject: String, name: String) = Attendance( + subject = subject, + studentId = 0, + diaryId = 0, + date = LocalDate.now(), + timeId = 0, + number = 1, + name = name, + presence = false, + absence = false, + exemption = false, + lateness = false, + excused = false, + deleted = false, + excusable = false, + excuseStatus = "" +) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/timetable.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/timetable.kt new file mode 100644 index 000000000..428c001d6 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/timetable.kt @@ -0,0 +1,39 @@ +package io.github.wulkanowy.ui.modules.debug.notification.mock + +import io.github.wulkanowy.data.db.entities.Timetable +import java.time.LocalDate +import java.time.LocalDateTime +import kotlin.random.Random + +val debugTimetableItems = listOf( + generateTimetable("Matematyka", "12", "01"), + generateTimetable("Język angielski", "23", "12"), + generateTimetable("Geografia", "34", "23"), + generateTimetable("Sieci komputerowe", "45", "34"), + generateTimetable("Systemy operacyjne", "56", "45"), + generateTimetable("Język niemiecki", "67", "56"), + generateTimetable("Biologia", "78", "67"), + generateTimetable("Chemia", "89", "78"), + generateTimetable("Fizyka", "90", "89"), + generateTimetable("Matematyka", "01", "90"), +) + +private fun generateTimetable(subject: String, room: String, roomOld: String) = Timetable( + subject = subject, + studentId = 0, + diaryId = 0, + date = LocalDate.now().minusDays(Random.nextLong(0, 8)), + number = 1, + start = LocalDateTime.now().plusHours(1), + end = LocalDateTime.now(), + subjectOld = "", + group = "", + room = room, + roomOld = roomOld, + teacher = "Wtorkowska Renata", + teacherOld = "", + info = "", + isStudentPlan = true, + changes = true, + canceled = true +) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt index e71fc0f6f..04e35d318 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt @@ -66,7 +66,14 @@ class LoginStudentSelectFragment : } override fun openMainView() { - activity?.let { startActivity(MainActivity.getStartIntent(context = it, clear = true)) } + activity?.let { + startActivity( + MainActivity.getStartIntent( + context = it, + startNewTask = true + ) + ) + } } override fun showProgress(show: Boolean) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt index 49a199431..51585ac58 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt @@ -18,8 +18,8 @@ import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.data.repositories.LuckyNumberRepository import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.main.MainActivity -import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.toFirstResult import kotlinx.coroutines.runBlocking import timber.log.Timber @@ -39,6 +39,8 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { companion object { + const val LUCKY_NUMBER_PENDING_INTENT_ID = 200 + fun getStudentWidgetKey(appWidgetId: Int) = "lucky_number_widget_student_$appWidgetId" fun getThemeWidgetKey(appWidgetId: Int) = "lucky_number_widget_theme_$appWidgetId" @@ -48,18 +50,31 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { fun getWidthWidgetKey(appWidgetId: Int) = "lucky_number_widget_width_$appWidgetId" } - override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray?) { + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray? + ) { super.onUpdate(context, appWidgetManager, appWidgetIds) appWidgetIds?.forEach { appWidgetId -> + val luckyNumber = + getLuckyNumber(sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0), appWidgetId) + val appIntent = PendingIntent.getActivity( + context, + LUCKY_NUMBER_PENDING_INTENT_ID, + MainActivity.getStartIntent(context, Destination.LuckyNumber, true), + FLAG_UPDATE_CURRENT + ) - val luckyNumber = getLuckyNumber(sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0), appWidgetId) - val appIntent = PendingIntent.getActivity(context, MainView.Section.LUCKY_NUMBER.id, - MainActivity.getStartIntent(context, MainView.Section.LUCKY_NUMBER, true), FLAG_UPDATE_CURRENT) - - val remoteView = RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context)).apply { - setTextViewText(R.id.luckyNumberWidgetNumber, luckyNumber?.luckyNumber?.toString() ?: "#") - setOnClickPendingIntent(R.id.luckyNumberWidgetContainer, appIntent) - } + val remoteView = + RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context)) + .apply { + setTextViewText( + R.id.luckyNumberWidgetNumber, + luckyNumber?.luckyNumber?.toString() ?: "#" + ) + setOnClickPendingIntent(R.id.luckyNumberWidgetContainer, appIntent) + } setStyles(remoteView, appWidgetId) appWidgetManager.updateAppWidget(appWidgetId, remoteView) @@ -78,7 +93,12 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { } } - override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, newOptions: Bundle?) { + override fun onAppWidgetOptionsChanged( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetId: Int, + newOptions: Bundle? + ) { super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions) val remoteView = RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context)) @@ -88,8 +108,12 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { } private fun setStyles(views: RemoteViews, appWidgetId: Int, options: Bundle? = null) { - val width = options?.getInt(OPTION_APPWIDGET_MIN_WIDTH) ?: sharedPref.getLong(getWidthWidgetKey(appWidgetId), 74).toInt() - val height = options?.getInt(OPTION_APPWIDGET_MAX_HEIGHT) ?: sharedPref.getLong(getHeightWidgetKey(appWidgetId), 74).toInt() + val width = options?.getInt(OPTION_APPWIDGET_MIN_WIDTH) ?: sharedPref.getLong( + getWidthWidgetKey(appWidgetId), 74 + ).toInt() + val height = options?.getInt(OPTION_APPWIDGET_MAX_HEIGHT) ?: sharedPref.getLong( + getHeightWidgetKey(appWidgetId), 74 + ).toInt() with(sharedPref) { putLong(getWidthWidgetKey(appWidgetId), width.toLong()) @@ -112,7 +136,11 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { } } - private fun RemoteViews.setVisibility(imageTop: Boolean, imageLeft: Boolean, title: Boolean = false) { + private fun RemoteViews.setVisibility( + imageTop: Boolean, + imageLeft: Boolean, + title: Boolean = false + ) { setViewVisibility(R.id.luckyNumberWidgetImageTop, if (imageTop) VISIBLE else GONE) setViewVisibility(R.id.luckyNumberWidgetImageLeft, if (imageLeft) VISIBLE else GONE) setViewVisibility(R.id.luckyNumberWidgetTitle, if (title) VISIBLE else GONE) @@ -152,7 +180,8 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { private fun getCorrectLayoutId(appWidgetId: Int, context: Context): Int { val savedTheme = sharedPref.getLong(getThemeWidgetKey(appWidgetId), 0) - val isSystemDarkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + val isSystemDarkMode = + context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES return if (savedTheme == 1L || (savedTheme == 2L && isSystemDarkMode)) { R.layout.widget_luckynumber_dark diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt index d758ac0da..92bc36bc7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt @@ -5,16 +5,10 @@ import android.content.Context import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK import android.content.Intent.FLAG_ACTIVITY_NEW_TASK -import android.content.pm.ShortcutInfo -import android.content.pm.ShortcutManager -import android.graphics.drawable.Icon -import android.os.Build import android.os.Build.VERSION_CODES.P import android.os.Bundle import android.view.Menu import android.view.MenuItem -import androidx.annotation.RequiresApi -import androidx.core.content.getSystemService import androidx.core.view.ViewCompat import androidx.core.view.isVisible import androidx.fragment.app.DialogFragment @@ -29,20 +23,10 @@ import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.databinding.ActivityMainBinding +import io.github.wulkanowy.services.shortcuts.ShortcutsHelper import io.github.wulkanowy.ui.base.BaseActivity +import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog -import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment -import io.github.wulkanowy.ui.modules.conference.ConferenceFragment -import io.github.wulkanowy.ui.modules.dashboard.DashboardFragment -import io.github.wulkanowy.ui.modules.exam.ExamFragment -import io.github.wulkanowy.ui.modules.grade.GradeFragment -import io.github.wulkanowy.ui.modules.homework.HomeworkFragment -import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment -import io.github.wulkanowy.ui.modules.message.MessageFragment -import io.github.wulkanowy.ui.modules.more.MoreFragment -import io.github.wulkanowy.ui.modules.note.NoteFragment -import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment -import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.InAppReviewHelper @@ -75,6 +59,9 @@ class MainActivity : BaseActivity(), MainVie @Inject lateinit var appInfo: AppInfo + @Inject + lateinit var shortcutsHelper: ShortcutsHelper + private var accountMenu: MenuItem? = null private val overlayProvider by lazy { ElevationOverlayProvider(this) } @@ -83,15 +70,19 @@ class MainActivity : BaseActivity(), MainVie FragNavController(supportFragmentManager, R.id.main_fragment_container) companion object { - const val EXTRA_START_MENU = "extraStartMenu" + + private const val EXTRA_START_DESTINATION = "start_destination" fun getStartIntent( context: Context, - startMenu: MainView.Section? = null, - clear: Boolean = false + destination: Destination? = null, + startNewTask: Boolean = false ) = Intent(context, MainActivity::class.java).apply { - if (clear) flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK - startMenu?.let { putExtra(EXTRA_START_MENU, it.id) } + putExtra(EXTRA_START_DESTINATION, destination) + + if (startNewTask) { + flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK + } } } @@ -106,42 +97,21 @@ class MainActivity : BaseActivity(), MainVie override val currentViewSubtitle get() = (navController.currentFrag as? MainView.TitledView)?.subtitleString - override var startMenuIndex = 0 - - override var startMenuMoreIndex = -1 - - private val moreMenuFragments = mapOf( - MainView.Section.MESSAGE.id to MessageFragment.newInstance(), - MainView.Section.EXAM.id to ExamFragment.newInstance(), - MainView.Section.HOMEWORK.id to HomeworkFragment.newInstance(), - MainView.Section.NOTE.id to NoteFragment.newInstance(), - MainView.Section.CONFERENCE.id to ConferenceFragment.newInstance(), - MainView.Section.SCHOOL_ANNOUNCEMENT.id to SchoolAnnouncementFragment.newInstance(), - MainView.Section.LUCKY_NUMBER.id to LuckyNumberFragment.newInstance(), - ) + private var savedInstanceState: Bundle? = null @SuppressLint("NewApi") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(ActivityMainBinding.inflate(layoutInflater).apply { binding = this }.root) setSupportActionBar(binding.mainToolbar) + this.savedInstanceState = savedInstanceState messageContainer = binding.mainMessageContainer updateHelper.messageContainer = binding.mainFragmentContainer - val section = MainView.Section.values() - .singleOrNull { it.id == intent.getIntExtra(EXTRA_START_MENU, -1) } - - presenter.onAttachView(this, section) - - with(navController) { - initialize(startMenuIndex, savedInstanceState) - pushFragment(moreMenuFragments[startMenuMoreIndex]) - } - - if (appInfo.systemVersion >= Build.VERSION_CODES.N_MR1) { - initShortcuts() - } + val destination = intent.getSerializableExtra(EXTRA_START_DESTINATION) as Destination? + ?: shortcutsHelper.getDestination(intent) + presenter.onAttachView(this, destination) updateHelper.checkAndInstallUpdates(this) } @@ -157,54 +127,6 @@ class MainActivity : BaseActivity(), MainVie updateHelper.onActivityResult(requestCode, resultCode) } - @RequiresApi(Build.VERSION_CODES.N_MR1) - fun initShortcuts() { - val shortcutsList = mutableListOf() - - listOf( - Triple( - getString(R.string.grade_title), - R.drawable.ic_shortcut_grade, - MainView.Section.GRADE - ), - Triple( - getString(R.string.attendance_title), - R.drawable.ic_shortcut_attendance, - MainView.Section.ATTENDANCE - ), - Triple( - getString(R.string.exam_title), - R.drawable.ic_shortcut_exam, - MainView.Section.EXAM - ), - Triple( - getString(R.string.timetable_title), - R.drawable.ic_shortcut_timetable, - MainView.Section.TIMETABLE - ) - ).forEach { (title, icon, enum) -> - shortcutsList.add( - ShortcutInfo.Builder(applicationContext, title) - .setShortLabel(title) - .setLongLabel(title) - .setIcon(Icon.createWithResource(applicationContext, icon)) - .setIntents( - arrayOf( - Intent(applicationContext, MainActivity::class.java) - .setAction(Intent.ACTION_VIEW), - Intent(applicationContext, MainActivity::class.java) - .putExtra(EXTRA_START_MENU, enum.id) - .setAction(Intent.ACTION_VIEW) - .addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK) - ) - ) - .build() - ) - } - - getSystemService()?.dynamicShortcuts = shortcutsList - } - override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.action_menu_main, menu) accountMenu = menu?.findItem(R.id.mainMenuAccount) @@ -213,15 +135,38 @@ class MainActivity : BaseActivity(), MainVie return true } - @SuppressLint("NewApi") - override fun initView() { + override fun initView(startMenuIndex: Int, rootDestinations: List) { + initializeToolbar() + initializeBottomNavigation(startMenuIndex) + initializeNavController(startMenuIndex, rootDestinations) + } + + private fun initializeNavController(startMenuIndex: Int, rootDestinations: List) { + with(navController) { + setOnViewChangeListener { destinationView -> + presenter.onViewChange(destinationView) + analytics.setCurrentScreen( + this@MainActivity, + destinationView::class.java.simpleName + ) + } + fragmentHideStrategy = HIDE + rootFragments = rootDestinations.map { it.fragment } + + initialize(startMenuIndex, savedInstanceState) + } + } + + private fun initializeToolbar() { with(binding.mainToolbar) { stateListAnimator = null setBackgroundColor( overlayProvider.compositeOverlayWithThemeSurfaceColorIfNeeded(dpToPx(4f)) ) } + } + private fun initializeBottomNavigation(startMenuIndex: Int) { with(binding.mainBottomNav) { with(menu) { add(Menu.NONE, 0, Menu.NONE, R.string.dashboard_title) @@ -239,36 +184,6 @@ class MainActivity : BaseActivity(), MainVie setOnItemSelectedListener { presenter.onTabSelected(it.itemId, false) } setOnItemReselectedListener { presenter.onTabSelected(it.itemId, true) } } - - with(navController) { - setOnViewChangeListener { section, name -> - if (section == MainView.Section.ACCOUNT || section == MainView.Section.STUDENT_INFO) { - binding.mainBottomNav.isVisible = false - - if (appInfo.systemVersion >= P) { - window.navigationBarColor = getThemeAttrColor(R.attr.colorSurface) - } - } else { - binding.mainBottomNav.isVisible = true - - if (appInfo.systemVersion >= P) { - window.navigationBarColor = - getThemeAttrColor(android.R.attr.navigationBarColor) - } - } - - analytics.setCurrentScreen(this@MainActivity, name) - presenter.onViewChange(section) - } - fragmentHideStrategy = HIDE - rootFragments = listOf( - DashboardFragment.newInstance(), - GradeFragment.newInstance(), - AttendanceFragment.newInstance(), - TimetableFragment.newInstance(), - MoreFragment.newInstance() - ) - } } override fun onPreferenceStartFragment( @@ -317,6 +232,22 @@ class MainActivity : BaseActivity(), MainVie ViewCompat.setElevation(binding.mainToolbar, if (show) dpToPx(4f) else 0f) } + override fun showBottomNavigation(show: Boolean) { + binding.mainBottomNav.isVisible = show + + if (appInfo.systemVersion >= P) { + window.navigationBarColor = if (show) { + getThemeAttrColor(android.R.attr.navigationBarColor) + } else { + getThemeAttrColor(R.attr.colorSurface) + } + } + } + + override fun openMoreDestination(destination: Destination) { + pushView(destination.fragment) + } + override fun notifyMenuViewReselected() { (navController.currentStack?.getOrNull(0) as? MainView.MainChildView)?.onFragmentReselected() } @@ -373,6 +304,6 @@ class MainActivity : BaseActivity(), MainVie override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) navController.onSaveInstanceState(outState) - intent.removeExtra(EXTRA_START_MENU) + intent.removeExtra(EXTRA_START_DESTINATION) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt index 4805b5a1c..0aca51afe 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt @@ -6,10 +6,15 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.ErrorHandler -import io.github.wulkanowy.ui.modules.main.MainView.Section.GRADE -import io.github.wulkanowy.ui.modules.main.MainView.Section.MESSAGE -import io.github.wulkanowy.ui.modules.main.MainView.Section.SCHOOL +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.account.AccountView +import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsView +import io.github.wulkanowy.ui.modules.grade.GradeView +import io.github.wulkanowy.ui.modules.message.MessageView +import io.github.wulkanowy.ui.modules.schoolandteachers.school.SchoolView +import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.flowWithResource import kotlinx.coroutines.flow.onEach @@ -27,19 +32,40 @@ class MainPresenter @Inject constructor( private var studentsWitSemesters: List? = null - fun onAttachView(view: MainView, initMenu: MainView.Section?) { - super.onAttachView(view) - view.apply { - getProperViewIndexes(initMenu).let { (main, more) -> - startMenuIndex = main - startMenuMoreIndex = more + private val rootDestinationTypeList = listOf( + Destination.Type.DASHBOARD, + Destination.Type.GRADE, + Destination.Type.ATTENDANCE, + Destination.Type.TIMETABLE, + Destination.Type.MORE + ) + + private val Destination?.startMenuIndex + get() = when { + this == null -> prefRepository.startMenuIndex + type in rootDestinationTypeList -> { + rootDestinationTypeList.indexOf(type) } - initView() - Timber.i("Main view was initialized with $startMenuIndex menu index and $startMenuMoreIndex more index") + else -> 4 + } + + fun onAttachView(view: MainView, initDestination: Destination?) { + super.onAttachView(view) + + val startMenuIndex = initDestination.startMenuIndex + val destinations = rootDestinationTypeList.map { + if (it == initDestination?.type) initDestination else it.defaultDestination + } + + view.initView(startMenuIndex, destinations) + if (initDestination != null && startMenuIndex == 4) { + view.openMoreDestination(initDestination) } syncManager.startPeriodicSyncWorker() - analytics.logEvent("app_open", "destination" to initMenu?.name) + + analytics.logEvent("app_open", "destination" to initDestination.toString()) + Timber.i("Main view was initialized with $initDestination") } fun onActionMenuCreated() { @@ -64,9 +90,10 @@ class MainPresenter @Inject constructor( }.launch("avatar") } - fun onViewChange(section: MainView.Section?) { + fun onViewChange(destinationView: BaseView) { view?.apply { - showActionBarElevation(section != GRADE && section != MESSAGE && section != SCHOOL) + showBottomNavigation(destinationView !is AccountView && destinationView !is StudentInfoView && destinationView !is AccountDetailsView) + showActionBarElevation(destinationView !is GradeView && destinationView !is MessageView && destinationView !is SchoolView) currentViewTitle?.let { setViewTitle(it) } currentViewSubtitle?.let { setViewSubTitle(it.ifBlank { null }) } currentStackSize?.let { @@ -134,10 +161,4 @@ class MainPresenter @Inject constructor( view?.showStudentAvatar(currentStudent) } - - private fun getProperViewIndexes(initMenu: MainView.Section?) = when (initMenu?.id) { - in 0..3 -> initMenu!!.id to -1 - in 4..100 -> 4 to initMenu!!.id - else -> prefRepository.startMenuIndex to -1 - } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt index 8851f5878..3a57fcc6b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt @@ -3,13 +3,10 @@ package io.github.wulkanowy.ui.modules.main import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.ui.base.BaseView +import io.github.wulkanowy.ui.modules.Destination interface MainView : BaseView { - var startMenuIndex: Int - - var startMenuMoreIndex: Int - val isRootView: Boolean val currentViewTitle: String? @@ -18,7 +15,7 @@ interface MainView : BaseView { val currentStackSize: Int? - fun initView() + fun initView(startMenuIndex: Int, rootDestinations: List) fun switchMenuView(position: Int) @@ -28,6 +25,8 @@ interface MainView : BaseView { fun showActionBarElevation(show: Boolean) + fun showBottomNavigation(show: Boolean) + fun notifyMenuViewReselected() fun notifyMenuViewChanged() @@ -42,6 +41,8 @@ interface MainView : BaseView { fun showInAppReview() + fun openMoreDestination(destination: Destination) + interface MainChildView { fun onFragmentReselected() @@ -57,25 +58,4 @@ interface MainView : BaseView { get() = "" set(_) {} } - - enum class Section { - DASHBOARD, - GRADE, - ATTENDANCE, - TIMETABLE, - MORE, - MESSAGE, - EXAM, - HOMEWORK, - NOTE, - CONFERENCE, - SCHOOL_ANNOUNCEMENT, - SCHOOL, - LUCKY_NUMBER, - ACCOUNT, - STUDENT_INFO, - SETTINGS; - - val id get() = ordinal - } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterAdapter.kt index 7ee326f8f..27b3637ad 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterAdapter.kt @@ -5,7 +5,6 @@ import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView -import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Notification import io.github.wulkanowy.databinding.ItemNotificationsCenterBinding import io.github.wulkanowy.services.sync.notifications.NotificationType @@ -28,26 +27,12 @@ class NotificationsCenterAdapter @Inject constructor() : notificationsCenterItemTitle.text = item.title notificationsCenterItemContent.text = item.content notificationsCenterItemDate.text = item.date.toFormattedString("HH:mm, d MMM") - notificationsCenterItemIcon.setImageResource(item.type.toDrawableResId()) + notificationsCenterItemIcon.setImageResource(item.type.icon) root.setOnClickListener { onItemClickListener(item.type) } } } - private fun NotificationType.toDrawableResId() = when (this) { - NotificationType.NEW_CONFERENCE -> R.drawable.ic_more_conferences - NotificationType.NEW_EXAM -> R.drawable.ic_main_exam - NotificationType.NEW_GRADE_DETAILS -> R.drawable.ic_stat_grade - NotificationType.NEW_GRADE_PREDICTED -> R.drawable.ic_stat_grade - NotificationType.NEW_GRADE_FINAL -> R.drawable.ic_stat_grade - NotificationType.NEW_HOMEWORK -> R.drawable.ic_more_homework - NotificationType.NEW_LUCKY_NUMBER -> R.drawable.ic_stat_luckynumber - NotificationType.NEW_MESSAGE -> R.drawable.ic_stat_message - NotificationType.NEW_NOTE -> R.drawable.ic_stat_note - NotificationType.NEW_ANNOUNCEMENT -> R.drawable.ic_all_about - NotificationType.PUSH -> R.drawable.ic_stat_all - } - class ViewHolder(val binding: ItemNotificationsCenterBinding) : RecyclerView.ViewHolder(binding.root) @@ -59,4 +44,4 @@ class NotificationsCenterAdapter @Inject constructor() : override fun areItemsTheSame(oldItem: Notification, newItem: Notification) = oldItem.id == newItem.id } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterFragment.kt index b9bfb447e..f3bbc42de 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterFragment.kt @@ -11,6 +11,7 @@ import io.github.wulkanowy.data.db.entities.Notification import io.github.wulkanowy.databinding.FragmentNotificationsCenterBinding import io.github.wulkanowy.services.sync.notifications.NotificationType import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment import io.github.wulkanowy.ui.modules.conference.ConferenceFragment import io.github.wulkanowy.ui.modules.exam.ExamFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment @@ -21,6 +22,7 @@ import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.message.MessageFragment import io.github.wulkanowy.ui.modules.note.NoteFragment import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment +import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import javax.inject.Inject @AndroidEntryPoint @@ -104,5 +106,7 @@ class NotificationsCenterFragment : NotificationType.NEW_NOTE -> NoteFragment.newInstance() NotificationType.NEW_ANNOUNCEMENT -> SchoolAnnouncementFragment.newInstance() NotificationType.PUSH -> null + NotificationType.CHANGE_TIMETABLE -> TimetableFragment.newInstance() + NotificationType.NEW_ATTENDANCE -> AttendanceFragment.newInstance() } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt index 2612fab3a..d56cdfa7c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt @@ -6,7 +6,7 @@ import io.github.wulkanowy.R import io.github.wulkanowy.ui.modules.main.MainView import timber.log.Timber -class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView { +class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView, SettingsView { companion object { @@ -19,4 +19,16 @@ class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView { setPreferencesFromResource(R.xml.scheme_preferences, rootKey) Timber.i("Settings view was initialized") } + + override fun showError(text: String, error: Throwable) {} + + override fun showMessage(text: String) {} + + override fun showExpiredDialog() {} + + override fun openClearLoginView() {} + + override fun showErrorDetailsDialog(error: Throwable) {} + + override fun showChangePasswordSnackbar(redirectUrl: String) {} } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsView.kt new file mode 100644 index 000000000..79f91bc5d --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsView.kt @@ -0,0 +1,5 @@ +package io.github.wulkanowy.ui.modules.settings + +import io.github.wulkanowy.ui.base.BaseView + +interface SettingsView : BaseView \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt index f9079b5f9..63fc84962 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt @@ -24,8 +24,8 @@ import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.services.HiltBroadcastReceiver import io.github.wulkanowy.services.widgets.TimetableWidgetService +import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.main.MainActivity -import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.capitalise import io.github.wulkanowy.utils.createNameInitialsDrawable @@ -60,6 +60,8 @@ class TimetableWidgetProvider : HiltBroadcastReceiver() { companion object { + private const val TIMETABLE_PENDING_INTENT_ID = 201 + private const val EXTRA_TOGGLED_WIDGET_ID = "extraToggledWidget" private const val EXTRA_BUTTON_TYPE = "extraButtonType" @@ -174,8 +176,8 @@ class TimetableWidgetProvider : HiltBroadcastReceiver() { ) val appIntent = PendingIntent.getActivity( context, - MainView.Section.TIMETABLE.id, - MainActivity.getStartIntent(context, MainView.Section.TIMETABLE, true), + TIMETABLE_PENDING_INTENT_ID, + MainActivity.getStartIntent(context, Destination.Timetable(), true), FLAG_UPDATE_CURRENT ) diff --git a/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt index b89ad57d3..397c95953 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt @@ -29,7 +29,7 @@ private fun calculatePercentage(presence: Double, absence: Double): Double { return if ((presence + absence) == 0.0) 0.0 else (presence / (presence + absence)) * 100 } -inline val Attendance.description +inline val Attendance.descriptionRes get() = when (AttendanceCategory.getCategoryByName(name)) { AttendanceCategory.PRESENCE -> R.string.attendance_present AttendanceCategory.ABSENCE_UNEXCUSED -> R.string.attendance_absence_unexcused diff --git a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt index 2cd4459eb..ecd982a18 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt @@ -18,6 +18,7 @@ import androidx.annotation.AttrRes import androidx.annotation.ColorInt import androidx.annotation.ColorRes import androidx.annotation.DrawableRes +import androidx.annotation.PluralsRes import androidx.core.content.ContextCompat import androidx.core.graphics.ColorUtils import androidx.core.graphics.applyCanvas @@ -57,6 +58,9 @@ fun Context.getCompatDrawable(@DrawableRes drawableRes: Int, @ColorRes colorRes: fun Context.getCompatBitmap(@DrawableRes drawableRes: Int, @ColorRes colorRes: Int) = getCompatDrawable(drawableRes, colorRes)?.toBitmap() +fun Context.getPlural(@PluralsRes pluralRes: Int, quantity: Int, vararg arguments: Any) = + resources.getQuantityString(pluralRes, quantity, *arguments) + fun Context.openInternetBrowser(uri: String, onActivityNotFound: (uri: String) -> Unit = {}) { Intent.parseUri(uri, 0).let { try { diff --git a/app/src/main/java/io/github/wulkanowy/utils/FragNavControlerExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/FragNavControlerExtension.kt index 9dc1e18a0..01c876dd2 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/FragNavControlerExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/FragNavControlerExtension.kt @@ -2,16 +2,19 @@ package io.github.wulkanowy.utils import androidx.fragment.app.Fragment import com.ncapdevi.fragnav.FragNavController -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.ui.base.BaseView -inline fun FragNavController.setOnViewChangeListener(crossinline listener: (section: MainView.Section?, name: String?) -> Unit) { +inline fun FragNavController.setOnViewChangeListener(crossinline listener: (view: BaseView) -> Unit) { transactionListener = object : FragNavController.TransactionListener { - override fun onFragmentTransaction(fragment: Fragment?, transactionType: FragNavController.TransactionType) { - listener(fragment?.toSection(), fragment?.let { it::class.java.simpleName }) + override fun onFragmentTransaction( + fragment: Fragment?, + transactionType: FragNavController.TransactionType + ) { + fragment?.let { listener(it as BaseView) } } override fun onTabTransaction(fragment: Fragment?, index: Int) { - listener(fragment?.toSection(), fragment?.let { it::class.java.simpleName }) + fragment?.let { listener(it as BaseView) } } } } diff --git a/app/src/main/java/io/github/wulkanowy/utils/FragmentExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/FragmentExtension.kt deleted file mode 100644 index 210a62090..000000000 --- a/app/src/main/java/io/github/wulkanowy/utils/FragmentExtension.kt +++ /dev/null @@ -1,44 +0,0 @@ -package io.github.wulkanowy.utils - -import androidx.fragment.app.Fragment -import io.github.wulkanowy.ui.modules.account.AccountFragment -import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsFragment -import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment -import io.github.wulkanowy.ui.modules.conference.ConferenceFragment -import io.github.wulkanowy.ui.modules.dashboard.DashboardFragment -import io.github.wulkanowy.ui.modules.exam.ExamFragment -import io.github.wulkanowy.ui.modules.grade.GradeFragment -import io.github.wulkanowy.ui.modules.homework.HomeworkFragment -import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment -import io.github.wulkanowy.ui.modules.main.MainView -import io.github.wulkanowy.ui.modules.message.MessageFragment -import io.github.wulkanowy.ui.modules.more.MoreFragment -import io.github.wulkanowy.ui.modules.note.NoteFragment -import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersFragment -import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment -import io.github.wulkanowy.ui.modules.settings.SettingsFragment -import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoFragment -import io.github.wulkanowy.ui.modules.timetable.TimetableFragment - -fun Fragment.toSection(): MainView.Section? { - return when (this) { - is GradeFragment -> MainView.Section.GRADE - is AttendanceFragment -> MainView.Section.ATTENDANCE - is ExamFragment -> MainView.Section.EXAM - is TimetableFragment -> MainView.Section.TIMETABLE - is MoreFragment -> MainView.Section.MORE - is MessageFragment -> MainView.Section.MESSAGE - is HomeworkFragment -> MainView.Section.HOMEWORK - is NoteFragment -> MainView.Section.NOTE - is LuckyNumberFragment -> MainView.Section.LUCKY_NUMBER - is SettingsFragment -> MainView.Section.SETTINGS - is SchoolAndTeachersFragment -> MainView.Section.SCHOOL - is AccountFragment -> MainView.Section.ACCOUNT - is AccountDetailsFragment -> MainView.Section.ACCOUNT - is StudentInfoFragment -> MainView.Section.STUDENT_INFO - is ConferenceFragment -> MainView.Section.CONFERENCE - is SchoolAnnouncementFragment -> MainView.Section.SCHOOL_ANNOUNCEMENT - is DashboardFragment -> MainView.Section.DASHBOARD - else -> null - } -} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e88ee1ad..008069562 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -163,6 +163,26 @@ Now: %s Next: %s Later: %s + %1$s lesson %2$d - %3$s + Change of room from %1$s to %2$s + Change of teacher from %1$s to %2$s + Change of subject from %1$s to %2$s + + Timetable change + Timetable changes + + + %1$s - %2$d change in timetable + %1$s - %2$d changes in timetable + + + %1$d change in timetable + %1$d changes in timetable + + + %d change + %d changes + @@ -200,6 +220,18 @@ Excuse z powodu Dzień dobry,\nProszę o usprawiedliwienie mojego dziecka w dniu %s z lekcji %s%s%s.\n\nPozdrawiam. + + New attendance + New attendance + + + %1$d new attendance + %1$d attendance + + + %d attendance + %d attendance + @@ -706,6 +738,8 @@ Push notifications Upcoming lessons Debug + Timetable change + New attendance diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt index 2c44e9fad..7557d745a 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt @@ -42,20 +42,12 @@ class MainPresenterTest { MockKAnnotations.init(this) clearMocks(mainView) - every { mainView.startMenuIndex = any() } just Runs - every { mainView.startMenuMoreIndex = any() } just Runs - every { mainView.startMenuIndex } returns 1 - every { mainView.startMenuMoreIndex } returns 1 - every { mainView.initView() } just Runs - presenter = MainPresenter(errorHandler, studentRepository, prefRepository, syncManager, analytics) + every { mainView.initView(any(), any()) } just Runs + presenter = + MainPresenter(errorHandler, studentRepository, prefRepository, syncManager, analytics) presenter.onAttachView(mainView, null) } - @Test - fun initMenuTest() { - verify { mainView.initView() } - } - @Test fun onTabSelectedTest() { every { mainView.notifyMenuViewChanged() } just Runs From 007d62e61d79c89ab5d6f22563280f354086e32b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 11 Nov 2021 15:23:20 +0100 Subject: [PATCH 263/869] Update project to Android SDK 31 (#1570) --- app/build.gradle | 11 ++--- app/src/main/AndroidManifest.xml | 1 - .../wulkanowy/data/db/SharedPrefProvider.kt | 9 ++-- .../alarm/TimetableNotificationReceiver.kt | 8 ++-- .../TimetableNotificationSchedulerHelper.kt | 12 +++-- .../services/shortcuts/ShortcutsHelper.kt | 10 ++-- .../notifications/AppNotificationManager.kt | 7 +-- .../ChangeTimetableNotification.kt | 6 +-- .../NewAttendanceNotification.kt | 6 +-- .../NewConferenceNotification.kt | 6 +-- .../sync/notifications/NewExamNotification.kt | 6 +-- .../notifications/NewGradeNotification.kt | 14 +++--- .../notifications/NewHomeworkNotification.kt | 6 +-- .../NewLuckyNumberNotification.kt | 4 +- .../notifications/NewMessageNotification.kt | 6 +-- .../sync/notifications/NewNoteNotification.kt | 6 +-- .../NewSchoolAnnouncementNotification.kt | 6 +-- .../github/wulkanowy/ui/base/BaseActivity.kt | 8 +--- .../github/wulkanowy/ui/base/BasePresenter.kt | 2 +- .../github/wulkanowy/ui/base/ThemeManager.kt | 11 +++-- .../wulkanowy/ui/modules/Destination.kt | 30 ++++++------ .../accountedit/AccountEditColorAdapter.kt | 32 +++---------- .../modules/attendance/AttendanceFragment.kt | 6 +-- .../summary/AttendanceSummaryFragment.kt | 2 +- .../wulkanowy/ui/modules/exam/ExamFragment.kt | 2 +- .../ui/modules/grade/GradeFragment.kt | 4 +- .../statistics/GradeStatisticsFragment.kt | 2 +- .../ui/modules/homework/HomeworkFragment.kt | 2 +- .../ui/modules/login/LoginActivity.kt | 4 +- .../LoginStudentSelectFragment.kt | 13 ++---- .../history/LuckyNumberHistoryFragment.kt | 2 +- .../LuckyNumberWidgetProvider.kt | 8 ++-- .../wulkanowy/ui/modules/main/MainActivity.kt | 12 ----- .../ui/modules/message/MessageFragment.kt | 5 +- .../message/preview/MessagePreviewFragment.kt | 10 +--- .../preview/MessagePreviewPresenter.kt | 24 +++++----- .../message/preview/MessagePreviewView.kt | 7 +-- .../message/tab/MessageTabPresenter.kt | 9 ++-- .../SchoolAndTeachersFragment.kt | 4 +- .../notifications/NotificationsFragment.kt | 2 - .../ui/modules/splash/SplashActivity.kt | 43 +++++++++++++++--- .../ui/modules/splash/SplashPresenter.kt | 27 ++++++----- .../wulkanowy/ui/modules/splash/SplashView.kt | 3 +- .../ui/modules/timetable/TimetableFragment.kt | 2 +- .../additional/AdditionalLessonsFragment.kt | 2 +- .../completed/CompletedLessonsFragment.kt | 2 +- .../TimetableWidgetProvider.kt | 34 +++++++------- .../ui/widgets/FittedScrollableTabLayout.kt | 12 ++--- .../ui/widgets/MaterialLinearLayout.kt | 25 +++------- .../wulkanowy/ui/widgets/MaterialTabLayout.kt | 25 ---------- .../wulkanowy/utils/PendingIntentCompat.kt | 11 +++++ .../github/wulkanowy/utils/StringExtension.kt | 4 +- .../wulkanowy/utils/security/Scrambler.kt | 3 -- ...ompat_splash_screen_no_icon_background.xml | 24 ++++++++++ .../main/res/drawable-v23/img_splash_logo.png | Bin 19474 -> 0 bytes .../drawable-v23/layer_splash_background.xml | 12 ----- app/src/main/res/drawable/ic_splash_logo.xml | 12 +++++ app/src/main/res/drawable/img_splash_logo.png | Bin 20340 -> 0 bytes .../res/drawable/layer_splash_background.xml | 12 ----- .../main/res/layout/fragment_attendance.xml | 8 ++-- app/src/main/res/layout/fragment_exam.xml | 8 ++-- .../layout/fragment_lucky_number_history.xml | 8 ++-- app/src/main/res/layout/fragment_message.xml | 2 +- app/src/main/res/layout/fragment_school.xml | 8 ++-- .../res/layout/fragment_schoolandteachers.xml | 2 +- .../main/res/layout/fragment_timetable.xml | 8 ++-- .../layout/fragment_timetable_additional.xml | 8 ++-- .../layout/fragment_timetable_completed.xml | 8 ++-- .../layout/subitem_dashboard_conferences.xml | 7 +-- app/src/main/res/values-v23/styles.xml | 8 +--- app/src/main/res/values-v26/styles.xml | 7 --- app/src/main/res/values-v28/styles.xml | 8 ---- app/src/main/res/values-v29/styles.xml | 8 ---- app/src/main/res/values/styles.xml | 12 ++--- .../wulkanowy/utils/CrashlyticsUtils.kt | 6 --- .../ui/modules/splash/SplashPresenterTest.kt | 6 +-- 76 files changed, 316 insertions(+), 384 deletions(-) delete mode 100644 app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialTabLayout.kt create mode 100644 app/src/main/java/io/github/wulkanowy/utils/PendingIntentCompat.kt create mode 100644 app/src/main/res/drawable-v23/compat_splash_screen_no_icon_background.xml delete mode 100644 app/src/main/res/drawable-v23/img_splash_logo.png delete mode 100644 app/src/main/res/drawable-v23/layer_splash_background.xml create mode 100644 app/src/main/res/drawable/ic_splash_logo.xml delete mode 100644 app/src/main/res/drawable/img_splash_logo.png delete mode 100644 app/src/main/res/drawable/layer_splash_background.xml diff --git a/app/build.gradle b/app/build.gradle index 53814dae3..bef984aa2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,17 +15,16 @@ apply from: 'sonarqube.gradle' apply from: 'hooks.gradle' android { - compileSdkVersion 30 + compileSdkVersion 31 defaultConfig { applicationId "io.github.wulkanowy" testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 - targetSdkVersion 30 + targetSdkVersion 31 versionCode 97 versionName "1.3.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - vectorDrawables.useSupportLibrary = true resValue "string", "app_name", "Wulkanowy" @@ -166,7 +165,7 @@ huaweiPublish { } ext { - work_manager = "2.6.0" + work_manager = "2.7.0" android_hilt = "1.0.0" room = "2.3.0" chucker = "3.5.2" @@ -183,9 +182,9 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" implementation "androidx.core:core-ktx:1.6.0" + implementation 'androidx.core:core-splashscreen:1.0.0-alpha02' implementation "androidx.activity:activity-ktx:1.3.1" - implementation "androidx.appcompat:appcompat:1.3.1" - implementation "androidx.appcompat:appcompat-resources:1.3.1" + implementation "androidx.appcompat:appcompat:1.4.0-beta01" implementation "androidx.fragment:fragment-ktx:1.3.6" implementation "androidx.annotation:annotation:1.2.0" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e43a0ec08..5928c23a0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -42,7 +42,6 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="false" android:theme="@style/WulkanowyTheme" - android:usesCleartextTraffic="true" tools:ignore="GoogleAppIndexingWarning,UnusedAttribute"> , requestCode: Int) { if (now() in range) cancelNotification() + alarmManager.cancel( - PendingIntent.getBroadcast(context, requestCode, Intent(), FLAG_UPDATE_CURRENT) + PendingIntent.getBroadcast( + context, + requestCode, + Intent(), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) ) } @@ -156,7 +162,7 @@ class TimetableNotificationSchedulerHelper @Inject constructor( alarmManager, RTC_WAKEUP, time.toTimestamp(), PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also { it.putExtra(LESSON_TYPE, notificationType) - }, FLAG_UPDATE_CURRENT) + }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE) ) Timber.d( "TimetableNotification scheduled: type: $notificationType, subject: ${ diff --git a/app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt b/app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt index d1c215f26..bb09434b5 100644 --- a/app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt +++ b/app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt @@ -8,7 +8,7 @@ import androidx.core.graphics.drawable.IconCompat import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import javax.inject.Inject import javax.inject.Singleton @@ -35,7 +35,7 @@ class ShortcutsHelper @Inject constructor(@ApplicationContext private val contex .setShortLabel(context.getString(R.string.grade_title)) .setLongLabel(context.getString(R.string.grade_title)) .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_grade)) - .setIntent(MainActivity.getStartIntent(context, startNewTask = true) + .setIntent(SplashActivity.getStartIntent(context, startNewTask = true) .apply { action = Intent.ACTION_VIEW putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "grade") @@ -47,7 +47,7 @@ class ShortcutsHelper @Inject constructor(@ApplicationContext private val contex .setShortLabel(context.getString(R.string.attendance_title)) .setLongLabel(context.getString(R.string.attendance_title)) .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_attendance)) - .setIntent(MainActivity.getStartIntent(context, startNewTask = true) + .setIntent(SplashActivity.getStartIntent(context, startNewTask = true) .apply { action = Intent.ACTION_VIEW putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "attendance") @@ -59,7 +59,7 @@ class ShortcutsHelper @Inject constructor(@ApplicationContext private val contex .setShortLabel(context.getString(R.string.exam_title)) .setLongLabel(context.getString(R.string.exam_title)) .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_exam)) - .setIntent(MainActivity.getStartIntent(context, startNewTask = true) + .setIntent(SplashActivity.getStartIntent(context, startNewTask = true) .apply { action = Intent.ACTION_VIEW putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "exam") @@ -71,7 +71,7 @@ class ShortcutsHelper @Inject constructor(@ApplicationContext private val contex .setShortLabel(context.getString(R.string.timetable_title)) .setLongLabel(context.getString(R.string.timetable_title)) .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_timetable)) - .setIntent(MainActivity.getStartIntent(context, startNewTask = true) + .setIntent(SplashActivity.getStartIntent(context, startNewTask = true) .apply { action = Intent.ACTION_VIEW putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "timetable") diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt index 542b9346b..2848fe303 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt @@ -13,6 +13,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.data.repositories.NotificationRepository +import io.github.wulkanowy.utils.PendingIntentCompat import io.github.wulkanowy.utils.getCompatBitmap import io.github.wulkanowy.utils.getCompatColor import io.github.wulkanowy.utils.nickOrName @@ -45,7 +46,7 @@ class AppNotificationManager @Inject constructor( context, Random.nextInt(), notificationData.intentToStart, - PendingIntent.FLAG_UPDATE_CURRENT + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) ) .setContentTitle(notificationData.title) @@ -86,7 +87,7 @@ class AppNotificationManager @Inject constructor( context, Random.nextInt(), notificationData.intentToStart, - PendingIntent.FLAG_UPDATE_CURRENT + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) ) .setContentTitle(notificationData.title) @@ -134,7 +135,7 @@ class AppNotificationManager @Inject constructor( context, Random.nextInt(), groupNotificationData.intentToStart, - PendingIntent.FLAG_UPDATE_CURRENT + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) ) .setLocalOnly(true) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt index 6d2d3a59f..f8860b7f1 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt @@ -8,7 +8,7 @@ import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDate @@ -32,7 +32,7 @@ class ChangeTimetableNotification @Inject constructor( 1 ), content = it, - intentToStart = MainActivity.getStartIntent( + intentToStart = SplashActivity.getStartIntent( context = context, destination = Destination.Timetable(date), startNewTask = true @@ -54,7 +54,7 @@ class ChangeTimetableNotification @Inject constructor( changedLessons.size, changedLessons.size ), - intentToStart = MainActivity.getStartIntent(context, Destination.Timetable(), true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Timetable(), true), type = NotificationType.CHANGE_TIMETABLE ) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewAttendanceNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewAttendanceNotification.kt index 695552721..3792725ca 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewAttendanceNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewAttendanceNotification.kt @@ -8,7 +8,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.descriptionRes import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString @@ -31,7 +31,7 @@ class NewAttendanceNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.attendance_notify_new_items_title, 1), content = it, - intentToStart = MainActivity.getStartIntent(context, Destination.Attendance, true) + intentToStart = SplashActivity.getStartIntent(context, Destination.Attendance, true) ) } @@ -46,7 +46,7 @@ class NewAttendanceNotification @Inject constructor( notificationDataList.size, notificationDataList.size ), - intentToStart = MainActivity.getStartIntent(context, Destination.Attendance, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Attendance, true), type = NotificationType.NEW_ATTENDANCE ) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt index 97b1332d8..4ec359b08 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt @@ -8,7 +8,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDateTime @@ -31,7 +31,7 @@ class NewConferenceNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.conference_notify_new_item_title, 1), content = it, - intentToStart = MainActivity.getStartIntent(context, Destination.Conference, true) + intentToStart = SplashActivity.getStartIntent(context, Destination.Conference, true) ) } @@ -43,7 +43,7 @@ class NewConferenceNotification @Inject constructor( lines.size, lines.size ), - intentToStart = MainActivity.getStartIntent(context, Destination.Conference, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Conference, true), type = NotificationType.NEW_CONFERENCE ) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt index 6f8ed8961..feb7c320a 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt @@ -8,7 +8,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDate @@ -31,7 +31,7 @@ class NewExamNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.exam_notify_new_item_title, 1), content = it, - intentToStart = MainActivity.getStartIntent(context, Destination.Exam, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Exam, true), ) } @@ -43,7 +43,7 @@ class NewExamNotification @Inject constructor( lines.size, lines.size ), - intentToStart = MainActivity.getStartIntent(context, Destination.Exam, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Exam, true), type = NotificationType.NEW_EXAM ) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt index 09692fb7c..425a57fc6 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt @@ -9,7 +9,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.getPlural import javax.inject.Inject @@ -23,7 +23,7 @@ class NewGradeNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.grade_new_items, 1), content = "${it.subject}: ${it.entry}", - intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Grade, true), ) } @@ -31,7 +31,7 @@ class NewGradeNotification @Inject constructor( notificationDataList = notificationDataList, title = context.getPlural(R.plurals.grade_new_items, items.size), content = context.getPlural(R.plurals.grade_notify_new_items, items.size, items.size), - intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Grade, true), type = NotificationType.NEW_GRADE_DETAILS ) @@ -43,7 +43,7 @@ class NewGradeNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.grade_new_items_predicted, 1), content = "${it.subject}: ${it.predictedGrade}", - intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Grade, true), ) } @@ -55,7 +55,7 @@ class NewGradeNotification @Inject constructor( items.size, items.size ), - intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Grade, true), type = NotificationType.NEW_GRADE_PREDICTED ) @@ -67,7 +67,7 @@ class NewGradeNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.grade_new_items_final, 1), content = "${it.subject}: ${it.finalGrade}", - intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Grade, true), ) } @@ -79,7 +79,7 @@ class NewGradeNotification @Inject constructor( items.size, items.size ), - intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Grade, true), type = NotificationType.NEW_GRADE_FINAL ) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt index cdada844e..fcaed10f0 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt @@ -8,7 +8,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDate @@ -31,7 +31,7 @@ class NewHomeworkNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.homework_notify_new_item_title, 1), content = it, - intentToStart = MainActivity.getStartIntent(context, Destination.Homework, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Homework, true), ) } @@ -42,7 +42,7 @@ class NewHomeworkNotification @Inject constructor( lines.size, lines.size ), - intentToStart = MainActivity.getStartIntent(context, Destination.Homework, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Homework, true), type = NotificationType.NEW_HOMEWORK, notificationDataList = notificationDataList ) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt index d9f138b5c..ab2c1ca76 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt @@ -7,7 +7,7 @@ import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import javax.inject.Inject class NewLuckyNumberNotification @Inject constructor( @@ -22,7 +22,7 @@ class NewLuckyNumberNotification @Inject constructor( R.string.lucky_number_notify_new_item, item.luckyNumber.toString() ), - intentToStart = MainActivity.getStartIntent(context, Destination.LuckyNumber, true) + intentToStart = SplashActivity.getStartIntent(context, Destination.LuckyNumber, true) ) appNotificationManager.sendSingleNotification( diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt index cdb5ab9c8..1a06cf6fa 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt @@ -8,7 +8,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.getPlural import javax.inject.Inject @@ -22,7 +22,7 @@ class NewMessageNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.message_new_items, 1), content = "${it.sender}: ${it.subject}", - intentToStart = MainActivity.getStartIntent(context, Destination.Message, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Message, true), ) } @@ -30,7 +30,7 @@ class NewMessageNotification @Inject constructor( notificationDataList = notificationDataList, title = context.getPlural(R.plurals.message_new_items, items.size), content = context.getPlural(R.plurals.message_notify_new_items, items.size, items.size), - intentToStart = MainActivity.getStartIntent(context, Destination.Message, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Message, true), type = NotificationType.NEW_MESSAGE ) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt index 16be1ca50..533d7f387 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt @@ -9,7 +9,7 @@ import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.sdk.scrapper.notes.NoteCategory import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.getPlural import javax.inject.Inject @@ -29,13 +29,13 @@ class NewNoteNotification @Inject constructor( NotificationData( title = context.getPlural(titleRes, 1), content = "${it.teacher}: ${it.category}", - intentToStart = MainActivity.getStartIntent(context, Destination.Note, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Note, true), ) } val groupNotificationData = GroupNotificationData( notificationDataList = notificationDataList, - intentToStart = MainActivity.getStartIntent(context, Destination.Note, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Note, true), title = context.getPlural(R.plurals.note_new_items, items.size), content = context.getPlural(R.plurals.note_notify_new_items, items.size, items.size), type = NotificationType.NEW_NOTE diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt index 1f603624b..1efe13ae9 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt @@ -8,7 +8,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.getPlural import javax.inject.Inject @@ -20,7 +20,7 @@ class NewSchoolAnnouncementNotification @Inject constructor( suspend fun notify(items: List, student: Student) { val notificationDataList = items.map { NotificationData( - intentToStart = MainActivity.getStartIntent( + intentToStart = SplashActivity.getStartIntent( context = context, destination = Destination.SchoolAnnouncement, startNewTask = true @@ -34,7 +34,7 @@ class NewSchoolAnnouncementNotification @Inject constructor( } val groupNotificationData = GroupNotificationData( type = NotificationType.NEW_ANNOUNCEMENT, - intentToStart = MainActivity.getStartIntent( + intentToStart = SplashActivity.getStartIntent( context = context, destination = Destination.SchoolAnnouncement, startNewTask = true diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt index 0521b4a08..075557a5c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt @@ -1,14 +1,11 @@ package io.github.wulkanowy.ui.base import android.app.ActivityManager -import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK -import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.os.Bundle import android.view.View import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.app.AppCompatDelegate import androidx.viewbinding.ViewBinding import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar.LENGTH_LONG @@ -40,7 +37,6 @@ abstract class BaseActivity, VB : ViewBinding> : themeManager.applyActivityTheme(this) super.onCreate(savedInstanceState) supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleLogger, true) - AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) @Suppress("DEPRECATION") setTaskDescription( @@ -83,8 +79,8 @@ abstract class BaseActivity, VB : ViewBinding> : } override fun openClearLoginView() { - startActivity(LoginActivity.getStartIntent(this) - .apply { addFlags(FLAG_ACTIVITY_CLEAR_TASK or FLAG_ACTIVITY_NEW_TASK) }) + startActivity(LoginActivity.getStartIntent(this)) + finishAffinity() } override fun onDestroy() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt index 5cd5d0109..40d548607 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt @@ -20,7 +20,7 @@ open class BasePresenter( ) { private val job = SupervisorJob() - protected val presenterScope = CoroutineScope(job + Dispatchers.Main) + protected val presenterScope = CoroutineScope(job + Dispatchers.Main.immediate) private val childrenJobs = mutableMapOf() diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt b/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt index b560ed2e7..e934a1822 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt @@ -41,14 +41,15 @@ class ThemeManager @Inject constructor(private val preferencesRepository: Prefer ) } - private fun isThemeApplicable(activity: AppCompatActivity): Boolean { - return activity.packageManager + private fun isThemeApplicable(activity: AppCompatActivity) = + activity.packageManager .getPackageInfo(activity.packageName, GET_ACTIVITIES) - .activities.singleOrNull { it.name == activity::class.java.canonicalName } - ?.theme.let { + .activities + .singleOrNull { it.name == activity::class.java.canonicalName } + ?.theme + .let { it == R.style.WulkanowyTheme_Black || it == R.style.WulkanowyTheme_NoActionBar || it == R.style.WulkanowyTheme_Login || it == R.style.WulkanowyTheme_Login_Black || it == R.style.WulkanowyTheme_MessageSend || it == R.style.WulkanowyTheme_MessageSend_Black } - } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt index 0406afa41..43d4b5f9f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt @@ -19,6 +19,10 @@ import java.time.LocalDate sealed interface Destination : Serializable { + /* + Type in children classes have to be as getter to avoid null in enums + https://stackoverflow.com/questions/68866453/kotlin-enum-val-is-returning-null-despite-being-set-at-compile-time + */ val type: Type val fragment: Fragment @@ -41,91 +45,91 @@ sealed interface Destination : Serializable { object Dashboard : Destination { - override val type = Type.DASHBOARD + override val type get() = Type.DASHBOARD override val fragment get() = DashboardFragment.newInstance() } object Grade : Destination { - override val type = Type.GRADE + override val type get() = Type.GRADE override val fragment get() = GradeFragment.newInstance() } object Attendance : Destination { - override val type = Type.ATTENDANCE + override val type get() = Type.ATTENDANCE override val fragment get() = AttendanceFragment.newInstance() } object Exam : Destination { - override val type = Type.EXAM + override val type get() = Type.EXAM override val fragment get() = ExamFragment.newInstance() } data class Timetable(val date: LocalDate? = null) : Destination { - override val type = Type.TIMETABLE + override val type get() = Type.TIMETABLE override val fragment get() = TimetableFragment.newInstance(date) } object Homework : Destination { - override val type = Type.HOMEWORK + override val type get() = Type.HOMEWORK override val fragment get() = HomeworkFragment.newInstance() } object Note : Destination { - override val type = Type.NOTE + override val type get() = Type.NOTE override val fragment get() = NoteFragment.newInstance() } object Conference : Destination { - override val type = Type.CONFERENCE + override val type get() = Type.CONFERENCE override val fragment get() = ConferenceFragment.newInstance() } object SchoolAnnouncement : Destination { - override val type = Type.SCHOOL_ANNOUNCEMENT + override val type get() = Type.SCHOOL_ANNOUNCEMENT override val fragment get() = SchoolAnnouncementFragment.newInstance() } object School : Destination { - override val type = Type.SCHOOL + override val type get() = Type.SCHOOL override val fragment get() = SchoolFragment.newInstance() } object LuckyNumber : Destination { - override val type = Type.LUCKY_NUMBER + override val type get() = Type.LUCKY_NUMBER override val fragment get() = LuckyNumberFragment.newInstance() } object More : Destination { - override val type = Type.MORE + override val type get() = Type.MORE override val fragment get() = MoreFragment.newInstance() } object Message : Destination { - override val type = Type.MESSAGE + override val type get() = Type.MESSAGE override val fragment get() = MessageFragment.newInstance() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditColorAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditColorAdapter.kt index ab6eec417..66e39fc72 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditColorAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditColorAdapter.kt @@ -3,12 +3,9 @@ package io.github.wulkanowy.ui.modules.account.accountedit import android.annotation.SuppressLint import android.content.res.ColorStateList import android.graphics.Color -import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable import android.graphics.drawable.RippleDrawable -import android.graphics.drawable.StateListDrawable -import android.os.Build import android.view.LayoutInflater import android.view.ViewGroup import androidx.core.view.isVisible @@ -52,30 +49,13 @@ class AccountEditColorAdapter @Inject constructor() : } } - private fun Int.createForegroundDrawable(): Drawable = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - val mask = GradientDrawable().apply { - shape = GradientDrawable.OVAL - setColor(Color.BLACK) - } - RippleDrawable(ColorStateList.valueOf(this.rippleColor), null, mask) - } else { - val foreground = StateListDrawable().apply { - alpha = 80 - setEnterFadeDuration(250) - setExitFadeDuration(250) - } - - val mask = GradientDrawable().apply { - shape = GradientDrawable.OVAL - setColor(this@createForegroundDrawable.rippleColor) - } - - foreground.apply { - addState(intArrayOf(android.R.attr.state_pressed), mask) - addState(intArrayOf(), ColorDrawable(Color.TRANSPARENT)) - } + private fun Int.createForegroundDrawable(): Drawable { + val mask = GradientDrawable().apply { + shape = GradientDrawable.OVAL + setColor(Color.BLACK) } + return RippleDrawable(ColorStateList.valueOf(this.rippleColor), null, mask) + } private inline val Int.rippleColor: Int get() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt index 092c9d0d8..c6ee60ee4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt @@ -121,9 +121,7 @@ class AttendanceFragment : BaseFragment(R.layout.frag attendanceSwipe.setOnRefreshListener(presenter::onSwipeRefresh) attendanceSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) attendanceSwipe.setProgressBackgroundColorSchemeColor( - requireContext().getThemeAttrColor( - R.attr.colorSwipeRefresh - ) + requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh) ) attendanceErrorRetry.setOnClickListener { presenter.onRetry() } attendanceErrorDetails.setOnClickListener { presenter.onDetailsClick() } @@ -134,7 +132,7 @@ class AttendanceFragment : BaseFragment(R.layout.frag attendanceExcuseButton.setOnClickListener { presenter.onExcuseButtonClick() } - attendanceNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + attendanceNavContainer.elevation = requireContext().dpToPx(8f) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryFragment.kt index 118971e62..dd1644a98 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryFragment.kt @@ -71,7 +71,7 @@ class AttendanceSummaryFragment : setOnItemSelectedListener { presenter.onSubjectSelected(it?.text?.toString()) } } - binding.attendanceSummarySubjectsContainer.setElevationCompat(requireContext().dpToPx(1f)) + binding.attendanceSummarySubjectsContainer.elevation = requireContext().dpToPx(1f) } override fun updateSubjects(data: ArrayList) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt index fb7939bc5..ddd0e4a19 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt @@ -64,7 +64,7 @@ class ExamFragment : BaseFragment(R.layout.fragment_exam), examPreviousButton.setOnClickListener { presenter.onPreviousWeek() } examNextButton.setOnClickListener { presenter.onNextWeek() } - examNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + examNavContainer.elevation = requireContext().dpToPx(8f) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt index f9ecb681b..0a8561eec 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt @@ -96,9 +96,7 @@ class GradeFragment : BaseFragment(R.layout.fragment_grade TabLayoutMediator(binding.gradeTabLayout, binding.gradeViewPager, this).attach() } - with(binding.gradeTabLayout) { - setElevationCompat(context.dpToPx(4f)) - } + binding.gradeTabLayout.elevation = requireContext().dpToPx(4f) with(binding) { gradeErrorRetry.setOnClickListener { presenter.onRetry() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsFragment.kt index 0adac300a..dbc4c10c6 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsFragment.kt @@ -68,7 +68,7 @@ class GradeStatisticsFragment : } with(binding) { - gradeStatisticsSubjectsContainer.setElevationCompat(requireContext().dpToPx(1f)) + gradeStatisticsSubjectsContainer.elevation = requireContext().dpToPx(1f) gradeStatisticsSwipe.setOnRefreshListener(presenter::onSwipeRefresh) gradeStatisticsSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt index a723b0483..d4eaade2c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt @@ -67,7 +67,7 @@ class HomeworkFragment : BaseFragment(R.layout.fragment openAddHomeworkButton.setOnClickListener { presenter.onHomeworkAddButtonClicked() } - homeworkNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + homeworkNavContainer.elevation = requireContext().dpToPx(8f) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt index f7bab526c..70b54c493 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt @@ -35,13 +35,13 @@ class LoginActivity : BaseActivity(), Logi @Inject lateinit var updateHelper: UpdateHelper + override val currentViewIndex get() = binding.loginViewpager.currentItem + companion object { fun getStartIntent(context: Context) = Intent(context, LoginActivity::class.java) } - override val currentViewIndex get() = binding.loginViewpager.currentItem - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(ActivityLoginBinding.inflate(layoutInflater).apply { binding = this }.root) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt index 04e35d318..87cb505c4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt @@ -66,14 +66,8 @@ class LoginStudentSelectFragment : } override fun openMainView() { - activity?.let { - startActivity( - MainActivity.getStartIntent( - context = it, - startNewTask = true - ) - ) - } + startActivity(MainActivity.getStartIntent(requireContext())) + requireActivity().finish() } override fun showProgress(show: Boolean) { @@ -115,7 +109,8 @@ class LoginStudentSelectFragment : chooserTitle = requireContext().getString(R.string.login_email_intent_title), email = "wulkanowyinc@gmail.com", subject = requireContext().getString(R.string.login_email_subject), - body = requireContext().getString(R.string.login_email_text, appInfo.systemModel, + body = requireContext().getString( + R.string.login_email_text, appInfo.systemModel, appInfo.systemVersion.toString(), appInfo.versionName, "Select users to log in", diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt index 3a84b2dd5..49d094b78 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt @@ -65,7 +65,7 @@ class LuckyNumberHistoryFragment : luckyNumberHistoryPreviousButton.setOnClickListener { presenter.onPreviousWeek() } luckyNumberHistoryNextButton.setOnClickListener { presenter.onNextWeek() } - luckyNumberHistoryNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + luckyNumberHistoryNavContainer.elevation = requireContext().dpToPx(8f) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt index 51585ac58..9ef410683 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt @@ -1,7 +1,6 @@ package io.github.wulkanowy.ui.modules.luckynumberwidget import android.app.PendingIntent -import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH @@ -19,7 +18,8 @@ import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.data.repositories.LuckyNumberRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity +import io.github.wulkanowy.utils.PendingIntentCompat import io.github.wulkanowy.utils.toFirstResult import kotlinx.coroutines.runBlocking import timber.log.Timber @@ -62,8 +62,8 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { val appIntent = PendingIntent.getActivity( context, LUCKY_NUMBER_PENDING_INTENT_ID, - MainActivity.getStartIntent(context, Destination.LuckyNumber, true), - FLAG_UPDATE_CURRENT + SplashActivity.getStartIntent(context, Destination.LuckyNumber, true), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) val remoteView = diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt index 92bc36bc7..3969e1d3e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt @@ -3,8 +3,6 @@ package io.github.wulkanowy.ui.modules.main import android.annotation.SuppressLint import android.content.Context import android.content.Intent -import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK -import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.os.Build.VERSION_CODES.P import android.os.Bundle import android.view.Menu @@ -23,7 +21,6 @@ import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.databinding.ActivityMainBinding -import io.github.wulkanowy.services.shortcuts.ShortcutsHelper import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog @@ -59,9 +56,6 @@ class MainActivity : BaseActivity(), MainVie @Inject lateinit var appInfo: AppInfo - @Inject - lateinit var shortcutsHelper: ShortcutsHelper - private var accountMenu: MenuItem? = null private val overlayProvider by lazy { ElevationOverlayProvider(this) } @@ -76,13 +70,8 @@ class MainActivity : BaseActivity(), MainVie fun getStartIntent( context: Context, destination: Destination? = null, - startNewTask: Boolean = false ) = Intent(context, MainActivity::class.java).apply { putExtra(EXTRA_START_DESTINATION, destination) - - if (startNewTask) { - flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK - } } } @@ -109,7 +98,6 @@ class MainActivity : BaseActivity(), MainVie updateHelper.messageContainer = binding.mainFragmentContainer val destination = intent.getSerializableExtra(EXTRA_START_DESTINATION) as Destination? - ?: shortcutsHelper.getDestination(intent) presenter.onAttachView(this, destination) updateHelper.checkAndInstallUpdates(this) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt index 51076c649..a45ab6234 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt @@ -77,9 +77,8 @@ class MessageFragment : BaseFragment(R.layout.fragment_m TabLayoutMediator(binding.messageTabLayout, binding.messageViewPager, this).attach() } - with(binding.messageTabLayout) { - setElevationCompat(context.dpToPx(4f)) - } + binding.messageTabLayout.elevation = requireContext().dpToPx(4f) + binding.openSendMessageButton.setOnClickListener { presenter.onSendMessageButtonClicked() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt index 74f8f57ec..e1cc2e374 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.message.preview -import android.os.Build import android.os.Bundle import android.print.PrintAttributes import android.print.PrintManager @@ -13,7 +12,6 @@ import android.view.View.VISIBLE import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient -import androidx.annotation.RequiresApi import androidx.core.content.getSystemService import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint @@ -25,7 +23,6 @@ import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.message.send.SendMessageActivity -import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.shareText import javax.inject.Inject @@ -40,9 +37,6 @@ class MessagePreviewFragment : @Inject lateinit var previewAdapter: MessagePreviewAdapter - @Inject - lateinit var appInfo: AppInfo - private var menuReplyButton: MenuItem? = null private var menuForwardButton: MenuItem? = null @@ -140,7 +134,7 @@ class MessagePreviewFragment : menuForwardButton?.isVisible = show menuDeleteButton?.isVisible = show menuShareButton?.isVisible = show - menuPrintButton?.isVisible = show && appInfo.systemVersion >= Build.VERSION_CODES.LOLLIPOP + menuPrintButton?.isVisible = show } override fun setDeletedOptionsLabels() { @@ -175,7 +169,6 @@ class MessagePreviewFragment : context?.shareText(text, subject) } - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) override fun printDocument(html: String, jobName: String) { val webView = WebView(requireContext()) webView.webViewClient = object : WebViewClient() { @@ -190,7 +183,6 @@ class MessagePreviewFragment : webView.loadDataWithBaseURL("file:///android_asset/", html, "text/HTML", "UTF-8", null) } - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) private fun createWebPrintJob(webView: WebView, jobName: String) { activity?.getSystemService()?.let { printManager -> val printAdapter = webView.createPrintDocumentAdapter(jobName) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt index 702e54676..eb33ee6ea 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt @@ -1,7 +1,6 @@ package io.github.wulkanowy.ui.modules.message.preview import android.annotation.SuppressLint -import android.os.Build import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.MessageAttachment @@ -11,7 +10,6 @@ import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper -import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.flowWithResourceIn @@ -24,8 +22,7 @@ class MessagePreviewPresenter @Inject constructor( errorHandler: ErrorHandler, studentRepository: StudentRepository, private val messageRepository: MessageRepository, - private val analytics: AnalyticsHelper, - private var appInfo: AppInfo + private val analytics: AnalyticsHelper ) : BasePresenter(errorHandler, studentRepository) { var message: Message? = null @@ -112,10 +109,11 @@ class MessagePreviewPresenter @Inject constructor( fun onShare(): Boolean { message?.let { - var text = "Temat: ${it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }}\n" + when (it.sender.isNotEmpty()) { - true -> "Od: ${it.sender}\n" - false -> "Do: ${it.recipient}\n" - } + "Data: ${it.date.toFormattedString("yyyy-MM-dd HH:mm:ss")}\n\n${it.content}" + var text = + "Temat: ${it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }}\n" + when (it.sender.isNotEmpty()) { + true -> "Od: ${it.sender}\n" + false -> "Do: ${it.recipient}\n" + } + "Data: ${it.date.toFormattedString("yyyy-MM-dd HH:mm:ss")}\n\n${it.content}" attachments?.let { attachments -> if (attachments.isNotEmpty()) { @@ -127,7 +125,10 @@ class MessagePreviewPresenter @Inject constructor( } } - view?.shareText(text, "FW: ${it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }}") + view?.shareText( + text, + "FW: ${it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }}" + ) return true } return false @@ -135,7 +136,6 @@ class MessagePreviewPresenter @Inject constructor( @SuppressLint("NewApi") fun onPrint(): Boolean { - if (appInfo.systemVersion < Build.VERSION_CODES.LOLLIPOP) return false message?.let { val dateString = it.date.toFormattedString("yyyy-MM-dd HH:mm:ss") val infoContent = "

Data wysłania

$dateString
" + when { @@ -154,7 +154,9 @@ class MessagePreviewPresenter @Inject constructor( view?.apply { val html = printHTML - .replace("%SUBJECT%", it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }) + .replace( + "%SUBJECT%", + it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }) .replace("%CONTENT%", messageContent) .replace("%INFO%", infoContent) printDocument(html, jobName) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt index 583ba6878..88fe77d94 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt @@ -1,7 +1,5 @@ package io.github.wulkanowy.ui.modules.message.preview -import android.os.Build -import androidx.annotation.RequiresApi import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.MessageWithAttachment import io.github.wulkanowy.ui.base.BaseView @@ -42,8 +40,7 @@ interface MessagePreviewView : BaseView { fun shareText(text: String, subject: String) - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - fun printDocument(html: String, jobName: String) - fun popView() + + fun printDocument(html: String, jobName: String) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt index 4571390b5..9b76a7f08 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt @@ -127,10 +127,13 @@ class MessageTabPresenter @Inject constructor( onlyUnread, onlyWithAttachments ) - val newItems = listOf(MessageTabDataItem.Header) + filteredData.map { - MessageTabDataItem.MessageItem(it) + val messageItems = filteredData.map { message -> + MessageTabDataItem.MessageItem(message) } - updateData(newItems, folder.id == MessageFolder.SENT.id) + val messageItemsWithHeader = + listOf(MessageTabDataItem.Header) + messageItems + + updateData(messageItemsWithHeader, folder.id == MessageFolder.SENT.id) notifyParentDataLoaded() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt index 9892b77ad..f4fa8e01d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt @@ -77,9 +77,7 @@ class SchoolAndTeachersFragment : ).attach() } - with(binding.schoolandteachersTabLayout) { - setElevationCompat(context.dpToPx(4f)) - } + binding.schoolandteachersTabLayout.elevation = requireContext().dpToPx(4f) } override fun showContent(show: Boolean) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt index 43920588c..dd6cf0ecd 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.settings.notifications -import android.annotation.SuppressLint import android.content.Intent import android.content.SharedPreferences import android.net.Uri @@ -152,7 +151,6 @@ class NotificationsFragment : PreferenceFragmentCompat(), .show() } - @SuppressLint("InlinedApi") override fun openSystemSettings() { val intent = if (appInfo.systemVersion >= Build.VERSION_CODES.O) { Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashActivity.kt index 376ef3743..b27f06ef4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashActivity.kt @@ -1,29 +1,60 @@ package io.github.wulkanowy.ui.modules.splash +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent import android.os.Bundle import android.widget.Toast import android.widget.Toast.LENGTH_LONG +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.viewbinding.ViewBinding import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.services.shortcuts.ShortcutsHelper import io.github.wulkanowy.ui.base.BaseActivity +import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.main.MainActivity -import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.openInternetBrowser import javax.inject.Inject +@SuppressLint("CustomSplashScreen") @AndroidEntryPoint class SplashActivity : BaseActivity(), SplashView { @Inject - lateinit var appInfo: AppInfo + override lateinit var presenter: SplashPresenter @Inject - override lateinit var presenter: SplashPresenter + lateinit var shortcutsHelper: ShortcutsHelper + + companion object { + + private const val EXTRA_START_DESTINATION = "start_destination" + + private const val EXTRA_EXTERNAL_URL = "external_url" + + fun getStartIntent( + context: Context, + destination: Destination? = null, + startNewTask: Boolean = false + ) = Intent(context, SplashActivity::class.java).apply { + putExtra(EXTRA_START_DESTINATION, destination) + + if (startNewTask) { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + } + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - presenter.onAttachView(this, intent?.getStringExtra("external_url")) + installSplashScreen().setKeepVisibleCondition { true } + + val externalLink = intent?.getStringExtra(EXTRA_EXTERNAL_URL) + val startDestination = intent?.getSerializableExtra(EXTRA_START_DESTINATION) as Destination? + ?: shortcutsHelper.getDestination(intent) + + presenter.onAttachView(this, externalLink, startDestination) } override fun openLoginView() { @@ -31,8 +62,8 @@ class SplashActivity : BaseActivity(), SplashView finish() } - override fun openMainView() { - startActivity(MainActivity.getStartIntent(this)) + override fun openMainView(destination: Destination?) { + startActivity(MainActivity.getStartIntent(this, destination)) finish() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashPresenter.kt index 03e43efa8..0b7409020 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashPresenter.kt @@ -1,12 +1,10 @@ package io.github.wulkanowy.ui.modules.splash -import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler -import io.github.wulkanowy.utils.flowWithResource -import kotlinx.coroutines.flow.onEach -import timber.log.Timber +import io.github.wulkanowy.ui.modules.Destination +import kotlinx.coroutines.launch import javax.inject.Inject class SplashPresenter @Inject constructor( @@ -14,7 +12,7 @@ class SplashPresenter @Inject constructor( studentRepository: StudentRepository, ) : BasePresenter(errorHandler, studentRepository) { - fun onAttachView(view: SplashView, externalUrl: String?) { + fun onAttachView(view: SplashView, externalUrl: String?, startDestination: Destination?) { super.onAttachView(view) if (!externalUrl.isNullOrBlank()) { @@ -22,15 +20,16 @@ class SplashPresenter @Inject constructor( return } - flowWithResource { studentRepository.isCurrentStudentSet() }.onEach { - when (it.status) { - Status.LOADING -> Timber.d("Is current user set check started") - Status.SUCCESS -> { - if (it.data!!) view.openMainView() - else view.openLoginView() + presenterScope.launch { + runCatching { studentRepository.isCurrentStudentSet() } + .onFailure(errorHandler::dispatch) + .onSuccess { + if (it) { + view.openMainView(startDestination) + } else { + view.openLoginView() + } } - Status.ERROR -> errorHandler.dispatch(it.error!!) - } - }.launch() + } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashView.kt index a5aa14091..1c5d8bfd4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashView.kt @@ -1,12 +1,13 @@ package io.github.wulkanowy.ui.modules.splash import io.github.wulkanowy.ui.base.BaseView +import io.github.wulkanowy.ui.modules.Destination interface SplashView : BaseView { fun openLoginView() - fun openMainView() + fun openMainView(destination: Destination?) fun openExternalUrlAndFinish(url: String) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt index 4478a2a66..07a9f6c76 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt @@ -97,7 +97,7 @@ class TimetableFragment : BaseFragment(R.layout.fragme timetableNavDate.setOnClickListener { presenter.onPickDate() } timetableNextButton.setOnClickListener { presenter.onNextDay() } - timetableNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + timetableNavContainer.elevation = requireContext().dpToPx(8f) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt index 47bee1e39..e8fb9e440 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt @@ -72,7 +72,7 @@ class AdditionalLessonsFragment : additionalLessonsNavDate.setOnClickListener { presenter.onPickDate() } additionalLessonsNextButton.setOnClickListener { presenter.onNextDay() } - additionalLessonsNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + additionalLessonsNavContainer.elevation = requireContext().dpToPx(8f) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsFragment.kt index b8da1c0fd..a6b126447 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsFragment.kt @@ -79,7 +79,7 @@ class CompletedLessonsFragment : completedLessonsNavDate.setOnClickListener { presenter.onPickDate() } completedLessonsNextButton.setOnClickListener { presenter.onNextDay() } - completedLessonsNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + completedLessonsNavContainer.elevation = requireContext().dpToPx(8f) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt index 63fc84962..2c8c5a677 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt @@ -2,7 +2,6 @@ package io.github.wulkanowy.ui.modules.timetablewidget import android.annotation.SuppressLint import android.app.PendingIntent -import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager.ACTION_APPWIDGET_DELETED import android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE @@ -25,8 +24,9 @@ import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.services.HiltBroadcastReceiver import io.github.wulkanowy.services.widgets.TimetableWidgetService import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.AnalyticsHelper +import io.github.wulkanowy.utils.PendingIntentCompat import io.github.wulkanowy.utils.capitalise import io.github.wulkanowy.utils.createNameInitialsDrawable import io.github.wulkanowy.utils.getCompatColor @@ -167,18 +167,20 @@ class TimetableWidgetProvider : HiltBroadcastReceiver() { action = appWidgetId.toString() } val accountIntent = PendingIntent.getActivity( - context, -Int.MAX_VALUE + appWidgetId, + context, + -Int.MAX_VALUE + appWidgetId, Intent(context, TimetableWidgetConfigureActivity::class.java).apply { addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK) putExtra(EXTRA_APPWIDGET_ID, appWidgetId) putExtra(EXTRA_FROM_PROVIDER, true) - }, FLAG_UPDATE_CURRENT + }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) val appIntent = PendingIntent.getActivity( context, TIMETABLE_PENDING_INTENT_ID, - MainActivity.getStartIntent(context, Destination.Timetable(), true), - FLAG_UPDATE_CURRENT + SplashActivity.getStartIntent(context, Destination.Timetable(), true), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) val remoteView = RemoteViews(context.packageName, layoutId).apply { @@ -222,16 +224,16 @@ class TimetableWidgetProvider : HiltBroadcastReceiver() { code: Int, appWidgetId: Int, buttonType: String - ): PendingIntent { - return PendingIntent.getBroadcast( - context, code, - Intent(context, TimetableWidgetProvider::class.java).apply { - action = ACTION_APPWIDGET_UPDATE - putExtra(EXTRA_BUTTON_TYPE, buttonType) - putExtra(EXTRA_TOGGLED_WIDGET_ID, appWidgetId) - }, FLAG_UPDATE_CURRENT - ) - } + ) = PendingIntent.getBroadcast( + context, + code, + Intent(context, TimetableWidgetProvider::class.java).apply { + action = ACTION_APPWIDGET_UPDATE + putExtra(EXTRA_BUTTON_TYPE, buttonType) + putExtra(EXTRA_TOGGLED_WIDGET_ID, appWidgetId) + }, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) private suspend fun getStudent(studentId: Long, appWidgetId: Int) = try { val students = studentRepository.getSavedStudents(false) diff --git a/app/src/main/java/io/github/wulkanowy/ui/widgets/FittedScrollableTabLayout.kt b/app/src/main/java/io/github/wulkanowy/ui/widgets/FittedScrollableTabLayout.kt index 0f121dc5b..6b7fb4aa9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/widgets/FittedScrollableTabLayout.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/widgets/FittedScrollableTabLayout.kt @@ -3,17 +3,15 @@ package io.github.wulkanowy.ui.widgets import android.content.Context import android.util.AttributeSet import android.view.ViewGroup +import com.google.android.material.tabs.TabLayout /** * @see
Tabs don't fit to screen with tabmode=scrollable, Even with a Custom Tab Layout */ -class FittedScrollableTabLayout : MaterialTabLayout { - - constructor(context: Context) : super(context) - - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) +class FittedScrollableTabLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : TabLayout(context, attrs) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { setMeasuredDimension(widthMeasureSpec, heightMeasureSpec) diff --git a/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialLinearLayout.kt b/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialLinearLayout.kt index a04922e5d..4e1ca1a97 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialLinearLayout.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialLinearLayout.kt @@ -1,24 +1,19 @@ package io.github.wulkanowy.ui.widgets import android.content.Context -import android.os.Build.VERSION.SDK_INT -import android.os.Build.VERSION_CODES.LOLLIPOP import android.util.AttributeSet import android.widget.LinearLayout import androidx.core.view.ViewCompat -import com.google.android.material.elevation.ElevationOverlayProvider import com.google.android.material.shape.MaterialShapeDrawable -class MaterialLinearLayout : LinearLayout { - - constructor(context: Context) : super(context) - - constructor(context: Context, attr: AttributeSet) : super(context, attr) - - constructor(context: Context, attr: AttributeSet, defStyleAttr: Int) : super(context, attr, defStyleAttr) +class MaterialLinearLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : LinearLayout(context, attrs) { init { - val drawable = MaterialShapeDrawable.createWithElevationOverlay(context, ViewCompat.getElevation(this)) + val drawable = + MaterialShapeDrawable.createWithElevationOverlay(context, ViewCompat.getElevation(this)) ViewCompat.setBackground(this, drawable) } @@ -28,12 +23,4 @@ class MaterialLinearLayout : LinearLayout { (background as MaterialShapeDrawable).elevation = elevation } } - - fun setElevationCompat(elevation: Float) { - if (SDK_INT >= LOLLIPOP) { - setElevation(elevation) - } else { - setBackgroundColor(ElevationOverlayProvider(context).compositeOverlayWithThemeSurfaceColorIfNeeded(elevation)) - } - } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialTabLayout.kt b/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialTabLayout.kt deleted file mode 100644 index e19d01116..000000000 --- a/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialTabLayout.kt +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.wulkanowy.ui.widgets - -import android.content.Context -import android.os.Build.VERSION.SDK_INT -import android.os.Build.VERSION_CODES.LOLLIPOP -import android.util.AttributeSet -import com.google.android.material.elevation.ElevationOverlayProvider -import com.google.android.material.tabs.TabLayout - -open class MaterialTabLayout : TabLayout { - - constructor(context: Context) : super(context) - - constructor(context: Context, attr: AttributeSet) : super(context, attr) - - constructor(context: Context, attr: AttributeSet, defStyleAttr: Int) : super(context, attr, defStyleAttr) - - fun setElevationCompat(elevation: Float) { - if (SDK_INT >= LOLLIPOP) { - setElevation(elevation) - } else { - setBackgroundColor(ElevationOverlayProvider(context).compositeOverlayWithThemeSurfaceColorIfNeeded(elevation)) - } - } -} diff --git a/app/src/main/java/io/github/wulkanowy/utils/PendingIntentCompat.kt b/app/src/main/java/io/github/wulkanowy/utils/PendingIntentCompat.kt new file mode 100644 index 000000000..45ee431a2 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/utils/PendingIntentCompat.kt @@ -0,0 +1,11 @@ +package io.github.wulkanowy.utils + +import android.app.PendingIntent +import android.os.Build + +object PendingIntentCompat { + + val FLAG_IMMUTABLE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PendingIntent.FLAG_IMMUTABLE + } else 0 +} diff --git a/app/src/main/java/io/github/wulkanowy/utils/StringExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/StringExtension.kt index 5c888f30a..bddd7df4c 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/StringExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/StringExtension.kt @@ -4,6 +4,4 @@ inline fun String?.ifNullOrBlank(defaultValue: () -> String) = if (isNullOrBlank()) defaultValue() else this fun String.capitalise() = - replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } - -fun String.decapitalise() = replaceFirstChar { it.lowercase() } + replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt b/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt index 74ae19326..c994ebab6 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt @@ -2,10 +2,8 @@ package io.github.wulkanowy.utils.security -import android.annotation.TargetApi import android.content.Context import android.os.Build.VERSION.SDK_INT -import android.os.Build.VERSION_CODES.JELLY_BEAN_MR2 import android.os.Build.VERSION_CODES.M import android.security.KeyPairGeneratorSpec import android.security.keystore.KeyGenParameterSpec @@ -116,7 +114,6 @@ fun decrypt(cipherText: String): String { } } -@TargetApi(JELLY_BEAN_MR2) private fun generateKeyPair(context: Context) { (if (SDK_INT >= M) { KeyGenParameterSpec.Builder(KEY_ALIAS, PURPOSE_DECRYPT or PURPOSE_ENCRYPT) diff --git a/app/src/main/res/drawable-v23/compat_splash_screen_no_icon_background.xml b/app/src/main/res/drawable-v23/compat_splash_screen_no_icon_background.xml new file mode 100644 index 000000000..dad56a171 --- /dev/null +++ b/app/src/main/res/drawable-v23/compat_splash_screen_no_icon_background.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-v23/img_splash_logo.png b/app/src/main/res/drawable-v23/img_splash_logo.png deleted file mode 100644 index 61489d81b973aae9ed13341ea9bc0f0902168b82..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19474 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 o% - - - - - - - diff --git a/app/src/main/res/drawable/ic_splash_logo.xml b/app/src/main/res/drawable/ic_splash_logo.xml new file mode 100644 index 000000000..e2e747316 --- /dev/null +++ b/app/src/main/res/drawable/ic_splash_logo.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/img_splash_logo.png b/app/src/main/res/drawable/img_splash_logo.png deleted file mode 100644 index fb521bf65ee01a4c75b85550fd214c7c606f4444..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20340 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 - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_attendance.xml b/app/src/main/res/layout/fragment_attendance.xml index 8016081b8..6f6f648a8 100644 --- a/app/src/main/res/layout/fragment_attendance.xml +++ b/app/src/main/res/layout/fragment_attendance.xml @@ -142,8 +142,8 @@ android:paddingRight="12dp" android:paddingBottom="8dp" android:scaleType="fitStart" - android:tint="?colorPrimary" - app:srcCompat="@drawable/ic_chevron_left" /> + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/fragment_exam.xml b/app/src/main/res/layout/fragment_exam.xml index ca88849c5..0c62aab5f 100644 --- a/app/src/main/res/layout/fragment_exam.xml +++ b/app/src/main/res/layout/fragment_exam.xml @@ -128,8 +128,8 @@ android:paddingRight="12dp" android:paddingBottom="8dp" android:scaleType="fitStart" - android:tint="?colorPrimary" - app:srcCompat="@drawable/ic_chevron_left" /> + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/fragment_lucky_number_history.xml b/app/src/main/res/layout/fragment_lucky_number_history.xml index 5f50d1261..a5698e2e4 100644 --- a/app/src/main/res/layout/fragment_lucky_number_history.xml +++ b/app/src/main/res/layout/fragment_lucky_number_history.xml @@ -118,8 +118,8 @@ android:paddingRight="12dp" android:paddingBottom="8dp" android:scaleType="fitStart" - android:tint="?colorPrimary" - app:srcCompat="@drawable/ic_chevron_left" /> + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/fragment_message.xml b/app/src/main/res/layout/fragment_message.xml index f6fe39ddb..5269d95e8 100644 --- a/app/src/main/res/layout/fragment_message.xml +++ b/app/src/main/res/layout/fragment_message.xml @@ -5,7 +5,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + android:layout_gravity="center_vertical" + app:tint="?colorPrimary" /> + android:layout_gravity="center_vertical" + app:tint="?colorPrimary" /> - + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/fragment_timetable_additional.xml b/app/src/main/res/layout/fragment_timetable_additional.xml index 61eb44454..a71f7545b 100644 --- a/app/src/main/res/layout/fragment_timetable_additional.xml +++ b/app/src/main/res/layout/fragment_timetable_additional.xml @@ -131,8 +131,8 @@ android:paddingRight="12dp" android:paddingBottom="8dp" android:scaleType="fitStart" - android:tint="?colorPrimary" - app:srcCompat="@drawable/ic_chevron_left" /> + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/fragment_timetable_completed.xml b/app/src/main/res/layout/fragment_timetable_completed.xml index 1a890fe12..e089275d9 100644 --- a/app/src/main/res/layout/fragment_timetable_completed.xml +++ b/app/src/main/res/layout/fragment_timetable_completed.xml @@ -130,8 +130,8 @@ android:paddingRight="12dp" android:paddingBottom="8dp" android:scaleType="fitStart" - android:tint="?colorPrimary" - app:srcCompat="@drawable/ic_chevron_left" /> + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/subitem_dashboard_conferences.xml b/app/src/main/res/layout/subitem_dashboard_conferences.xml index e80809365..8da2e19b4 100644 --- a/app/src/main/res/layout/subitem_dashboard_conferences.xml +++ b/app/src/main/res/layout/subitem_dashboard_conferences.xml @@ -2,7 +2,8 @@ + android:layout_height="wrap_content" + xmlns:tools="http://schemas.android.com/tools"> + - - \ No newline at end of file diff --git a/app/src/main/res/values-v26/styles.xml b/app/src/main/res/values-v26/styles.xml index 55413c053..3fb0a5dd8 100644 --- a/app/src/main/res/values-v26/styles.xml +++ b/app/src/main/res/values-v26/styles.xml @@ -5,11 +5,4 @@ false @android:color/darker_gray - - diff --git a/app/src/main/res/values-v28/styles.xml b/app/src/main/res/values-v28/styles.xml index ee77091d3..a936566f0 100644 --- a/app/src/main/res/values-v28/styles.xml +++ b/app/src/main/res/values-v28/styles.xml @@ -6,12 +6,4 @@ true @android:color/white - - \ No newline at end of file diff --git a/app/src/main/res/values-v29/styles.xml b/app/src/main/res/values-v29/styles.xml index ee77091d3..a936566f0 100644 --- a/app/src/main/res/values-v29/styles.xml +++ b/app/src/main/res/values-v29/styles.xml @@ -6,12 +6,4 @@ true @android:color/white - - \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 45382e6d2..7cd0f7258 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -23,10 +23,11 @@ true - @@ -50,8 +50,6 @@ 11sp - + + + + + diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml index 881d5bd4f..7d2f0cfed 100644 --- a/app/src/main/res/values-night/styles.xml +++ b/app/src/main/res/values-night/styles.xml @@ -1,20 +1,41 @@ - - - - - - + + diff --git a/app/src/main/res/values-v23/styles.xml b/app/src/main/res/values-v23/styles.xml index 840f53573..95450ae1a 100644 --- a/app/src/main/res/values-v23/styles.xml +++ b/app/src/main/res/values-v23/styles.xml @@ -3,6 +3,11 @@ - \ No newline at end of file + + + diff --git a/app/src/main/res/values-v26/styles.xml b/app/src/main/res/values-v26/styles.xml deleted file mode 100644 index 3fb0a5dd8..000000000 --- a/app/src/main/res/values-v26/styles.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - diff --git a/app/src/main/res/values-v27/styles.xml b/app/src/main/res/values-v27/styles.xml new file mode 100644 index 000000000..1cbe97911 --- /dev/null +++ b/app/src/main/res/values-v27/styles.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/app/src/main/res/values-v28/styles.xml b/app/src/main/res/values-v28/styles.xml deleted file mode 100644 index a936566f0..000000000 --- a/app/src/main/res/values-v28/styles.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/values-v29/styles.xml b/app/src/main/res/values-v29/styles.xml deleted file mode 100644 index a936566f0..000000000 --- a/app/src/main/res/values-v29/styles.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/values-v31/styles.xml b/app/src/main/res/values-v31/styles.xml new file mode 100644 index 000000000..cffb284ec --- /dev/null +++ b/app/src/main/res/values-v31/styles.xml @@ -0,0 +1,60 @@ + + + + + + + - - - - + + diff --git a/app/src/main/res/xml/provider_widget_lucky_number.xml b/app/src/main/res/xml/provider_widget_lucky_number.xml index 064f20570..330bd53fe 100644 --- a/app/src/main/res/xml/provider_widget_lucky_number.xml +++ b/app/src/main/res/xml/provider_widget_lucky_number.xml @@ -4,11 +4,13 @@ android:configure="io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetConfigureActivity" android:initialLayout="@layout/widget_luckynumber" android:minWidth="110dp" - android:minHeight="40dp" - android:minResizeWidth="40dp" + android:minHeight="110dp" android:minResizeHeight="40dp" android:previewImage="@drawable/img_luckynumber_widget_preview" + android:previewLayout="@layout/widget_luckynumber" android:resizeMode="horizontal|vertical" + android:targetCellWidth="2" + android:targetCellHeight="2" android:updatePeriodMillis="3600000" android:widgetCategory="home_screen" - tools:targetApi="jelly_bean_mr1" /> + tools:targetApi="s" /> diff --git a/app/src/main/res/xml/provider_widget_timetable.xml b/app/src/main/res/xml/provider_widget_timetable.xml index 5392dd50b..3cdad0c81 100644 --- a/app/src/main/res/xml/provider_widget_timetable.xml +++ b/app/src/main/res/xml/provider_widget_timetable.xml @@ -8,7 +8,10 @@ android:minResizeWidth="250dp" android:minResizeHeight="110dp" android:previewImage="@drawable/img_timetable_widget_preview" + android:previewLayout="@layout/widget_timetable_preview" android:resizeMode="horizontal|vertical" + android:targetCellWidth="3" + android:targetCellHeight="2" android:updatePeriodMillis="3600000" android:widgetCategory="home_screen" - tools:targetApi="jelly_bean_mr1" /> + tools:targetApi="s" /> From 8d2d7922f94bf7a15607aaad1bdb49234cb3f610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Wed, 29 Mar 2023 22:23:42 +0200 Subject: [PATCH 713/869] Fix collapse garde subject when grade is unread (#2158) --- .../github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt index 38bae3761..2d63aae4d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt @@ -15,6 +15,7 @@ import io.github.wulkanowy.utils.changeModifier import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import javax.inject.Inject @@ -77,7 +78,7 @@ class GradeAverageProvider @Inject constructor( ) } } - } + }.distinctUntilChanged() private fun calculateCombinedAverage( student: Student, From 7aa65e98ce3393a6e5a1e615d5ddc4ef0743a17c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Apr 2023 20:30:27 +0000 Subject: [PATCH 714/869] Bump com.android.tools:desugar_jdk_libs from 2.0.2 to 2.0.3 (#2162) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 3fd11ccc7..e96d9b801 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -188,7 +188,7 @@ ext { dependencies { implementation "io.github.wulkanowy:sdk:1.9.2" - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" From cb914fe32b862fe5ab2f6aba597410e0bc2af916 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Apr 2023 20:30:56 +0000 Subject: [PATCH 715/869] Bump com.google.android.gms:play-services-ads from 21.5.0 to 22.0.0 (#2161) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index e96d9b801..a0bc17aa7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -249,7 +249,7 @@ dependencies { playImplementation 'com.google.firebase:firebase-config-ktx' playImplementation 'com.google.android.play:core:1.10.3' playImplementation 'com.google.android.play:core-ktx:1.8.1' - playImplementation 'com.google.android.gms:play-services-ads:21.5.0' + playImplementation 'com.google.android.gms:play-services-ads:22.0.0' hmsImplementation 'com.huawei.hms:hianalytics:6.9.1.200' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.8.1.300' From a7cf54897ac6d05c042a2b7e39a138d01adccac7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Apr 2023 20:32:06 +0000 Subject: [PATCH 716/869] Bump kotlin_version from 1.8.10 to 1.8.20 (#2160) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c14e0dbd3..2aa3f758e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - kotlin_version = '1.8.10' + kotlin_version = '1.8.20' about_libraries = '10.6.1' hilt_version = "2.45" } From 253e55f70e63e7fe24e75669c01a84a4a3448fd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Wed, 5 Apr 2023 22:33:01 +0200 Subject: [PATCH 717/869] New Crowdin updates (#2159) --- app/src/main/res/values-cs/strings.xml | 1 + app/src/main/res/values-da-rDK/strings.xml | 1 + app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values-es-rES/strings.xml | 1 + app/src/main/res/values-pl/strings.xml | 1 + app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-sk/strings.xml | 1 + app/src/main/res/values-uk/strings.xml | 1 + 8 files changed, 8 insertions(+) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 1897a48b4..bedb491bc 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -682,6 +682,7 @@ Zrušit Žádné lekce + Synchronizováno %1$s v %2$s Vybrat motiv Světlý Tmavý diff --git a/app/src/main/res/values-da-rDK/strings.xml b/app/src/main/res/values-da-rDK/strings.xml index 3875b3d98..ebec11b25 100644 --- a/app/src/main/res/values-da-rDK/strings.xml +++ b/app/src/main/res/values-da-rDK/strings.xml @@ -594,6 +594,7 @@ Cancel No lessons + Synchronized on %1$s at %2$s Choose theme Light Dark diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index c03181e48..1e1785bff 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -594,6 +594,7 @@ Cancel Keine Lektionen + Synchronized on %1$s at %2$s Thema wählen Licht Dunkel diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 3875b3d98..ebec11b25 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -594,6 +594,7 @@ Cancel No lessons + Synchronized on %1$s at %2$s Choose theme Light Dark diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 7f0c32915..f797a4dcb 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -682,6 +682,7 @@ Anuluj Brak lekcji + Zsynchronizowano %1$s o %2$s Wybierz motyw Jasny Ciemny diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 42e1e0bb9..7a42e3880 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -682,6 +682,7 @@ Отменить Нет уроков + Synchronized on %1$s at %2$s Выбрать тему Светлая Тёмная diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 8979a95fc..cde3178bb 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -682,6 +682,7 @@ Zrušiť Žiadne lekcie + Synchronizované %1$s v %2$s Vybrať motív Svetlý Tmavý diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 0c7369040..9602aabba 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -682,6 +682,7 @@ Скасувати Немаэ уроків + Synchronized on %1$s at %2$s Увібрати тему Яскрава Темна From c67d2d767d1b037c571efea34787838b92c2fec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Thu, 6 Apr 2023 01:28:47 +0200 Subject: [PATCH 718/869] Set error tint to password toggle icon when error occured (#2163) --- .../ui/modules/login/form/LoginFormFragment.kt | 11 +++++------ .../io/github/wulkanowy/utils/ContextExtension.kt | 6 ++---- app/src/main/res/layout/fragment_login_form.xml | 4 ++-- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt index a0e7608d6..bbc382190 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt @@ -15,12 +15,7 @@ import io.github.wulkanowy.databinding.FragmentLoginFormBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.LoginData -import io.github.wulkanowy.utils.AppInfo -import io.github.wulkanowy.utils.hideSoftInput -import io.github.wulkanowy.utils.openEmailClient -import io.github.wulkanowy.utils.openInternetBrowser -import io.github.wulkanowy.utils.setOnEditorDoneSignIn -import io.github.wulkanowy.utils.showSoftInput +import io.github.wulkanowy.utils.* import javax.inject.Inject @AndroidEntryPoint @@ -149,12 +144,14 @@ class LoginFormFragment : BaseFragment(R.layout.fragme override fun setErrorPassRequired(focus: Boolean) { with(binding.loginFormPassLayout) { error = getString(R.string.error_field_required) + setEndIconTintList(requireContext().getAttrColorStateList(R.attr.colorError)) } } override fun setErrorPassInvalid(focus: Boolean) { with(binding.loginFormPassLayout) { error = getString(R.string.login_invalid_password) + setEndIconTintList(requireContext().getAttrColorStateList(R.attr.colorError)) } } @@ -162,6 +159,7 @@ class LoginFormFragment : BaseFragment(R.layout.fragme with(binding) { loginFormUsernameLayout.error = " " loginFormPassLayout.error = " " + loginFormPassLayout.setEndIconTintList(requireContext().getAttrColorStateList(R.attr.colorError)) loginFormHostLayout.error = " " loginFormErrorBox.text = message ?: getString(R.string.login_incorrect_password_default) loginFormErrorBox.isVisible = true @@ -181,6 +179,7 @@ class LoginFormFragment : BaseFragment(R.layout.fragme override fun clearPassError() { binding.loginFormPassLayout.error = null + binding.loginFormPassLayout.setEndIconTintList(null) binding.loginFormErrorBox.isVisible = false } diff --git a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt index cc4c5aaa4..77f3eb64a 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt @@ -6,7 +6,6 @@ import android.content.res.ColorStateList import android.graphics.* import android.text.TextPaint import android.util.DisplayMetrics.DENSITY_DEFAULT -import android.widget.ImageView import androidx.annotation.* import androidx.core.content.ContextCompat import androidx.core.graphics.ColorUtils @@ -14,7 +13,6 @@ import androidx.core.graphics.applyCanvas import androidx.core.graphics.drawable.RoundedBitmapDrawable import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import androidx.core.graphics.drawable.toBitmap -import androidx.core.widget.ImageViewCompat @ColorInt @@ -89,6 +87,6 @@ fun Context.createNameInitialsDrawable( .apply { isCircular = true } } -fun ImageView.setTint(@ColorInt color: Int) { - ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(color)) +fun Context.getAttrColorStateList(@AttrRes color: Int): ColorStateList { + return ColorStateList.valueOf(getThemeAttrColor(color)) } diff --git a/app/src/main/res/layout/fragment_login_form.xml b/app/src/main/res/layout/fragment_login_form.xml index 1aea70660..fac3960e9 100644 --- a/app/src/main/res/layout/fragment_login_form.xml +++ b/app/src/main/res/layout/fragment_login_form.xml @@ -180,12 +180,12 @@ android:layout_marginEnd="24dp" android:layout_marginRight="24dp" android:hint="@string/login_password_hint" + app:endIconMode="password_toggle" app:errorEnabled="true" app:errorIconDrawable="@null" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/loginFormUsernameLayout" - app:passwordToggleEnabled="true"> + app:layout_constraintTop_toBottomOf="@+id/loginFormUsernameLayout"> Date: Thu, 6 Apr 2023 01:29:46 +0200 Subject: [PATCH 719/869] Use segmented toggle buttons instead of option group in grades statistics (#2164) --- .../statistics/GradeStatisticsAdapter.kt | 4 ++- .../layout/item_grade_statistics_header.xml | 26 +++++++++++-------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsAdapter.kt index fd0ac5471..3fce8d57e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsAdapter.kt @@ -116,7 +116,9 @@ class GradeStatisticsAdapter @Inject constructor() : } ) - binding.gradeStatisticsTypeSwitch.setOnCheckedChangeListener { _, checkedId -> + binding.gradeStatisticsTypeSwitch.addOnButtonCheckedListener { _, checkedId, isChecked -> + if (!isChecked) return@addOnButtonCheckedListener + currentDataType = when (checkedId) { R.id.gradeStatisticsTypePartial -> GradeStatisticsItem.DataType.PARTIAL R.id.gradeStatisticsTypeSemester -> GradeStatisticsItem.DataType.SEMESTER diff --git a/app/src/main/res/layout/item_grade_statistics_header.xml b/app/src/main/res/layout/item_grade_statistics_header.xml index 92f522ba4..cc35f6066 100644 --- a/app/src/main/res/layout/item_grade_statistics_header.xml +++ b/app/src/main/res/layout/item_grade_statistics_header.xml @@ -1,8 +1,10 @@ + android:layout_height="wrap_content" + tools:context=".ui.modules.grade.statistics.GradeStatisticsAdapter"> - + android:paddingEnd="16dp" + app:selectionRequired="true" + app:singleSelection="true"> - - - - + - \ No newline at end of file + From bce2c39ccc0801e7831ed9936aa76b287fb435eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 6 Apr 2023 10:13:34 +0200 Subject: [PATCH 720/869] Disable error dialog for admin messages (#2165) --- .../wulkanowy/ui/modules/dashboard/DashboardPresenter.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt index 22b0d267e..ac2c896dc 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt @@ -606,7 +606,7 @@ class DashboardPresenter @Inject constructor( } is Resource.Error -> { Timber.i("Loading dashboard admin message result: An exception occurred") - errorHandler.dispatch(it.error) + Timber.e(it.error) updateData( dashboardItem = DashboardItem.AdminMessages( adminMessage = null, @@ -748,7 +748,7 @@ class DashboardPresenter @Inject constructor( itemsLoadedList.find { it.type == DashboardItem.Type.ACCOUNT }?.error != null val isGeneralError = filteredItems.none { it.error == null } && filteredItems.isNotEmpty() || isAccountItemError - val firstError = itemsLoadedList.mapNotNull { it.error }.firstOrNull() + val firstError = itemsLoadedList.firstNotNullOfOrNull { it.error } val filteredOriginalLoadedList = dashboardItemLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT } From 2c9434766861adbc5f7d5d497a4caac12a8c852b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 16:29:14 +0000 Subject: [PATCH 721/869] Bump androidx.core:core-ktx from 1.9.0 to 1.10.0 (#2167) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index a0bc17aa7..edb0ca36d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -193,7 +193,7 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" - implementation "androidx.core:core-ktx:1.9.0" + implementation "androidx.core:core-ktx:1.10.0" implementation 'androidx.core:core-splashscreen:1.0.0' implementation "androidx.activity:activity-ktx:1.7.0" implementation "androidx.appcompat:appcompat:1.6.1" From 327e61bbdd0c3dbc37da1029c2aad4aae804b444 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Apr 2023 16:29:50 +0000 Subject: [PATCH 722/869] Bump about_libraries from 10.6.1 to 10.6.2 (#2166) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2aa3f758e..d53632feb 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { kotlin_version = '1.8.20' - about_libraries = '10.6.1' + about_libraries = '10.6.2' hilt_version = "2.45" } repositories { From 6978ad11ebab55ffb221b060ba82443e9612929d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Apr 2023 05:11:17 +0000 Subject: [PATCH 723/869] Bump com.google.firebase:firebase-crashlytics-gradle from 2.9.4 to 2.9.5 (#2174) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d53632feb..ad23becdb 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.15' classpath 'com.huawei.agconnect:agcp:1.8.1.300' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.4' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.5' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.3.5" classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.0.0.2929" From e7054bb5b94c716f76e997fc4542d56ca981ffef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Apr 2023 05:11:45 +0000 Subject: [PATCH 724/869] Bump com.google.firebase:firebase-bom from 31.4.0 to 31.5.0 (#2173) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index edb0ca36d..0cc3592f7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -242,7 +242,7 @@ dependencies { implementation 'com.fredporciuncula:flow-preferences:1.9.0' implementation 'org.apache.commons:commons-text:1.10.0' - playImplementation platform('com.google.firebase:firebase-bom:31.4.0') + playImplementation platform('com.google.firebase:firebase-bom:31.5.0') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' From de8b38dd9ca1a5ab5b2b4618b8d406031c2e8b19 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Apr 2023 05:12:06 +0000 Subject: [PATCH 725/869] Bump org.robolectric:robolectric from 4.9.2 to 4.10 (#2169) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 0cc3592f7..c5c8992c8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -265,7 +265,7 @@ dependencies { testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines" testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" - testImplementation 'org.robolectric:robolectric:4.9.2' + testImplementation 'org.robolectric:robolectric:4.10' testImplementation "androidx.test:runner:1.5.2" testImplementation "androidx.test.ext:junit:1.1.5" testImplementation "androidx.test:core:1.5.0" From 1f30cc1f902b25cf2b56cc17a8b896d73ec946ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Apr 2023 05:12:24 +0000 Subject: [PATCH 726/869] Bump mockk from 1.13.4 to 1.13.5 (#2170) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index c5c8992c8..d0199a0e7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -181,7 +181,7 @@ ext { android_hilt = "1.0.0" room = "2.5.1" chucker = "3.5.2" - mockk = "1.13.4" + mockk = "1.13.5" coroutines = "1.6.4" } From 623f0339e692a362a03e202d3767cba8f7b6c9ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Apr 2023 05:21:17 +0000 Subject: [PATCH 727/869] Bump com.fredporciuncula:flow-preferences from 1.9.0 to 1.9.1 (#2172) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index d0199a0e7..b8d4e1ca5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -239,7 +239,7 @@ dependencies { implementation "io.coil-kt:coil:2.3.0" implementation "io.github.wulkanowy:AppKillerManager:3.0.1" implementation 'me.xdrop:fuzzywuzzy:1.4.0' - implementation 'com.fredporciuncula:flow-preferences:1.9.0' + implementation 'com.fredporciuncula:flow-preferences:1.9.1' implementation 'org.apache.commons:commons-text:1.10.0' playImplementation platform('com.google.firebase:firebase-bom:31.5.0') From b1d22843b59cbcabca741bdb8cdb9d92c2cc3d9f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 21:29:42 +0000 Subject: [PATCH 728/869] Bump androidx.core:core-splashscreen from 1.0.0 to 1.0.1 (#2180) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index b8d4e1ca5..77b3d8c40 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -194,7 +194,7 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" implementation "androidx.core:core-ktx:1.10.0" - implementation 'androidx.core:core-splashscreen:1.0.0' + implementation 'androidx.core:core-splashscreen:1.0.1' implementation "androidx.activity:activity-ktx:1.7.0" implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.fragment:fragment-ktx:1.5.6" From b2af5ed57de63a39cd392c3db3c93b91fd7690a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 21:30:39 +0000 Subject: [PATCH 729/869] Bump com.squareup.okhttp3:logging-interceptor from 4.10.0 to 4.11.0 (#2177) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 77b3d8c40..2a1d58885 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -230,7 +230,7 @@ dependencies { implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0" - implementation "com.squareup.okhttp3:logging-interceptor:4.10.0" + implementation "com.squareup.okhttp3:logging-interceptor:4.11.0" implementation "com.jakewharton.timber:timber:5.0.1" implementation "at.favre.lib:slf4j-timber:1.0.1" From 56d7e94946dca8536764050fdcf0080df10e9cd7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 21:31:46 +0000 Subject: [PATCH 730/869] Bump com.huawei.agconnect:agcp from 1.8.1.300 to 1.9.0.300 (#2179) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ad23becdb..b8813f89f 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ buildscript { classpath 'com.android.tools.build:gradle:7.4.2' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.15' - classpath 'com.huawei.agconnect:agcp:1.8.1.300' + classpath 'com.huawei.agconnect:agcp:1.9.0.300' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.5' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.3.5" From 4fedb74005e3a3cb02d6792f7e156ec62149c189 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 21:32:02 +0000 Subject: [PATCH 731/869] Bump kotlin_version from 1.8.20 to 1.8.21 (#2182) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b8813f89f..3c8552d20 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - kotlin_version = '1.8.20' + kotlin_version = '1.8.21' about_libraries = '10.6.2' hilt_version = "2.45" } From f7fa89638a4e0e55ce56b2f7e254816c1b9b82b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 21:32:49 +0000 Subject: [PATCH 732/869] Bump com.huawei.agconnect:agconnect-crash from 1.8.1.300 to 1.9.0.300 (#2178) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 2a1d58885..77a17a293 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -252,7 +252,7 @@ dependencies { playImplementation 'com.google.android.gms:play-services-ads:22.0.0' hmsImplementation 'com.huawei.hms:hianalytics:6.9.1.200' - hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.8.1.300' + hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.0.300' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 4be66637526c7ef1a77194c87532a52c4dbf4b1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 21:50:34 +0000 Subject: [PATCH 733/869] Bump com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter (#2175) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 77a17a293..732de13c0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -229,7 +229,7 @@ dependencies { implementation "com.github.YarikSOffice:lingver:1.3.0" implementation 'com.squareup.retrofit2:retrofit:2.9.0' - implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0" + implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0" implementation "com.squareup.okhttp3:logging-interceptor:4.11.0" implementation "com.jakewharton.timber:timber:5.0.1" From b1a5a77559df78866d7d194771485ec8746557e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 21:50:48 +0000 Subject: [PATCH 734/869] Bump androidx.fragment:fragment-ktx from 1.5.6 to 1.5.7 (#2176) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 732de13c0..f54d80cb9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -197,7 +197,7 @@ dependencies { implementation 'androidx.core:core-splashscreen:1.0.1' implementation "androidx.activity:activity-ktx:1.7.0" implementation "androidx.appcompat:appcompat:1.6.1" - implementation "androidx.fragment:fragment-ktx:1.5.6" + implementation "androidx.fragment:fragment-ktx:1.5.7" implementation "androidx.annotation:annotation:1.6.0" implementation "androidx.preference:preference-ktx:1.2.0" From b195fda026d7d262a3a1c408bcc2fdd11e01527c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 22:02:43 +0000 Subject: [PATCH 735/869] Bump androidx.activity:activity-ktx from 1.7.0 to 1.7.1 (#2181) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index f54d80cb9..79cb37ff8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -195,7 +195,7 @@ dependencies { implementation "androidx.core:core-ktx:1.10.0" implementation 'androidx.core:core-splashscreen:1.0.1' - implementation "androidx.activity:activity-ktx:1.7.0" + implementation "androidx.activity:activity-ktx:1.7.1" implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.fragment:fragment-ktx:1.5.7" implementation "androidx.annotation:annotation:1.6.0" From f8431d7ad6d10b67594599015bd5f6c9d5a45ae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sun, 7 May 2023 23:21:59 +0200 Subject: [PATCH 736/869] SDK update (#2168) --- .gitignore | 1 + app/build.gradle | 2 +- .../io/github/wulkanowy/data/DataModule.kt | 2 - .../github/wulkanowy/data/db/entities/Exam.kt | 1 + .../data/mappers/ConferenceMapper.kt | 6 +- .../wulkanowy/data/mappers/ExamMapper.kt | 2 +- .../wulkanowy/data/mappers/MessageMapper.kt | 2 +- .../data/mappers/MobileDeviceMapper.kt | 2 +- .../data/mappers/RegisterUserMapper.kt | 31 +++---- .../wulkanowy/data/mappers/StudentMapper.kt | 37 -------- .../wulkanowy/data/mappers/TimetableMapper.kt | 16 ++-- .../wulkanowy/data/pojos/RegisterUser.kt | 11 ++- .../data/repositories/AppCreatorRepository.kt | 1 - .../data/repositories/AttendanceRepository.kt | 2 +- .../data/repositories/ExamRepository.kt | 2 +- .../data/repositories/NoteRepository.kt | 2 +- .../data/repositories/SemesterRepository.kt | 2 +- .../data/repositories/StudentRepository.kt | 36 ++++---- .../data/repositories/TeacherRepository.kt | 2 +- .../data/repositories/TimetableRepository.kt | 2 +- .../ui/modules/login/LoginErrorHandler.kt | 14 ++-- .../login/advanced/LoginAdvancedFragment.kt | 8 +- .../login/advanced/LoginAdvancedPresenter.kt | 84 ++++--------------- .../login/advanced/LoginAdvancedView.kt | 1 - .../modules/login/form/LoginFormFragment.kt | 4 + .../modules/login/form/LoginFormPresenter.kt | 3 + .../ui/modules/login/form/LoginFormView.kt | 2 + .../LoginStudentSelectFragment.kt | 1 - .../io/github/wulkanowy/utils/SdkExtension.kt | 11 ++- .../main/res/layout/fragment_login_form.xml | 1 - .../io/github/wulkanowy/TestEnityCreator.kt | 2 +- .../data/mappers/AttendanceMapperKtTest.kt | 4 +- .../repositories/AttendanceRepositoryTest.kt | 12 +-- .../data/repositories/ExamRemoteTest.kt | 13 ++- .../data/repositories/GradeRepositoryTest.kt | 33 ++++++-- .../repositories/MessageRepositoryTest.kt | 2 +- .../MobileDeviceRepositoryTest.kt | 17 ++-- .../repositories/SemesterRepositoryTest.kt | 3 +- .../data/repositories/StudentTest.kt | 81 ------------------ .../repositories/TimetableRepositoryTest.kt | 80 ++++++++++++++---- .../domain/GetMailboxByStudentUseCaseTest.kt | 2 +- .../modules/grade/GradeAverageProviderTest.kt | 2 +- .../login/form/LoginFormPresenterTest.kt | 23 ++++- .../LoginStudentSelectPresenterTest.kt | 16 +++- 44 files changed, 260 insertions(+), 321 deletions(-) delete mode 100644 app/src/main/java/io/github/wulkanowy/data/mappers/StudentMapper.kt delete mode 100644 app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt diff --git a/.gitignore b/.gitignore index cd5ff7146..921bd0a9a 100644 --- a/.gitignore +++ b/.gitignore @@ -119,3 +119,4 @@ Thumbs.db app/src/release/agconnect-services.json app/src/release/agconnect-credentials.json .idea/deploymentTargetDropDown.xml +.idea/kotlinc.xml diff --git a/app/build.gradle b/app/build.gradle index 79cb37ff8..22de4b8b6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -186,7 +186,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:1.9.2" + implementation "io.github.wulkanowy:sdk:14267a9a" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' diff --git a/app/src/main/java/io/github/wulkanowy/data/DataModule.kt b/app/src/main/java/io/github/wulkanowy/data/DataModule.kt index e538b2b2f..c9e4990f9 100644 --- a/app/src/main/java/io/github/wulkanowy/data/DataModule.kt +++ b/app/src/main/java/io/github/wulkanowy/data/DataModule.kt @@ -20,7 +20,6 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.RemoteConfigHelper -import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient @@ -81,7 +80,6 @@ internal class DataModule { .readTimeout(30, TimeUnit.SECONDS) .build() - @OptIn(ExperimentalSerializationApi::class) @Singleton @Provides fun provideRetrofit( diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Exam.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Exam.kt index 50299e607..2292c3e62 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Exam.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Exam.kt @@ -22,6 +22,7 @@ data class Exam( val subject: String, + @Deprecated("not available anymore") val group: String, val type: String, diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/ConferenceMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/ConferenceMapper.kt index 17a9e5cdb..add6439d4 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/ConferenceMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/ConferenceMapper.kt @@ -10,9 +10,9 @@ fun List.mapToEntities(semester: Semester) = map { diaryId = semester.diaryId, agenda = it.agenda, conferenceId = it.id, - date = it.dateZoned.toInstant(), + date = it.date.toInstant(), presentOnConference = it.presentOnConference, - subject = it.subject, - title = it.title + subject = it.topic, + title = it.place, ) } diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/ExamMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/ExamMapper.kt index bdb5efbba..173dfebf9 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/ExamMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/ExamMapper.kt @@ -11,7 +11,7 @@ fun List.mapToEntities(semester: Semester) = map { date = it.date, entryDate = it.entryDate, subject = it.subject, - group = it.group, + group = "", type = it.type, description = it.description, teacher = it.teacher, diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/MessageMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/MessageMapper.kt index 6fc5dc950..a26d76651 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/MessageMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/MessageMapper.kt @@ -26,7 +26,7 @@ fun List.mapToEntities( messageId = it.id, correspondents = it.correspondents, subject = it.subject.trim(), - date = it.dateZoned.toInstant(), + date = it.date.toInstant(), folderId = it.folderId, unread = it.unread, unreadBy = it.unreadBy, diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/MobileDeviceMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/MobileDeviceMapper.kt index 1a1c501f6..1f4178fae 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/MobileDeviceMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/MobileDeviceMapper.kt @@ -9,7 +9,7 @@ import io.github.wulkanowy.sdk.pojo.Token as SdkToken fun List.mapToEntities(student: Student) = map { MobileDevice( userLoginId = student.userLoginId, - date = it.createDateZoned.toInstant(), + date = it.createDate.toInstant(), deviceId = it.id, name = it.name ) diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/RegisterUserMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/RegisterUserMapper.kt index 2dfd7e062..bcf26a5e1 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/RegisterUserMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/RegisterUserMapper.kt @@ -3,22 +3,24 @@ package io.github.wulkanowy.data.mappers import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.pojos.* -import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.sdk.mapper.mapSemesters import java.time.Instant -import io.github.wulkanowy.sdk.scrapper.register.RegisterStudent as SdkRegisterStudent -import io.github.wulkanowy.sdk.scrapper.register.RegisterUser as SdkRegisterUser +import io.github.wulkanowy.sdk.pojo.RegisterStudent as SdkRegisterStudent +import io.github.wulkanowy.sdk.pojo.RegisterUser as SdkRegisterUser -fun SdkRegisterUser.mapToPojo(password: String) = RegisterUser( +fun SdkRegisterUser.mapToPojo(password: String?) = RegisterUser( email = email, login = login, password = password, - baseUrl = baseUrl, + scrapperBaseUrl = scrapperBaseUrl, + loginMode = loginMode, loginType = loginType, symbols = symbols.map { registerSymbol -> RegisterSymbol( symbol = registerSymbol.symbol, error = registerSymbol.error, + hebeBaseUrl = registerSymbol.hebeBaseUrl, + keyId = registerSymbol.keyId, + privatePem = registerSymbol.privatePem, userName = registerSymbol.userName, schools = registerSymbol.schools.map { RegisterUnit( @@ -42,14 +44,13 @@ fun SdkRegisterUser.mapToPojo(password: String) = RegisterUser( classId = registerSubject.classId, isParent = registerSubject.isParent, semesters = registerSubject.semesters - .mapSemesters() .mapToEntities(registerSubject.studentId), ) }, ) } ) - } + }, ) fun RegisterStudent.mapToStudentWithSemesters( @@ -68,17 +69,17 @@ fun RegisterStudent.mapToStudentWithSemesters( classId = classId, studentId = studentId, symbol = symbol.symbol, - loginType = user.loginType.name, + loginType = user.loginType?.name.orEmpty(), schoolName = unit.schoolName, schoolShortName = unit.schoolShortName, schoolSymbol = unit.schoolId, studentName = "$studentName $studentSurname", - loginMode = Sdk.Mode.SCRAPPER.name, - scrapperBaseUrl = user.baseUrl, - mobileBaseUrl = "", - certificateKey = "", - privateKey = "", - password = user.password, + loginMode = user.loginMode.name, + scrapperBaseUrl = user.scrapperBaseUrl.orEmpty(), + mobileBaseUrl = symbol.hebeBaseUrl.orEmpty(), + certificateKey = symbol.keyId.orEmpty(), + privateKey = symbol.privatePem.orEmpty(), + password = user.password.orEmpty(), isCurrent = false, registrationDate = Instant.now(), ).apply { diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/StudentMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/StudentMapper.kt deleted file mode 100644 index a2110d7f5..000000000 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/StudentMapper.kt +++ /dev/null @@ -1,37 +0,0 @@ -package io.github.wulkanowy.data.mappers - -import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.db.entities.StudentWithSemesters -import java.time.Instant -import io.github.wulkanowy.sdk.pojo.Student as SdkStudent - -fun List.mapToEntities(password: String = "", colors: List) = map { - StudentWithSemesters( - student = Student( - email = it.email, - password = password, - isParent = it.isParent, - symbol = it.symbol, - studentId = it.studentId, - userLoginId = it.userLoginId, - userName = it.userName, - studentName = it.studentName + " " + it.studentSurname, - schoolSymbol = it.schoolSymbol, - schoolShortName = it.schoolShortName, - schoolName = it.schoolName, - className = it.className, - classId = it.classId, - scrapperBaseUrl = it.scrapperBaseUrl, - loginType = it.loginType.name, - isCurrent = false, - registrationDate = Instant.now(), - mobileBaseUrl = it.mobileBaseUrl, - privateKey = it.privateKey, - certificateKey = it.certificateKey, - loginMode = it.loginMode.name, - ).apply { - avatarColor = colors.random() - }, - semesters = it.semesters.mapToEntities(it.studentId) - ) -} diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/TimetableMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/TimetableMapper.kt index e55aa3cf7..ee525e108 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/TimetableMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/TimetableMapper.kt @@ -5,10 +5,10 @@ import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.db.entities.TimetableAdditional import io.github.wulkanowy.data.db.entities.TimetableHeader import io.github.wulkanowy.data.pojos.TimetableFull -import io.github.wulkanowy.sdk.pojo.TimetableFull as SdkTimetableFull +import io.github.wulkanowy.sdk.pojo.Timetable as SdkTimetableFull import io.github.wulkanowy.sdk.pojo.TimetableDayHeader as SdkTimetableHeader -import io.github.wulkanowy.sdk.pojo.Timetable as SdkTimetable -import io.github.wulkanowy.sdk.pojo.TimetableAdditional as SdkTimetableAdditional +import io.github.wulkanowy.sdk.pojo.Lesson as SdkLesson +import io.github.wulkanowy.sdk.pojo.LessonAdditional as SdkTimetableAdditional fun SdkTimetableFull.mapToEntities(semester: Semester) = TimetableFull( lessons = lessons.mapToEntities(semester), @@ -16,13 +16,13 @@ fun SdkTimetableFull.mapToEntities(semester: Semester) = TimetableFull( headers = headers.mapToEntities(semester) ) -fun List.mapToEntities(semester: Semester) = map { +fun List.mapToEntities(semester: Semester) = map { Timetable( studentId = semester.studentId, diaryId = semester.diaryId, number = it.number, - start = it.startZoned.toInstant(), - end = it.endZoned.toInstant(), + start = it.start.toInstant(), + end = it.end.toInstant(), date = it.date, subject = it.subject, subjectOld = it.subjectOld, @@ -45,8 +45,8 @@ fun List.mapToEntities(semester: Semester) = map { diaryId = semester.diaryId, subject = it.subject, date = it.date, - start = it.startZoned.toInstant(), - end = it.endZoned.toInstant(), + start = it.start.toInstant(), + end = it.end.toInstant(), ) } diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/RegisterUser.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/RegisterUser.kt index 4aea33771..98bf1402b 100644 --- a/app/src/main/java/io/github/wulkanowy/data/pojos/RegisterUser.kt +++ b/app/src/main/java/io/github/wulkanowy/data/pojos/RegisterUser.kt @@ -1,20 +1,25 @@ package io.github.wulkanowy.data.pojos import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.scrapper.Scrapper data class RegisterUser( val email: String, - val password: String, + val password: String?, val login: String, // may be the same as email - val baseUrl: String, - val loginType: Scrapper.LoginType, + val scrapperBaseUrl: String?, + val loginType: Scrapper.LoginType?, + val loginMode: Sdk.Mode, val symbols: List, ) : java.io.Serializable data class RegisterSymbol( val symbol: String, val error: Throwable?, + val hebeBaseUrl: String?, + val keyId: String?, + val privatePem: String?, val userName: String, val schools: List, ) : java.io.Serializable diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt index cbaa12bd3..bec2797db 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt @@ -19,7 +19,6 @@ class AppCreatorRepository @Inject constructor( ) { @OptIn(ExperimentalSerializationApi::class) - @Suppress("BlockingMethodInNonBlockingContext") suspend fun getAppCreators() = withContext(dispatchers.io) { val inputStream = context.assets.open("contributors.json").buffered() json.decodeFromStream>(inputStream) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt index fd5d8bd16..3afb99077 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt @@ -59,7 +59,7 @@ class AttendanceRepository @Inject constructor( } sdk.init(student) .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) - .getAttendance(start.monday, end.sunday, semester.semesterId) + .getAttendance(start.monday, end.sunday) .mapToEntities(semester, lessons) }, saveFetchResult = { old, new -> diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt index faa80b93e..013c0951d 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt @@ -52,7 +52,7 @@ class ExamRepository @Inject constructor( fetch = { sdk.init(student) .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) - .getExams(start.startExamsDay, start.endExamsDay, semester.semesterId) + .getExams(start.startExamsDay, start.endExamsDay) .mapToEntities(semester) }, saveFetchResult = { old, new -> diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt index e5d7bc5cb..4101803f3 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt @@ -42,7 +42,7 @@ class NoteRepository @Inject constructor( fetch = { sdk.init(student) .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) - .getNotes(semester.semesterId) + .getNotes() .mapToEntities(semester) }, saveFetchResult = { old, new -> diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt index 96f019223..92bb37081 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt @@ -40,7 +40,7 @@ class SemesterRepository @Inject constructor( val isNoSemesters = semesters.isEmpty() val isRefreshOnModeChangeRequired = when { - Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API -> { + Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE -> { semesters.firstOrNull { it.isCurrent }?.let { 0 == it.diaryId && 0 == it.kindergartenDiaryId } == true diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt index b1d1ba832..4c7069ef6 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt @@ -10,11 +10,9 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.exceptions.NoCurrentStudentException -import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToPojo import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.DispatchersProvider import io.github.wulkanowy.utils.security.decrypt import io.github.wulkanowy.utils.security.encrypt @@ -29,37 +27,35 @@ class StudentRepository @Inject constructor( private val studentDb: StudentDao, private val semesterDb: SemesterDao, private val sdk: Sdk, - private val appInfo: AppInfo, private val appDatabase: AppDatabase ) { - suspend fun isStudentSaved() = getSavedStudents(false).isNotEmpty() - suspend fun isCurrentStudentSet() = studentDb.loadCurrent()?.isCurrent ?: false suspend fun getStudentsApi( pin: String, symbol: String, token: String - ): List = - sdk.getStudentsFromMobileApi(token, pin, symbol, "") - .mapToEntities(colors = appInfo.defaultColorsForAvatar) + ): RegisterUser = sdk + .getStudentsFromHebe(token, pin, symbol, "") + .mapToPojo(null) suspend fun getStudentsScrapper( email: String, password: String, scrapperBaseUrl: String, symbol: String - ): List = - sdk.getStudentsFromScrapper(email, password, scrapperBaseUrl, symbol) - .mapToEntities(password, appInfo.defaultColorsForAvatar) + ): RegisterUser = sdk + .getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, symbol) + .mapToPojo(password) suspend fun getUserSubjectsFromScrapper( email: String, password: String, scrapperBaseUrl: String, symbol: String - ): RegisterUser = sdk.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, symbol) + ): RegisterUser = sdk + .getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, symbol) .mapToPojo(password) suspend fun getStudentsHybrid( @@ -67,15 +63,15 @@ class StudentRepository @Inject constructor( password: String, scrapperBaseUrl: String, symbol: String - ): List = - sdk.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol) - .mapToEntities(password, appInfo.defaultColorsForAvatar) + ): RegisterUser = sdk + .getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol) + .mapToPojo(password) suspend fun getSavedStudents(decryptPass: Boolean = true) = studentDb.loadStudentsWithSemesters() .map { it.apply { - if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { + if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { student.password = withContext(dispatchers.io) { decrypt(student.password) } @@ -85,7 +81,7 @@ class StudentRepository @Inject constructor( suspend fun getSavedStudentById(id: Long, decryptPass: Boolean = true) = studentDb.loadStudentWithSemestersById(id)?.apply { - if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { + if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { student.password = withContext(dispatchers.io) { decrypt(student.password) } @@ -95,7 +91,7 @@ class StudentRepository @Inject constructor( suspend fun getStudentById(id: Long, decryptPass: Boolean = true): Student { val student = studentDb.loadById(id) ?: throw NoCurrentStudentException() - if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { + if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { student.password = withContext(dispatchers.io) { decrypt(student.password) } @@ -106,7 +102,7 @@ class StudentRepository @Inject constructor( suspend fun getCurrentStudent(decryptPass: Boolean = true): Student { val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException() - if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { + if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { student.password = withContext(dispatchers.io) { decrypt(student.password) } @@ -119,7 +115,7 @@ class StudentRepository @Inject constructor( val students = studentsWithSemesters.map { it.student } .map { it.apply { - if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.API) { + if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.HEBE) { password = withContext(dispatchers.io) { encrypt(password, context) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt index acd71e1f4..4e3b40f96 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt @@ -40,7 +40,7 @@ class TeacherRepository @Inject constructor( fetch = { sdk.init(student) .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) - .getTeachers(semester.semesterId) + .getTeachers() .mapToEntities(semester) }, saveFetchResult = { old, new -> diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt index 26e1f3fff..136fb8d5b 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt @@ -66,7 +66,7 @@ class TimetableRepository @Inject constructor( fetch = { val timetableFull = sdk.init(student) .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) - .getTimetableFull(start.monday, end.sunday) + .getTimetable(start.monday, end.sunday) timetableFull.mapToEntities(semester) }, diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt index 37ab71dce..4f709438a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt @@ -4,13 +4,15 @@ import android.content.Context import android.database.sqlite.SQLiteConstraintException import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R -import io.github.wulkanowy.sdk.mobile.exception.InvalidPinException -import io.github.wulkanowy.sdk.mobile.exception.InvalidSymbolException -import io.github.wulkanowy.sdk.mobile.exception.InvalidTokenException -import io.github.wulkanowy.sdk.mobile.exception.TokenDeadException +import io.github.wulkanowy.sdk.hebe.exception.InvalidPinException +import io.github.wulkanowy.sdk.hebe.exception.InvalidTokenException +import io.github.wulkanowy.sdk.hebe.exception.TokenDeadException +import io.github.wulkanowy.sdk.hebe.exception.UnknownTokenException import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException import io.github.wulkanowy.ui.base.ErrorHandler import javax.inject.Inject +import io.github.wulkanowy.sdk.hebe.exception.InvalidSymbolException as InvalidHebeSymbolException +import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException as InvalidScrapperSymbolException class LoginErrorHandler @Inject constructor( @ApplicationContext context: Context, @@ -32,9 +34,11 @@ class LoginErrorHandler @Inject constructor( is BadCredentialsException -> onBadCredentials(error.message) is SQLiteConstraintException -> onStudentDuplicate(resources.getString(R.string.login_duplicate_student)) is TokenDeadException -> onInvalidToken(resources.getString(R.string.login_expired_token)) + is UnknownTokenException, is InvalidTokenException -> onInvalidToken(resources.getString(R.string.login_invalid_token)) is InvalidPinException -> onInvalidPin(resources.getString(R.string.login_invalid_pin)) - is InvalidSymbolException -> onInvalidSymbol(resources.getString(R.string.login_invalid_symbol)) + is InvalidScrapperSymbolException, + is InvalidHebeSymbolException -> onInvalidSymbol(resources.getString(R.string.login_invalid_symbol)) else -> super.proceed(error) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt index 8c90623e1..ead2d71af 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt @@ -34,9 +34,9 @@ class LoginAdvancedFragment : override val formLoginType: String get() = when (binding.loginTypeSwitch.checkedRadioButtonId) { - R.id.loginTypeApi -> "API" - R.id.loginTypeScrapper -> "SCRAPPER" - else -> "HYBRID" + R.id.loginTypeApi -> Sdk.Mode.HEBE.name + R.id.loginTypeScrapper -> Sdk.Mode.SCRAPPER.name + else -> Sdk.Mode.HYBRID.name } override val formUsernameValue: String @@ -99,7 +99,7 @@ class LoginAdvancedFragment : loginTypeSwitch.setOnCheckedChangeListener { _, checkedId -> presenter.onLoginModeSelected( when (checkedId) { - R.id.loginTypeApi -> Sdk.Mode.API + R.id.loginTypeApi -> Sdk.Mode.HEBE R.id.loginTypeScrapper -> Sdk.Mode.SCRAPPER else -> Sdk.Mode.HYBRID } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt index 33a76e5f9..ab56bd786 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt @@ -1,17 +1,12 @@ package io.github.wulkanowy.ui.modules.login.advanced import io.github.wulkanowy.data.Resource -import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.logResourceStatus import io.github.wulkanowy.data.onResourceNotLoading -import io.github.wulkanowy.data.pojos.RegisterStudent -import io.github.wulkanowy.data.pojos.RegisterSymbol -import io.github.wulkanowy.data.pojos.RegisterUnit import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.sdk.scrapper.Scrapper import io.github.wulkanowy.sdk.scrapper.getNormalizedSymbol import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.modules.login.LoginData @@ -97,14 +92,16 @@ class LoginAdvancedPresenter @Inject constructor( fun onLoginModeSelected(type: Sdk.Mode) { view?.run { when (type) { - Sdk.Mode.API -> { + Sdk.Mode.HEBE -> { showOnlyMobileApiModeInputs() showMobileApiWarningMessage() } + Sdk.Mode.SCRAPPER -> { showOnlyScrapperModeInputs() showScraperWarningMessage() } + Sdk.Mode.HYBRID -> { showOnlyHybridModeInputs() showHybridWarningMessage() @@ -145,11 +142,12 @@ class LoginAdvancedPresenter @Inject constructor( showProgress(true) showContent(false) } + is Resource.Success -> { analytics.logEvent( "registration_form", "success" to true, - "students" to it.data.size, + "scrapperBaseUrl" to view?.formHostValue.orEmpty(), "error" to "No error" ) val loginData = LoginData( @@ -158,14 +156,15 @@ class LoginAdvancedPresenter @Inject constructor( baseUrl = view?.formHostValue.orEmpty().trim(), symbol = view?.formSymbolValue.orEmpty().trim().getNormalizedSymbol(), ) - when (it.data.size) { + when (it.data.symbols.size) { 0 -> view?.navigateToSymbol(loginData) else -> view?.navigateToStudentSelect( loginData = loginData, - registerUser = it.data.toRegisterUser(loginData), + registerUser = it.data, ) } } + is Resource.Error -> { analytics.logEvent( "registration_form", @@ -183,59 +182,7 @@ class LoginAdvancedPresenter @Inject constructor( }.launch("login") } - private fun List.toRegisterUser(loginData: LoginData) = RegisterUser( - email = loginData.login, - password = loginData.password, - login = loginData.login, - baseUrl = loginData.baseUrl, - loginType = firstOrNull()?.student?.loginType?.let( - Scrapper.LoginType::valueOf - ) ?: Scrapper.LoginType.AUTO, - symbols = this - .groupBy { students -> students.student.symbol } - .map { (symbol, students) -> - RegisterSymbol( - symbol = symbol, - error = null, - userName = "", - schools = students - .groupBy { student -> - Triple( - first = student.student.schoolSymbol, - second = student.student.userLoginId, - third = student.student.schoolShortName - ) - } - .map { (groupKey, students) -> - val (schoolId, loginId, schoolName) = groupKey - RegisterUnit( - students = students.map { - RegisterStudent( - studentId = it.student.studentId, - studentName = it.student.studentName, - studentSecondName = it.student.studentName, - studentSurname = it.student.studentName, - className = it.student.className, - classId = it.student.classId, - isParent = it.student.isParent, - semesters = it.semesters, - ) - }, - userLoginId = loginId, - schoolId = schoolId, - schoolName = schoolName, - schoolShortName = schoolName, - parentIds = listOf(), - studentIds = listOf(), - employeeIds = listOf(), - error = null - ) - } - ) - }, - ) - - private suspend fun getStudentsAppropriatesToLoginType(): List { + private suspend fun getStudentsAppropriatesToLoginType(): RegisterUser { val email = view?.formUsernameValue.orEmpty() val password = view?.formPassValue.orEmpty() val endpoint = view?.formHostValue.orEmpty() @@ -245,10 +192,11 @@ class LoginAdvancedPresenter @Inject constructor( val token = view?.formTokenValue.orEmpty() return when (Sdk.Mode.valueOf(view?.formLoginType.orEmpty())) { - Sdk.Mode.API -> studentRepository.getStudentsApi(pin, symbol, token) + Sdk.Mode.HEBE -> studentRepository.getStudentsApi(pin, symbol, token) Sdk.Mode.SCRAPPER -> studentRepository.getStudentsScrapper( email, password, endpoint, symbol ) + Sdk.Mode.HYBRID -> studentRepository.getStudentsHybrid( email, password, endpoint, symbol ) @@ -267,8 +215,8 @@ class LoginAdvancedPresenter @Inject constructor( var isCorrect = true - when (Sdk.Mode.valueOf(view?.formLoginType ?: "")) { - Sdk.Mode.API -> { + when (Sdk.Mode.valueOf(view?.formLoginType.orEmpty())) { + Sdk.Mode.HEBE -> { if (pin.isEmpty()) { view?.setErrorPinRequired() isCorrect = false @@ -284,17 +232,17 @@ class LoginAdvancedPresenter @Inject constructor( isCorrect = false } } + Sdk.Mode.HYBRID, Sdk.Mode.SCRAPPER -> { if (login.isEmpty()) { view?.setErrorUsernameRequired() isCorrect = false } else { - if ("@" in login && "standard" !in host) { + if ("@" in login && "login" in host) { view?.setErrorLoginRequired() isCorrect = false } - - if ("@" !in login && "standard" in host) { + if ("@" !in login && "email" in host) { view?.setErrorEmailRequired() isCorrect = false } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedView.kt index 824fa0288..34062d938 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedView.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.login.advanced -import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.modules.login.LoginData diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt index bbc382190..43ba3fe1d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt @@ -204,6 +204,10 @@ class LoginFormFragment : BaseFragment(R.layout.fragme binding.loginFormContainer.visibility = if (show) VISIBLE else GONE } + override fun showOtherOptionsButton(show: Boolean) { + binding.loginFormAdvancedButton.isVisible = show + } + @SuppressLint("SetTextI18n") override fun showVersion() { binding.loginFormVersion.text = "v${appInfo.versionName}" diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt index 8035ea0ad..ed70eb128 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt @@ -7,6 +7,7 @@ import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper +import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.ifNullOrBlank import timber.log.Timber import java.net.URL @@ -15,6 +16,7 @@ import javax.inject.Inject class LoginFormPresenter @Inject constructor( studentRepository: StudentRepository, private val loginErrorHandler: LoginErrorHandler, + private val appInfo: AppInfo, private val analytics: AnalyticsHelper ) : BasePresenter(loginErrorHandler, studentRepository) { @@ -25,6 +27,7 @@ class LoginFormPresenter @Inject constructor( view.run { initView() showContact(false) + showOtherOptionsButton(appInfo.isDebug) showVersion() loginErrorHandler.onBadCredentials = { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt index 5a816fb32..e5c680d6f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt @@ -56,6 +56,8 @@ interface LoginFormView : BaseView { fun showContent(show: Boolean) + fun showOtherOptionsButton(show: Boolean) + fun showVersion() fun navigateToSymbol(loginData: LoginData) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt index 169702151..c33d12faa 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt @@ -55,7 +55,6 @@ class LoginStudentSelectFragment : } } - @Suppress("UNCHECKED_CAST") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentLoginStudentSelectBinding.bind(view) diff --git a/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt index 63a30db8c..481cad113 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt @@ -12,18 +12,17 @@ fun Sdk.init(student: Student): Sdk { studentId = student.studentId classId = student.classId - if (Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { + if (Sdk.Mode.valueOf(student.loginMode) == Sdk.Mode.HEBE) { + mobileBaseUrl = student.mobileBaseUrl + } else { scrapperBaseUrl = student.scrapperBaseUrl loginType = Sdk.ScrapperLoginType.valueOf(student.loginType) } - loginId = student.userLoginId mode = Sdk.Mode.valueOf(student.loginMode) mobileBaseUrl = student.mobileBaseUrl - certKey = student.certificateKey - privateKey = student.privateKey - - emptyCookieJarInterceptor = true + keyId = student.certificateKey + privatePem = student.privateKey Timber.d("Sdk in ${student.loginMode} mode reinitialized") diff --git a/app/src/main/res/layout/fragment_login_form.xml b/app/src/main/res/layout/fragment_login_form.xml index fac3960e9..3bfe0c34f 100644 --- a/app/src/main/res/layout/fragment_login_form.xml +++ b/app/src/main/res/layout/fragment_login_form.xml @@ -247,7 +247,6 @@ android:layout_marginEnd="16dp" android:text="@string/login_advanced" android:textAppearance="?android:textAppearance" - android:visibility="gone" app:backgroundTint="?android:windowBackground" app:fontFamily="sans-serif-medium" app:layout_constraintBottom_toBottomOf="@id/loginFormSignIn" diff --git a/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt b/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt index 84a0cb405..c8d95829e 100644 --- a/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt +++ b/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt @@ -48,7 +48,7 @@ fun getSemesterPojo(diaryId: Int, semesterId: Int, start: LocalDate, end: LocalD end = end, ) -fun getStudentEntity(mode: Sdk.Mode = Sdk.Mode.API) = Student( +fun getStudentEntity(mode: Sdk.Mode = Sdk.Mode.HEBE) = Student( scrapperBaseUrl = "http://fakelog.cf", email = "jan@fakelog.cf", certificateKey = "", diff --git a/app/src/test/java/io/github/wulkanowy/data/mappers/AttendanceMapperKtTest.kt b/app/src/test/java/io/github/wulkanowy/data/mappers/AttendanceMapperKtTest.kt index a35e5d303..ac73becdc 100644 --- a/app/src/test/java/io/github/wulkanowy/data/mappers/AttendanceMapperKtTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/mappers/AttendanceMapperKtTest.kt @@ -3,7 +3,7 @@ package io.github.wulkanowy.data.mappers import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.sdk.pojo.Attendance -import io.github.wulkanowy.sdk.scrapper.attendance.SentExcuse +import io.github.wulkanowy.sdk.scrapper.attendance.SentExcuseStatus import org.junit.Test import java.time.Instant import java.time.LocalDate @@ -98,7 +98,7 @@ class AttendanceMapperTest { timeId = 1, categoryId = 1, deleted = false, - excuseStatus = SentExcuse.Status.WAITING, + excuseStatus = SentExcuseStatus.WAITING, excusable = false, absence = false, excused = false, diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt index 896491ef0..d0e500f19 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/AttendanceRepositoryTest.kt @@ -63,7 +63,7 @@ class AttendanceRepositoryTest { @Test fun `force refresh without difference`() { // prepare - coEvery { sdk.getAttendance(startDate, endDate, 1) } returns remoteList + coEvery { sdk.getAttendance(startDate, endDate) } returns remoteList coEvery { attendanceDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf( flowOf(remoteList.mapToEntities(semester, emptyList())), flowOf(remoteList.mapToEntities(semester, emptyList())) @@ -77,7 +77,7 @@ class AttendanceRepositoryTest { // verify assertEquals(null, res.errorOrNull) assertEquals(2, res.dataOrNull?.size) - coVerify { sdk.getAttendance(startDate, endDate, 1) } + coVerify { sdk.getAttendance(startDate, endDate) } coVerify { attendanceDb.loadAll(1, 1, startDate, endDate) } coVerify { attendanceDb.insertAll(match { it.isEmpty() }) } coVerify { attendanceDb.deleteAll(match { it.isEmpty() }) } @@ -86,7 +86,7 @@ class AttendanceRepositoryTest { @Test fun `force refresh with more items in remote`() { // prepare - coEvery { sdk.getAttendance(startDate, endDate, 1) } returns remoteList + coEvery { sdk.getAttendance(startDate, endDate) } returns remoteList coEvery { attendanceDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf( flowOf(remoteList.dropLast(1).mapToEntities(semester, emptyList())), flowOf(remoteList.dropLast(1).mapToEntities(semester, emptyList())), // after fetch end before save result @@ -101,7 +101,7 @@ class AttendanceRepositoryTest { // verify assertEquals(null, res.errorOrNull) assertEquals(2, res.dataOrNull?.size) - coVerify { sdk.getAttendance(startDate, endDate, 1) } + coVerify { sdk.getAttendance(startDate, endDate) } coVerify { attendanceDb.loadAll(1, 1, startDate, endDate) } coVerify { attendanceDb.insertAll(match { @@ -114,7 +114,7 @@ class AttendanceRepositoryTest { @Test fun `force refresh with more items in local`() { // prepare - coEvery { sdk.getAttendance(startDate, endDate, 1) } returns remoteList.dropLast(1) + coEvery { sdk.getAttendance(startDate, endDate) } returns remoteList.dropLast(1) coEvery { attendanceDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf( flowOf(remoteList.mapToEntities(semester, emptyList())), flowOf(remoteList.mapToEntities(semester, emptyList())), // after fetch end before save result @@ -129,7 +129,7 @@ class AttendanceRepositoryTest { // verify assertEquals(null, res.errorOrNull) assertEquals(1, res.dataOrNull?.size) - coVerify { sdk.getAttendance(startDate, endDate, 1) } + coVerify { sdk.getAttendance(startDate, endDate) } coVerify { attendanceDb.loadAll(1, 1, startDate, endDate) } coVerify { attendanceDb.insertAll(match { it.isEmpty() }) } coVerify { diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt index e3790662e..fb037a87e 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/ExamRemoteTest.kt @@ -59,7 +59,7 @@ class ExamRemoteTest { @Test fun `force refresh without difference`() { // prepare - coEvery { sdk.getExams(startDate, realEndDate, 1) } returns remoteList + coEvery { sdk.getExams(startDate, realEndDate) } returns remoteList coEvery { examDb.loadAll(1, 1, startDate, realEndDate) } returnsMany listOf( flowOf(remoteList.mapToEntities(semester)), flowOf(remoteList.mapToEntities(semester)) @@ -73,7 +73,7 @@ class ExamRemoteTest { // verify assertEquals(null, res.errorOrNull) assertEquals(2, res.dataOrNull?.size) - coVerify { sdk.getExams(startDate, realEndDate, 1) } + coVerify { sdk.getExams(startDate, realEndDate) } coVerify { examDb.loadAll(1, 1, startDate, realEndDate) } coVerify { examDb.insertAll(match { it.isEmpty() }) } coVerify { examDb.deleteAll(match { it.isEmpty() }) } @@ -82,7 +82,7 @@ class ExamRemoteTest { @Test fun `force refresh with more items in remote`() { // prepare - coEvery { sdk.getExams(startDate, realEndDate, 1) } returns remoteList + coEvery { sdk.getExams(startDate, realEndDate) } returns remoteList coEvery { examDb.loadAll(1, 1, startDate, realEndDate) } returnsMany listOf( flowOf(remoteList.dropLast(1).mapToEntities(semester)), flowOf(remoteList.dropLast(1).mapToEntities(semester)), // after fetch end before save result @@ -97,7 +97,7 @@ class ExamRemoteTest { // verify assertEquals(null, res.errorOrNull) assertEquals(2, res.dataOrNull?.size) - coVerify { sdk.getExams(startDate, realEndDate, 1) } + coVerify { sdk.getExams(startDate, realEndDate) } coVerify { examDb.loadAll(1, 1, startDate, realEndDate) } coVerify { examDb.insertAll(match { @@ -110,7 +110,7 @@ class ExamRemoteTest { @Test fun `force refresh with more items in local`() { // prepare - coEvery { sdk.getExams(startDate, realEndDate, 1) } returns remoteList.dropLast(1) + coEvery { sdk.getExams(startDate, realEndDate) } returns remoteList.dropLast(1) coEvery { examDb.loadAll(1, 1, startDate, realEndDate) } returnsMany listOf( flowOf(remoteList.mapToEntities(semester)), flowOf(remoteList.mapToEntities(semester)), // after fetch end before save result @@ -125,7 +125,7 @@ class ExamRemoteTest { // verify assertEquals(null, res.errorOrNull) assertEquals(1, res.dataOrNull?.size) - coVerify { sdk.getExams(startDate, realEndDate, 1) } + coVerify { sdk.getExams(startDate, realEndDate) } coVerify { examDb.loadAll(1, 1, startDate, realEndDate) } coVerify { examDb.insertAll(match { it.isEmpty() }) } coVerify { @@ -137,7 +137,6 @@ class ExamRemoteTest { private fun getExam(date: LocalDate) = SdkExam( subject = "", - group = "", type = "", description = "", teacher = "", diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt index e8d0b6c8f..1d6dfaff0 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt @@ -9,13 +9,21 @@ import io.github.wulkanowy.data.toFirstResult import io.github.wulkanowy.getSemesterEntity import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.sdk.pojo.Grades import io.github.wulkanowy.utils.AutoRefreshHelper -import io.mockk.* +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.SpyK +import io.mockk.just import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import java.time.LocalDate @@ -72,7 +80,7 @@ class GradeRepositoryTest { createGradeApi(5, 4.0, of(2019, 2, 27), "Ocena z dnia logowania"), createGradeApi(5, 4.0, of(2019, 2, 28), "Ocena jeszcze nowsza") ) - coEvery { sdk.getGrades(1) } returns (remoteList to emptyList()) + coEvery { sdk.getGrades(1) } returns createGrades(remoteList) coEvery { gradeDb.loadAll(1, 1) } returnsMany listOf( flowOf(listOf()), // empty because it is new user @@ -122,7 +130,7 @@ class GradeRepositoryTest { ), createGradeApi(2, 5.0, of(2019, 2, 28), "Ta jest już w ogóle nowa") ) - coEvery { sdk.getGrades(1) } returns (remoteList to emptyList()) + coEvery { sdk.getGrades(1) } returns createGrades(remoteList) val localList = listOf( createGradeApi(5, 3.0, of(2019, 2, 25), "Jedna ocena"), @@ -169,7 +177,7 @@ class GradeRepositoryTest { createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"), createGradeApi(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena") ) - coEvery { sdk.getGrades(1) } returns (remoteList to emptyList()) + coEvery { sdk.getGrades(1) } returns createGrades(remoteList) val localList = listOf( createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"), @@ -200,7 +208,7 @@ class GradeRepositoryTest { createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"), // will be added... createGradeApi(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena") ) - coEvery { sdk.getGrades(1) } returns (remoteList to emptyList()) + coEvery { sdk.getGrades(1) } returns createGrades(remoteList) val localList = listOf( createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"), @@ -230,7 +238,7 @@ class GradeRepositoryTest { createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"), createGradeApi(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena") ) - coEvery { sdk.getGrades(1) } returns (remoteList to emptyList()) + coEvery { sdk.getGrades(1) } returns createGrades(remoteList) coEvery { gradeDb.loadAll(1, 1) } returnsMany listOf( flowOf(listOf()), @@ -250,7 +258,7 @@ class GradeRepositoryTest { fun `force refresh when remote is empty`() { // prepare val remoteList = emptyList() - coEvery { sdk.getGrades(semester.semesterId) } returns (remoteList to emptyList()) + coEvery { sdk.getGrades(semester.semesterId) } returns createGrades(remoteList) val localList = listOf( createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"), @@ -284,4 +292,13 @@ class GradeRepositoryTest { weight = weight.toString(), weightValue = weight ) + + private fun createGrades(grades: List): Grades = Grades( + details = grades, + summary = listOf(), + isAverage = false, + isPoints = false, + isForAdults = false, + type = 0, + ) } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt index 9a2c22fd6..3a18ee979 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt @@ -224,7 +224,7 @@ class MessageRepositoryTest { recipients = listOf(), subject = "", content = "Test", - dateZoned = Instant.EPOCH.atZone(ZoneOffset.UTC), + date = Instant.EPOCH.atZone(ZoneOffset.UTC), folderId = 1, unread = true, readBy = 1, diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt index 6865aa7da..1a3f96795 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/MobileDeviceRepositoryTest.kt @@ -10,16 +10,21 @@ import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.Device import io.github.wulkanowy.utils.AutoRefreshHelper -import io.mockk.* +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.SpyK +import io.mockk.just import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import org.junit.Assert import org.junit.Before import org.junit.Test -import java.time.LocalDateTime.of -import java.time.ZoneId +import java.time.ZoneOffset +import java.time.ZonedDateTime.of class MobileDeviceRepositoryTest { @@ -134,9 +139,7 @@ class MobileDeviceRepositoryTest { id = 0, name = "", deviceId = "", - createDate = of(2019, 5, day, 0, 0, 0), - modificationDate = of(2019, 5, day, 0, 0, 0), - createDateZoned = of(2019, 5, day, 0, 0, 0).atZone(ZoneId.systemDefault()), - modificationDateZoned = of(2019, 5, day, 0, 0, 0).atZone(ZoneId.systemDefault()) + createDate = of(2019, 5, day, 0, 0, 0, 0, ZoneOffset.UTC), + modificationDate = of(2019, 5, day, 0, 0, 0, 0, ZoneOffset.UTC), ) } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/SemesterRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/SemesterRepositoryTest.kt index 0ed008851..d8256869f 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/SemesterRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/SemesterRepositoryTest.kt @@ -75,7 +75,7 @@ class SemesterRepositoryTest { coEvery { semesterDb.deleteAll(any()) } just Runs coEvery { semesterDb.insertSemesters(any()) } returns listOf() - val items = runBlocking { semesterRepository.getSemesters(student.copy(loginMode = Sdk.Mode.API.name)) } + val items = runBlocking { semesterRepository.getSemesters(student.copy(loginMode = Sdk.Mode.HEBE.name)) } assertEquals(2, items.size) assertEquals(0, items[0].diaryId) } @@ -215,6 +215,7 @@ class SemesterRepositoryTest { @Test(expected = RuntimeException::class) fun getCurrentSemester_emptyList() { coEvery { semesterDb.loadAll(student.studentId, student.classId) } returns emptyList() + coEvery { sdk.getSemesters() } returns emptyList() runBlocking { semesterRepository.getCurrentSemester(student) } } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt deleted file mode 100644 index 9d3d7a2ec..000000000 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt +++ /dev/null @@ -1,81 +0,0 @@ -package io.github.wulkanowy.data.repositories - -import io.github.wulkanowy.TestDispatchersProvider -import io.github.wulkanowy.data.db.dao.SemesterDao -import io.github.wulkanowy.data.db.dao.StudentDao -import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.sdk.pojo.Student -import io.github.wulkanowy.utils.AppInfo -import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.impl.annotations.MockK -import io.mockk.mockk -import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test - -class StudentTest { - - @MockK - private lateinit var mockSdk: Sdk - - @MockK - private lateinit var studentDb: StudentDao - - @MockK - private lateinit var semesterDb: SemesterDao - - private lateinit var studentRepository: StudentRepository - - @Before - fun initApi() { - MockKAnnotations.init(this) - studentRepository = StudentRepository( - context = mockk(), - dispatchers = TestDispatchersProvider(), - studentDb = studentDb, - semesterDb = semesterDb, - sdk = mockSdk, - appInfo = AppInfo(), - appDatabase = mockk() - ) - } - - @Test - fun testRemoteAll() { - coEvery { mockSdk.getStudentsFromScrapper(any(), any(), any(), any()) } returns listOf( - getStudent("test") - ) - - val students = runBlocking { studentRepository.getStudentsScrapper("", "", "http://fakelog.cf", "") } - assertEquals(1, students.size) - assertEquals("test Kowalski", students.first().student.studentName) - } - - private fun getStudent(name: String): Student { - return Student( - email = "", - symbol = "", - studentId = 0, - userLoginId = 0, - userLogin = "", - userName = "", - studentName = name, - studentSurname = "Kowalski", - schoolSymbol = "", - schoolShortName = "", - schoolName = "", - className = "", - classId = 0, - certificateKey = "", - privateKey = "", - loginMode = Sdk.Mode.SCRAPPER, - mobileBaseUrl = "", - loginType = Sdk.ScrapperLoginType.STANDARD, - scrapperBaseUrl = "", - isParent = false, - semesters = emptyList() - ) - } -} diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt index e56aaa5d0..92ad01b18 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt @@ -10,12 +10,17 @@ import io.github.wulkanowy.data.toFirstResult import io.github.wulkanowy.getSemesterEntity import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.sdk.pojo.TimetableFull import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper import io.github.wulkanowy.utils.AutoRefreshHelper -import io.mockk.* +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.SpyK +import io.mockk.just +import io.mockk.mockk import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals @@ -25,7 +30,7 @@ import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalDateTime.of import java.time.ZoneId -import io.github.wulkanowy.sdk.pojo.Timetable as SdkTimetable +import io.github.wulkanowy.sdk.pojo.Lesson as SdkLesson class TimetableRepositoryTest { @@ -62,18 +67,43 @@ class TimetableRepositoryTest { MockKAnnotations.init(this) every { refreshHelper.shouldBeRefreshed(any()) } returns false - timetableRepository = TimetableRepository(timetableDb, timetableAdditionalDao, timetableHeaderDao, sdk, timetableNotificationSchedulerHelper, refreshHelper) + timetableRepository = TimetableRepository( + timetableDb, + timetableAdditionalDao, + timetableHeaderDao, + sdk, + timetableNotificationSchedulerHelper, + refreshHelper + ) } @Test fun `force refresh without difference`() { val remoteList = listOf( - createTimetableRemote(of(2021, 1, 4, 8, 0), 1, "123", "Język polski", "Jan Kowalski", false), - createTimetableRemote(of(2021, 1, 4, 8, 50), 2, "124", "Język niemiecki", "Joanna Czarniecka", true) + createTimetableRemote( + start = of(2021, 1, 4, 8, 0), + number = 1, + room = "123", + subject = "Język polski", + teacher = "Jan Kowalski", + changes = false + ), + createTimetableRemote( + start = of(2021, 1, 4, 8, 50), + number = 2, + room = "124", + subject = "Język niemiecki", + teacher = "Joanna Czarniecka", + changes = true + ) ) // prepare - coEvery { sdk.getTimetableFull(startDate, endDate) } returns TimetableFull(emptyList(), remoteList, emptyList()) + coEvery { sdk.getTimetable(startDate, endDate) } returns mockk { + every { headers } returns emptyList() + every { lessons } returns remoteList + every { additional } returns emptyList() + } coEvery { timetableDb.loadAll(1, 1, startDate, endDate) } returnsMany listOf( flowOf(remoteList.mapToEntities(semester)), flowOf(remoteList.mapToEntities(semester)) @@ -81,7 +111,14 @@ class TimetableRepositoryTest { coEvery { timetableDb.insertAll(any()) } returns listOf(1, 2, 3) coEvery { timetableDb.deleteAll(any()) } just Runs - coEvery { timetableAdditionalDao.loadAll(1, 1, startDate, endDate) } returns flowOf(listOf()) + coEvery { + timetableAdditionalDao.loadAll( + diaryId = 1, + studentId = 1, + from = startDate, + end = endDate + ) + } returns flowOf(listOf()) coEvery { timetableAdditionalDao.deleteAll(emptyList()) } just Runs coEvery { timetableAdditionalDao.insertAll(emptyList()) } returns listOf(1, 2, 3) @@ -90,23 +127,36 @@ class TimetableRepositoryTest { coEvery { timetableHeaderDao.deleteAll(emptyList()) } just Runs // execute - val res = runBlocking { timetableRepository.getTimetable(student, semester, startDate, endDate, true).toFirstResult() } + val res = runBlocking { + timetableRepository.getTimetable( + student = student, + semester = semester, + start = startDate, + end = endDate, + forceRefresh = true + ).toFirstResult() + } // verify assertEquals(null, res.errorOrNull) assertEquals(2, res.dataOrNull!!.lessons.size) - coVerify { sdk.getTimetableFull(startDate, endDate) } + coVerify { sdk.getTimetable(startDate, endDate) } coVerify { timetableDb.loadAll(1, 1, startDate, endDate) } coVerify { timetableDb.insertAll(match { it.isEmpty() }) } coVerify { timetableDb.deleteAll(match { it.isEmpty() }) } } - private fun createTimetableRemote(start: LocalDateTime, number: Int = 1, room: String = "", subject: String = "", teacher: String = "", changes: Boolean = false) = SdkTimetable( + private fun createTimetableRemote( + start: LocalDateTime, + number: Int = 1, + room: String = "", + subject: String = "", + teacher: String = "", + changes: Boolean = false + ) = SdkLesson( number = number, - start = start, - end = start.plusMinutes(45), - startZoned = start.atZone(ZoneId.systemDefault()), - endZoned = start.plusMinutes(45).atZone(ZoneId.systemDefault()), + start = start.atZone(ZoneId.systemDefault()), + end = start.plusMinutes(45).atZone(ZoneId.systemDefault()), date = start.toLocalDate(), subject = subject, group = "", diff --git a/app/src/test/java/io/github/wulkanowy/domain/GetMailboxByStudentUseCaseTest.kt b/app/src/test/java/io/github/wulkanowy/domain/GetMailboxByStudentUseCaseTest.kt index 6db16d2f5..f58a5381d 100644 --- a/app/src/test/java/io/github/wulkanowy/domain/GetMailboxByStudentUseCaseTest.kt +++ b/app/src/test/java/io/github/wulkanowy/domain/GetMailboxByStudentUseCaseTest.kt @@ -207,7 +207,7 @@ class GetMailboxByStudentUseCaseTest { className = "", isCurrent = false, isParent = false, - loginMode = Sdk.Mode.API.name, + loginMode = Sdk.Mode.HEBE.name, loginType = Sdk.ScrapperLoginType.STANDARD.name, mobileBaseUrl = "", password = "", diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt index 10c84efcf..b94002c0d 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt @@ -456,7 +456,7 @@ class GradeAverageProviderTest { @Test fun `force calc current semester average with custom modifiers in api mode`() { - val student = student.copy(loginMode = Sdk.Mode.API.name) + val student = student.copy(loginMode = Sdk.Mode.HEBE.name) every { preferencesRepository.gradeAverageForceCalcFlow } returns flowOf(true) every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenterTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenterTest.kt index bf2d9f2cc..eb1f53006 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenterTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenterTest.kt @@ -3,11 +3,18 @@ package io.github.wulkanowy.ui.modules.login.form import io.github.wulkanowy.MainCoroutineRule import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.scrapper.Scrapper import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper -import io.mockk.* +import io.github.wulkanowy.utils.AppInfo +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.every import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.verify import org.junit.Before import org.junit.Rule import org.junit.Test @@ -30,13 +37,17 @@ class LoginFormPresenterTest { @MockK(relaxed = true) lateinit var analytics: AnalyticsHelper + @MockK + lateinit var appInfo: AppInfo + private lateinit var presenter: LoginFormPresenter private val registerUser = RegisterUser( email = "", password = "", login = "", - baseUrl = "", + scrapperBaseUrl = "", + loginMode = Sdk.Mode.HEBE, loginType = Scrapper.LoginType.AUTO, symbols = listOf(), ) @@ -54,8 +65,14 @@ class LoginFormPresenterTest { every { loginFormView.setErrorPassInvalid(any()) } just Runs every { loginFormView.setErrorPassRequired(any()) } just Runs every { loginFormView.setErrorUsernameRequired() } just Runs + every { appInfo.isDebug } returns false - presenter = LoginFormPresenter(repository, errorHandler, analytics) + presenter = LoginFormPresenter( + studentRepository = repository, + loginErrorHandler = errorHandler, + appInfo = appInfo, + analytics = analytics, + ) presenter.onAttachView(loginFormView) } diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt index cf426a50b..da292c51c 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt @@ -6,14 +6,22 @@ import io.github.wulkanowy.data.pojos.RegisterSymbol import io.github.wulkanowy.data.pojos.RegisterUnit import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.scrapper.Scrapper import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AppInfo -import io.mockk.* +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.clearMocks +import io.mockk.coEvery +import io.mockk.every import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.slot +import io.mockk.verify import org.junit.Before import org.junit.Rule import org.junit.Test @@ -76,6 +84,9 @@ class LoginStudentSelectPresenterTest { symbol = "", error = null, userName = "", + keyId = null, + privatePem = null, + hebeBaseUrl = null, schools = listOf(school), ) @@ -83,7 +94,8 @@ class LoginStudentSelectPresenterTest { email = "", password = "", login = "", - baseUrl = "", + scrapperBaseUrl = "", + loginMode = Sdk.Mode.SCRAPPER, loginType = Scrapper.LoginType.AUTO, symbols = listOf(symbol), ) From d99c93ec052f48c3ceb95207397212e086b2e2a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sun, 7 May 2023 23:40:18 +0200 Subject: [PATCH 737/869] Version 2.0.0 --- app/build.gradle | 6 +++--- app/src/main/play/release-notes/pl-PL/default.txt | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 22de4b8b6..bcca55873 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,8 +23,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 33 - versionCode 121 - versionName "1.9.2" + versionCode 122 + versionName "2.0.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" @@ -186,7 +186,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:14267a9a" + implementation "io.github.wulkanowy:sdk:2.0.0" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index b3fec438f..c5ec9de3d 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,7 +1,8 @@ -Wersja 1.9.2 +Wersja 2.0.0 -- naprawiliśmy oznaczanie wiadomości jako odczytanych (problem dotyczył głównie kont rodziców z wieloma dziećmi w tej samej szkole) -- naprawiliśmy zapisywanie załączników do wiadomości w sytuacji, gdy ten sam załącznik był dodany do więcej niż jednej wiadomości -- usprawniliśmy ekran z wyborem uczniów i wpisywaniem symbolu przy pierwszym logowaniu +— zaktualizowaliśmy wygląd aplikacji na (częściowo) zgodny z wytycznymi Material 3 +— dodaliśmy możliwość zmiany kolejności pozycji w menu dolnym +— poprawiliśmy sposób wyświetlania błędu o nieprawidłowym haśle na ekranie logowania +— od teraz zmiana ustawień liczenia średniej automatycznie odświeży listę ocen Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases From 3fdd47c22196b1f6957c67f957e6dcadaf3826b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 11 May 2023 16:44:10 +0200 Subject: [PATCH 738/869] Fix ime overlaping message content (#2199) --- .../ui/modules/message/send/SendMessageActivity.kt | 9 +++++++-- app/src/main/res/layout/activity_send_message.xml | 6 ++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt index 28147faed..0ba82f1a0 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessageActivity.kt @@ -142,10 +142,15 @@ class SendMessageActivity : BaseActivity - val bottomInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars()) + val navigationBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars()) + val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()) view.updateLayoutParams { - bottomMargin = bottomInsets.bottom + bottomMargin = if (imeInsets.bottom > navigationBarInsets.bottom) { + imeInsets.bottom + } else { + navigationBarInsets.bottom + } } WindowInsetsCompat.CONSUMED } diff --git a/app/src/main/res/layout/activity_send_message.xml b/app/src/main/res/layout/activity_send_message.xml index fac27d99f..e50cf6b39 100644 --- a/app/src/main/res/layout/activity_send_message.xml +++ b/app/src/main/res/layout/activity_send_message.xml @@ -22,17 +22,15 @@ + android:layout_height="wrap_content"> Date: Thu, 11 May 2023 23:45:20 +0200 Subject: [PATCH 739/869] Add auth dialog (#2198) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mikołaj Pich --- app/build.gradle | 2 +- .../wulkanowy/data/db/dao/StudentDao.kt | 4 + .../wulkanowy/data/db/entities/StudentName.kt | 18 +++ .../data/repositories/StudentRepository.kt | 20 +++ .../github/wulkanowy/ui/base/BaseActivity.kt | 5 + .../wulkanowy/ui/base/BaseDialogFragment.kt | 8 +- .../github/wulkanowy/ui/base/BaseFragment.kt | 5 + .../github/wulkanowy/ui/base/BasePresenter.kt | 8 +- .../io/github/wulkanowy/ui/base/BaseView.kt | 2 + .../github/wulkanowy/ui/base/ErrorHandler.kt | 5 + .../wulkanowy/ui/modules/auth/AuthDialog.kt | 81 +++++++++++ .../ui/modules/auth/AuthPresenter.kt | 100 ++++++++++++++ .../wulkanowy/ui/modules/auth/AuthView.kt | 20 +++ .../ui/modules/settings/SettingsFragment.kt | 2 + .../settings/advanced/AdvancedFragment.kt | 5 + .../settings/appearance/AppearanceFragment.kt | 5 + .../notifications/NotificationsFragment.kt | 5 + .../ui/modules/settings/sync/SyncFragment.kt | 5 + .../wulkanowy/utils/StudentExtension.kt | 2 +- app/src/main/res/drawable/ic_auth_success.xml | 10 ++ app/src/main/res/layout/dialog_auth.xml | 128 ++++++++++++++++++ app/src/main/res/values/strings.xml | 11 ++ app/src/main/res/values/styles.xml | 5 + .../ui/modules/settings/ads/AdsFragment.kt | 5 + 24 files changed, 455 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/entities/StudentName.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthDialog.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthPresenter.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthView.kt create mode 100644 app/src/main/res/drawable/ic_auth_success.xml create mode 100644 app/src/main/res/layout/dialog_auth.xml diff --git a/app/build.gradle b/app/build.gradle index bcca55873..16127d9a3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -186,7 +186,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:2.0.0" + implementation 'com.github.wulkanowy:sdk:c1573f04c5' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt index cfa7a72a2..a2f0abac6 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt @@ -2,6 +2,7 @@ package io.github.wulkanowy.data.db.dao import androidx.room.* import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.StudentName import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar import io.github.wulkanowy.data.db.entities.StudentWithSemesters import javax.inject.Singleton @@ -19,6 +20,9 @@ abstract class StudentDao { @Update(entity = Student::class) abstract suspend fun update(studentNickAndAvatar: StudentNickAndAvatar) + @Update(entity = Student::class) + abstract suspend fun update(studentName: StudentName) + @Query("SELECT * FROM Students WHERE is_current = 1") abstract suspend fun loadCurrent(): Student? diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/StudentName.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/StudentName.kt new file mode 100644 index 000000000..46f754b5e --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/StudentName.kt @@ -0,0 +1,18 @@ +package io.github.wulkanowy.data.db.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import java.io.Serializable + +@Entity +data class StudentName( + + @ColumnInfo(name = "student_name") + val studentName: String + +) : Serializable { + + @PrimaryKey + var id: Long = 0 +} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt index 4c7069ef6..a6bb72433 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt @@ -6,7 +6,9 @@ import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.dao.SemesterDao import io.github.wulkanowy.data.db.dao.StudentDao +import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.StudentName import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.exceptions.NoCurrentStudentException @@ -14,6 +16,7 @@ import io.github.wulkanowy.data.mappers.mapToPojo import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.DispatchersProvider +import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.security.decrypt import io.github.wulkanowy.utils.security.encrypt import kotlinx.coroutines.withContext @@ -146,4 +149,21 @@ class StudentRepository @Inject constructor( suspend fun isOneUniqueStudent() = getSavedStudents(false) .distinctBy { it.student.studentName }.size == 1 + + suspend fun authorizePermission(student: Student, semester: Semester, pesel: String) = + sdk.init(student) + .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .authorizePermission(pesel) + + suspend fun refreshStudentName(student: Student, semester: Semester) { + val newCurrentApiStudent = sdk.init(student) + .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .getCurrentStudent() ?: return + + val studentName = StudentName( + studentName = "${newCurrentApiStudent.studentName} ${newCurrentApiStudent.studentSurname}" + ).apply { id = student.id } + + studentDb.update(studentName) + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt index 7914df81c..f622209a7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt @@ -10,6 +10,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar.LENGTH_LONG import io.github.wulkanowy.R +import io.github.wulkanowy.ui.modules.auth.AuthDialog import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.utils.FragmentLifecycleLogger import io.github.wulkanowy.utils.getThemeAttrColor @@ -76,6 +77,10 @@ abstract class BaseActivity, VB : ViewBinding> : .show() } + override fun showAuthDialog() { + AuthDialog.newInstance().show(supportFragmentManager, "auth_dialog") + } + override fun showChangePasswordSnackbar(redirectUrl: String) { messageContainer?.let { Snackbar.make(it, R.string.error_password_change_required, LENGTH_LONG) diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt index 561d181a9..84540b1ca 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt @@ -5,10 +5,10 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast -import androidx.annotation.CallSuper import androidx.fragment.app.DialogFragment import androidx.viewbinding.ViewBinding import com.google.android.material.elevation.SurfaceColors +import io.github.wulkanowy.ui.modules.auth.AuthDialog import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.lifecycleAwareVariable import javax.inject.Inject @@ -40,17 +40,19 @@ abstract class BaseDialogFragment : DialogFragment(), BaseView (activity as? BaseActivity<*, *>)?.showChangePasswordSnackbar(redirectUrl) } + override fun showAuthDialog() { + AuthDialog.newInstance().show(childFragmentManager, "auth_dialog") + } + override fun showErrorDetailsDialog(error: Throwable) { ErrorDialog.newInstance(error).show(childFragmentManager, error.toString()) } - @CallSuper override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) view.setBackgroundColor(SurfaceColors.SURFACE_3.getColor(requireContext())) } - @CallSuper override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt index dbc5af3a9..b25346a7e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt @@ -7,6 +7,7 @@ import androidx.viewbinding.ViewBinding import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar.LENGTH_LONG import io.github.wulkanowy.R +import io.github.wulkanowy.ui.modules.auth.AuthDialog import io.github.wulkanowy.utils.lifecycleAwareVariable abstract class BaseFragment(@LayoutRes layoutId: Int) : Fragment(layoutId), @@ -42,6 +43,10 @@ abstract class BaseFragment(@LayoutRes layoutId: Int) : Fragme (activity as? BaseActivity<*, *>)?.showExpiredDialog() } + override fun showAuthDialog() { + AuthDialog.newInstance().show(childFragmentManager, "auth_dialog") + } + override fun openClearLoginView() { (activity as? BaseActivity<*, *>)?.openClearLoginView() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt index 15c069f54..2d913103b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt @@ -1,10 +1,15 @@ package io.github.wulkanowy.ui.base import io.github.wulkanowy.data.repositories.StudentRepository -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.launch import timber.log.Timber open class BasePresenter( @@ -26,6 +31,7 @@ open class BasePresenter( onSessionExpired = view::showExpiredDialog onNoCurrentStudent = view::openClearLoginView onPasswordChangeRequired = view::showChangePasswordSnackbar + onAuthorizationRequired = view::showAuthDialog } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt index d3165ea44..b31737e2b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt @@ -8,6 +8,8 @@ interface BaseView { fun showExpiredDialog() + fun showAuthDialog() + fun openClearLoginView() fun showErrorDetailsDialog(error: Throwable) diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt index afe200e9a..0a41a47b3 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt @@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.base import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.data.exceptions.NoCurrentStudentException +import io.github.wulkanowy.sdk.scrapper.exception.AuthorizationRequiredException import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException import io.github.wulkanowy.utils.getErrorString @@ -20,6 +21,8 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co var onPasswordChangeRequired: (String) -> Unit = {} + var onAuthorizationRequired: () -> Unit = {} + fun dispatch(error: Throwable) { Timber.e(error, "An exception occurred while the Wulkanowy was running") proceed(error) @@ -31,6 +34,7 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl) is ScramblerException, is BadCredentialsException -> onSessionExpired() is NoCurrentStudentException -> onNoCurrentStudent() + is AuthorizationRequiredException -> onAuthorizationRequired() } } @@ -39,5 +43,6 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co onSessionExpired = {} onNoCurrentStudent = {} onPasswordChangeRequired = {} + onAuthorizationRequired = {} } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthDialog.kt new file mode 100644 index 000000000..fa29df473 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthDialog.kt @@ -0,0 +1,81 @@ +package io.github.wulkanowy.ui.modules.auth + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.text.parseAsHtml +import androidx.core.view.isVisible +import androidx.core.widget.doOnTextChanged +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +import io.github.wulkanowy.databinding.DialogAuthBinding +import io.github.wulkanowy.ui.base.BaseDialogFragment +import javax.inject.Inject + +@AndroidEntryPoint +class AuthDialog : BaseDialogFragment(), AuthView { + + @Inject + lateinit var presenter: AuthPresenter + + companion object { + fun newInstance() = AuthDialog() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NO_TITLE, R.style.FullScreenDialogStyle) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return DialogAuthBinding.inflate(inflater).apply { binding = this }.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + presenter.onAttachView(this) + + binding.authInput.doOnTextChanged { text, _, _, _ -> + presenter.onPeselChange(text?.toString()) + } + + binding.authButton.setOnClickListener { presenter.authorize() } + binding.authSuccessButton.setOnClickListener { + activity?.recreate() + dismiss() + } + binding.authButtonSkip.setOnClickListener { dismiss() } + } + + override fun enableAuthButton(isEnabled: Boolean) { + binding.authButton.isEnabled = isEnabled + } + + override fun showProgress(show: Boolean) { + binding.authProgress.isVisible = show + } + + override fun showPeselError(show: Boolean) { + binding.authInputLayout.error = getString(R.string.auth_api_error).takeIf { show } + } + + override fun showInvalidPeselError(show: Boolean) { + binding.authInputLayout.error = getString(R.string.auth_invalid_error).takeIf { show } + } + + override fun showSuccess(show: Boolean) { + binding.authSuccess.isVisible = show + } + + override fun showContent(show: Boolean) { + binding.authForm.isVisible = show + } + + override fun showDescriptionWithName(name: String) { + binding.authDescription.text = getString(R.string.auth_description, name).parseAsHtml() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthPresenter.kt new file mode 100644 index 000000000..8f579712b --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthPresenter.kt @@ -0,0 +1,100 @@ +package io.github.wulkanowy.ui.modules.auth + +import io.github.wulkanowy.data.repositories.SemesterRepository +import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.ui.base.ErrorHandler +import kotlinx.coroutines.launch +import javax.inject.Inject + +class AuthPresenter @Inject constructor( + private val semesterRepository: SemesterRepository, + errorHandler: ErrorHandler, + studentRepository: StudentRepository +) : BasePresenter(errorHandler, studentRepository) { + + private var pesel: String = "" + + override fun onAttachView(view: AuthView) { + super.onAttachView(view) + view.enableAuthButton(pesel.length == 11) + view.showSuccess(false) + view.showProgress(false) + + loadName() + } + + private fun loadName() { + presenterScope.launch { + runCatching { studentRepository.getCurrentStudent(false) } + .onSuccess { view?.showDescriptionWithName(it.studentName) } + .onFailure { errorHandler.dispatch(it) } + } + } + + fun onPeselChange(newPesel: String?) { + pesel = newPesel.orEmpty() + + view?.enableAuthButton(pesel.length == 11) + view?.showPeselError(false) + view?.showInvalidPeselError(false) + } + + fun authorize() { + presenterScope.launch { + view?.showProgress(true) + view?.showContent(false) + + if (!isValidPESEL(pesel)) { + view?.showInvalidPeselError(true) + view?.showProgress(false) + view?.showContent(true) + return@launch + } + + runCatching { + val student = studentRepository.getCurrentStudent() + val semester = semesterRepository.getCurrentSemester(student) + + val isSuccess = studentRepository.authorizePermission(student, semester, pesel) + if (isSuccess) { + studentRepository.refreshStudentName(student, semester) + } + isSuccess + } + .onFailure { errorHandler.dispatch(it) } + .onSuccess { + if (it) { + view?.showSuccess(true) + view?.showContent(false) + view?.showPeselError(false) + } else { + view?.showSuccess(false) + view?.showContent(true) + view?.showPeselError(true) + } + } + + view?.showProgress(false) + } + } + + private fun isValidPESEL(peselString: String): Boolean { + if (peselString.length != 11) { + return false + } + + val weights = intArrayOf(1, 3, 7, 9, 1, 3, 7, 9, 1, 3) + var sum = 0 + + for (i in 0 until 10) { + sum += weights[i] * Character.getNumericValue(peselString[i]) + } + + sum %= 10 + sum = 10 - sum + sum %= 10 + + return sum == Character.getNumericValue(peselString[10]) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthView.kt new file mode 100644 index 000000000..d7e1917c2 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthView.kt @@ -0,0 +1,20 @@ +package io.github.wulkanowy.ui.modules.auth + +import io.github.wulkanowy.ui.base.BaseView + +interface AuthView : BaseView { + + fun enableAuthButton(isEnabled: Boolean) + + fun showProgress(show: Boolean) + + fun showPeselError(show: Boolean) + + fun showInvalidPeselError(show: Boolean) + + fun showSuccess(show: Boolean) + + fun showContent(show: Boolean) + + fun showDescriptionWithName(name: String) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt index d56cdfa7c..21f564988 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt @@ -31,4 +31,6 @@ class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView, Settin override fun showErrorDetailsDialog(error: Throwable) {} override fun showChangePasswordSnackbar(redirectUrl: String) {} + + override fun showAuthDialog() {} } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt index b4ba5bc4b..41e9e8b1a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt @@ -8,6 +8,7 @@ import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.ErrorDialog +import io.github.wulkanowy.ui.modules.auth.AuthDialog import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.AppInfo import javax.inject.Inject @@ -62,6 +63,10 @@ class AdvancedFragment : PreferenceFragmentCompat(), ErrorDialog.newInstance(error).show(childFragmentManager, error.toString()) } + override fun showAuthDialog() { + AuthDialog.newInstance().show(childFragmentManager, "auth_dialog") + } + override fun onResume() { super.onResume() preferenceScreen.sharedPreferences?.registerOnSharedPreferenceChangeListener(this) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt index 1f6d5143b..493ab5d7f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt @@ -9,6 +9,7 @@ import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.ErrorDialog +import io.github.wulkanowy.ui.modules.auth.AuthDialog import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.AppInfo import javax.inject.Inject @@ -78,6 +79,10 @@ class AppearanceFragment : PreferenceFragmentCompat(), ErrorDialog.newInstance(error).show(childFragmentManager, error.toString()) } + override fun showAuthDialog() { + AuthDialog.newInstance().show(childFragmentManager, "auth_dialog") + } + override fun onResume() { super.onResume() preferenceScreen.sharedPreferences?.registerOnSharedPreferenceChangeListener(this) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt index 98ac15739..35c1faa45 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt @@ -21,6 +21,7 @@ import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.ErrorDialog +import io.github.wulkanowy.ui.modules.auth.AuthDialog import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.openInternetBrowser @@ -148,6 +149,10 @@ class NotificationsFragment : PreferenceFragmentCompat(), ErrorDialog.newInstance(error).show(childFragmentManager, error.toString()) } + override fun showAuthDialog() { + AuthDialog.newInstance().show(childFragmentManager, "auth_dialog") + } + override fun showFixSyncDialog() { MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.pref_notify_fix_sync_issues) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt index 2a804d9f6..df2e1348a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt @@ -10,6 +10,7 @@ import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.ErrorDialog +import io.github.wulkanowy.ui.modules.auth.AuthDialog import io.github.wulkanowy.ui.modules.main.MainView import javax.inject.Inject @@ -99,6 +100,10 @@ class SyncFragment : PreferenceFragmentCompat(), ErrorDialog.newInstance(error).show(childFragmentManager, "error_details") } + override fun showAuthDialog() { + AuthDialog.newInstance().show(childFragmentManager, "auth_dialog") + } + override fun onResume() { super.onResume() preferenceScreen.sharedPreferences?.registerOnSharedPreferenceChangeListener(this) diff --git a/app/src/main/java/io/github/wulkanowy/utils/StudentExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/StudentExtension.kt index fdd0610a0..132a3085e 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/StudentExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/StudentExtension.kt @@ -2,4 +2,4 @@ package io.github.wulkanowy.utils import io.github.wulkanowy.data.db.entities.Student -inline val Student.nickOrName get() = if (nick.isBlank()) studentName else nick +inline val Student.nickOrName get() = nick.ifBlank { studentName } diff --git a/app/src/main/res/drawable/ic_auth_success.xml b/app/src/main/res/drawable/ic_auth_success.xml new file mode 100644 index 000000000..015553c02 --- /dev/null +++ b/app/src/main/res/drawable/ic_auth_success.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/dialog_auth.xml b/app/src/main/res/layout/dialog_auth.xml new file mode 100644 index 000000000..e2e2aa304 --- /dev/null +++ b/app/src/main/res/layout/dialog_auth.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4e53fd2f1..1eff95698 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -808,6 +808,17 @@ Restart + + Authorization has been rejected. The data provided does not match the records in the secretary\'s office. + Invalid PESEL + PESEL + Authorize + Authorization completed successfully + Authorization + To operate the application, we need to confirm your identity. Please enter the student\'s PESEL <b>%1$s</b> in the field below + Skip for now + + No internet connection An error occurred. Check your device clock diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b0bf28194..9d1f07458 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -78,4 +78,9 @@ @drawable/background_material_alert_dialog @layout/m3_alert_dialog + + diff --git a/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt index de4c591e1..af6a83404 100644 --- a/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt +++ b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt @@ -13,6 +13,7 @@ import io.github.wulkanowy.R import io.github.wulkanowy.databinding.DialogAdsConsentBinding import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.ErrorDialog +import io.github.wulkanowy.ui.modules.auth.AuthDialog import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.openInternetBrowser import javax.inject.Inject @@ -146,6 +147,10 @@ class AdsFragment : PreferenceFragmentCompat(), MainView.TitledView, AdsView { (activity as? BaseActivity<*, *>)?.openClearLoginView() } + override fun showAuthDialog() { + AuthDialog.newInstance().show(childFragmentManager, "auth_dialog") + } + override fun showErrorDetailsDialog(error: Throwable) { ErrorDialog.newInstance(error).show(childFragmentManager, error.toString()) } From 5a2622871f9e5a61b419e100dd546457a36c63bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Fri, 12 May 2023 00:21:24 +0200 Subject: [PATCH 740/869] Version 2.0.1 --- app/build.gradle | 8 ++++---- app/src/main/play/release-notes/pl-PL/default.txt | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 16127d9a3..48ca28a0e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,8 +23,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 33 - versionCode 122 - versionName "2.0.0" + versionCode 123 + versionName "2.0.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" @@ -161,7 +161,7 @@ play { defaultToAppBundles = false track = 'production' releaseStatus = com.github.triplet.gradle.androidpublisher.ReleaseStatus.IN_PROGRESS - userFraction = 0.50d + userFraction = 0.10d updatePriority = 2 enabled.set(false) } @@ -186,7 +186,7 @@ ext { } dependencies { - implementation 'com.github.wulkanowy:sdk:c1573f04c5' + implementation 'io.github.wulkanowy:sdk:2.0.1' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index c5ec9de3d..d079f986c 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -4,5 +4,6 @@ Wersja 2.0.0 — dodaliśmy możliwość zmiany kolejności pozycji w menu dolnym — poprawiliśmy sposób wyświetlania błędu o nieprawidłowym haśle na ekranie logowania — od teraz zmiana ustawień liczenia średniej automatycznie odświeży listę ocen +— dodaliśmy okienko na wpisanie numeru PESEL, gdy dziennik wymaga dodatkowej autoryzacji dostępu Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases From cbef160adaead03b862035c8cfc3b6dab9355716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Fri, 12 May 2023 00:41:53 +0200 Subject: [PATCH 741/869] New Crowdin updates (#2193) --- app/src/main/res/values-cs/strings.xml | 9 +++++++++ app/src/main/res/values-da-rDK/strings.xml | 9 +++++++++ app/src/main/res/values-de/strings.xml | 9 +++++++++ app/src/main/res/values-es-rES/strings.xml | 9 +++++++++ app/src/main/res/values-pl/strings.xml | 9 +++++++++ app/src/main/res/values-ru/strings.xml | 11 ++++++++++- app/src/main/res/values-sk/strings.xml | 9 +++++++++ app/src/main/res/values-uk/strings.xml | 9 +++++++++ 8 files changed, 73 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index bedb491bc..beb2996be 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -809,6 +809,15 @@ Restartování aplikace Pro uložení změn je nutné aplikaci restartovat Restartovat + + Authorization has been rejected. The data provided does not match the records in the secretary\'s office. + Invalid PESEL + PESEL + Authorize + Authorization completed successfully + Authorization + To operate the application, we need to confirm your identity. Please enter the student\'s PESEL <b>%1$s</b> in the field below + Skip for now Žádné internetové připojení Vyskytla se chyba. Zkontrolujte hodiny svého zařízení diff --git a/app/src/main/res/values-da-rDK/strings.xml b/app/src/main/res/values-da-rDK/strings.xml index ebec11b25..b9de57f77 100644 --- a/app/src/main/res/values-da-rDK/strings.xml +++ b/app/src/main/res/values-da-rDK/strings.xml @@ -721,6 +721,15 @@ Application restart The application must restart for the changes to be saved Restart + + Authorization has been rejected. The data provided does not match the records in the secretary\'s office. + Invalid PESEL + PESEL + Authorize + Authorization completed successfully + Authorization + To operate the application, we need to confirm your identity. Please enter the student\'s PESEL <b>%1$s</b> in the field below + Skip for now No internet connection An error occurred. Check your device clock diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 1e1785bff..4fdd71b50 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -721,6 +721,15 @@ Application restart The application must restart for the changes to be saved Restart + + Authorization has been rejected. The data provided does not match the records in the secretary\'s office. + Invalid PESEL + PESEL + Authorize + Authorization completed successfully + Authorization + To operate the application, we need to confirm your identity. Please enter the student\'s PESEL <b>%1$s</b> in the field below + Skip for now Keine Internetverbindung Es ist ein Fehler aufgetreten. Überprüfen Sie Ihre Geräteuhr diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index ebec11b25..b9de57f77 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -721,6 +721,15 @@ Application restart The application must restart for the changes to be saved Restart + + Authorization has been rejected. The data provided does not match the records in the secretary\'s office. + Invalid PESEL + PESEL + Authorize + Authorization completed successfully + Authorization + To operate the application, we need to confirm your identity. Please enter the student\'s PESEL <b>%1$s</b> in the field below + Skip for now No internet connection An error occurred. Check your device clock diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index f797a4dcb..207b12e98 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -809,6 +809,15 @@ Ponowne uruchomienie aplikacji W celu zapisania zmian aplikacja musi zostać ponownie uruchomiona Uruchom ponownie + + Autoryzacja została odrzucona. Podano dane niezgodne z danymi w sekretariacie. + Nieprawidłowy PESEL + PESEL + Potwierdź + Autoryzacja zakończona pomyślnie + Autoryzacja + Rodzicu, musimy mieć pewność, że Twój adres e-mail został powiązany z prawidłowym kontem ucznia. W celu autoryzacji konta podaj numer PESEL ucznia <b>%1$s</b> w polu poniżej + Na razie pomiń Brak połączenia z internetem Wystąpił błąd. Sprawdź poprawność daty w urządzeniu diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 7a42e3880..726e95465 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -778,7 +778,7 @@ Значения плюса и минуса, расчёт средней оценки Расширенные Версия приложения, разработчики, соц. сети - Посмотреть рекламу, чтобы поддержать преокт + Displaying advertisements, project support Новые оценки Новое домашнее задание @@ -809,6 +809,15 @@ Перезапуск приложение Для сохранения изменений необходимо перезапустить приложение Перезапустить + + Authorization has been rejected. The data provided does not match the records in the secretary\'s office. + Invalid PESEL + PESEL + Authorize + Authorization completed successfully + Authorization + To operate the application, we need to confirm your identity. Please enter the student\'s PESEL <b>%1$s</b> in the field below + Skip for now Интернет-соединение отсутствует Произошла ошибка. Проверьте время на вашем устройстве diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index cde3178bb..7e42c6cd4 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -809,6 +809,15 @@ Reštartovanie aplikácie Pre uloženie zmien je nutné aplikáciu reštartovať Reštartovať + + Authorization has been rejected. The data provided does not match the records in the secretary\'s office. + Invalid PESEL + PESEL + Authorize + Authorization completed successfully + Authorization + To operate the application, we need to confirm your identity. Please enter the student\'s PESEL <b>%1$s</b> in the field below + Skip for now Žiadne internetové pripojenie Vyskytla sa chyba. Skontrolujte hodiny svojho zariadenia diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 9602aabba..66cd23870 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -809,6 +809,15 @@ Перезавантаження додатку Додаток потрібно перезавантажити для збереження змін Перезавантажити + + Authorization has been rejected. The data provided does not match the records in the secretary\'s office. + Invalid PESEL + PESEL + Authorize + Authorization completed successfully + Authorization + To operate the application, we need to confirm your identity. Please enter the student\'s PESEL <b>%1$s</b> in the field below + Skip for now Немає з\'єднання з інтернетом Сталася помилка. Перевірте годинник пристрою From b8ac72c247a70ae4a68349a3239d3ff280bd0335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Fri, 12 May 2023 00:45:48 +0200 Subject: [PATCH 742/869] Version 2.0.2 --- app/build.gradle | 4 ++-- app/src/main/play/release-notes/pl-PL/default.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 48ca28a0e..de0c2a3ce 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,8 +23,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 33 - versionCode 123 - versionName "2.0.1" + versionCode 124 + versionName "2.0.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index d079f986c..378dedceb 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,4 +1,4 @@ -Wersja 2.0.0 +Wersja 2.0.2 — zaktualizowaliśmy wygląd aplikacji na (częściowo) zgodny z wytycznymi Material 3 — dodaliśmy możliwość zmiany kolejności pozycji w menu dolnym From 54fbd56b7382f956dc745addd4e91d3cf23dc17a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 May 2023 04:33:56 +0000 Subject: [PATCH 743/869] Bump com.google.firebase:firebase-bom from 31.5.0 to 32.0.0 (#2190) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index de0c2a3ce..0f6789e8e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -242,7 +242,7 @@ dependencies { implementation 'com.fredporciuncula:flow-preferences:1.9.1' implementation 'org.apache.commons:commons-text:1.10.0' - playImplementation platform('com.google.firebase:firebase-bom:31.5.0') + playImplementation platform('com.google.firebase:firebase-bom:32.0.0') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' From cb8303f33dc1953db0dcf31516ca36f396aede29 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 May 2023 04:34:14 +0000 Subject: [PATCH 744/869] Bump com.google.android.material:material from 1.8.0 to 1.9.0 (#2191) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 0f6789e8e..85ac4833f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -206,7 +206,7 @@ dependencies { implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.constraintlayout:constraintlayout:2.1.4" implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0" - implementation "com.google.android.material:material:1.8.0" + implementation "com.google.android.material:material:1.9.0" implementation "com.github.wulkanowy:material-chips-input:2.3.1" implementation "com.github.PhilJay:MPAndroidChart:v3.1.0" implementation 'com.github.lopspower:CircularImageView:4.3.0' From a0af55825d3c8e0a39ca559b6de804f76ee3d309 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 May 2023 04:34:32 +0000 Subject: [PATCH 745/869] Bump about_libraries from 10.6.2 to 10.6.3 (#2189) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 3c8552d20..c63a8fd0c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { kotlin_version = '1.8.21' - about_libraries = '10.6.2' + about_libraries = '10.6.3' hilt_version = "2.45" } repositories { From c33b309cf068efd51566fb52048da140f9046208 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 May 2023 04:34:48 +0000 Subject: [PATCH 746/869] Bump org.robolectric:robolectric from 4.10 to 4.10.2 (#2188) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 85ac4833f..4ea086a19 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -265,7 +265,7 @@ dependencies { testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines" testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" - testImplementation 'org.robolectric:robolectric:4.10' + testImplementation 'org.robolectric:robolectric:4.10.2' testImplementation "androidx.test:runner:1.5.2" testImplementation "androidx.test.ext:junit:1.1.5" testImplementation "androidx.test:core:1.5.0" From 030fe8c218a2508789c5750d9d3412334aa1fb03 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 May 2023 04:45:15 +0000 Subject: [PATCH 747/869] Bump coroutines from 1.6.4 to 1.7.0 (#2186) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 4ea086a19..3a6744c14 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -182,7 +182,7 @@ ext { room = "2.5.1" chucker = "3.5.2" mockk = "1.13.5" - coroutines = "1.6.4" + coroutines = "1.7.0" } dependencies { From f2faa7e8b7f27c7f7f182117fc8b83d02e0e8122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Fri, 12 May 2023 22:45:24 +0200 Subject: [PATCH 748/869] Fix button color in high priority admin message (#2202) --- .../ui/modules/dashboard/adapters/DashboardAdapter.kt | 5 +++-- app/src/main/res/values-night-v31/styles.xml | 4 +++- app/src/main/res/values-night/styles.xml | 4 +++- app/src/main/res/values-v31/styles.xml | 4 +++- app/src/main/res/values/attrs.xml | 2 ++ app/src/main/res/values/colors.xml | 6 ++++-- app/src/main/res/values/styles.xml | 4 +++- 7 files changed, 21 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardAdapter.kt index 2c06e45fd..4ad4e9d67 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardAdapter.kt @@ -738,8 +738,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter { - context.getThemeAttrColor(R.attr.colorPrimary) to - context.getThemeAttrColor(R.attr.colorOnPrimary) + context.getThemeAttrColor(R.attr.colorMessageHigh) to + context.getThemeAttrColor(R.attr.colorOnMessageHigh) } "MEDIUM" -> { context.getThemeAttrColor(R.attr.colorMessageMedium) to Color.BLACK @@ -754,6 +754,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter@color/colorErrorLight @color/colorDividerInverse @color/material_dynamic_secondary20 - @color/dashboard_message_medium_light + @color/dashboard_message_medium_dark + @color/dashboard_message_high_dark + @android:color/black ?colorSurface @color/material_dynamic_neutral90 @android:color/transparent diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml index 7d2f0cfed..5d9aa22a6 100644 --- a/app/src/main/res/values-night/styles.xml +++ b/app/src/main/res/values-night/styles.xml @@ -24,7 +24,9 @@ @color/colorErrorLight @color/colorDividerInverse @color/colorSwipeRefreshDark - @color/dashboard_message_medium_light + @color/dashboard_message_medium_dark + @color/dashboard_message_high_dark + @android:color/black ?colorSurface ?android:textColorPrimary @color/colorNavigationBarDark diff --git a/app/src/main/res/values-v31/styles.xml b/app/src/main/res/values-v31/styles.xml index cffb284ec..bb47b22ed 100644 --- a/app/src/main/res/values-v31/styles.xml +++ b/app/src/main/res/values-v31/styles.xml @@ -37,7 +37,9 @@ @color/colorError @color/colorDivider @color/material_dynamic_secondary90 - @color/dashboard_message_medium_dark + @color/dashboard_message_medium_light + @color/dashboard_message_high_light + @android:color/white @color/material_dynamic_neutral10 @android:color/transparent @android:color/transparent diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 8986f3576..aa58fa09e 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -5,4 +5,6 @@ + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index ac1b1c196..87057c61d 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -39,8 +39,10 @@ #342826 #181010 - #FFD980 - #ffd54f + #ffd54f + #FFD980 + #B91B21 + #e57373 #d32f2f #e57373 diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 9d1f07458..603e22abc 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -22,7 +22,9 @@ @color/colorError @color/colorDivider @color/colorSwipeRefresh - @color/dashboard_message_medium_dark + @color/dashboard_message_medium_light + @color/dashboard_message_high_light + @android:color/white ?android:textColorPrimary @android:color/black @style/PreferenceThemeOverlay From cc752ab0ad36510bff1df10bfee1eda641768ead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Fri, 12 May 2023 22:45:50 +0200 Subject: [PATCH 749/869] New Crowdin updates (#2201) --- app/src/main/res/values-cs/strings.xml | 14 +++++----- app/src/main/res/values-de/strings.xml | 36 +++++++++++++------------- app/src/main/res/values-sk/strings.xml | 14 +++++----- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index beb2996be..f8c19dff0 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -810,14 +810,14 @@ Pro uložení změn je nutné aplikaci restartovat Restartovat - Authorization has been rejected. The data provided does not match the records in the secretary\'s office. - Invalid PESEL + Autorizace byla zamítnuta. Uvedené údaje se neshodují se záznamy v kanceláři tajemníka. + Neplatný PESEL PESEL - Authorize - Authorization completed successfully - Authorization - To operate the application, we need to confirm your identity. Please enter the student\'s PESEL <b>%1$s</b> in the field below - Skip for now + Autorizovat + Autorizace byla úspěšně dokončena + Autorizace + Pro provoz aplikace potřebujeme potvrdit vaši identitu. Zadejte PESEL žáka <b>%1$s</b> v níže uvedeném poli + Zatím přeskočit Žádné internetové připojení Vyskytla se chyba. Zkontrolujte hodiny svého zařízení diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4fdd71b50..500553e27 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -26,7 +26,7 @@ Schülerinfo Übersicht Benachrichtigungszentrum - Menu configuartion + Menü Konfiguration Semester %1$d, %2$d/%3$d @@ -56,7 +56,7 @@ Ungültige symbol Schüler nicht gefunden. Überprüfen Sie das Symbol und die gewählte Variation des UONET+ Registers Ausgewählter Student ist bereits angemeldet. - The symbol can be found on the register page in Uczeń → Dostęp Mobilny → Wygeneruj kod dostępu.\n\nMake sure that you have set the appropriate register variant in the UONET+ register variant field on the first login screen + Das Symbol kann auf der Registerseite in Student → Tost Möbeln → Registrieren Sie Ihr Mobilgerätgefunden werden.\n\nStellen Sie sicher, dass Sie die entsprechende Registervariante im Feld UONET+ Registervariante auf dem vorherigen Bildschirm festgelegt haben Wählen Sie die Studenten aus, die sich bei der Anwendung anmelden sollen Andere Optionen In diesem Modus funktioniert eine Glücknummer, eine Klassenstatistik, eine Zusammenfassung der Anwesenheit, eine Entschuldigung für die Abwesenheit, abgeschlossene Lektionen, Schulinformationen und eine Vorschau der Liste der registrierten Geräte nicht @@ -73,14 +73,14 @@ Wiederherstellen Student ist bereits angemeldet Standard - Other search locations - No active students found - Enter a different symbol + Andere Suchorte + Keine aktiven Schüler gefunden + Geben Sie ein anderes Symbol ein - Enable notifications - Enable notifications so you don\'t miss message from teacher or new grade - Skip - Enable + Benachrichtigungen aktivieren + Aktivieren Sie Benachrichtigungen, damit Sie keine Nachricht vom Lehrer oder eine neue Klasse verpassen + Überspringen + Ermöglichen Kundenbetreuer Anmelden @@ -288,7 +288,7 @@ Nur ungelesen Nur mit Anhängen Lesen: %s - Read by: %1$d of %2$d people + Lesen von: %1$d von %2$d Personen %1$d Nachricht %1$d Nachrichten @@ -422,8 +422,8 @@ Teilnahme an einem Meeting Agenda - Place - Topic + Ort + Thema Schulankündigungen Keine schulankündigungen @@ -591,10 +591,10 @@ lösen Ändern Zum Kalender hinzufügen - Cancel + Stornieren Keine Lektionen - Synchronized on %1$s at %2$s + Synchronisiert am %1$s am %2$s Thema wählen Licht Dunkel @@ -614,8 +614,8 @@ Farbschema der Noten Schulfachen sortieren Sprache - Menu configuration - Set the order of functions in the menu + Menü Konfiguration + Legen Sie die Reihenfolge der Funktionen im Menü fest Benachrichtigungen Sonstiges Benachrichtigungen anzeigen @@ -718,8 +718,8 @@ Neustart Update fehlgeschlagen! Wulkanowy funktioniert möglicherweise nicht richtig. Überlegen Sie die Aktualisierung - Application restart - The application must restart for the changes to be saved + Neustart der Anwendung + Die Anwendung muss neu gestartet werden, damit die Änderungen gespeichert werden Restart Authorization has been rejected. The data provided does not match the records in the secretary\'s office. diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 7e42c6cd4..950eb01e0 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -810,14 +810,14 @@ Pre uloženie zmien je nutné aplikáciu reštartovať Reštartovať - Authorization has been rejected. The data provided does not match the records in the secretary\'s office. - Invalid PESEL + Autorizácia bola zamietnutá. Uvedené údaje sa nezhodujú so záznamami v kancelárii tajomníka. + Neplatný PESEL PESEL - Authorize - Authorization completed successfully - Authorization - To operate the application, we need to confirm your identity. Please enter the student\'s PESEL <b>%1$s</b> in the field below - Skip for now + Autorizovať + Autorizácia bola úspešne dokončená + Autorizácia + Na prevádzku aplikácie potrebujeme potvrdiť vašu identitu. Zadajte PESEL žiaka <b>%1$s</b> v nižšie uvedenom poli + Zatiaľ preskočiť Žiadne internetové pripojenie Vyskytla sa chyba. Skontrolujte hodiny svojho zariadenia From 1e9a6a5c42bc11d80bb9f73fcb788f5a0b051cc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Fri, 12 May 2023 22:59:40 +0200 Subject: [PATCH 750/869] Version 2.0.3 --- app/build.gradle | 10 +++++----- app/src/main/play/release-notes/pl-PL/default.txt | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 3a6744c14..a4f230b88 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,8 +23,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 33 - versionCode 124 - versionName "2.0.2" + versionCode 125 + versionName "2.0.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" @@ -161,8 +161,8 @@ play { defaultToAppBundles = false track = 'production' releaseStatus = com.github.triplet.gradle.androidpublisher.ReleaseStatus.IN_PROGRESS - userFraction = 0.10d - updatePriority = 2 + userFraction = 0.50d + updatePriority = 3 enabled.set(false) } @@ -186,7 +186,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.0.1' + implementation 'io.github.wulkanowy:sdk:2.0.3' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index 378dedceb..aee30290a 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,4 +1,4 @@ -Wersja 2.0.2 +Wersja 2.0.3 — zaktualizowaliśmy wygląd aplikacji na (częściowo) zgodny z wytycznymi Material 3 — dodaliśmy możliwość zmiany kolejności pozycji w menu dolnym From adf418cc689c479ac898a14bac0d7a4219d4620f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sat, 13 May 2023 10:44:09 +0200 Subject: [PATCH 751/869] Fix delete user homework button visibility (#2204) --- app/src/main/res/layout/item_homework_dialog_details.xml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/layout/item_homework_dialog_details.xml b/app/src/main/res/layout/item_homework_dialog_details.xml index 1b1c1d39b..a40b5dcee 100644 --- a/app/src/main/res/layout/item_homework_dialog_details.xml +++ b/app/src/main/res/layout/item_homework_dialog_details.xml @@ -14,16 +14,13 @@ + android:textColor="?attr/colorOnSurface" /> Date: Sun, 14 May 2023 17:28:29 +0000 Subject: [PATCH 752/869] Bump hilt_version from 2.45 to 2.46.1 (#2205) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c63a8fd0c..7161e4c31 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { kotlin_version = '1.8.21' about_libraries = '10.6.3' - hilt_version = "2.45" + hilt_version = "2.46.1" } repositories { mavenCentral() From a06add070ecc9543a9a5235bf822b89d4d6775fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 May 2023 18:20:56 +0000 Subject: [PATCH 753/869] Bump com.android.tools.build:gradle from 7.4.2 to 8.0.1 (#2187) --- .github/workflows/deploy-store.yml | 4 ++-- .github/workflows/deploy-test.yml | 4 ++-- .github/workflows/test.yml | 6 +++--- app/build.gradle | 6 +++++- build.gradle | 2 +- gradle.properties | 9 +++++++-- gradle/wrapper/gradle-wrapper.jar | Bin 61574 -> 61608 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 4 ++-- 9 files changed, 23 insertions(+), 14 deletions(-) diff --git a/.github/workflows/deploy-store.yml b/.github/workflows/deploy-store.yml index 3ce618ca7..e8a220ddf 100644 --- a/.github/workflows/deploy-store.yml +++ b/.github/workflows/deploy-store.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/setup-java@v2 with: distribution: 'zulu' - java-version: 11 + java-version: 17 - uses: actions/cache@v3 with: path: | @@ -52,7 +52,7 @@ jobs: - uses: actions/setup-java@v2 with: distribution: 'zulu' - java-version: 11 + java-version: 17 - uses: actions/cache@v3 with: path: | diff --git a/.github/workflows/deploy-test.yml b/.github/workflows/deploy-test.yml index 20082590d..f2e9f016f 100644 --- a/.github/workflows/deploy-test.yml +++ b/.github/workflows/deploy-test.yml @@ -22,7 +22,7 @@ jobs: - uses: actions/setup-java@v2 with: distribution: 'zulu' - java-version: 11 + java-version: 17 - uses: actions/cache@v3 with: path: | @@ -92,7 +92,7 @@ jobs: - uses: actions/setup-java@v2 with: distribution: 'zulu' - java-version: 11 + java-version: 17 - uses: actions/cache@v3 with: path: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7f8591bb1..bc4b36470 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,7 +22,7 @@ jobs: - uses: actions/setup-java@v2 with: distribution: 'zulu' - java-version: 11 + java-version: 17 - uses: actions/cache@v3 with: path: | @@ -48,7 +48,7 @@ jobs: - uses: actions/setup-java@v2 with: distribution: 'zulu' - java-version: 11 + java-version: 17 - uses: actions/cache@v3 with: path: | @@ -74,7 +74,7 @@ jobs: - uses: actions/setup-java@v2 with: distribution: 'zulu' - java-version: 11 + java-version: 17 - uses: actions/cache@v3 with: path: | diff --git a/app/build.gradle b/app/build.gradle index a4f230b88..ad83461a3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -156,7 +156,11 @@ android { kapt { correctErrorTypes true } - +kotlin { + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(11)) + } +} play { defaultToAppBundles = false track = 'production' diff --git a/build.gradle b/build.gradle index 7161e4c31..88079e54d 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ buildscript { dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" - classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.android.tools.build:gradle:8.0.1' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.15' classpath 'com.huawei.agconnect:agcp:1.9.0.300' diff --git a/gradle.properties b/gradle.properties index 38603830b..5a8099a12 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,13 +11,18 @@ org.gradle.jvmargs=-Xmx1536m # android.enableJetifier=true android.useAndroidX=true +android.defaults.buildfeatures.buildconfig=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false # kotlin.code.style=official -# kapt.use.worker.api=true kapt.include.compile.classpath=false # +# https://developer.huawei.com/consumer/en/doc/development/AppGallery-connect-Guides/agc-common-faq-0000001063210244#section17273113244910 +apmsInstrumentationEnabled=false +# # 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 \ No newline at end of file +# org.gradle.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 943f0cbfa754578e88a3dae77fce6e3dea56edbf..ccebba7710deaf9f98673a68957ea02138b60d0a 100644 GIT binary patch delta 5094 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 delta 5107 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e8be595e3..6ec1567a0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-all.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 65dcd68d6..79a61d421 100755 --- a/gradlew +++ b/gradlew @@ -144,7 +144,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +152,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac From 5b0fe2c006a2509aeb912d8c834db8aa6355c635 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 May 2023 19:28:49 +0000 Subject: [PATCH 754/869] Bump ru.cian:huawei-publish-gradle-plugin from 1.3.5 to 1.4.0 (#2185) --- app/build.gradle | 8 +++++++- build.gradle | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ad83461a3..f500e7795 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -175,7 +175,13 @@ huaweiPublish { hmsRelease { credentialsPath = "$rootDir/app/src/release/agconnect-credentials.json" buildFormat = "aab" - deployType = "draft" + deployType = "publish" + releaseNotes = [ + new ru.cian.huawei.publish.ReleaseNote( + "pl-PL", + "$projectDir/src/main/play/release-notes/pl-PL/default.txt" + ) + ] } } } diff --git a/build.gradle b/build.gradle index 88079e54d..eac46b05c 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ buildscript { classpath 'com.huawei.agconnect:agcp:1.9.0.300' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.5' classpath "com.github.triplet.gradle:play-publisher:3.6.0" - classpath "ru.cian:huawei-publish-gradle-plugin:1.3.5" + classpath "ru.cian:huawei-publish-gradle-plugin:1.4.0" classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.0.0.2929" classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0" classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries" From ea312c3e12c9bbc3dc4a379d092678ea099dad89 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 May 2023 17:13:02 +0000 Subject: [PATCH 755/869] Bump androidx.core:core-ktx from 1.10.0 to 1.10.1 (#2208) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index f500e7795..aaa27b9c7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -203,7 +203,7 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" - implementation "androidx.core:core-ktx:1.10.0" + implementation "androidx.core:core-ktx:1.10.1" implementation 'androidx.core:core-splashscreen:1.0.1' implementation "androidx.activity:activity-ktx:1.7.1" implementation "androidx.appcompat:appcompat:1.6.1" From 8a7b7103eb375b22231a748a6360f85640033489 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 May 2023 18:36:26 +0000 Subject: [PATCH 756/869] Bump coroutines from 1.7.0 to 1.7.1 (#2207) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index aaa27b9c7..2cde7cdaa 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -192,7 +192,7 @@ ext { room = "2.5.1" chucker = "3.5.2" mockk = "1.13.5" - coroutines = "1.7.0" + coroutines = "1.7.1" } dependencies { From 48bcf581cfde80e282eb18866870c42be5e91c74 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 May 2023 18:36:50 +0000 Subject: [PATCH 757/869] Bump org.jetbrains.kotlinx:kotlinx-serialization-json (#2209) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 2cde7cdaa..3b5fadb13 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -200,7 +200,7 @@ dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' - implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" implementation "androidx.core:core-ktx:1.10.1" From 94664828938ee2b25cdd6a367323fe4c2923477a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 18 May 2023 16:14:14 +0200 Subject: [PATCH 758/869] Add foojay-resolver and update dependencies (#2212) --- app/build.gradle | 10 +++++----- gradle/wrapper/gradle-wrapper.jar | Bin 61608 -> 62076 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 7 ++++--- settings.gradle | 3 +++ 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 3b5fadb13..83be53fc5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -156,11 +156,11 @@ android { kapt { correctErrorTypes true } + kotlin { - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(11)) - } + jvmToolchain(11) } + play { defaultToAppBundles = false track = 'production' @@ -261,7 +261,7 @@ dependencies { playImplementation 'com.google.android.play:core-ktx:1.8.1' playImplementation 'com.google.android.gms:play-services-ads:22.0.0' - hmsImplementation 'com.huawei.hms:hianalytics:6.9.1.200' + hmsImplementation 'com.huawei.hms:hianalytics:6.10.0.300' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.0.300' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" @@ -275,7 +275,7 @@ dependencies { testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines" testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" - testImplementation 'org.robolectric:robolectric:4.10.2' + testImplementation 'org.robolectric:robolectric:4.10.3' testImplementation "androidx.test:runner:1.5.2" testImplementation "androidx.test.ext:junit:1.1.5" testImplementation "androidx.test:core:1.5.0" diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index ccebba7710deaf9f98673a68957ea02138b60d0a..c1962a79e29d3e0ab67b14947c167a862655af9b 100644 GIT binary patch delta 8979 zcmZ4SkonIO<_)vv@fPgYH8N#oV3^L$z@Riav0QAj!YcN96+^w`oW$bdsbMdJMZyLC z+mcX>2X!Q%+SqSK1AW}G=^H+^Danz6C=tQo)eyW$aGi`|D`@36;#wB^rg7yY{&3MV8XH8;a9xSNyjJYAVae-7 zGi42QH8&)_SXJliKf|x-<6@@QGt_gd>h{0nGSN$1H}llbWy$@!uWg#3TmSdSkDrc0 zw~pkrCC3{YTZX6GHEvTpepbb3-MnonN$0L^j(n-6c6jUhDN~h5hzj1sUXJf|v$;+nX${6KZPMYoOS?=RsC?wrx zv0Qjpwo#mdo243K?uBihGnZD+Qz`jguT{6bB)B8+l8ffL{;g}*^rW@r)Ly={VA8F| zcS=v~B;zl|p7J_%CGzgviFr3zUQgbacZTQn&8F9{R=#dKQ*>t@lY7GFNygLi(uLB4 z!VC^{LzDzfe}cacbkI4}Kf`J=LE}8GXsr z+~${7zj(>~O}wW+`dAz5{LNHftFZc^>kg?~7Z*Mhv{|)qsv}RX)T0~g7Yfh$7o;s! zW23iW?Tf5QQn$~|IWsdXYSKEHb(u2`mo|DVH(vaZ?eW|S&%>v8tb8cSw%+Ki>7I2D zdH1Y*sJmzF16khpk8FA0KW1i?&-%e3pZ%j{r~T1%F1za=>RI=Ne3;s{{$p=jeqnGz z^~LVbmbcB*pB7!asis-cGxzS!sQm%+*ZAx{urBno-i2Qcu}<6fuc*0cUi9bRrx54E zQ+MhA&P?I>60&FM)c4m<<@F!=acb>(kNS}342RYQaL8Xwe|l;j&+8MyO{KRp8~bkT zJ^tNGM9aTv>ptO0EZ>f5)%W%oxjwmNb$PL%r)2pZ>BimSCwIMUt;n4ad%$VeJm#J?BmroJ*Ia)H{HxLyS;DL%_7gfG|z7gr^D8zrrw;oRVVY( z6W;UFzlgXl@jicgTI#vLt~1MjEU}Az^wn`&P0iV9r=u5bY~p>jL?>D7>*r}+^>dwA z*Liw-|JwB8tm3z{xBBNlMF-byn>Ib&o%LB=`nhc~bE9HEe$g(vWBRhRbL#Az=$`@E zODhaF9hQkaE~Ui%F0JCK`xBYT7kuQUe=A&QdiASH?gYcywXIW+6>Yeb!WTB{)|a;; z3**&=RIa-2HVj$VcJ0}Zf0Z2-zin1MlKtFKe`NNe&mG5RuX-LR8}D%T{E^_+ye3l> z&M%kEB{odWQ0Z>A;9;@erubt1GmW_uSqmkvHlE#GzQE^by>qDC*75-Jtqi6|u0367 zx<-A%@ix|~mz`247ylFMdviYdvTlj6qk*AKMuf}bg5OGJ_nmZgqx1eZt=*D$`my4p z{8>5$;TP-mqb971JMv9P^`h&W6Upg;{>Ji~*15h>lHINDe0-J~^WX9f>0P`!3x1wg zsGrj+e8Z!lq3DlEt-`!x0&EkHNX8s62$FDQd;a38*QMLWZSM{HW#IgCCOSQFSAN)C{ zKSeWHK~_GgBdJHlZSq2KWA6{|*2uUm+_L%YmH#KC^-Lyan0&m%{a@p8wI8>1GTXG& z_m=t9^X~uNW4rwC=kLe$8Mq_NCb&kn&6H`4lzex(#LwNe?=xFb#D$F~5=@x)z3dYI zH!neA<+~5x?-bR%-dX>+%e^GA+d9u$;I$G@<>WAtN1}S)XNBo<$*lKW{Ut@T`R65W z<#)TXZy&lBx>CK#bb9+j4#_ZY^{bad?K7e_a=vyx5Und1!MXH??_#T4v-W$sx~=Y0 zkK6iPN~z+o^tz&_x9_a|d|+Z_QQ&=RleO)+x0kgfE-l$}*l|_5y!AZcdfUVqZyLX3 zuJTJ;oZGI&7j&@T&@PTSy6!nfOS~`VOMcqgaeMdcJyT6m3$A(=?h8tsp*vSnFLGu7 zRK5g9rZ6^!FLJ%k7Z$Os<>RrcYvM~hJyoxpKQ!p`?MB^{i|0Phn#>_Qz2N2%j&m~& zCYmQE+&EHt=fcJ#2BmKHYU>`X;^D8CIA(P&&b+Ke*0ATl{NimZ`Z%lR7k#<4G++W> zM%aY1j5C3L?;M^_-lUpg!rU|eMBau^<}c;>GhWv2n(*$ERB7c|^#`-RoL4aYGo_dJ zp;%~dW!Rz96W*Sw^FQCK=f}2Pwf#uBL8X&VT9tf6eW76a?~cpq!uPFjeeC;T9kjgj zvzJZjmYa8z%4S*Idp@b_vPAH1e|epz3vPdz1H73fkP_zeObf@Wk_-&5O&J&zpltxY z`Y6ttkgHeipP&7tHkmy|@xTT?50#WucOe&NA(a^{J`p?-sgw4oES@~GFpcTR*3y-W zGS*&;+EV)3X|41Y)5ajz?A6z{Wv#vYCM)}U?a!#KS^NI)e1F!Bt>xdV{yX3A#h$Z0 z{a@#M-}(K&m#TMUOx$+STXj#=EVfLky;n~9TCFdv7uf%4$@G|-Fu7gVy)NsVwaPsA z?GpE$M^D4@K3{sgCvFz6)%?dxnewK)%39@5v7B9U@6FPrdrDi*PhED|`*X$WJ-L&g z+04DKe*SamK8NRT<7OT8mE$|H*?U$V|MwHZ+G(3s7O!vr-O{8P@_Q+#SMkDg;xA=B z1)1_!tn4~`$z@4r{i~Xd?a6U{#fJ_z$K5+;z{Vl~GuU%~a;^FI0KdbKP%A0!>FK+){@iM+<@;USB zmA*fInAdw$BI4- zKRNa9pZ#9(BTjOszr>DswYp1(=YRUN{HO7zk9KqIUoW-Go7B&qa`k!2qh`4}kLT^B zbw>{OS5@rqk^69op+0Vwenq8Gecc#meEitAXIE#NnXS*?o#6cU z*jAr#tvDBrb26=CNk|GJUZZoHI?WlN&Rx^jzJt!g0daFD_+m&J!K@ zFQ17Ko-O^hFi}y;1cF*x> z=7$ZB)~boOE=$&9u>5{1!D2?l(UQ8Gg-@*Zsk}e??ax`Cb8Q~|CruWrN|@bdW8Zb= z#xVxAqc#)YIb;+oh)v1geR<{!{zGdn7r)D4WV};+@OLpB6IB zaewRHv_-q^XE&csy(zM()iUc%izy#V5BKae|2>9J750eMFO!J8c;s9C!dc!;7o|4L zdL;N~ddjQqi|rZs-%7|ZlvFb(FqV9o(OKBZ^w>(M@m+Tmb6~5|-1yf0JrfO@kK9z} zo0+ro`ExGKdq*5hmZq6cxT$Pssk*i3YeN2dN#p8US35kWIjROHEmr5&v}byG(7XXY0<~OAeiT&i>j@b%FKfLa)uPcP#BwjT5Rrbc3b!MQ>xo z{FxiJ#7;0^U*wq*8S6UZlC6YO#R-ESa~2;xlj`0kykqy2jWf37u3p$#S1kUkxx;&J zeb){%hC63Bh#f!s?qGz0VaVmK6I~l_NxU(h8Yi~2_hHgoT2|gE1TTY02 zu*~@6aPO9EXAY11YW8NugWc!sul`K8`pYPFKk-=8Ugw!B^7>tbxGq?hOxKJ%cOuRG z%FdaZVFy){o864>$#T1`m3a6jFyTW~$u&2&GbZ)H+eAes?$D^sycuD3Z?f+UOMN*> z7M&1}v*zWlXWzBlTzqDR_zOQCHm6sy@*A%3^|q~%;5hbY=i51*QPU%Yg4J_Q&s(** zkJqZH_Mtx3OooWyzM)3nq zEmsjx-5D|eqW;am6{~m*Et~U#D+&(yEG=xmbzk$y#s`y9WaisU6SQ8hcgi;__2|Zk z=@s8+o!igwY?t!Ig*^oaLSm;G`#cfznB&atG?$N6$=xI5HLphL`PdB!jKT5ej!e^; z^KZJL)9tMevsy3dcolDSso&8t*F^k(vzGJitr4?SGvlUl>4&Nx?4IzkY{i4*$7kk+ z>t5zLw_INGOX#mzj`^3O*=$ekXZbsM)t=Ogui0#U`X#pX+ht#1{$klIx1^otpXmRn z=l4&1-p=;FuRi~S{EzP%llqb;wK~1)XZzpzpC``P)$oV@pYWCm$@*qL^4}QOznIQa z7S|f{r~Kb)$M=_3INn#6=~^bY+sycdYn7&@^TXw}26H(0W~`Jfn4g?toN_*8kwNvJ znU%-%3vcFsu>Udt(jWOdx8!?%9~S)I=b&-pF}rw{*H(_d>zC{?D|oQ;YT?v(y#Kbp zc*i^OSzw)TNYV5}4G|;Jw9=%`89xH+PCuys-@vqAS55C+;9A8*a~C#e+Xwm&!iyuw+#o-rpXSAxJ1hNP|uMzgJLOv_l&iDY)PEXr<3Kk zOuMqr=W4yFzQBs7(z6s>j-AZ04oXJFDsXRto%2xLo!_H5yvzRmc#!XDm0GTB$aL5 zf70)-QmOu?iTCfc{*jmZr0>HW$(y_M?B;#1ol2x{@#HRBSbz4?g6%hNg)I9nJDIck zy~VbUAMF3E`FL{s%DUE_HQf^QHe^}pd);uVH#x88y>!|HAuHw`$hT z*;&7NO4PwT(XW3d-iulN=i0}NvdkSOcRC}c6`ylR;HbEp_-nV1@f&{@wndL5|IIz< zQeT=G!Fv5hgZ)K$j|GN*>pvIC9ewt5@kS0YwYEP3i5}lJCHYR3&0h7k@WrK1c|VHF zRn)qgw^pZg+6ypeOU{_`_RZ6O_aaglR(T#_dz{3wT4#;e!9O!qH^@&-TD#vQPRg(8 zw!XTsYb=Uye!@nDFuX>ZYPI4eLTBqLzlGW?YT(z2qBeu%`bglXFo0KgnB6 zg|k|tb{-67d3A4H!VSJ{E0X&{cc(ryjr_BF*Kc+ujUzIrr|X$+an&lz?B>nwY&)OI zlo;Qqb+9k;R?%CNnfsNx&xeF|ZcwxmSiT~4R^^{Z0oPygw$vMMt%{A#&ESn%ywEf=+hEUSEe+ncI=O2X znr6QaV0*r1BhS3$b8|Cv7u^fv%$r;!9=7XRTE^?GRnE5+e$B5dSr9)zRK@RPPs&W^ zMZ1e#y5F+tKWJ^cwouggto6Ylj;kd_5p!6UK3g<-|A~`(W7h?#YfI<8>31!Qa)0;n zX8Pom`m$izvz(fN%0=y!bDFc$j83fy;6AQkc{)#0)Zp@wnAFu)jte#wO*J)N9c<(G z{^lX^DQE3}Jv;M1&MRftgOqoF|E)f~bc)UdtWvnpf0i>-DC-r+P|fs%5_4or616_oxec z9y8g#L2LQxRWjEZl2HD11x>gx8wPw2=oj$pfra67?w$Hch%c_${{+hloEKqz$E+IjuI z?%PVd<>c6HWwplP@w<)8vp47LyYnp7#pq<*CvJUFjq!{u&Z~J`zvQkumz&Dr zxXY)d`q({eCGw>@)Y_h`ib_rVDt%O9(``SNXQeM6zcI2rH*a2WuzFoi`LX8d4xW4~ z>U(x*UViT7wpw{2i<6VWt*)xA%8kOUEApE{j_NiZG~ilsW0!*t@2ACiXN{Vc>YghS zOg}o~rn+9&?L87pqyMa6m~iFEwKerRR|Pm*FXYA^eL6Eq*4lU4(;Z4@CUYM(y=I!I zxS@Cp$IpWm$(#PP>oMCNNMtm*CBWf+XvI0_Y36=frwd;%HA;;6qTZ9f%D%RB`FkD~ z;p`*b$FPR{TbdTdzwT=+du6pEK~EWOFu5KI$T83 zr~cZEo2l;wUj1a$=E&L5FW|F$Ucfw$kI9lgo3_Qw$b04S_ug#7faNlKR5uA*8Y?Wc z+Ufpm&YbFsN$Som-+lH;{bJt8$8g@|PLM)zn@FEmkip4ik53;8|918H9%pj6 zW-?z;-F9kvM^1g(b%(5-m(nZ*CUNahR=GZnbyCcv`mYK1?VIYIsWg{OF6@HOs-?jz`t~(UEb3OPohYXq zlz&KgwfRG@;QT|=nfD$xX_8x5C;wFQl(%-<$qe29N>3lB#zbE==v`mBXl}r3r{%|= zTluigeQG!Td}O4dQ|sZ7FDjz%{WqF(Iti|Is#fb=UbSI|l#aR9wXpg~=_L`-UhFRg zW)xgx`XwaG*80(;f1i*0fm<#!9(q=B9r9V4`9*Zk&KH?$3X}dNo%y3xdp2o~yhFU4 zTi#d8_ROTUr#GH?8@H_ar)Txa;79kocNNQ9%AU4Sowqk2)AcEbuikp8g_=({9gt9q zZ#{476nt9yf+g?j?}5o{m%EhTuXk^?yS_OkK~80E;Oip2;`qh=(_YM95X^V^%(=rR ztBYIyxNdIGjlqx%W7)R@RP^ZTW*a-^Ss6DAM-|eX8sOa>Kc|mZQE1dk~abQN!;odyC=?LKM}j2 z!nT(A%bf?GqJF70th>D3pqRDt-qJ#=)|sDW(?8u#`Ji!Dsozi4O7yjf|5p7KoJXvD z>!aD0?PpC%lltIVb@CIlUDMek4gMUTZt-2-v!p()(MmofDrDM~*MFj(Cu+T0BeSfi znk!Cw)3OkzWlVk&KMg_~gG!f2Y)@+uyRxhHU1C(k>eI;+{qM)W-gwW(J!|fPvbakN zydBoOQqUEht+$zj|MI6ZlFJu{U)ZczP${^H{)lbrL;vdY!9Nj-?6;!v4+rLaauL~+MHZY{pl8&8+?CMCy8 zerjE!t6Ln_sk+~s%i7YoeQ9{l&)Q#&vTkuKhBv>(Jk8kpyZ*(TZ9YfWZmD@%vhk*t z$<%MZH(uotm@`poja88Y=L0t7(rs4hOw(QDUGK-f*vq`*uf(B8akpH`FNyuC(9)=S zD1QDTzv!*=zM4<(ObMT}y2deh)d}Ubk;`}ePw*7lTq;pDwJzoOyZojemR+S!Ie62z zg~=_dTDEPKbBk1szV2b~dgkW1LbDB0xi{v_n-(_V)|S-!FE+DZv3to;&ws00kmY@| z@qPd736--Z=1w|v#_hz9ja>Z?6jjeysWN?1W_suUV)=s~=`T!!Z0)xnmG-}7{pis8 zmtVTKPf6LowD7L zb>i9^VidDiU6{AmwQRGGg?sC&+KM`{8NXyIY6avz3K;w|$aOx(qmaI=I6BZs%rqc3ufqB~)S4mDhu3Ub$?A4BFR+~k$yQ27lvQH;wy>`)yyZ9gghpRwV?}>mR3b&-Re2ivVg-}#DH<-Zh@24Tyv_}?o{ZB1h9$fxP6P^leqGz zQf(gZf%@-J9rb)h&&-~jse97VEqJwkg1lYBzSj(?fp04h)F@v0ZhfSZeTCoR`cZ=gz{h<{H{;B;ko}yD%A^W%WAJi z|5Cp|=~#rvFAa{}Ay0lx_xtbmrlR?U@2p*H3t26fGTgU1Tifc+k zJ?FkOEB7In^`C4Dcj-PV5$--$YUek1Zr`cC<}F_pa&Pxf{2Z|NUh~55vbIkwT%Ko7 zYyUNe{bf_SVdn3^$temO?4M3wzDnn-P|H8FBejy8-$fNZ?fx)dUV-bsKu7J=ZHKj8 z=X&e#1$(^RY8!m-A9HC|{en$%@3r+?Z737lyST>k?OLO6#nMNGPkc1{`EYIU)U>XR zKVmfw7G0mdabin%+p6@Fl?>;uw`TbkT-h7G@96EWsuI<3SNG+?k8<6YuU@tI^4sFB zIa~EKQ&n8&RjIIUDPr+H^v|z~_euQnnjbe^euQ#9Fcdv7gPmFbU@_~N*?Mp4rGhRg zeOqZYQ{>OQprvlis~W2v?dSU?^XB`l`L@Y5}?1PsNBwdWCoWH`ksM`mXe(wsuBS^~n{V zUbeq*>fILexb6DiEn2ycvNpIoEaltr^l(5utHEIxiFB@%kIoIE347&y)*t@OR<_({ zFVj9{xrM90%n5Pco%@9;h1LARI!2@JoW<(CxnEU$W4+eZh=f>GyA@_!csr@fR%q4D zX5Y+b(od2OE<6_Z=c&5!q%>96Mau;b>IvOm{UcVyX+d5p%d$ql?~g^^m58`9G%wO{ zV9<%VQ*Y>G7_Xm+gKbpzFJm!wt~NWGczURHTa@KwL|Oz|z&DWU${*4eh_FQm+m zE9rb{Z0CR6Ahyfq+b)fto3_=O+*PV(i{s9I=n>9ydFrK)|F*44KQVu%sojKi%xl&Z zGF+OnKx3EOI@R;HUld8th%Vfxz_HG`fWLonlAOP4j6-+*&IwsNIjZ#B3}$%Tt`v$r zGWn&~w~JffEPq`ob@#yKmtuSFA2WVYHqW#Cqt34W^vlaD`D$CAYel;ye$@>*+orX6 zTd-#TVLq!x4|wJ=tf*=@S*CX&&(Gz?=A9hBcfMddGxgLV&+UhoolmUy4EA4M@L@|) ze%teR&xM}ysea+G*mJ#pTa|ES!h7A9LQ?+^S@FKGk(@fcFG^luW~W_e&ABt4E~^8V zt-1X2Q(@hLM6M%Slb+nX;j(Alju)$%OTIpTIV*A3wxx?+G#@URE90?t^Q`b?A8bTE zquzUGQS%iObFL!(i}iAzTFds_RatuHtdM5mH-Y6pE;#OT?UTy; z`NKe|>&KC`4*x&5zckx%&fTi^o#3w87Q3j`)5A|Lv99^WQR`Ea{N7Hgy}5nQ!v`Dl zzwo`OSS}(|`RUPV=hoLO=cb4@y)>)mshUSw^vq>V=Eq^)SY}c`ss~?dd)i9%kt!(Dv?@`7iZ`jk1mD`MU%{mzw7* zn&&Tdy61NJXU&nn+!=cL=RIC8)$H_UytH4t)4S5pL^-o|+NwD+8k^KV3prewbVcE9 zZqedH2Qz0}_7ShB(5%$qH=Qp=b5?5LMWX(d^6P{Ov&dgr)$~W+9)2hzx_e5EL+p*N{@txn13y;mcbw2m~@{cpMwqJdGMEhC61BP&>?5c?_YyWe|JrA1` z|M{Q%X4d_EdR&NgB9rI7b(4Y5WwJ3a=rSO{l19cQlNI-if>x1CmVT!!gEF^-P{**O z@z^q`Lf*+W@3fe19+<4SUvcuucUnxXhbM15tTg%kL5a!w@3ojRk51lrSYz_c2Lh8{ zpAz7K8M35t;~B6Ce5Yl>lE2R*OCG!kmaM3h{As9?jq|VRTyHsb2j8w6u<< z33r*@V~=`W+kI_u*523h--3_+`hE6|+V0t^s?R>3tp0rO_uc*P-+#Y1@ArFWe&&eQ z`g4xlI&wiaduFiDZa1mt@9POmQIVM_cxXe*VxyFlio%}#dAtu*ZT3$ok2`kwbfv|) z!?{M2Dq4H;_DpKO{rmynp053Ix{pnFO!j;?sn9%5`q1kcU%o2tX-wT4cx?8Ew0o7_ zs_za)&-{?Pr>0spslr|LQH46cvf`c7x4)mP5WlCpc>Y?CN47S5!Vmp+s;IB$kmq>V ztXSjWCTw?xT|O~WU8N@ESkxW;$?fu!KNNDRtJXLz_|=i6W4dNW)|>0W=C9h+47L{s z*e-G_)jsu_muaex@b$}nyYsluoL`@_*Z%Q~X*-Xbsm=5`+xhP0JB4MZXXvEOZ2#(V zENrGM2)x!K+pVIjzk9-zmXm%)rk%?1a~>+a$UeB_ zQs3O$eaE(h=p8Q#+S+b%TY0^asqVbI?QOn?gB~B*F@M9fF#hbfOCy41w6Cs|y~uM& z*)smE=kj9ZTb^4~c5JgKIUe-$!`0~*@^tu)r#)Kxs<8gbjSGCHPd9n|n9>p~B&U$A z?(6Sup<7UCHzUuj{2PCk;WgXk?*21YP2T8vciKx9RT+jQd9sT<1Q zBP6YtRXUz}zO3^khso_TGZL3_dRd#KAL#P_v89RKa8-hIUdRT)*&;d59#-yfJSdhE zdZDe1_4Y*7$ktUN^*xEx4;%{W$UFPOEoQT5&*Pq^_JV1jZl&;Q{@_`^{qbflxnnI& z=Rba^y!V@F---v`KQ=Y3f3R9;-?9gAEtGSA8cRq zzuZG0?Ut!?@r=12+y%G4NmhAL_$Tg=yvQFxe~ll)|JFRv|Di27ol#A3)!9=LAFD1G zy)u5bwrbTpt`+w-W(fH*EzI-vjZV|{{3~t!r~Hre!O1fOR?O~-D?e89Wr>;Qlw?=E zH%}Dw=dFzJtkz6pSvN<}gz0*0QfK|M$@8*fuWai+-8k#q6}R@qfsZ%6shJYf&9x|` zyl`pDtr=@q_C@bz(oMC_y(wq8*8Zo`9hKEWvo?FDuHS1lA>-~+wXK)5=Ir8)*yfqL z#4u>tTff_<4$hHq?t7Zc86&KD{hRmi@}=|g&NH7io&GB+f>UDCZzJ9W-S&pOSN z@?FN;S}OLb-Pvv2`W>-PW;*}N%-VF|)$7R5%ObP&+K&7&vf}X6jFW76&iZtM6DQxX zoeKku#HR+@m&SY6gumTbx~o_DD1S@p()`7HqF2v1tzYo8_)ey(kX}Uhfw{XA(wNGB zEIOni+%|QFQ7-T7ZH3>Kl|EtK^iZibPb*>Jk0;NsstRhe@u@4iZ1w-T@AjhUsh*Ky zP9<+Q&yGE0^Y^2`C3Wfe+$A4u?hACfNd;Vvy%qQ>x9;-XPi$+FnWfT`5B&S|c6Utq zmtUR_t5-bmG?$I5e|>Y>Y`ufioQ}kMoj$Fdk;p3j?|u3f?Jk*Hj+?Hqh(2R!?UwHXUe&}^T}5#UzR@6`tt71I^|z4>aFMb z-;w>YRbF&L#=dQdKQ&wqcLj!+nDiaGF7U1Y(0u{E9;FKce0@qc4A|;lGA>$kfIEXn z$ll{JcjlU>elW|EuMIYXn$jmA|zJ3%0xV4xW>mdh`WEq3`TQFd8)hq!;@{Q8IvyNz@`_= z$Zh`)dBZ}!!jBxbYtJV;*zm>LbUJ?2=jlvO2w_Rs`NNK9riq@C?rzSr>YQJzS6up| zQg8X~Quu@=vI=v5b3AVO5F_hyMEJ$x;Ksy4`Kvb>AKB`?J9su^uAPnQs{(GmNmD*Y zy!GOH;CiHfsleXyvW`UyFZmfi;l81+sIu^P=9#s-9Ag@kRldX@VB$37{J8w0na0AG z_jmlXk8lsU&~etki7z~Vp@8ZhA(tz=FmP}%FfceWG)mfk;jCt2V31^EV9*7(NZ!8} zuAe$3)?e6Br0ss@_Sju_oz~v}5xUePnk$-XmcUU7&ki+L{j6CJ?rz{oP0T!*xB8!Q z{*DVSi!L7V*8dZD%y{!{elK1V-}7f~&i!ur{^rcP_Vx9Dm;w$aGZ%`a&hQV=IJrUL zva4*WrJkh6f^F$nq$Gd3*1?r2>^K9De6L@y)F|x9te=#6edtiU`~6#5 zb^MLowwM2tuc%#}xYS(BQ9q7F@?}GsWM=y@mZwknN}M#K8{Lc#{OJ54rd`+kFLI?` zaL;M$9I^0>Jhs&Zp@uv+pJ+Hu>{Sr)6J#$WA?wt;KSGX+6)xC&km|9k5*`^?KeW zX}Wjn{Tm_-4E4H`?=4fWkL0Wgxq8)Jee<4^A8s%)ww(>+Oi_Jvnq{FP3ujwH%hJYO zdsOsKp3o_L#;bDaR@b$?Ti0&Yy>@q7$;P!SR1#KYY~2x^UB324RCe_1YoWQJ`~L6D zPc!nAWBO<3@vip0Ui#np#picd{%Nj$ufE9g&${_#pCU53mP)Fht;ke+vY4~p!#FTb z{pZRS^=zA?u7A4O|2%oV(x`lriKG4eGs``SSIu8uIC1{E=Pxgvxv=AgfAUV3Ytyr0 zo;@zIzk60x>in0>F6+KrwzctgOFMm8rgGNmk2dzKm7C^2|IB26^@CGotjgJ>OsB&y z6vBU2bRJuN@=K6??WE}^9~`fV(fwOwfBi!}m;CipXS^oZ(^nB;xG?Dp=0#hd4!D`uMr z+UHK2xMF_&wofVY$Cvbb3x6tBpa1@G%lyeXPm8;@cY9ga+)=IF&Qa2M%=+&Y#jv*z z{P`Ad{~3HKX2r+le?^|2|M64lo~nQPsaM^m`&=HM|D~xZEmX7e%`P94eRurdcj#L! zscc_=;fGi6`lL&*lRQs%xMW$KnU?#?G)*l_$;~KJZ6=d;R(O3_{+jy*=el+s<9+>l zTK=7Y?b{yCDZMHkW+-}PetM3%ma^NLy_+t2wIsg{^*NmV$Vopf*GNh8^6_xvO68B= z%8U=1thAe#u|CINVy)ojBKJct-k1na{hB3YUAy+9>_(3}8L6{gZP}3LS{1jWL@LbP z)TPJFz2%dppY*k->kcmKtWOK~R+-9^Zyw&JoL1`HJ=15MVN%(uC=$LA00 z&dBj}O}Ho1=&vc~vGj;$mBgbW*$r3cWYuR2M?A}oJ@@35!EOV&w*1>7hxceSrF8i! zWNup`l(>3#5$^@gjCCB_XM1vQiOFSZKBRMfS48?LCiTl5URzAd5C3jUHF(;*N0;$( zi(-&&_@s`-(z=UNZ%Zu>e(3W!+vH!9TeZnQrOP|srYYUIHaGj=*N+wxN{=b5ZP$9+ zG2O(ke)-d9Y8?i4;yl`CQoNP!Wd6Q!SIW=fcA#198F#JMhfnd;&Hl2gLZnUF+#q3> zwXvhH%$CBF4jZS3W&T-OXL|YJ>LXrV#m~k0L*LBkQr@(6#ak1LgBcrx+*}-0?IrzH zqkKdK4xd@)xA~}1^wl2yq-3F0Y;!+OS-s2sLeo;^?R@nyt1c!dT$0G^mkiTle73pI zF}uT*Vak%st7VMU(q1lA2M;BKJI{$eYA>zMevESqD#7hN5#TISJJiB&vd2e%A zTe0b&z=NmTX9)&h$~|<@E@Fjh@T-c(DHWf$mN?99;Y{ei`D%)KOv7%s^#_iwyuq57 zQ8Oud&eN&`$;Z~XlwadKb>K!+r%sr*@hcX$XB&cN@ujC+Zaw1LcJJ1l`a;i1PNq5v z((|3Sn@#JQbyMekkkGDH3$vIBCucow=bGfVzIcwOuBP>@7ee#mul=xc*sFT)=G@6I zXQX|LRx@X|(oS&}O)J+l*roJ{Y5&3)l8W1UgpN-u6NT)+c{&OKvdDAmZJLC8+g{T9$!{^CTHrxtpORKb2!%O{+%s;@|kge z^5@R9S9{j&{2L?2J$;q1n^T?zpZDZhwy#^R9?h#?Y^8W<Z9RD(O zuG|9l(qGJbmt0n?a#?=<(o(BfpXo2Ic>gZl)wQ4Ln0m<9Q_I%BDq)RXc{#OY=dt+9 zmFD&F>px%r$<~`cW%}%reKYef{tVm4YWkvmLG3ny-M-In|CA0;40l%PJQjLYRAWxb zKec-9nM&nm>K%@ZyFSVN+jj2$!}qWL*#7G}SN`Gq#0_&!OZ3(+_{{&)Z|?U`u~zrb ze)rhy|6FdLAubAB?EK!?ozq)}8{Lj3c`?>NGyK z)Uzx|+hA53#=f><1FyQ{g*S&b%Dv<}cW&M5d7WBI<9BR5`#dq?`QI9c=hZv?pUZ!Y z^+}fv?J#+-5^<#YV!vABx$htUO37dO$yhfb^Xo=avFnnYinFfTzSzJ$rTz^6%M$6k z>pma;BRrS?;F3EVJx|pyE9gG;=wQX{%hwWSGd|xoFGfVW%arj>U-lxm@(qV?g2RQpW0ZJMUJZvZ@4hypk&{DKI5jjofc+McO73`)ZLTQ5w=9p z%Gj2_nQdtkSKH(OYsG8Lr=wH7FL*d?DUoB@C9?a=w}faVxz^e>E?S!N>Nj-noVh5% zV7mj~qJ7+~-IH85`Z)1=m1+flE4VgY_obEbl08gD2eU$T_-0=CDY%dEYoPtUYs-_u zEV9yGOK|RT4-Ea1e*cxrp%>Fc)_hL>vglm@XJIRQO@IG`jU^FDhwjVtEqxrpVbiPi zC`0rHLrMBREgN0gixysL*AM;tT>7tGGl@53-o>Bkf7YC{7eCNnypCrk6JPsw+vT72 ze@u6J5q{}URYTCtQ-RO#cevh3^6%VqKW!TC8ivw3ziGaIKPUw}eqQ=dcK?FUi6`ei zZhtmo3#V81qm>@3yJV)E>^SH%SLV$d{VC14A6+ywem-~YtzZ9H{)l0ejiqs0SK#C6 zCI9M`_pkXlzof49|C-P7Ki1ES`Bj*!E;rexG3XcN`3B@%ZIM7 z-|jn?=M{Gr$HukJx^FbEsyIhK5 zHa6=%bvm+-edFff(qbby&&Xv*elO77S^qJq?53oByx#t>Tj5cQ84oIEwM^DFGSz#N zxrHfkx!mu6f;w)FGo+No`WL$O-ni7GUAChl@Ztm!@9rHt`(Eu&y;LTV$o-X_k6CoK z<;^g*TaE@|n_^=D4aToH70jom)x%)1l*On*3W_pyoX`kyDNN$RFM`nFHZ zaA}$0aZO9=j$o*YNZLW034RMKu4gjqUv(>!IBEK^r9M*HaPbGe2;T0Qv0=YTH_YBt z>C$}ljr7So#W5Gd`FbP;HF>3#i&D+CGPlf}>2&el)84$z`L~$=ZjdQz4x5?1f>FHG z{qX*D_H$~J{g%xQadMI4y0HAE@sSe2d!I7IMNb@MoyYw|%=zo|i%(bWZ(8SK&z8M@ z(dvtJeEIVkSqD=T1#7NXX>17UUs=A`1OXLYro2mLaNW$#S+8~$STf=BIP z@y%0XZbWzp(!Q_oMF zu3eS!O74<#TqQ>Z|FrKHJQd63S!Y+hd@|wq)&tQmcz3+t-<#IEOqsEKVfnk(-!G?3 z5W7(IBILWSiOvH_)79>}sCH7?h2<;e=pES?Yw+yojji2T7uPo>OFy`EwjkSB<~HY! zQsx42#;ov!H&S2cui#bFUGVY??=Q6;VTQB$7v*Mb5{9{jRSYqzlR(-Yz5$*Yn+OnJ?4_KdPa$b~)gwd4;jifk4; zdE|}tRGvrOFQ?S^e~WtYH#MV1mX+~zki({~eSB;47i?{Kw3FeXsaL#e;Mz=zY57&dcY4!bC54yA zwHkRBw|)_rr}FYj%GUJ@+e4G4sB>Pj`T9jz^i286D<}UmAB-uNU*vvCO3qP#;_hAj zV#?Q?CiSc;yDU`VEw$k59j?NMf4S@5291%#|M1t;-n}Ke z=I#wwtXBSf_3+=>HDv^sLS5GiYcGBZun|-EEqbs<&MG>g zV_o~XmkW!(JI!`lvgUN=;#cckUu{U-(RyrQt;n@Ho^RDm&!0|?_-LZ=Bbe)=@rD=Y zH++j^Ijx@|{a0xD`}G_D8)*C$d-=|?;;F$3+x~#RvQA%EAG~s9`1QTvmG`N|a~oc# z&Em{DzhZ(m)86!hYn$trT{zF^CvUNq{cSvxuA)_Q9qTW-fNz)b4;{9p*p*r-SL~jm5s)aBr7J*kP1$Cv;5ia6-(QrUZ3VVBbT*Xxti(s)%r>Hj?coU ze0%&xH*;s{i@(*PODaEIpKkJyeZgv#O{$S2!Y5m%4v~xze#m;-nnR@>n3cklW(dOss zpj~Vg0`Kc|cJAu8dh&(e{DF?W|Agv>Z6^hjP6f9tEDI5O{9{=WXV;CoF6X@y!sDza z`m3m&WB(=peld%a`c07!-zHu4UO6Fb)vNZmjBBTtdE}J5UHDLOeihdbzIT?NLJQtZ zGg6ALmt*?ru`BPvp5CMh+H5Xf^ZS|W#DB%ExKfr{e6{L{%%cB^-)f&|%~|Fl<~qln z`=;yZ8@^lLEDU|KSoh6xvug`47IZE2U-ymhL#{IzYTW&O1$!U%%Qai~H3H^9NGAca#dw-xoc%{+!g`tY7Z+zdUx&`nR%RUjL)V@n7-y{AG{x&;LtpLRC`w zJ9-no>-{yab-B}F*qC`S!`y3Uo6(K3qRUL*uV=bcwrV}lyHWh4w4qQ-$UpqQ;qt5? zJr#Zb1s7!&oV1aAdc0m_*0JDXzK@I69CO|9kAKQBSB{J3R~7Tbef6Xoi+|RJdfZ)F za=zNXYi{lPK*jIewx`_X4-}W>TuqujM=mN;vM)Eje&w4`9j~bzj@6uOIf5oKg4-^d z?vN`zq9MQg#$2_?&bVhblM3c?zWT;ATfwaQ#_J6?t7~SP9yohX#w%T+e*LPGbINaX z9-n>nX!dI+tDg;Pm}S4qs(YkMJjhc{lI77W_x8D9P*xbCDre7*(q0~%sTI&{>*ao&jNL>*)lC3G>$B5HoP(OTVRi4Wk=qc=Y~HV z1eWanaBzdLJWt(J*C+SC+VCz<`uCraar4aseR^DowFHw3-@D0xM)KhU`3wlKq)~JE zwd5h*Fm1ib3bS? zZ9F)6;bE)E(ys(2mmd}2f$3h-IQ8)4g@=_U2YxhQx_b(&WyVK$rnl!OKl~WU Date: Mon, 22 May 2023 17:09:57 +0200 Subject: [PATCH 759/869] Version 2.0.4 --- app/build.gradle | 8 ++++---- app/src/main/play/release-notes/pl-PL/default.txt | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 83be53fc5..729488737 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,8 +23,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 33 - versionCode 125 - versionName "2.0.3" + versionCode 126 + versionName "2.0.4" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" @@ -166,7 +166,7 @@ play { track = 'production' releaseStatus = com.github.triplet.gradle.androidpublisher.ReleaseStatus.IN_PROGRESS userFraction = 0.50d - updatePriority = 3 + updatePriority = 1 enabled.set(false) } @@ -196,7 +196,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.0.3' + implementation 'io.github.wulkanowy:sdk:2.0.4' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index aee30290a..858b44d42 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,4 +1,4 @@ -Wersja 2.0.3 +Wersja 2.0.4 — zaktualizowaliśmy wygląd aplikacji na (częściowo) zgodny z wytycznymi Material 3 — dodaliśmy możliwość zmiany kolejności pozycji w menu dolnym From aca88b57e0b005df6bf0f05efbe66dc066479451 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Tue, 23 May 2023 02:16:42 +0200 Subject: [PATCH 760/869] Add r8 rules for HMS SDK (#2217) --- app/proguard-rules.pro | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index fd9482613..ac7d1b029 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,5 +1,6 @@ # General -dontobfuscate +-ignorewarnings #Config for wulkanowy @@ -24,3 +25,13 @@ #Config for Material Components -keep class com.google.android.material.tabs.** { *; } + +#Config for HMS SDK +-keepattributes *Annotation* +-keepattributes Exceptions +-keepattributes InnerClasses +-keepattributes Signature +-keep class com.huawei.agconnect.**{*;} +-keep class com.huawei.hianalytics.**{*;} +-keep class com.huawei.updatesdk.**{*;} +-keep class com.huawei.hms.**{*;} From 4c1fe233c7d4fa2ef4d4cd088fbfecc0189ae4e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Tue, 23 May 2023 02:37:38 +0200 Subject: [PATCH 761/869] Version 2.0.5 --- app/build.gradle | 6 +++--- app/src/main/play/release-notes/pl-PL/default.txt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 729488737..b1116c588 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,8 +23,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 33 - versionCode 126 - versionName "2.0.4" + versionCode 127 + versionName "2.0.5" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" @@ -196,7 +196,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.0.4' + implementation 'io.github.wulkanowy:sdk:2.0.5' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index 858b44d42..720952730 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,4 +1,4 @@ -Wersja 2.0.4 +Wersja 2.0.5 — zaktualizowaliśmy wygląd aplikacji na (częściowo) zgodny z wytycznymi Material 3 — dodaliśmy możliwość zmiany kolejności pozycji w menu dolnym From c1706144615b9e024f3e8d1a7efe47d60dd1aa35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Tue, 23 May 2023 14:09:48 +0200 Subject: [PATCH 762/869] Add R8 rule for Wulkanowy SDK (#2220) --- app/proguard-rules.pro | 5 +++++ gradle.properties | 29 +++++++---------------------- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index ac7d1b029..0fd49f6ac 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -26,6 +26,7 @@ #Config for Material Components -keep class com.google.android.material.tabs.** { *; } + #Config for HMS SDK -keepattributes *Annotation* -keepattributes Exceptions @@ -35,3 +36,7 @@ -keep class com.huawei.hianalytics.**{*;} -keep class com.huawei.updatesdk.**{*;} -keep class com.huawei.hms.**{*;} + + +#Config for Wulkanowy SDK +-keep,allowobfuscation,allowshrinking class retrofit2.Response diff --git a/gradle.properties b/gradle.properties index 5a8099a12..4c54d414a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,28 +1,13 @@ -# Project-wide Gradle settings. -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. -# For more details on how to configure your build environment visit -# 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=-Xmx1536m # -android.enableJetifier=true -android.useAndroidX=true -android.defaults.buildfeatures.buildconfig=true -android.nonTransitiveRClass=false -android.nonFinalResIds=false -# -kotlin.code.style=official -kapt.use.worker.api=true kapt.include.compile.classpath=false +kotlin.code.style=official +# +android.useAndroidX=true +android.enableJetifier=true +android.nonTransitiveRClass=false +android.defaults.buildfeatures.buildconfig=true # # https://developer.huawei.com/consumer/en/doc/development/AppGallery-connect-Guides/agc-common-faq-0000001063210244#section17273113244910 apmsInstrumentationEnabled=false -# -# 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 + From 092e86b621459ec4cb0229a6aa65f43e623ea85f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Tue, 23 May 2023 16:26:31 +0200 Subject: [PATCH 763/869] Version 2.0.6 --- app/build.gradle | 10 +++++----- app/src/main/play/release-notes/pl-PL/default.txt | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index b1116c588..5cebcb31b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,8 +23,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 33 - versionCode 127 - versionName "2.0.5" + versionCode 128 + versionName "2.0.6" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" @@ -165,8 +165,8 @@ play { defaultToAppBundles = false track = 'production' releaseStatus = com.github.triplet.gradle.androidpublisher.ReleaseStatus.IN_PROGRESS - userFraction = 0.50d - updatePriority = 1 + userFraction = 0.25d + updatePriority = 4 enabled.set(false) } @@ -196,7 +196,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.0.5' + implementation 'io.github.wulkanowy:sdk:2.0.6' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index 720952730..df69a331d 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,4 +1,4 @@ -Wersja 2.0.5 +Wersja 2.0.6 — zaktualizowaliśmy wygląd aplikacji na (częściowo) zgodny z wytycznymi Material 3 — dodaliśmy możliwość zmiany kolejności pozycji w menu dolnym From e7733bfa2a72362e8736dc102383cce9d241cf6a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 May 2023 21:31:38 +0000 Subject: [PATCH 764/869] Bump com.google.android.gms:play-services-ads from 22.0.0 to 22.1.0 (#2216) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 5cebcb31b..40ea252c1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -259,7 +259,7 @@ dependencies { playImplementation 'com.google.firebase:firebase-config-ktx' playImplementation 'com.google.android.play:core:1.10.3' playImplementation 'com.google.android.play:core-ktx:1.8.1' - playImplementation 'com.google.android.gms:play-services-ads:22.0.0' + playImplementation 'com.google.android.gms:play-services-ads:22.1.0' hmsImplementation 'com.huawei.hms:hianalytics:6.10.0.300' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.0.300' From 19ed12146686d95d15101c58429403f8245425ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 May 2023 21:32:28 +0000 Subject: [PATCH 765/869] Bump io.coil-kt:coil from 2.3.0 to 2.4.0 (#2215) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 40ea252c1..3b7ee510d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -246,7 +246,7 @@ dependencies { implementation "at.favre.lib:slf4j-timber:1.0.1" implementation 'com.github.bastienpaulfr:Treessence:1.0.5' implementation "com.mikepenz:aboutlibraries-core:$about_libraries" - implementation "io.coil-kt:coil:2.3.0" + implementation "io.coil-kt:coil:2.4.0" implementation "io.github.wulkanowy:AppKillerManager:3.0.1" implementation 'me.xdrop:fuzzywuzzy:1.4.0' implementation 'com.fredporciuncula:flow-preferences:1.9.1' From 3096fa15384ce6d8083d1aad5143be1ee1fed1ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 May 2023 21:50:17 +0000 Subject: [PATCH 766/869] Bump about_libraries from 10.6.3 to 10.7.0 (#2214) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index eac46b05c..1a70f82af 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { kotlin_version = '1.8.21' - about_libraries = '10.6.3' + about_libraries = '10.7.0' hilt_version = "2.46.1" } repositories { From 70333737cf218734850ea5d4df16b439f75e789b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 May 2023 15:16:42 +0000 Subject: [PATCH 767/869] Bump androidx.activity:activity-ktx from 1.7.1 to 1.7.2 (#2226) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 3b7ee510d..e72beed28 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -205,7 +205,7 @@ dependencies { implementation "androidx.core:core-ktx:1.10.1" implementation 'androidx.core:core-splashscreen:1.0.1' - implementation "androidx.activity:activity-ktx:1.7.1" + implementation "androidx.activity:activity-ktx:1.7.2" implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.fragment:fragment-ktx:1.5.7" implementation "androidx.annotation:annotation:1.6.0" From 48e4a9fec58cc6beb07f7e086fe6fd964f318f66 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 May 2023 15:17:48 +0000 Subject: [PATCH 768/869] Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin (#2223) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 1a70f82af..b7ac37edc 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ buildscript { classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.5' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.4.0" - classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.0.0.2929" + classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.1.0.3113" classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0" classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries" } From db4e4d8cef31f399bfc5a4d48c0a1a98d1d53ffa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 May 2023 15:18:07 +0000 Subject: [PATCH 769/869] Bump com.huawei.hms:hianalytics from 6.10.0.300 to 6.10.0.301 (#2224) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index e72beed28..251aa4b6e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -261,7 +261,7 @@ dependencies { playImplementation 'com.google.android.play:core-ktx:1.8.1' playImplementation 'com.google.android.gms:play-services-ads:22.1.0' - hmsImplementation 'com.huawei.hms:hianalytics:6.10.0.300' + hmsImplementation 'com.huawei.hms:hianalytics:6.10.0.301' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.0.300' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 06fd7b0c368959d101a3baf0df021becfa73222a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 May 2023 15:18:30 +0000 Subject: [PATCH 770/869] Bump com.google.firebase:firebase-bom from 32.0.0 to 32.1.0 (#2225) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 251aa4b6e..05070d102 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -252,7 +252,7 @@ dependencies { implementation 'com.fredporciuncula:flow-preferences:1.9.1' implementation 'org.apache.commons:commons-text:1.10.0' - playImplementation platform('com.google.firebase:firebase-bom:32.0.0') + playImplementation platform('com.google.firebase:firebase-bom:32.1.0') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' From 556f42195b1bc5d7c5ac9ba26ee4cf7c75f3e931 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 May 2023 15:18:55 +0000 Subject: [PATCH 771/869] Bump com.android.tools.build:gradle from 8.0.1 to 8.0.2 (#2228) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b7ac37edc..69c0905af 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ buildscript { dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" - classpath 'com.android.tools.build:gradle:8.0.1' + classpath 'com.android.tools.build:gradle:8.0.2' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.15' classpath 'com.huawei.agconnect:agcp:1.9.0.300' From 41bde45731dce8c4cf39200ff98e7ca47c6c7bfd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 May 2023 15:19:25 +0000 Subject: [PATCH 772/869] Bump androidx.viewpager2:viewpager2 from 1.1.0-beta01 to 1.1.0-beta02 (#2227) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 05070d102..929a9db99 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -212,7 +212,7 @@ dependencies { implementation "androidx.preference:preference-ktx:1.2.0" implementation "androidx.recyclerview:recyclerview:1.3.0" - implementation "androidx.viewpager2:viewpager2:1.1.0-beta01" + implementation "androidx.viewpager2:viewpager2:1.1.0-beta02" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.constraintlayout:constraintlayout:2.1.4" implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0" From 1bc0f2d21457281d1c528b7c6cb9697b3211f9a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Thu, 1 Jun 2023 10:30:50 +0200 Subject: [PATCH 773/869] Add character limit to attendance excuse content (#2222) --- app/src/main/res/layout/dialog_excuse.xml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/dialog_excuse.xml b/app/src/main/res/layout/dialog_excuse.xml index 401aef313..8297bc142 100644 --- a/app/src/main/res/layout/dialog_excuse.xml +++ b/app/src/main/res/layout/dialog_excuse.xml @@ -1,5 +1,6 @@ + android:layout_height="wrap_content" + app:counterEnabled="true" + app:counterMaxLength="256"> + android:layout_weight="1" + android:hint="@string/attendance_excuse_dialog_reason" + android:maxLength="256" /> From 63487249b8ce8995e6b433ee31701ac9015d405b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 1 Jun 2023 10:31:42 +0200 Subject: [PATCH 774/869] New Crowdin updates (#2211) --- app/src/main/res/values-ru/strings.xml | 18 +++++++++--------- app/src/main/res/values-uk/strings.xml | 14 +++++++------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 726e95465..9029f4b91 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -682,7 +682,7 @@ Отменить Нет уроков - Synchronized on %1$s at %2$s + Синхронизировано %1$s в %2$s Выбрать тему Светлая Тёмная @@ -778,7 +778,7 @@ Значения плюса и минуса, расчёт средней оценки Расширенные Версия приложения, разработчики, соц. сети - Displaying advertisements, project support + Посмотреть рекламу, чтобы поддержать проект Новые оценки Новое домашнее задание @@ -810,14 +810,14 @@ Для сохранения изменений необходимо перезапустить приложение Перезапустить - Authorization has been rejected. The data provided does not match the records in the secretary\'s office. - Invalid PESEL - PESEL + Авторизация отклонена. Предоставленные данные не соответствуют записям в кабинете секретаря. + Неправильный номер PESEL + Номер PESEL Authorize - Authorization completed successfully - Authorization - To operate the application, we need to confirm your identity. Please enter the student\'s PESEL <b>%1$s</b> in the field below - Skip for now + Авторизация прошла успешно + Авторизация + Для работы приложения нам необходимо подтвердить вашу личность. Введите PESEL учащегося <b>%1$s</b> в поле ниже + Пропустить сейчас Интернет-соединение отсутствует Произошла ошибка. Проверьте время на вашем устройстве diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 66cd23870..be136ea29 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -682,7 +682,7 @@ Скасувати Немаэ уроків - Synchronized on %1$s at %2$s + Синхронізовано %1$s в %2$s Увібрати тему Яскрава Темна @@ -810,14 +810,14 @@ Додаток потрібно перезавантажити для збереження змін Перезавантажити - Authorization has been rejected. The data provided does not match the records in the secretary\'s office. - Invalid PESEL - PESEL + Авторизацію відхилено. Надані дані не збігаються із записами в кабінеті секретаря. + Неправильний PESEL + Число PESEL Authorize Authorization completed successfully - Authorization - To operate the application, we need to confirm your identity. Please enter the student\'s PESEL <b>%1$s</b> in the field below - Skip for now + Авторизувати + Для роботи програми нам потрібно підтвердити вашу особу. Будь ласка, введіть число PESEL <b>%1$s</b> студента в поле нижче + Поки що пропустити Немає з\'єднання з інтернетом Сталася помилка. Перевірте годинник пристрою From d4ae0d56d62215ea26ad829579ac909d9e313575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Thu, 1 Jun 2023 10:59:50 +0200 Subject: [PATCH 775/869] Version 2.0.7 --- app/build.gradle | 8 ++++---- app/src/main/play/release-notes/pl-PL/default.txt | 10 ++++------ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 929a9db99..ec62e19f1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,8 +23,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 33 - versionCode 128 - versionName "2.0.6" + versionCode 129 + versionName "2.0.7" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" @@ -166,7 +166,7 @@ play { track = 'production' releaseStatus = com.github.triplet.gradle.androidpublisher.ReleaseStatus.IN_PROGRESS userFraction = 0.25d - updatePriority = 4 + updatePriority = 1 enabled.set(false) } @@ -196,7 +196,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.0.6' + implementation 'io.github.wulkanowy:sdk:2.0.7' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index df69a331d..066485d3d 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,9 +1,7 @@ -Wersja 2.0.6 +Wersja 2.0.7 -— zaktualizowaliśmy wygląd aplikacji na (częściowo) zgodny z wytycznymi Material 3 -— dodaliśmy możliwość zmiany kolejności pozycji w menu dolnym -— poprawiliśmy sposób wyświetlania błędu o nieprawidłowym haśle na ekranie logowania -— od teraz zmiana ustawień liczenia średniej automatycznie odświeży listę ocen -— dodaliśmy okienko na wpisanie numeru PESEL, gdy dziennik wymaga dodatkowej autoryzacji dostępu +— poprawiliśmy wyświetlanie kilku rodzajów zmian w planie lekcji +— dodaliśmy limit znaków w okienku usprawiedliwiania +— naprawiliśmy wyświetlanie frekwencji w szkołach, gdzie działa już system eduOne (ciągle jednak brak opcji usprawiedliwiania) Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases From 5306044173d5a67b4d1d09f037148f08b4ea8574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Thu, 1 Jun 2023 23:22:10 +0200 Subject: [PATCH 776/869] Add custom register host field on login screen (#2221) --- app/build.gradle | 2 +- .../56.json | 2442 +++++++++++++++++ .../github/wulkanowy/data/db/AppDatabase.kt | 3 +- .../wulkanowy/data/db/entities/Student.kt | 3 + .../data/mappers/RegisterUserMapper.kt | 2 + .../data/repositories/StudentRepository.kt | 12 +- .../wulkanowy/ui/modules/login/LoginData.kt | 1 + .../login/advanced/LoginAdvancedFragment.kt | 7 + .../login/advanced/LoginAdvancedPresenter.kt | 6 +- .../login/advanced/LoginAdvancedView.kt | 2 + .../modules/login/form/LoginFormFragment.kt | 8 + .../modules/login/form/LoginFormPresenter.kt | 18 +- .../ui/modules/login/form/LoginFormView.kt | 4 + .../LoginStudentSelectPresenter.kt | 1 + .../login/symbol/LoginSymbolPresenter.kt | 1 + .../io/github/wulkanowy/utils/SdkExtension.kt | 1 + .../res/layout/fragment_login_advanced.xml | 29 +- .../main/res/layout/fragment_login_form.xml | 28 +- app/src/main/res/values/api_hosts.xml | 3 + app/src/main/res/values/strings.xml | 1 + .../io/github/wulkanowy/TestEnityCreator.kt | 1 + .../data/repositories/GradeRepositoryTest.kt | 1 + .../domain/GetMailboxByStudentUseCaseTest.kt | 1 + .../modules/grade/GradeAverageProviderTest.kt | 1 + .../login/form/LoginFormPresenterTest.kt | 13 +- .../LoginStudentSelectPresenterTest.kt | 1 + 26 files changed, 2564 insertions(+), 28 deletions(-) create mode 100644 app/schemas/io.github.wulkanowy.data.db.AppDatabase/56.json diff --git a/app/build.gradle b/app/build.gradle index ec62e19f1..19fbf6dec 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -196,7 +196,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.0.7' + implementation 'io.github.wulkanowy:sdk:2.0.8-SNAPSHOT' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/56.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/56.json new file mode 100644 index 000000000..1a26e717b --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/56.json @@ -0,0 +1,2442 @@ +{ + "formatVersion": 1, + "database": { + "version": 56, + "identityHash": "48f0538bd21601eb5322a7d850e04134", + "entities": [ + { + "tableName": "Students", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrapper_base_url` TEXT NOT NULL, `scrapper_domain_suffix` TEXT NOT NULL DEFAULT '', `mobile_base_url` TEXT NOT NULL, `login_type` TEXT NOT NULL, `login_mode` TEXT NOT NULL, `certificate_key` TEXT NOT NULL, `private_key` TEXT NOT NULL, `is_parent` INTEGER NOT NULL, `email` TEXT NOT NULL, `password` TEXT NOT NULL, `symbol` TEXT NOT NULL, `student_id` INTEGER NOT NULL, `user_login_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `student_name` TEXT NOT NULL, `school_id` TEXT NOT NULL, `school_short` TEXT NOT NULL, `school_name` TEXT NOT NULL, `class_name` TEXT NOT NULL, `class_id` INTEGER NOT NULL, `is_current` INTEGER NOT NULL, `registration_date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `nick` TEXT NOT NULL, `avatar_color` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "scrapperBaseUrl", + "columnName": "scrapper_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scrapperDomainSuffix", + "columnName": "scrapper_domain_suffix", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "mobileBaseUrl", + "columnName": "mobile_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginType", + "columnName": "login_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginMode", + "columnName": "login_mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificateKey", + "columnName": "certificate_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "private_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isParent", + "columnName": "is_parent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "user_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "student_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolSymbol", + "columnName": "school_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "school_short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolName", + "columnName": "school_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "className", + "columnName": "class_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCurrent", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registrationDate", + "columnName": "registration_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nick", + "columnName": "nick", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarColor", + "columnName": "avatar_color", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Students_email_symbol_student_id_school_id_class_id", + "unique": true, + "columnNames": [ + "email", + "symbol", + "student_id", + "school_id", + "class_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `kindergarten_diary_id` INTEGER NOT NULL DEFAULT 0, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "kindergartenDiaryId", + "columnName": "kindergarten_diary_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "diaryName", + "columnName": "diary_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolYear", + "columnName": "school_year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterName", + "columnName": "semester_name", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "kindergarten_diary_id", + "semester_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `kindergarten_diary_id`, `semester_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Exams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Timetable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `number` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `subjectOld` TEXT NOT NULL, `group` TEXT NOT NULL, `room` TEXT NOT NULL, `roomOld` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacherOld` TEXT NOT NULL, `info` TEXT NOT NULL, `student_plan` INTEGER NOT NULL, `changes` INTEGER NOT NULL, `canceled` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectOld", + "columnName": "subjectOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "room", + "columnName": "room", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roomOld", + "columnName": "roomOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherOld", + "columnName": "teacherOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isStudentPlan", + "columnName": "student_plan", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "changes", + "columnName": "changes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canceled", + "columnName": "canceled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Attendance", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `time_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `excusable` INTEGER NOT NULL, `excuse_status` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeId", + "columnName": "time_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excusable", + "columnName": "excusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excuseStatus", + "columnName": "excuse_status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AttendanceSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `subject_id` INTEGER NOT NULL, `month` INTEGER NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `absence_excused` INTEGER NOT NULL, `absence_for_school_reasons` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `lateness_excused` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subject_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "month", + "columnName": "month", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceExcused", + "columnName": "absence_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceForSchoolReasons", + "columnName": "absence_for_school_reasons", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latenessExcused", + "columnName": "lateness_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Grades", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `entry` TEXT NOT NULL, `value` REAL NOT NULL, `modifier` REAL NOT NULL, `comment` TEXT NOT NULL, `color` TEXT NOT NULL, `grade_symbol` TEXT NOT NULL, `description` TEXT NOT NULL, `weight` TEXT NOT NULL, `weightValue` REAL NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entry", + "columnName": "entry", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "modifier", + "columnName": "modifier", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gradeSymbol", + "columnName": "grade_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weightValue", + "columnName": "weightValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `subject` TEXT NOT NULL, `predicted_grade` TEXT NOT NULL, `final_grade` TEXT NOT NULL, `proposed_points` TEXT NOT NULL, `final_points` TEXT NOT NULL, `points_sum` TEXT NOT NULL, `average` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_predicted_grade_notified` INTEGER NOT NULL, `is_final_grade_notified` INTEGER NOT NULL, `predicted_grade_last_change` INTEGER NOT NULL, `final_grade_last_change` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "predictedGrade", + "columnName": "predicted_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalGrade", + "columnName": "final_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "proposedPoints", + "columnName": "proposed_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalPoints", + "columnName": "final_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pointsSum", + "columnName": "points_sum", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "average", + "columnName": "average", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPredictedGradeNotified", + "columnName": "is_predicted_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFinalGradeNotified", + "columnName": "is_final_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "predictedGradeLastChange", + "columnName": "predicted_grade_last_change", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "finalGradeLastChange", + "columnName": "final_grade_last_change", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradePartialStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `class_average` TEXT NOT NULL, `student_average` TEXT NOT NULL, `class_amounts` TEXT NOT NULL, `student_amounts` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAverage", + "columnName": "class_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAverage", + "columnName": "student_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAmounts", + "columnName": "class_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAmounts", + "columnName": "student_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesPointsStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `others` REAL NOT NULL, `student` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "others", + "columnName": "others", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "student", + "columnName": "student", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradeSemesterStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `amounts` TEXT NOT NULL, `student_grade` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amounts", + "columnName": "amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentGrade", + "columnName": "student_grade", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `message_global_key` TEXT NOT NULL, `mailbox_key` TEXT NOT NULL, `message_id` INTEGER NOT NULL, `correspondents` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `read_by` INTEGER, `unread_by` INTEGER, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `content` TEXT NOT NULL, `sender` TEXT, `recipients` TEXT)", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageGlobalKey", + "columnName": "message_global_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mailboxKey", + "columnName": "mailbox_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "correspondents", + "columnName": "correspondents", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasAttachments", + "columnName": "has_attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recipients", + "columnName": "recipients", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`message_global_key` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`message_global_key`, `url`, `filename`))", + "fields": [ + { + "fieldPath": "messageGlobalKey", + "columnName": "message_global_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "message_global_key", + "url", + "filename" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `category` TEXT NOT NULL, `category_type` INTEGER NOT NULL, `is_points_show` INTEGER NOT NULL, `points` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryType", + "columnName": "category_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPointsShow", + "columnName": "is_points_show", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Homework", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `attachments` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_done` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL, `is_added_by_user` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "is_done", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Subjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "LuckyNumbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `lucky_number` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "luckyNumber", + "columnName": "lucky_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CompletedLesson", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `topic` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `substitution` TEXT NOT NULL, `absence` TEXT NOT NULL, `resources` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "topic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "substitution", + "columnName": "substitution", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "resources", + "columnName": "resources", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Mailboxes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`globalKey` TEXT NOT NULL, `email` TEXT NOT NULL, `symbol` TEXT NOT NULL, `schoolId` TEXT NOT NULL, `fullName` TEXT NOT NULL, `userName` TEXT NOT NULL, `studentName` TEXT NOT NULL, `schoolNameShort` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`globalKey`))", + "fields": [ + { + "fieldPath": "globalKey", + "columnName": "globalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolId", + "columnName": "schoolId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "studentName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolNameShort", + "columnName": "schoolNameShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "globalKey" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mailboxGlobalKey` TEXT NOT NULL, `studentMailboxGlobalKey` TEXT NOT NULL, `fullName` TEXT NOT NULL, `userName` TEXT NOT NULL, `schoolShortName` TEXT NOT NULL, `type` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "mailboxGlobalKey", + "columnName": "mailboxGlobalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentMailboxGlobalKey", + "columnName": "studentMailboxGlobalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "schoolShortName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MobileDevices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_login_id` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Teachers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `short_name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "School", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `contact` TEXT NOT NULL, `headmaster` TEXT NOT NULL, `pedagogue` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contact", + "columnName": "contact", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headmaster", + "columnName": "headmaster", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pedagogue", + "columnName": "pedagogue", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Conferences", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `subject` TEXT NOT NULL, `agenda` TEXT NOT NULL, `present_on_conference` TEXT NOT NULL, `conference_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agenda", + "columnName": "agenda", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presentOnConference", + "columnName": "present_on_conference", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conference_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableAdditional", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `repeat_id` BLOB DEFAULT NULL, `is_added_by_user` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatId", + "columnName": "repeat_id", + "affinity": "BLOB", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StudentInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `full_name` TEXT NOT NULL, `first_name` TEXT NOT NULL, `second_name` TEXT NOT NULL, `surname` TEXT NOT NULL, `birth_date` INTEGER NOT NULL, `birth_place` TEXT NOT NULL, `gender` TEXT NOT NULL, `has_polish_citizenship` INTEGER NOT NULL, `family_name` TEXT NOT NULL, `parents_names` TEXT NOT NULL, `address` TEXT NOT NULL, `registered_address` TEXT NOT NULL, `correspondence_address` TEXT NOT NULL, `phone_number` TEXT NOT NULL, `cell_phone_number` TEXT NOT NULL, `email` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `first_guardian_full_name` TEXT, `first_guardian_kinship` TEXT, `first_guardian_address` TEXT, `first_guardian_phones` TEXT, `first_guardian_email` TEXT, `second_guardian_full_name` TEXT, `second_guardian_kinship` TEXT, `second_guardian_address` TEXT, `second_guardian_phones` TEXT, `second_guardian_email` TEXT)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "full_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondName", + "columnName": "second_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "surname", + "columnName": "surname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "birthDate", + "columnName": "birth_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthPlace", + "columnName": "birth_place", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gender", + "columnName": "gender", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPolishCitizenship", + "columnName": "has_polish_citizenship", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "familyName", + "columnName": "family_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentsNames", + "columnName": "parents_names", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "registeredAddress", + "columnName": "registered_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "correspondenceAddress", + "columnName": "correspondence_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cellPhoneNumber", + "columnName": "cell_phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstGuardian.fullName", + "columnName": "first_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.kinship", + "columnName": "first_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.address", + "columnName": "first_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.phones", + "columnName": "first_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.email", + "columnName": "first_guardian_email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.fullName", + "columnName": "second_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.kinship", + "columnName": "second_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.address", + "columnName": "second_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.phones", + "columnName": "second_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.email", + "columnName": "second_guardian_email", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableHeaders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SchoolAnnouncements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_login_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `type` TEXT NOT NULL, `destination` TEXT NOT NULL DEFAULT '{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}', `date` INTEGER NOT NULL, `data` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "destination", + "columnName": "destination", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}'" + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AdminMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `version_name` INTEGER, `version_max` INTEGER, `target_register_host` TEXT, `target_flavor` TEXT, `destination_url` TEXT, `priority` TEXT NOT NULL, `type` TEXT NOT NULL, `is_dismissible` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "versionMin", + "columnName": "version_name", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMax", + "columnName": "version_max", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "targetRegisterHost", + "columnName": "target_register_host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "targetFlavor", + "columnName": "target_flavor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "destinationUrl", + "columnName": "destination_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isDismissible", + "columnName": "is_dismissible", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "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, '48f0538bd21601eb5322a7d850e04134')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt index 0aea86dab..882a70167 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt @@ -49,6 +49,7 @@ import javax.inject.Singleton AutoMigration(from = 47, to = 48), AutoMigration(from = 51, to = 52), AutoMigration(from = 54, to = 55, spec = Migration55::class), + AutoMigration(from = 55, to = 56), ], version = AppDatabase.VERSION_SCHEMA, exportSchema = true @@ -57,7 +58,7 @@ import javax.inject.Singleton abstract class AppDatabase : RoomDatabase() { companion object { - const val VERSION_SCHEMA = 55 + const val VERSION_SCHEMA = 56 fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( Migration2(), diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Student.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Student.kt index 76da9643d..e1116733c 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Student.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Student.kt @@ -19,6 +19,9 @@ data class Student( @ColumnInfo(name = "scrapper_base_url") val scrapperBaseUrl: String, + @ColumnInfo(name = "scrapper_domain_suffix", defaultValue = "") + val scrapperDomainSuffix: String, + @ColumnInfo(name = "mobile_base_url") val mobileBaseUrl: String, diff --git a/app/src/main/java/io/github/wulkanowy/data/mappers/RegisterUserMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/RegisterUserMapper.kt index bcf26a5e1..72c4861c9 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/RegisterUserMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/RegisterUserMapper.kt @@ -55,6 +55,7 @@ fun SdkRegisterUser.mapToPojo(password: String?) = RegisterUser( fun RegisterStudent.mapToStudentWithSemesters( user: RegisterUser, + scrapperDomainSuffix: String, symbol: RegisterSymbol, unit: RegisterUnit, colors: List, @@ -76,6 +77,7 @@ fun RegisterStudent.mapToStudentWithSemesters( studentName = "$studentName $studentSurname", loginMode = user.loginMode.name, scrapperBaseUrl = user.scrapperBaseUrl.orEmpty(), + scrapperDomainSuffix = scrapperDomainSuffix, mobileBaseUrl = symbol.hebeBaseUrl.orEmpty(), certificateKey = symbol.keyId.orEmpty(), privateKey = symbol.privatePem.orEmpty(), diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt index a6bb72433..42d1eb84e 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt @@ -43,22 +43,14 @@ class StudentRepository @Inject constructor( .getStudentsFromHebe(token, pin, symbol, "") .mapToPojo(null) - suspend fun getStudentsScrapper( - email: String, - password: String, - scrapperBaseUrl: String, - symbol: String - ): RegisterUser = sdk - .getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, symbol) - .mapToPojo(password) - suspend fun getUserSubjectsFromScrapper( email: String, password: String, scrapperBaseUrl: String, + domainSuffix: String, symbol: String ): RegisterUser = sdk - .getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, symbol) + .getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, domainSuffix, symbol) .mapToPojo(password) suspend fun getStudentsHybrid( diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginData.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginData.kt index ae6c22492..2c11bb6d5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginData.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginData.kt @@ -6,5 +6,6 @@ data class LoginData( val login: String, val password: String, val baseUrl: String, + val domainSuffix: String, val symbol: String?, ) : Serializable diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt index ead2d71af..13d2c14a7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt @@ -5,6 +5,7 @@ import android.view.View import android.view.View.GONE import android.view.View.VISIBLE import android.widget.ArrayAdapter +import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -55,6 +56,9 @@ class LoginAdvancedFragment : get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())) .orEmpty() + override val formDomainSuffix: String + get() = binding.loginFormDomainSuffix.text.toString().trim() + override val formHostSymbol: String get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())) .orEmpty() @@ -279,6 +283,7 @@ class LoginAdvancedFragment : loginFormUsernameLayout.visibility = VISIBLE loginFormPassLayout.visibility = VISIBLE loginFormHostLayout.visibility = VISIBLE + loginFormDomainSuffixLayout.isVisible = true loginFormPinLayout.visibility = GONE loginFormSymbolLayout.visibility = VISIBLE loginFormTokenLayout.visibility = GONE @@ -290,6 +295,7 @@ class LoginAdvancedFragment : loginFormUsernameLayout.visibility = VISIBLE loginFormPassLayout.visibility = VISIBLE loginFormHostLayout.visibility = VISIBLE + loginFormDomainSuffixLayout.isVisible = true loginFormPinLayout.visibility = GONE loginFormSymbolLayout.visibility = VISIBLE loginFormTokenLayout.visibility = GONE @@ -301,6 +307,7 @@ class LoginAdvancedFragment : loginFormUsernameLayout.visibility = GONE loginFormPassLayout.visibility = GONE loginFormHostLayout.visibility = GONE + loginFormDomainSuffixLayout.isVisible = false loginFormPinLayout.visibility = VISIBLE loginFormSymbolLayout.visibility = VISIBLE loginFormTokenLayout.visibility = VISIBLE diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt index ab56bd786..a17ad0035 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt @@ -154,6 +154,7 @@ class LoginAdvancedPresenter @Inject constructor( login = view?.formUsernameValue.orEmpty().trim(), password = view?.formPassValue.orEmpty().trim(), baseUrl = view?.formHostValue.orEmpty().trim(), + domainSuffix = view?.formDomainSuffix.orEmpty().trim(), symbol = view?.formSymbolValue.orEmpty().trim().getNormalizedSymbol(), ) when (it.data.symbols.size) { @@ -186,6 +187,7 @@ class LoginAdvancedPresenter @Inject constructor( val email = view?.formUsernameValue.orEmpty() val password = view?.formPassValue.orEmpty() val endpoint = view?.formHostValue.orEmpty() + val domainSuffix = view?.formDomainSuffix.orEmpty() val pin = view?.formPinValue.orEmpty() val symbol = view?.formSymbolValue.orEmpty() @@ -193,8 +195,8 @@ class LoginAdvancedPresenter @Inject constructor( return when (Sdk.Mode.valueOf(view?.formLoginType.orEmpty())) { Sdk.Mode.HEBE -> studentRepository.getStudentsApi(pin, symbol, token) - Sdk.Mode.SCRAPPER -> studentRepository.getStudentsScrapper( - email, password, endpoint, symbol + Sdk.Mode.SCRAPPER -> studentRepository.getUserSubjectsFromScrapper( + email, password, endpoint, domainSuffix, symbol ) Sdk.Mode.HYBRID -> studentRepository.getStudentsHybrid( diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedView.kt index 34062d938..afd33e3b6 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedView.kt @@ -12,6 +12,8 @@ interface LoginAdvancedView : BaseView { val formHostValue: String + val formDomainSuffix: String + val formHostSymbol: String val formLoginType: String diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt index 43ba3fe1d..1085ff509 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt @@ -45,6 +45,9 @@ class LoginFormFragment : BaseFragment(R.layout.fragme get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())) .orEmpty() + override val formDomainSuffix: String + get() = binding.loginFormDomainSuffix.text.toString() + override val formHostSymbol: String get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())) .orEmpty() @@ -204,6 +207,10 @@ class LoginFormFragment : BaseFragment(R.layout.fragme binding.loginFormContainer.visibility = if (show) VISIBLE else GONE } + override fun showDomainSuffixInput(show: Boolean) { + binding.loginFormDomainSuffixLayout.isVisible = show + } + override fun showOtherOptionsButton(show: Boolean) { binding.loginFormAdvancedButton.isVisible = show } @@ -256,6 +263,7 @@ class LoginFormFragment : BaseFragment(R.layout.fragme override fun onResume() { super.onResume() presenter.updateUsernameLabel() + presenter.updateCustomDomainSuffixVisibility() } override fun openEmail(lastError: String) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt index ed70eb128..85f428415 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt @@ -1,8 +1,13 @@ package io.github.wulkanowy.ui.modules.login.form import androidx.core.net.toUri -import io.github.wulkanowy.data.* +import io.github.wulkanowy.data.logResourceStatus +import io.github.wulkanowy.data.onResourceError +import io.github.wulkanowy.data.onResourceLoading +import io.github.wulkanowy.data.onResourceNotLoading +import io.github.wulkanowy.data.onResourceSuccess import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginErrorHandler @@ -56,10 +61,17 @@ class LoginFormPresenter @Inject constructor( } else if (formUsernameValue == "jan@fakelog.cf" && formPassValue == "jan123") { setCredentials("", "") } + updateCustomDomainSuffixVisibility() updateUsernameLabel() } } + fun updateCustomDomainSuffixVisibility() { + view?.run { + showDomainSuffixInput("customSuffix" in formHostValue) + } + } + fun updateUsernameLabel() { view?.run { setUsernameLabel(if ("email" !in formHostValue) nicknameLabel else emailLabel) @@ -91,6 +103,7 @@ class LoginFormPresenter @Inject constructor( val email = view?.formUsernameValue.orEmpty().trim() val password = view?.formPassValue.orEmpty().trim() val host = view?.formHostValue.orEmpty().trim() + val domainSuffix = view?.formDomainSuffix.orEmpty().trim() val symbol = view?.formHostSymbol.orEmpty().trim() if (!validateCredentials(email, password, host)) return @@ -100,6 +113,7 @@ class LoginFormPresenter @Inject constructor( email = email, password = password, scrapperBaseUrl = host, + domainSuffix = domainSuffix, symbol = symbol ) } @@ -112,7 +126,7 @@ class LoginFormPresenter @Inject constructor( } } .onResourceSuccess { - val loginData = LoginData(email, password, host, symbol) + val loginData = LoginData(email, password, host, domainSuffix, symbol) when (it.symbols.size) { 0 -> view?.navigateToSymbol(loginData) else -> view?.navigateToStudentSelect(loginData, it) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt index e5c680d6f..5fb260620 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt @@ -14,6 +14,8 @@ interface LoginFormView : BaseView { val formHostValue: String + val formDomainSuffix: String + val formHostSymbol: String val nicknameLabel: String @@ -56,6 +58,8 @@ interface LoginFormView : BaseView { fun showContent(show: Boolean) + fun showDomainSuffixInput(show: Boolean) + fun showOtherOptionsButton(show: Boolean) fun showVersion() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt index 7e1fe3b21..70862e828 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt @@ -241,6 +241,7 @@ class LoginStudentSelectPresenter @Inject constructor( item.student.mapToStudentWithSemesters( user = registerUser, symbol = item.symbol, + scrapperDomainSuffix = loginData.domainSuffix, unit = item.unit, colors = appInfo.defaultColorsForAvatar, ) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt index 03ea95fa6..91fe1ac3c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt @@ -62,6 +62,7 @@ class LoginSymbolPresenter @Inject constructor( email = loginData.login, password = loginData.password, scrapperBaseUrl = loginData.baseUrl, + domainSuffix = loginData.domainSuffix, symbol = loginData.symbol.orEmpty(), ) }.onEach { user -> diff --git a/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt index 481cad113..889d64ea1 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt @@ -16,6 +16,7 @@ fun Sdk.init(student: Student): Sdk { mobileBaseUrl = student.mobileBaseUrl } else { scrapperBaseUrl = student.scrapperBaseUrl + domainSuffix = student.scrapperDomainSuffix loginType = Sdk.ScrapperLoginType.valueOf(student.loginType) } diff --git a/app/src/main/res/layout/fragment_login_advanced.xml b/app/src/main/res/layout/fragment_login_advanced.xml index 43016db4c..37551decb 100644 --- a/app/src/main/res/layout/fragment_login_advanced.xml +++ b/app/src/main/res/layout/fragment_login_advanced.xml @@ -172,7 +172,7 @@ android:layout_marginBottom="16dp" android:hint="@string/login_host_hint" android:orientation="vertical" - app:layout_constraintBottom_toTopOf="@+id/loginFormTokenLayout" + app:layout_constraintBottom_toTopOf="@+id/loginFormDomainSuffixLayout" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/loginFormPassLayout"> @@ -185,6 +185,31 @@ tools:ignore="Deprecated,LabelFor" /> + + + + + + app:layout_constraintTop_toBottomOf="@+id/loginFormDomainSuffixLayout"> + + + + + + app:layout_constraintTop_toBottomOf="@+id/loginFormDomainSuffixLayout" /> Gmina Ulan-Majorat - Platforma oświatowa Gmina Ozorków - Platforma edukacyjna Gmina Łopiennik Górny - Platforma oświatowa + Custom domain suffix Fakelog @@ -42,6 +43,7 @@ https://vulcan.net.pl/?login https://vulcan.net.pl/?login https://vulcan.net.pl/?login + https://vulcan.net.pl/?email&customSuffix https://fakelog.cf/?email @@ -64,6 +66,7 @@ gminaulanmajorat gminaozorkow gminalopiennikgorny + Default powiatwulkanowy diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1eff95698..98c316cb0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -42,6 +42,7 @@ Login, PESEL or e-mail Password UONET+ register variant + Custom domain suffix Mobile API Scraper Hybrid diff --git a/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt b/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt index c8d95829e..eac1389f4 100644 --- a/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt +++ b/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt @@ -50,6 +50,7 @@ fun getSemesterPojo(diaryId: Int, semesterId: Int, start: LocalDate, end: LocalD fun getStudentEntity(mode: Sdk.Mode = Sdk.Mode.HEBE) = Student( scrapperBaseUrl = "http://fakelog.cf", + scrapperDomainSuffix = "", email = "jan@fakelog.cf", certificateKey = "", classId = 0, diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt index 1d6dfaff0..5a1877cc0 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt @@ -296,6 +296,7 @@ class GradeRepositoryTest { private fun createGrades(grades: List): Grades = Grades( details = grades, summary = listOf(), + descriptive = emptyList(), isAverage = false, isPoints = false, isForAdults = false, diff --git a/app/src/test/java/io/github/wulkanowy/domain/GetMailboxByStudentUseCaseTest.kt b/app/src/test/java/io/github/wulkanowy/domain/GetMailboxByStudentUseCaseTest.kt index f58a5381d..34a8fe991 100644 --- a/app/src/test/java/io/github/wulkanowy/domain/GetMailboxByStudentUseCaseTest.kt +++ b/app/src/test/java/io/github/wulkanowy/domain/GetMailboxByStudentUseCaseTest.kt @@ -201,6 +201,7 @@ class GetMailboxByStudentUseCaseTest { schoolShortName: String = "test", ) = Student( scrapperBaseUrl = "http://fakelog.cf", + scrapperDomainSuffix = "", email = "jan@fakelog.cf", certificateKey = "", classId = 0, diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt index b94002c0d..31ea3322b 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt @@ -47,6 +47,7 @@ class GradeAverageProviderTest { private val student = Student( scrapperBaseUrl = "", + scrapperDomainSuffix = "", mobileBaseUrl = "", loginType = "", loginMode = "SCRAPPER", diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenterTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenterTest.kt index eb1f53006..a6440f72a 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenterTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenterTest.kt @@ -130,7 +130,7 @@ class LoginFormPresenterTest { @Test fun loginTest() { coEvery { - repository.getUserSubjectsFromScrapper(any(), any(), any(), any()) + repository.getUserSubjectsFromScrapper(any(), any(), any(), any(), any()) } returns registerUser every { loginFormView.formUsernameValue } returns "@" @@ -149,7 +149,7 @@ class LoginFormPresenterTest { @Test fun loginEmptyTest() { coEvery { - repository.getUserSubjectsFromScrapper(any(), any(), any(), any()) + repository.getUserSubjectsFromScrapper(any(), any(), any(), any(), any()) } returns registerUser every { loginFormView.formUsernameValue } returns "@" every { loginFormView.formPassValue } returns "123456" @@ -167,7 +167,7 @@ class LoginFormPresenterTest { @Test fun loginEmptyTwiceTest() { coEvery { - repository.getUserSubjectsFromScrapper(any(), any(), any(), any()) + repository.getUserSubjectsFromScrapper(any(), any(), any(), any(), any()) } returns registerUser every { loginFormView.formUsernameValue } returns "@" every { loginFormView.formPassValue } returns "123456" @@ -187,12 +187,7 @@ class LoginFormPresenterTest { fun loginErrorTest() { val testException = IOException("test") coEvery { - repository.getUserSubjectsFromScrapper( - any(), - any(), - any(), - any() - ) + repository.getUserSubjectsFromScrapper(any(), any(), any(), any(), any()) } throws testException every { loginFormView.formUsernameValue } returns "@" every { loginFormView.formPassValue } returns "123456" diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt index da292c51c..06aabec72 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt @@ -55,6 +55,7 @@ class LoginStudentSelectPresenterTest { password = "", baseUrl = "", symbol = null, + domainSuffix = "", ) private val subject = RegisterStudent( From fa44295d59ffa0b480f43b46b3139f10df88ebc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 1 Jun 2023 23:37:01 +0200 Subject: [PATCH 777/869] New Crowdin updates (#2231) --- app/src/main/res/values-cs/strings.xml | 1 + app/src/main/res/values-da-rDK/strings.xml | 1 + app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values-es-rES/strings.xml | 1 + app/src/main/res/values-pl/strings.xml | 1 + app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-sk/strings.xml | 1 + app/src/main/res/values-uk/strings.xml | 1 + 8 files changed, 8 insertions(+) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index f8c19dff0..964329dae 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -37,6 +37,7 @@ Přihlášení, číslo PESEL nebo e-mail Heslo Variace deníku UONET+ + Custom domain suffix Mobile API Scraper Hybridní diff --git a/app/src/main/res/values-da-rDK/strings.xml b/app/src/main/res/values-da-rDK/strings.xml index b9de57f77..5c7d02a0f 100644 --- a/app/src/main/res/values-da-rDK/strings.xml +++ b/app/src/main/res/values-da-rDK/strings.xml @@ -37,6 +37,7 @@ Login, PESEL or e-mail Password UONET+ register variant + Custom domain suffix Mobile API Scraper Hybrid diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 500553e27..96423e35e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -37,6 +37,7 @@ Anmeldung, PESEL oder e-mail Passwort UONET+ Registervariante + Custom domain suffix Mobile API Scraper Hybride diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index b9de57f77..5c7d02a0f 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -37,6 +37,7 @@ Login, PESEL or e-mail Password UONET+ register variant + Custom domain suffix Mobile API Scraper Hybrid diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 207b12e98..2c7077977 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -37,6 +37,7 @@ Login, PESEL lub adres e-mail Hasło Odmiana dziennika UONET+ + Niestandardowy sufiks domeny Mobilne API Scraper Hybrydowe diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 9029f4b91..eb8be0028 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -37,6 +37,7 @@ Логин, PESEL или электронная почта Пароль Тип дневника UONET+ + Custom domain suffix Мобильный API Scraper Hybrid diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 950eb01e0..bcbd832ae 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -37,6 +37,7 @@ Prihlásenie, číslo PESEL alebo e-mail Heslo Variácia denníka UONET+ + Custom domain suffix Mobile API Scraper Hybridné diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index be136ea29..30a587cc4 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -37,6 +37,7 @@ Логін, PESEL або e-mail Пароль Тип щоденника UONET+ + Custom domain suffix Мobile API Scraper Hybrid From fecd5c707d6005e37117d231d612ee59a92126c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Thu, 1 Jun 2023 23:43:01 +0200 Subject: [PATCH 778/869] Version 2.0.8 --- app/build.gradle | 6 +++--- app/src/main/play/release-notes/pl-PL/default.txt | 2 +- app/src/main/res/values/api_hosts.xml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 19fbf6dec..000cc09ca 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,8 +23,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 33 - versionCode 129 - versionName "2.0.7" + versionCode 130 + versionName "2.0.8" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" @@ -196,7 +196,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.0.8-SNAPSHOT' + implementation 'io.github.wulkanowy:sdk:2.0.8' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index 066485d3d..e881cfdaf 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,4 +1,4 @@ -Wersja 2.0.7 +Wersja 2.0.8 — poprawiliśmy wyświetlanie kilku rodzajów zmian w planie lekcji — dodaliśmy limit znaków w okienku usprawiedliwiania diff --git a/app/src/main/res/values/api_hosts.xml b/app/src/main/res/values/api_hosts.xml index 932093676..522b6e116 100644 --- a/app/src/main/res/values/api_hosts.xml +++ b/app/src/main/res/values/api_hosts.xml @@ -20,7 +20,7 @@ Gmina Ulan-Majorat - Platforma oświatowa Gmina Ozorków - Platforma edukacyjna Gmina Łopiennik Górny - Platforma oświatowa - Custom domain suffix + @string/login_domain_suffix_hint Fakelog From 2f749a690b4293a802c6fc00063c41a42c5008c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Jun 2023 23:18:23 +0000 Subject: [PATCH 779/869] Bump androidx.fragment:fragment-ktx from 1.5.7 to 1.6.0 (#2239) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 000cc09ca..2231da52b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -207,7 +207,7 @@ dependencies { implementation 'androidx.core:core-splashscreen:1.0.1' implementation "androidx.activity:activity-ktx:1.7.2" implementation "androidx.appcompat:appcompat:1.6.1" - implementation "androidx.fragment:fragment-ktx:1.5.7" + implementation "androidx.fragment:fragment-ktx:1.6.0" implementation "androidx.annotation:annotation:1.6.0" implementation "androidx.preference:preference-ktx:1.2.0" From f20ffe44d5b16c9954d9738b8f44214968f27853 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Jun 2023 23:24:27 +0000 Subject: [PATCH 780/869] Bump kotlin_version from 1.8.21 to 1.8.22 (#2238) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 69c0905af..c5a48ec9d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - kotlin_version = '1.8.21' + kotlin_version = '1.8.22' about_libraries = '10.7.0' hilt_version = "2.46.1" } From 8913b22a200685915c4da82a10802ab5ad0a7990 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Jun 2023 23:25:46 +0000 Subject: [PATCH 781/869] Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin (#2237) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c5a48ec9d..757910b1c 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ buildscript { classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.5' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.4.0" - classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.1.0.3113" + classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.2.0.3129" classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0" classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries" } From df8849639b3baa8826eafc3a2c1c7a8eccdc510d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Jun 2023 10:56:32 +0000 Subject: [PATCH 782/869] Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin (#2242) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 757910b1c..150bd4cfb 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ buildscript { classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.5' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.4.0" - classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.2.0.3129" + classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.2.1.3168" classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0" classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries" } From 03cd3aeab78cf3c3991cb28099cca6d67d5d2f1f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 17:53:21 +0000 Subject: [PATCH 783/869] Bump com.google.firebase:firebase-bom from 32.1.0 to 32.2.0 (#2259) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 2231da52b..46f709c85 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -252,7 +252,7 @@ dependencies { implementation 'com.fredporciuncula:flow-preferences:1.9.1' implementation 'org.apache.commons:commons-text:1.10.0' - playImplementation platform('com.google.firebase:firebase-bom:32.1.0') + playImplementation platform('com.google.firebase:firebase-bom:32.2.0') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' From 6e7c12a118775f9590a57e99ac6a4064116505d2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 17:53:38 +0000 Subject: [PATCH 784/869] Bump com.google.firebase:firebase-crashlytics-gradle from 2.9.5 to 2.9.7 (#2258) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 150bd4cfb..62f6389f0 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.15' classpath 'com.huawei.agconnect:agcp:1.9.0.300' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.5' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.7' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.4.0" classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.2.1.3168" From bb79b33b6d40d0d208b7fe546c32a04a09d02446 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 17:53:58 +0000 Subject: [PATCH 785/869] Bump com.google.android.gms:play-services-ads from 22.1.0 to 22.2.0 (#2256) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 46f709c85..4589331fc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -259,7 +259,7 @@ dependencies { playImplementation 'com.google.firebase:firebase-config-ktx' playImplementation 'com.google.android.play:core:1.10.3' playImplementation 'com.google.android.play:core-ktx:1.8.1' - playImplementation 'com.google.android.gms:play-services-ads:22.1.0' + playImplementation 'com.google.android.gms:play-services-ads:22.2.0' hmsImplementation 'com.huawei.hms:hianalytics:6.10.0.301' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.0.300' From dbe608f2dd6aa9ab616c40a5d0b377a88d77fe37 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 17:54:26 +0000 Subject: [PATCH 786/869] Bump about_libraries from 10.7.0 to 10.8.0 (#2249) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 62f6389f0..d12932db3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { kotlin_version = '1.8.22' - about_libraries = '10.7.0' + about_libraries = '10.8.0' hilt_version = "2.46.1" } repositories { From 29a36aaf6ed30318e2a8feb4666a0739daed822d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 18:11:57 +0000 Subject: [PATCH 787/869] Bump com.huawei.hms:hianalytics from 6.10.0.301 to 6.10.0.302 (#2253) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 4589331fc..54cc97ffd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -261,7 +261,7 @@ dependencies { playImplementation 'com.google.android.play:core-ktx:1.8.1' playImplementation 'com.google.android.gms:play-services-ads:22.2.0' - hmsImplementation 'com.huawei.hms:hianalytics:6.10.0.301' + hmsImplementation 'com.huawei.hms:hianalytics:6.10.0.302' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.0.300' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 8564e12b015b151f3ead4b13f7ae4fb5d4c8358d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 19:15:06 +0000 Subject: [PATCH 788/869] Bump com.huawei.agconnect:agcp from 1.9.0.300 to 1.9.1.300 (#2254) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d12932db3..75191aade 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ buildscript { classpath 'com.android.tools.build:gradle:8.0.2' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.15' - classpath 'com.huawei.agconnect:agcp:1.9.0.300' + classpath 'com.huawei.agconnect:agcp:1.9.1.300' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.7' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.4.0" From d0819928f3b61e4d020d2dd9b78d10fd9b32d4d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 19:15:21 +0000 Subject: [PATCH 789/869] Bump com.huawei.agconnect:agconnect-crash from 1.9.0.300 to 1.9.1.300 (#2252) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 54cc97ffd..fdb8d20f7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -262,7 +262,7 @@ dependencies { playImplementation 'com.google.android.gms:play-services-ads:22.2.0' hmsImplementation 'com.huawei.hms:hianalytics:6.10.0.302' - hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.0.300' + hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.1.300' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 88ea753fc69bba48fb367a9535e28c05e14c6b6b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 19:15:34 +0000 Subject: [PATCH 790/869] Bump coroutines from 1.7.1 to 1.7.2 (#2251) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index fdb8d20f7..9e0743715 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -192,7 +192,7 @@ ext { room = "2.5.1" chucker = "3.5.2" mockk = "1.13.5" - coroutines = "1.7.1" + coroutines = "1.7.2" } dependencies { From 86c7de6595fc6f71dbe616bffaa5474d1bdb215e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 19:15:48 +0000 Subject: [PATCH 791/869] Bump room from 2.5.1 to 2.5.2 (#2250) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 9e0743715..f3ea03c1e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -189,7 +189,7 @@ huaweiPublish { ext { work_manager = "2.8.1" android_hilt = "1.0.0" - room = "2.5.1" + room = "2.5.2" chucker = "3.5.2" mockk = "1.13.5" coroutines = "1.7.2" From 05741761a2117667d848d171b43096287d1aa811 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 20:08:56 +0000 Subject: [PATCH 792/869] Bump kotlin_version from 1.8.22 to 1.9.0 (#2255) --- app/build.gradle | 15 +++++---------- .../wulkanowy/data/db/dao/AdminMessageDao.kt | 2 +- build.gradle | 3 ++- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index f3ea03c1e..596393f54 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,6 +3,7 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlinx-serialization' apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-kapt' +apply plugin: 'com.google.devtools.ksp' apply plugin: 'dagger.hilt.android.plugin' apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.firebase.crashlytics' @@ -33,14 +34,6 @@ android { firebase_enabled: project.hasProperty("enableFirebase"), admob_project_id: "" ] - javaCompileOptions { - annotationProcessorOptions { - arguments += [ - "room.schemaLocation": "$projectDir/schemas".toString(), - "room.incremental" : "true" - ] - } - } buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null" buildConfigField "String", "DASHBOARD_TILE_AD_ID", "null" @@ -156,7 +149,9 @@ android { kapt { correctErrorTypes true } - +ksp { + arg("room.schemaLocation", "$projectDir/schemas".toString()) +} kotlin { jvmToolchain(11) } @@ -228,7 +223,7 @@ dependencies { implementation "androidx.room:room-runtime:$room" implementation "androidx.room:room-ktx:$room" - kapt "androidx.room:room-compiler:$room" + ksp "androidx.room:room-compiler:$room" implementation "com.google.dagger:hilt-android:$hilt_version" kapt "com.google.dagger:hilt-android-compiler:$hilt_version" diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt index 87f4812da..2b4cb5975 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt @@ -22,4 +22,4 @@ abstract class AdminMessageDao : BaseDao { deleteAll(oldMessages) insertAll(newMessages) } -} \ No newline at end of file +} diff --git a/build.gradle b/build.gradle index 75191aade..6a378d481 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - kotlin_version = '1.8.22' + kotlin_version = '1.9.0' about_libraries = '10.8.0' hilt_version = "2.46.1" } @@ -13,6 +13,7 @@ buildscript { dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" + classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$kotlin_version-1.0.11" classpath 'com.android.tools.build:gradle:8.0.2' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.15' From ef72218906b6a87d7a909fb3a561d99307276c17 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 21:30:37 +0000 Subject: [PATCH 793/869] Bump org.gradle.toolchains.foojay-resolver-convention (#2264) --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index a69aaa955..af9bb737a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,4 @@ plugins { - id 'org.gradle.toolchains.foojay-resolver-convention' version '0.5.0' + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.6.0' } include ':app' From c0161f38c61dc93a338b5907b374a48280e65031 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 21:33:17 +0000 Subject: [PATCH 794/869] Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin (#2262) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6a378d481..8bdcdedfa 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ buildscript { classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.7' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.4.0" - classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.2.1.3168" + classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.3.0.3225" classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0" classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries" } From e79c5d4d2bc9b7cb9359377fb7709bdc2633f8c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 21:36:06 +0000 Subject: [PATCH 795/869] Bump hilt_version from 2.46.1 to 2.47 (#2261) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 8bdcdedfa..dba021521 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { kotlin_version = '1.9.0' about_libraries = '10.8.0' - hilt_version = "2.46.1" + hilt_version = "2.47" } repositories { mavenCentral() From 5b2e2ffb34820e475a4553eb1795b070a4f0a789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Tue, 25 Jul 2023 11:37:43 +0200 Subject: [PATCH 796/869] Remove tests deprecations (#2260) --- app/build.gradle | 2 +- .../io/github/wulkanowy/MainCoroutineRule.kt | 12 +++---- .../db/migrations/AbstractMigrationTest.kt | 3 +- .../data/db/migrations/Migration12Test.kt | 18 +++++----- .../data/db/migrations/Migration13Test.kt | 34 +++++++++++-------- 5 files changed, 37 insertions(+), 32 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 596393f54..f8603cc88 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,7 +2,6 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlinx-serialization' apply plugin: 'kotlin-parcelize' -apply plugin: 'kotlin-kapt' apply plugin: 'com.google.devtools.ksp' apply plugin: 'dagger.hilt.android.plugin' apply plugin: 'com.google.gms.google-services' @@ -11,6 +10,7 @@ apply plugin: 'com.github.triplet.play' apply plugin: 'ru.cian.huawei-publish' apply plugin: 'com.mikepenz.aboutlibraries.plugin' apply plugin: 'com.huawei.agconnect' +apply plugin: 'kotlin-kapt' apply from: 'jacoco.gradle' apply from: 'sonarqube.gradle' apply from: 'hooks.gradle' diff --git a/app/src/test/java/io/github/wulkanowy/MainCoroutineRule.kt b/app/src/test/java/io/github/wulkanowy/MainCoroutineRule.kt index 10724868a..543c9540f 100644 --- a/app/src/test/java/io/github/wulkanowy/MainCoroutineRule.kt +++ b/app/src/test/java/io/github/wulkanowy/MainCoroutineRule.kt @@ -2,7 +2,8 @@ package io.github.wulkanowy import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain import org.junit.rules.TestWatcher @@ -10,17 +11,14 @@ import org.junit.runner.Description @OptIn(ExperimentalCoroutinesApi::class) class MainCoroutineRule( - private val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher() + private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher() ) : TestWatcher() { - override fun starting(description: Description?) { - super.starting(description) + override fun starting(description: Description) { Dispatchers.setMain(testDispatcher) } - override fun finished(description: Description?) { - super.finished(description) + override fun finished(description: Description) { Dispatchers.resetMain() - testDispatcher.cleanupTestCoroutines() } } diff --git a/app/src/test/java/io/github/wulkanowy/data/db/migrations/AbstractMigrationTest.kt b/app/src/test/java/io/github/wulkanowy/data/db/migrations/AbstractMigrationTest.kt index 18249ba8b..18ff93392 100644 --- a/app/src/test/java/io/github/wulkanowy/data/db/migrations/AbstractMigrationTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/db/migrations/AbstractMigrationTest.kt @@ -22,7 +22,8 @@ abstract class AbstractMigrationTest { @get:Rule val helper: MigrationTestHelper = MigrationTestHelper( InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java.canonicalName, + AppDatabase::class.java, + listOf(Migration55()), FrameworkSQLiteOpenHelperFactory() ) diff --git a/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration12Test.kt b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration12Test.kt index f614c8ca9..54c73e209 100644 --- a/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration12Test.kt +++ b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration12Test.kt @@ -22,12 +22,12 @@ class Migration12Test : AbstractMigrationTest() { fun twoNotRelatedStudents() { helper.createDatabase(dbName, 11).apply { // user 1 - createStudent(this, 1, true) + createStudent(this, 1) createSemester(this, 1, false, 5, 1) createSemester(this, 1, true, 5, 2) // user 2 - createStudent(this, 2, true) + createStudent(this, 2) createSemester(this, 2, false, 6, 1) createSemester(this, 2, true, 6, 2) close() @@ -56,9 +56,9 @@ class Migration12Test : AbstractMigrationTest() { fun removeStudentsWithoutClassId() { helper.createDatabase(dbName, 11).apply { // user 1 - createStudent(this, 1, true) + createStudent(this, 1) createSemester(this, 1, false, 0, 2) - createStudent(this, 2, true) + createStudent(this, 2) createSemester(this, 2, true, 1, 2) close() } @@ -81,11 +81,11 @@ class Migration12Test : AbstractMigrationTest() { fun ensureThereIsOnlyOneCurrentStudent() { helper.createDatabase(dbName, 11).apply { // user 1 - createStudent(this, 1, true) + createStudent(this, 1) createSemester(this, 1, true, 5, 2) - createStudent(this, 2, true) + createStudent(this, 2) createSemester(this, 2, true, 6, 2) - createStudent(this, 3, true) + createStudent(this, 3) createSemester(this, 3, false, 7, 2) close() } @@ -112,7 +112,7 @@ class Migration12Test : AbstractMigrationTest() { db.close() } - private fun createStudent(db: SupportSQLiteDatabase, studentId: Int, isCurrent: Boolean) { + private fun createStudent(db: SupportSQLiteDatabase, studentId: Int) { db.insert("Students", CONFLICT_FAIL, ContentValues().apply { put("endpoint", "https://fakelog.cf") put("loginType", "STANDARD") @@ -123,7 +123,7 @@ class Migration12Test : AbstractMigrationTest() { put("student_name", "Jan Kowalski") put("school_id", "000123") put("school_name", "") - put("is_current", isCurrent) + put("is_current", true) put("registration_date", "0") }) } diff --git a/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration13Test.kt b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration13Test.kt index b0c03fb11..9ba36876e 100644 --- a/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration13Test.kt +++ b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration13Test.kt @@ -95,22 +95,22 @@ class Migration13Test : AbstractMigrationTest() { fun markAtLeastAndOnlyOneSemesterAtCurrent() { helper.createDatabase(dbName, 12).apply { createStudent(this, 1, "", 5) - createSemester(this, 1, 5, 1, 1, false) - createSemester(this, 1, 5, 2, 1, false) - createSemester(this, 1, 5, 3, 2, false) - createSemester(this, 1, 5, 4, 2, false) + createSemester(this, 1, 1, 1, false) + createSemester(this, 1, 2, 1, false) + createSemester(this, 1, 3, 2, false) + createSemester(this, 1, 4, 2, false) createStudent(this, 2, "", 5) - createSemester(this, 2, 5, 5, 5, true) - createSemester(this, 2, 5, 6, 5, true) - createSemester(this, 2, 5, 7, 55, true) - createSemester(this, 2, 5, 8, 55, true) + createSemester(this, 2, 5, 5, true) + createSemester(this, 2, 6, 5, true) + createSemester(this, 2, 7, 55, true) + createSemester(this, 2, 8, 55, true) createStudent(this, 3, "", 5) - createSemester(this, 3, 5, 11, 99, false) - createSemester(this, 3, 5, 12, 99, false) - createSemester(this, 3, 5, 13, 100, false) - createSemester(this, 3, 5, 14, 100, true) + createSemester(this, 3, 11, 99, false) + createSemester(this, 3, 12, 99, false) + createSemester(this, 3, 13, 100, false) + createSemester(this, 3, 14, 100, true) close() } @@ -198,7 +198,13 @@ class Migration13Test : AbstractMigrationTest() { }) } - private fun createSemester(db: SupportSQLiteDatabase, studentId: Int, classId: Int, semesterId: Int, diaryId: Int, isCurrent: Boolean = false) { + private fun createSemester( + db: SupportSQLiteDatabase, + studentId: Int, + semesterId: Int, + diaryId: Int, + isCurrent: Boolean = false + ) { db.insert("Semesters", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply { put("student_id", studentId) put("diary_id", diaryId) @@ -206,7 +212,7 @@ class Migration13Test : AbstractMigrationTest() { put("semester_id", semesterId) put("semester_name", "1") put("is_current", isCurrent) - put("class_id", classId) + put("class_id", 5) put("unit_id", "99") }) } From 398bc513fb6e6e7284a1e1126f01af755ade050f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Jul 2023 10:00:31 +0000 Subject: [PATCH 797/869] Bump about_libraries from 10.8.0 to 10.8.3 (#2263) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index dba021521..9584caaca 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { kotlin_version = '1.9.0' - about_libraries = '10.8.0' + about_libraries = '10.8.3' hilt_version = "2.47" } repositories { From b2969264231a65109ed70b24c7636c0c3425802a Mon Sep 17 00:00:00 2001 From: Bartosz Bieniek Date: Tue, 25 Jul 2023 23:05:14 +0200 Subject: [PATCH 798/869] Timetable widget improvements (#2219) --- .../timetablewidget/TimetableWidgetFactory.kt | 8 +- .../TimetableWidgetProvider.kt | 270 +++++++++------ .../background_timetable_widget_avatar.xml | 2 +- .../background_widget_item_timetable.xml | 2 +- .../res/drawable/ic_timetable_widget_swap.xml | 4 +- .../main/res/drawable/ic_widget_chevron.xml | 4 +- .../drawable/img_timetable_widget_preview.png | Bin 21111 -> 76378 bytes .../layout-v31/widget_timetable_preview.xml | 321 ++++++++++++++++-- .../main/res/layout/item_widget_timetable.xml | 20 +- app/src/main/res/layout/widget_timetable.xml | 89 ++--- .../res/xml/provider_widget_timetable.xml | 10 +- 11 files changed, 524 insertions(+), 206 deletions(-) mode change 100755 => 100644 app/src/main/res/drawable/img_timetable_widget_preview.png diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt index 9c5abe1c2..d545413da 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt @@ -124,14 +124,12 @@ class TimetableWidgetFactory( val lessonStartTime = lesson.start.toFormattedString(TIME_FORMAT_STYLE) val lessonEndTime = lesson.end.toFormattedString(TIME_FORMAT_STYLE) - val roomText = "${context.getString(R.string.timetable_room)} ${lesson.room}" val remoteViews = RemoteViews(context.packageName, R.layout.item_widget_timetable).apply { setTextViewText(R.id.timetableWidgetItemNumber, lesson.number.toString()) setTextViewText(R.id.timetableWidgetItemTimeStart, lessonStartTime) setTextViewText(R.id.timetableWidgetItemTimeFinish, lessonEndTime) setTextViewText(R.id.timetableWidgetItemSubject, lesson.subject) - setTextViewText(R.id.timetableWidgetItemRoom, roomText) setTextViewText(R.id.timetableWidgetItemTeacher, lesson.teacher) setTextViewText(R.id.timetableWidgetItemDescription, lesson.info) setOnClickFillInIntent(R.id.timetableWidgetItemContainer, Intent()) @@ -140,6 +138,12 @@ class TimetableWidgetFactory( updateTheme() clearLessonStyles(remoteViews) + if (lesson.room.isBlank()) { + remoteViews.setViewVisibility(R.id.timetableWidgetItemRoom, GONE) + } else { + remoteViews.setTextViewText(R.id.timetableWidgetItemRoom, lesson.room) + } + when { lesson.canceled -> applyCancelledLessonStyles(remoteViews) lesson.changes or lesson.info.isNotBlank() -> applyChangedLessonStyles( diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt index 624ca30f4..cc48539a5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt @@ -8,6 +8,7 @@ import android.content.Context import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.graphics.Bitmap import android.widget.RemoteViews import androidx.appcompat.content.res.AppCompatResources import androidx.core.graphics.drawable.DrawableCompat @@ -76,110 +77,151 @@ class TimetableWidgetProvider : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { GlobalScope.launch { when (intent.action) { - ACTION_APPWIDGET_UPDATE -> onUpdate(context, intent) - ACTION_APPWIDGET_DELETED -> onDelete(intent) + ACTION_APPWIDGET_UPDATE -> onWidgetUpdate(context, intent) + ACTION_APPWIDGET_DELETED -> onWidgetDeleted(intent) } } } - private suspend fun onUpdate(context: Context, intent: Intent) { - if (intent.getStringExtra(EXTRA_BUTTON_TYPE) == null) { - val isFromConfigure = intent.getBooleanExtra(EXTRA_FROM_CONFIGURE, false) - val appWidgetIds = intent.getIntArrayExtra(EXTRA_APPWIDGET_IDS) ?: return + private suspend fun onWidgetUpdate(context: Context, intent: Intent) { + val pressedButton = intent.getPressedButton() - appWidgetIds.forEach { appWidgetId -> - val student = - getStudent(sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0), appWidgetId) - val savedDataEpochDay = sharedPref.getLong(getDateWidgetKey(appWidgetId), 0) - - val dateToLoad = if (isFromConfigure && savedDataEpochDay != 0L) { - LocalDate.ofEpochDay(savedDataEpochDay) - } else { - getWidgetDefaultDateToLoad(appWidgetId) - } - - updateWidget(context, appWidgetId, dateToLoad, student) - } + if (pressedButton == null) { + val updatedWidgetIds = intent.getWidgetIds() ?: return + updatedWidgetIds.forEach { updateWidgetLayout(context, it) } } else { - val buttonType = intent.getStringExtra(EXTRA_BUTTON_TYPE) - val toggledWidgetId = intent.getIntExtra(EXTRA_TOGGLED_WIDGET_ID, 0) - val student = getStudent( - sharedPref.getLong(getStudentWidgetKey(toggledWidgetId), 0), toggledWidgetId - ) - val savedDate = - LocalDate.ofEpochDay(sharedPref.getLong(getDateWidgetKey(toggledWidgetId), 0)) - val date = when (buttonType) { - BUTTON_RESET -> getWidgetDefaultDateToLoad(toggledWidgetId) - BUTTON_NEXT -> savedDate.nextSchoolDay - BUTTON_PREV -> savedDate.previousSchoolDay - else -> getWidgetDefaultDateToLoad(toggledWidgetId) - } - if (!buttonType.isNullOrBlank()) { - analytics.logEvent( - "changed_timetable_widget_day", "button" to buttonType - ) - } - updateWidget(context, toggledWidgetId, date, student) + val widgetId = intent.getToggledWidgetId() ?: return + reportChangedDay(pressedButton) + updateSavedWidgetDate(widgetId, pressedButton) + updateWidgetLayout(context, widgetId) } } - private fun onDelete(intent: Intent) { - val appWidgetId = intent.getIntExtra(EXTRA_APPWIDGET_ID, 0) + private fun Intent.getPressedButton(): String? { + return getStringExtra(EXTRA_BUTTON_TYPE) + } - if (appWidgetId != 0) { - with(sharedPref) { - delete(getStudentWidgetKey(appWidgetId)) - delete(getDateWidgetKey(appWidgetId)) - } + private fun Intent.getWidgetIds(): IntArray? { + return getIntArrayExtra(EXTRA_APPWIDGET_IDS) + } + + private fun Intent.getToggledWidgetId(): Int? { + val toggledWidgetId = getIntExtra(EXTRA_TOGGLED_WIDGET_ID, INVALID_APPWIDGET_ID) + return toggledWidgetId.takeIf { it != INVALID_APPWIDGET_ID } + } + + private fun reportChangedDay(buttonType: String) { + if (buttonType.isNotBlank()) { + analytics.logEvent("changed_timetable_widget_day", "button" to buttonType) } } - private fun updateWidget( - context: Context, appWidgetId: Int, date: LocalDate, student: Student? + private fun updateSavedWidgetDate(widgetId: Int, buttonType: String) { + val savedDate = getSavedWidgetDate(widgetId) + val newDate = savedDate?.let { getNewDate(it, widgetId, buttonType) } + ?: getWidgetDefaultDateToLoad(widgetId) + setWidgetDate(widgetId, newDate) + } + + private fun getSavedWidgetDate(widgetId: Int): LocalDate? { + val epochDay = sharedPref.getLong(getDateWidgetKey(widgetId), 0) + return if (epochDay == 0L) null else LocalDate.ofEpochDay(epochDay) + } + + private fun getNewDate( + currentDate: LocalDate, + widgetId: Int, + selectedButton: String + ): LocalDate { + return when (selectedButton) { + BUTTON_NEXT -> currentDate.nextSchoolDay + BUTTON_PREV -> currentDate.previousSchoolDay + else -> getWidgetDefaultDateToLoad(widgetId) + } + } + + private fun setWidgetDate(widgetId: Int, dateToSet: LocalDate) { + val widgetDateKey = getDateWidgetKey(widgetId) + sharedPref.putLong(widgetDateKey, dateToSet.toEpochDay(), true) + } + + private fun getWidgetDefaultDateToLoad(widgetId: Int): LocalDate { + val lastLessonEndDateTime = getLastLessonDateTime(widgetId) + + val todayDate = LocalDate.now() + val isLastLessonToday = lastLessonEndDateTime.toLocalDate() == todayDate + val isEndOfLessons = LocalDateTime.now() > lastLessonEndDateTime + + return if (isLastLessonToday && isEndOfLessons) { + todayDate.nextSchoolDay + } else { + todayDate.nextOrSameSchoolDay + } + } + + private fun getLastLessonDateTime(widgetId: Int): LocalDateTime { + val lastLessonTimestamp = sharedPref + .getLong(getTodayLastLessonEndDateTimeWidgetKey(widgetId), 0) + return LocalDateTime.ofEpochSecond(lastLessonTimestamp, 0, ZoneOffset.UTC) + } + + private suspend fun updateWidgetLayout( + context: Context, widgetId: Int ) { - val nextNavIntent = createNavIntent(context, appWidgetId, appWidgetId, BUTTON_NEXT) - val prevNavIntent = createNavIntent(context, -appWidgetId, appWidgetId, BUTTON_PREV) - val resetNavIntent = - createNavIntent(context, Int.MAX_VALUE - appWidgetId, appWidgetId, BUTTON_RESET) - val adapterIntent = Intent(context, TimetableWidgetService::class.java).apply { - putExtra(EXTRA_APPWIDGET_ID, appWidgetId) - action = appWidgetId.toString() //make Intent unique - } - val appIntent = PendingIntent.getActivity( - context, - TIMETABLE_PENDING_INTENT_ID, - SplashActivity.getStartIntent(context, Destination.Timetable()), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE - ) + val widgetRemoteViews = RemoteViews(context.packageName, R.layout.widget_timetable) + // Apply the click action intent + val appIntent = createPendingAppIntent(context) + widgetRemoteViews.setPendingIntentTemplate(R.id.timetableWidgetList, appIntent) + + // Display saved date + val date = getSavedWidgetDate(widgetId) ?: getWidgetDefaultDateToLoad(widgetId) val formattedDate = date.toFormattedString("EEE, dd.MM").capitalise() - val remoteView = RemoteViews(context.packageName, R.layout.widget_timetable).apply { - setEmptyView(R.id.timetableWidgetList, R.id.timetableWidgetEmpty) - setTextViewText(R.id.timetableWidgetDate, formattedDate) - setRemoteAdapter(R.id.timetableWidgetList, adapterIntent) + widgetRemoteViews.setTextViewText(R.id.timetableWidgetDate, formattedDate) + + // Apply intents to the date switcher buttons + val nextNavIntent = createNavButtonIntent(context, widgetId, widgetId, BUTTON_NEXT) + val prevNavIntent = createNavButtonIntent(context, -widgetId, widgetId, BUTTON_PREV) + val resetNavIntent = + createNavButtonIntent(context, Int.MAX_VALUE - widgetId, widgetId, BUTTON_RESET) + widgetRemoteViews.run { setOnClickPendingIntent(R.id.timetableWidgetNext, nextNavIntent) setOnClickPendingIntent(R.id.timetableWidgetPrev, prevNavIntent) setOnClickPendingIntent(R.id.timetableWidgetDate, resetNavIntent) - setPendingIntentTemplate(R.id.timetableWidgetList, appIntent) } - student?.let { - setupAccountView(context, student, remoteView, appWidgetId) + // Setup the lesson list adapter + val lessonListAdapterIntent = createLessonListAdapterIntent(context, widgetId) + // --- Ensure the selected date is stored in the shared preferences, + // --- on which the TimetableWidgetFactory relies + setWidgetDate(widgetId, date) + // --- + widgetRemoteViews.apply { + setEmptyView(R.id.timetableWidgetList, R.id.timetableWidgetEmpty) + setRemoteAdapter(R.id.timetableWidgetList, lessonListAdapterIntent) } - with(sharedPref) { - putLong(getDateWidgetKey(appWidgetId), date.toEpochDay(), true) + // Setup profile picture + getWidgetStudent(widgetId)?.let { student -> + setupAccountView(context, student, widgetRemoteViews, widgetId) } + // Apply updates with(appWidgetManager) { - partiallyUpdateAppWidget(appWidgetId, remoteView) - notifyAppWidgetViewDataChanged(appWidgetId, R.id.timetableWidgetList) + partiallyUpdateAppWidget(widgetId, widgetRemoteViews) + notifyAppWidgetViewDataChanged(widgetId, R.id.timetableWidgetList) } Timber.d("TimetableWidgetProvider updated") } - private fun createNavIntent( + private fun createPendingAppIntent(context: Context) = PendingIntent.getActivity( + context, TIMETABLE_PENDING_INTENT_ID, + SplashActivity.getStartIntent(context, Destination.Timetable()), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) + + private fun createNavButtonIntent( context: Context, code: Int, appWidgetId: Int, buttonType: String ) = PendingIntent.getBroadcast( context, code, Intent(context, TimetableWidgetProvider::class.java).apply { @@ -189,6 +231,17 @@ class TimetableWidgetProvider : BroadcastReceiver() { }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) + private fun createLessonListAdapterIntent(context: Context, widgetId: Int) = + Intent(context, TimetableWidgetService::class.java).apply { + putExtra(EXTRA_APPWIDGET_ID, widgetId) + action = widgetId.toString() //make Intent unique + } + + private suspend fun getWidgetStudent(widgetId: Int): Student? { + val studentId = sharedPref.getLong(getStudentWidgetKey(widgetId), 0) + return getStudent(studentId, widgetId) + } + private suspend fun getStudent(studentId: Long, appWidgetId: Int) = try { val students = studentRepository.getSavedStudents(false) val student = students.singleOrNull { it.student.id == studentId }?.student @@ -199,6 +252,7 @@ class TimetableWidgetProvider : BroadcastReceiver() { sharedPref.putLong(getStudentWidgetKey(appWidgetId), it.id) } } + else -> null } } catch (e: Exception) { @@ -208,60 +262,64 @@ class TimetableWidgetProvider : BroadcastReceiver() { null } - private fun getWidgetDefaultDateToLoad(appWidgetId: Int): LocalDate { - val lastLessonEndTimestamp = - sharedPref.getLong(getTodayLastLessonEndDateTimeWidgetKey(appWidgetId), 0) - val lastLessonEndDateTime = - LocalDateTime.ofEpochSecond(lastLessonEndTimestamp, 0, ZoneOffset.UTC) + private fun setupAccountView( + context: Context, student: Student, remoteViews: RemoteViews, widgetId: Int + ) { + val accountInitials = getAccountInitials(student.nickOrName) + val accountPickerPendingIntent = createAccountPickerPendingIntent(context, widgetId) - val todayDate = LocalDate.now() - val isLastLessonEndDateNow = lastLessonEndDateTime.toLocalDate() == todayDate - val isLastLessonEndDateAfterNowTime = LocalDateTime.now() > lastLessonEndDateTime + getAvatarBackgroundBitmap(context, student.avatarColor)?.let { + remoteViews.setImageViewBitmap(R.id.timetableWidgetAccountBackground, it) + } - return if (isLastLessonEndDateNow && isLastLessonEndDateAfterNowTime) { - todayDate.nextSchoolDay - } else { - todayDate.nextOrSameSchoolDay + remoteViews.apply { + setTextViewText(R.id.timetableWidgetAccountInitials, accountInitials) + setOnClickPendingIntent(R.id.timetableWidgetAccount, accountPickerPendingIntent) } } - private fun setupAccountView( - context: Context, - student: Student, - remoteViews: RemoteViews, - appWidgetId: Int - ) { - val accountInitials = student.nickOrName - .split(" ") - .mapNotNull { it.firstOrNull() }.take(2) - .joinToString(separator = "").uppercase() + private fun getAccountInitials(name: String): String { + val firstLetters = name.split(" ").mapNotNull { it.firstOrNull() } + return firstLetters.joinToString(separator = "").uppercase() + } - val accountPickerIntent = PendingIntent.getActivity( + private fun createAccountPickerPendingIntent(context: Context, widgetId: Int) = + PendingIntent.getActivity( context, - -Int.MAX_VALUE + appWidgetId, + -Int.MAX_VALUE + widgetId, Intent(context, TimetableWidgetConfigureActivity::class.java).apply { addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK) - putExtra(EXTRA_APPWIDGET_ID, appWidgetId) + putExtra(EXTRA_APPWIDGET_ID, widgetId) putExtra(EXTRA_FROM_PROVIDER, true) }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) - // Create background bitmap + private fun getAvatarBackgroundBitmap(context: Context, avatarColor: Long): Bitmap? { val avatarDrawableResource = R.drawable.background_timetable_widget_avatar - AppCompatResources.getDrawable(context, avatarDrawableResource)?.let { drawable -> + return AppCompatResources.getDrawable(context, avatarDrawableResource)?.let { drawable -> val screenDensity = context.resources.displayMetrics.density val avatarSize = (48 * screenDensity).toInt() - val backgroundBitmap = DrawableCompat.wrap(drawable).run { - DrawableCompat.setTint(this, student.avatarColor.toInt()) + DrawableCompat.wrap(drawable).run { + DrawableCompat.setTint(this, avatarColor.toInt()) toBitmap(avatarSize, avatarSize) } - remoteViews.setImageViewBitmap(R.id.timetableWidgetAccountBackground, backgroundBitmap) } + } - remoteViews.apply { - setTextViewText(R.id.timetableWidgetAccountInitials, accountInitials) - setOnClickPendingIntent(R.id.timetableWidgetAccount, accountPickerIntent) + private fun onWidgetDeleted(intent: Intent) { + val deletedWidgetId = intent.getWidgetId() + deleteWidgetPreferences(deletedWidgetId) + } + + private fun Intent.getWidgetId(): Int { + return getIntExtra(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID) + } + + private fun deleteWidgetPreferences(widgetId: Int) { + with(sharedPref) { + delete(getStudentWidgetKey(widgetId)) + delete(getDateWidgetKey(widgetId)) } } } diff --git a/app/src/main/res/drawable/background_timetable_widget_avatar.xml b/app/src/main/res/drawable/background_timetable_widget_avatar.xml index 7f64c4ebe..48298d675 100644 --- a/app/src/main/res/drawable/background_timetable_widget_avatar.xml +++ b/app/src/main/res/drawable/background_timetable_widget_avatar.xml @@ -2,5 +2,5 @@ - + diff --git a/app/src/main/res/drawable/background_widget_item_timetable.xml b/app/src/main/res/drawable/background_widget_item_timetable.xml index 096357584..510c70c0c 100644 --- a/app/src/main/res/drawable/background_widget_item_timetable.xml +++ b/app/src/main/res/drawable/background_widget_item_timetable.xml @@ -1,5 +1,5 @@ - + diff --git a/app/src/main/res/drawable/ic_timetable_widget_swap.xml b/app/src/main/res/drawable/ic_timetable_widget_swap.xml index 2f91489a9..09c50b4f1 100644 --- a/app/src/main/res/drawable/ic_timetable_widget_swap.xml +++ b/app/src/main/res/drawable/ic_timetable_widget_swap.xml @@ -1,6 +1,6 @@ 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 literal 21111 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 bi-Ccg@uSalw&_c=6hVAXS3j3^P6 @@ -16,23 +16,47 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_vertical" - android:orientation="horizontal" - android:paddingVertical="16dp"> + android:paddingTop="12dp" + android:paddingBottom="8dp"> + + + + + + + + - + android:paddingBottom="12dp"> - + android:background="@drawable/background_widget_item_timetable" + android:backgroundTint="?attr/colorSurface" + android:gravity="center_vertical" + android:minHeight="48dp" + android:orientation="horizontal" + android:paddingHorizontal="12dp" + android:paddingVertical="8dp"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + android:background="@drawable/background_widget_item_timetable" + android:backgroundTint="?attr/colorSurface" + android:gravity="center_vertical" + android:minHeight="48dp" + android:orientation="horizontal" + android:paddingHorizontal="12dp" + android:paddingVertical="8dp"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:layout_height="wrap_content" + android:background="@drawable/background_widget_item_timetable" + android:backgroundTint="?attr/colorSurface" + android:gravity="center_vertical" + android:minHeight="48dp" + android:orientation="horizontal" + android:paddingHorizontal="12dp" + android:paddingVertical="8dp"> + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_widget_timetable.xml b/app/src/main/res/layout/item_widget_timetable.xml index 27c9db66c..01f4525ec 100644 --- a/app/src/main/res/layout/item_widget_timetable.xml +++ b/app/src/main/res/layout/item_widget_timetable.xml @@ -6,11 +6,12 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/background_widget_item_timetable" - android:backgroundTint="?attr/backgroundColor" + android:backgroundTint="?attr/colorSurface" android:gravity="center_vertical" + android:minHeight="48dp" android:orientation="horizontal" - android:paddingHorizontal="16dp" - android:paddingVertical="12dp" + android:paddingHorizontal="12dp" + android:paddingVertical="8dp" android:theme="@style/Wulkanowy.Widget.Theme" tools:context=".ui.modules.timetablewidget.TimetableWidgetFactory"> @@ -18,15 +19,14 @@ android:id="@+id/timetableWidgetItemNumber" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textAppearance="?attr/textAppearanceHeadline6" - android:textSize="24sp" + android:textSize="22sp" tools:text="1" tools:textColor="?attr/colorTimetableChange" /> @@ -41,7 +41,7 @@ android:id="@+id/timetableWidgetItemTimeFinish" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="4dp" + android:layout_marginTop="2dp" android:textAppearance="?attr/textAppearanceBodySmall" tools:text="09:45" /> @@ -60,7 +60,7 @@ android:layout_height="wrap_content" android:ellipsize="end" android:lines="1" - android:textAppearance="?attr/textAppearanceTitleMedium" + android:textSize="14sp" tools:text="Programowanie aplikacji mobilnych i desktopowych" /> + tools:context=".ui.modules.timetablewidget.TimetableWidgetProvider" + tools:targetApi="s"> + android:paddingTop="12dp" + android:paddingBottom="8dp"> + + + + + + + + + android:textSize="18sp" + tools:text="Friday, 19.05" /> - - - - - - @@ -111,5 +114,7 @@ android:text="@string/widget_timetable_no_items" android:textAppearance="?attr/textAppearanceBody1" android:visibility="gone" /> + + diff --git a/app/src/main/res/xml/provider_widget_timetable.xml b/app/src/main/res/xml/provider_widget_timetable.xml index 3cdad0c81..555d8cb10 100644 --- a/app/src/main/res/xml/provider_widget_timetable.xml +++ b/app/src/main/res/xml/provider_widget_timetable.xml @@ -3,15 +3,15 @@ xmlns:tools="http://schemas.android.com/tools" android:configure="io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetConfigureActivity" android:initialLayout="@layout/widget_timetable" - android:minWidth="250dp" - android:minHeight="110dp" - android:minResizeWidth="250dp" - android:minResizeHeight="110dp" + android:minWidth="245dp" + android:minHeight="102dp" + android:minResizeWidth="245dp" + android:minResizeHeight="102dp" android:previewImage="@drawable/img_timetable_widget_preview" android:previewLayout="@layout/widget_timetable_preview" android:resizeMode="horizontal|vertical" android:targetCellWidth="3" - android:targetCellHeight="2" + android:targetCellHeight="3" android:updatePeriodMillis="3600000" android:widgetCategory="home_screen" tools:targetApi="s" /> From 91d7ee442edc87b045ae2693874d612bf0bcfe87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Wed, 26 Jul 2023 19:37:06 +0200 Subject: [PATCH 799/869] New Crowdin updates (#2257) --- .../res/values-it-rIT/preferences_values.xml | 65 ++ app/src/main/res/values-it-rIT/strings.xml | 747 ++++++++++++++++++ app/src/main/res/values-uk/strings.xml | 6 +- 3 files changed, 815 insertions(+), 3 deletions(-) create mode 100644 app/src/main/res/values-it-rIT/preferences_values.xml create mode 100644 app/src/main/res/values-it-rIT/strings.xml diff --git a/app/src/main/res/values-it-rIT/preferences_values.xml b/app/src/main/res/values-it-rIT/preferences_values.xml new file mode 100644 index 000000000..ac2b6e9e5 --- /dev/null +++ b/app/src/main/res/values-it-rIT/preferences_values.xml @@ -0,0 +1,65 @@ + + + + Light + Dark + Black (AMOLED) + + + System language + Polski + English + Pусский + Українська + Deutsch + Čeština + Slovenčina + + + 15 minutes + 30 minutes + 1 hour + 2 hours + 6 hours + 12 hours + 24 hours + + + 0,00 + 0,25 + 0,33 + 0,5 + 0,75 + + + Alphabetically + By date + By average + + + Dzienniczek+ + Wulkanowy + Grade colors in register + + + Up to 1 at once + Always expanded + Unlimited expansions + + + Average of grades only from selected semester + Average of averages from both semesters + Average of grades from the whole year + + + Lucky number + Unread messages + Attendance + Lessons + Grades + Homework + School announcements + Exams + Conferences + + diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml new file mode 100644 index 000000000..5c7d02a0f --- /dev/null +++ b/app/src/main/res/values-it-rIT/strings.xml @@ -0,0 +1,747 @@ + + + + Login + Wulkanowy + Grades + Attendance + Exams + Timetable + Settings + More + About + Log viewer + Debug + Notification debug + Contributors + Licenses + Messages + New message + New homework + Notes and achievements + Homework + Accounts manager + Select account + Account details + Student info + Dashboard + Notifications center + Menu configuartion + + Semester %1$d, %2$d/%3$d + + Sign in with the student or parent account + Enter the symbol from the register page for account: <b>%1$s</b> + Username + Email + Login, PESEL or e-mail + Password + UONET+ register variant + Custom domain suffix + Mobile API + Scraper + Hybrid + Token + PIN + Symbol + Sign in + Password too short + Login details are incorrect + %1$s. Make sure the correct UONET+ register variation is selected below + Invalid PIN + Invalid token + Token expired + Invalid email + Use the assigned login instead of email + Use the assigned login or email in @%1$s + Invalid symbol + Student not found. Validate the symbol and the chosen variation of the UONET+ register + Selected student is already logged in + The symbol can be found on the register page in Uczeń → Dostęp Mobilny → Wygeneruj kod dostępu.\n\nMake sure that you have set the appropriate register variant in the UONET+ register variant field on the first login screen + Select students to log in to the application + Other options + In this mode, a lucky number does not work, a class grade stats, summary of attendance, excuse for absence, completed lessons, school information and preview of the list of registered devices + This mode displays the same data as it appears on the register website + The combination of the best features of the other two modes. It works faster than scraper and provides features not available in the Mobile API mode. It is in the experimental phase + Privacy policy + Trouble signing in? Contact us! + Email + Discord + Send email + Make sure you select the correct UONET+ register variation! + I forgot my password + Recover your account + Recover + Student is already signed in + Standard + Other search locations + No active students found + Enter a different symbol + + Enable notifications + Enable notifications so you don\'t miss message from teacher or new grade + Skip + Enable + + Account manager + Log in + Session expired + Session expired, log in again + Application support + Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time + Enable ads + + Grade + Semester %d + Change semester + No grades + Weight + Weight: %s + Comment + Number of new ratings: %1$d + Average: %1$.2f + Points: %s + No average + Total points + Final grade + Predicted grade + Calculated average + How does Calculated Average work? + The Calculated Average is the arithmetic average calculated from the subjects averages. It allows you to know the approximate final average. It is calculated in a way selected by the user in the application settings. It is recommended that you choose the appropriate option. This is because the calculation of school averages differs. Additionally, if your school reports the average of the subjects on the Vulcan page, the application downloads them and does not calculate these averages. This can be changed by forcing the calculation of the average in the application settings.\n\nAverage of grades only from selected semester:\n1. Calculating the weighted average for each subject in a given semester\n2.Adding calculated averages\n3. Calculation of the arithmetic average of the summed averages\n\nAverage of averages from both semesters:\n1.Calculating the weighted average for each subject in semester 1 and 2\n2. Calculating the arithmetic average of the calculated averages for semesters 1 and 2 for each subject.\n3. Adding calculated averages\n4. Calculation of the arithmetic average of the summed averages\n\nAverage of grades from the whole year:\n1. Calculating weighted average over the year for each subject. The final average in the 1st semester is irrelevant.\n2. Adding calculated averages\n3. Calculating the arithmetic average of summed averages + How does the Final Average work? + The Final Average is the arithmetic average calculated from all currently available final grades in the given semester.\n\nThe calculation scheme consists of the following steps:\n1. Summing up the final grades given by teachers\n2. Divide by the number of subjects that have already been graded + Final average + from %1$d of %2$d subjects + Summary + Class + Mark as read + Partial + Semester + Points + Legend + Class average: %1$s + Your average: %1$s + Your grade: %1$s + Class + Student + + %d grade + %d grades + + + New grade + New grades + + + New predicted grade + New predicted grades + + + New final grade + New final grades + + + You received %1$d grade + You received %1$d grades + + + You received %1$d predicted grade + You received %1$d predicted grades + + + You received %1$d final grade + You received %1$d final grades + + + Lesson + Room + Group + Hours + Changes + No lessons this day + %s min + %s sec + %1$s left + in %1$s + Finished + Now: %s + Next: %s + Later: %s + %1$s lesson %2$d - %3$s + Change of room from %1$s to %2$s + Change of teacher from %1$s to %2$s + Change of subject from %1$s to %2$s + + Timetable change + Timetable changes + + + %1$s - %2$d change in timetable + %1$s - %2$d changes in timetable + + + %1$d change in timetable + %1$d changes in timetable + + + %d change + %d changes + + + Completed lessons + Show completed lessons + No info about completed lessons + Topic + Absence + Resources + + Additional lessons + Show additional lessons + No info about additional lessons + New lesson + New additional lesson + Additional lesson added successfully + Additional lesson deleted successfully + Repeat weekly + Delete additional lesson + Just this lesson + All in the series + Start time + End time + End time must be greater than start time + + Attendance summary + Absent for school reasons + Excused absence + Unexcused absence + Exemption + Excused lateness + Unexcused lateness + Present + Deleted + Unknown + Number of lesson + No entries + Absence reason (optional) + Send + Absence excuse request sent successfully! + You must select at least one absence! + Excuse + + New attendance + New attendance + + + %1$d new attendance + %1$d attendance + + + %d attendance + %d attendance + + + Total + + No exams this week + Type + Entry date + + New exam + New exams + + + %d new exam + %d new exams + + + %d exam + %d exams + + + Inbox + Sent + Trash + (no subject) + No messages + From: + To: + Date: %1$s + Reply + Forward + Select all + Unselect all + Move to trash + Delete permanently + Message deleted successfully + student + parent + guardian + employee + Share + Print + Subject + Content + Message sent successfully + Message does not exist + You need to choose at least 1 recipient + The message content must be at least 3 characters + All mailboxes + Only unread + Only with attachments + Read: %s + Read by: %1$d of %2$d people + + %1$d message + %1$d messages + + + New message + New messages + + Do you want to restore draft message? + Do you want to restore draft message with recipients: %s? + + You received %1$d message + You received %1$d messages + + + %1$d selected + %1$d selected + + Messages deleted + Choose mailbox + + No info about notes + Points + + %d note + %d notes + + + New note + New notes + + + You received %1$d note + You received %1$d notes + + + + %d praise + %d praises + + + New praise + New praises + + + You received %1$d praise + You received %1$d praises + + + + %d neutral note + %d neutral notes + + + New neutral note + New neutral notes + + + You received %1$d neutral note + You received %1$d neutral notes + + + No info about homework + Mark as done + Mark as undone + Add homework + Homework added successfully + Homework deleted successfully + Attachments + + New homework + New homework + + + You received %d new homework + You received %d new homework + + + %d homework + %d homework + + + Lucky number + Today\'s lucky number is + No info about the lucky number + Lucky number for today + Today\'s lucky number is: %s + Show history + + Lucky number history + No info about lucky numbers + + Mobile devices + No devices + Deregister + Device removed + QR code + Token + Symbol + PIN + + School and teachers + + School + No info about school + School name + School address + Telephone + Name of headmaster + Name of pedagogue + Show on map + Call + + Teachers + No info about teachers + No subject + + Conferences + No info about conferences + + %d conference + %d conferences + + + New conference + New conferences + + + You have %1$d new conference + You have %1$d new conferences + + Present at conference + Agenda + Place + Topic + + School announcements + No school announcements + + %d school announcement + %d school announcements + + + New school announcement + New school announcements + + + You have %1$d new school announcement + You have %1$d new school announcements + + + Add account + Logout + Do you want to log out this student? + Student logout + Student account + Parent account + Edit data + Accounts manager + Select student + Family + Contact + Residence details + Personal information + + App version + Contributors + List of Wulkanowy developers + Report a bug + Send a bug report via e-mail + FAQ + Read Frequently Asked Questions + Discord server + Join the Wulkanowy community + Facebook fanpage + Twitter page + Follow us on twitter + Like our facebook fanpage + Privacy policy + Rules for collecting personal data + System settings + Open system settings + Homepage + Visit the website and help develop the application + Licenses + Licenses of libraries used in the application + + License + + Avatar + See more on GitHub + + No info about student or student family + Name + Second name + Gender + Polish citizenship + Family name + Mother\'s and father\'s names + Phone + Cellphone + E-mail + Address of residence + Address of registration + Correspondence address + Surname and first name + Degree of kinship + Address + Phones + Male + Female + Last name + Guardian + + Nick + Add nick + Choose avatar color + + Share logs + Refresh + + Lessons + (Tomorrow) + (Today and tomorrow) + In a moment: + Soon: + First: + Now: + End of lessons + Next: + Later: + + %1$d more lesson + %1$d more lessons + + until %1$s + No upcoming lessons + An error occurred while loading the lessons + Homework + No homework to do + An error occurred while loading the homework + + %1$d more homework + %1$d more homework + + due %1$s + Last grades + No new grades + An error occurred while loading the grades + School announcements + No current announcements + An error occurred while loading the announcements + + %1$d more announcement + %1$d more announcements + + Exams + No upcoming exams + An error occurred while loading the exams + + %1$d more exam + %1$d more exams + + Conferences + No upcoming conferences + An error occurred while loading the conferences + + %1$d more conference + %1$d more conferences + + An error occurred while loading data + None + + Check for updates + Before reporting a bug, check first if an update with the bug fix is available + + Content + Retry + Description + No description + Teacher + Date + Entry date + Color + Details + Category + Close + No data + Subject + Prev + Next + Search + Search… + Yes + No + Save + Title + Add + Copied + Undo + Change + Add to calendar + Cancel + + No lessons + Synchronized on %1$s at %2$s + Choose theme + Light + Dark + System Theme + + App + Default view + Calculated average options + Force average calculation by app + Show presence + Theme + Grades expanding + Mark current lesson + Show groups next to subjects + Show chart list in class grades + Show subjects without grades + Grades color scheme + Subjects sorting + Language + Menu configuration + Set the order of functions in the menu + Notifications + Other + Show notifications + Show upcoming lesson notifications + Make upcoming lesson notification persistent + Turn off when notification is not showing in your watch/band + Open system notification settings + Fix synchronization & notifications issues + Your device may have data synchronization issues and with notifications.\n\nTo fix them, you need to add Wulkanowy to the autostart and turn off battery optimization/saving in the phone settings. + Show debug notifications + Synchronization is disabled + Official app notifications + Capture official app notifications + Remove official app notifications after capture + Capture notifications + With this feature you can gain a substitute of push notifications like in the official app. All you need to do is allow Wulkanowy to receive all notifications in your system settings.\n\nHow it works?\nWhen you get a notification in Dziennik VULCAN, Wulkanowy will be notified (that\'s what these extra permissions are for) and will trigger a sync so that can send its own notification.\n\nFOR ADVANCED USERS ONLY + Upcoming lesson notifications + You must allow the Wulkanowy app to set alarms and reminders in your system settings to use this feature. + Go to settings + Synchronization + Automatic update + Suspended on holidays + Updates interval + Wi-Fi only + Sync now + Synced! + Sync failed + Sync in progress + Last full sync: %s + Value of the plus + Value of the minus + Reply with message history + Show arithmetic average when no weights provided + Support + Privacy Policy + Agreements + Consent to processing of data related to ads + Show ads in app + Watch single ad to support project + Consent to data processing + To view an advertisement you must agree to the data processing terms of our Privacy Policy + Agree + Privacy policy + Ad is loading + Thank you for your support, come back later for more ads + Can we use your data to display ads? + You can change your choice anytime in the app settings. We may use your data to display ads tailored to you or, using less of your data, display non-personalized ads. Please see our Privacy Policy for details + Personalized ads + Non-personalized ads + I am over 18 years old + Yes, personalized ads + Yes, non-personalized ads + Advanced + Appearance & Behavior + Notifications + Synchronization + Advertisements + Grades + Dashboard + Tiles visibility + Attendance + Timetable + Grades + Calculated average + Messages + Appearance & Behavior + Languages, themes, subjects sorting + App notifications, fix problems + Notifications + Synchronization + Automatic update, synchronization interval + Plus and minus values, average calculation + Advanced + App version, contributors, social portals + Displaying advertisements, project support + + New grades + New homework + New conferences + New exams + Lucky number + New messages + New notes + New school announcements + Push notifications + Upcoming lessons + Debug + Timetable change + New attendance + + Black + Red + Blue + Green + Purple + No color + + Download of updates has started… + An update has just been downloaded. + Restart + Update failed! Wulkanowy may not function properly. Consider updating + + Application restart + The application must restart for the changes to be saved + Restart + + Authorization has been rejected. The data provided does not match the records in the secretary\'s office. + Invalid PESEL + PESEL + Authorize + Authorization completed successfully + Authorization + To operate the application, we need to confirm your identity. Please enter the student\'s PESEL <b>%1$s</b> in the field below + Skip for now + + No internet connection + An error occurred. Check your device clock + Connection to register failed. Servers can be overloaded. Please try again later + Loading data failed. Please try again later + Register password change required + Maintenance underway UONET + register. Try again later + Unknown UONET + register error. Try again later + Unknown application error. Please try again later + An unexpected error occurred + Feature disabled by your school + Feature not available. Login in a mode other than Mobile API + This field is required + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 30a587cc4..f89e38fcd 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -37,7 +37,7 @@ Логін, PESEL або e-mail Пароль Тип щоденника UONET+ - Custom domain suffix + Користувацький суфікс домену Мobile API Scraper Hybrid @@ -814,8 +814,8 @@ Авторизацію відхилено. Надані дані не збігаються із записами в кабінеті секретаря. Неправильний PESEL Число PESEL - Authorize - Authorization completed successfully + Авторизовать + Авторизація пройшла успішно Авторизувати Для роботи програми нам потрібно підтвердити вашу особу. Будь ласка, введіть число PESEL <b>%1$s</b> студента в поле нижче Поки що пропустити From 64cc24ae6012470b59c9e7038c2145147f09c29a Mon Sep 17 00:00:00 2001 From: Mateusz Idziejczak Date: Wed, 26 Jul 2023 22:17:58 +0200 Subject: [PATCH 800/869] Add incognito mode in messages (#1970) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mikołaj Pich --- .../data/repositories/MessageRepository.kt | 22 ++++++++++++++----- .../repositories/PreferencesRepository.kt | 6 +++++ .../ui/modules/message/MessageFragment.kt | 9 +++++++- .../ui/modules/message/MessagePresenter.kt | 13 ++++++++++- .../ui/modules/message/MessageView.kt | 3 +++ .../message/preview/MessagePreviewFragment.kt | 5 +++++ .../preview/MessagePreviewPresenter.kt | 13 ++++++++++- .../message/preview/MessagePreviewView.kt | 3 +++ .../modules/message/tab/MessageTabFragment.kt | 17 +++++++++----- .../message/tab/MessageTabPresenter.kt | 5 +++-- .../ui/modules/message/tab/MessageTabView.kt | 3 ++- .../main/res/values/preferences_defaults.xml | 1 + app/src/main/res/values/preferences_keys.xml | 1 + app/src/main/res/values/strings.xml | 5 +++++ .../res/xml/scheme_preferences_advanced.xml | 7 ++++++ 15 files changed, 96 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt index 53d9beadd..c8fccb23d 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt @@ -3,18 +3,26 @@ package io.github.wulkanowy.data.repositories import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R -import io.github.wulkanowy.data.* +import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.dao.MailboxDao import io.github.wulkanowy.data.db.dao.MessageAttachmentDao import io.github.wulkanowy.data.db.dao.MessagesDao -import io.github.wulkanowy.data.db.entities.* +import io.github.wulkanowy.data.db.entities.Mailbox +import io.github.wulkanowy.data.db.entities.Message +import io.github.wulkanowy.data.db.entities.MessageWithAttachment +import io.github.wulkanowy.data.db.entities.Recipient +import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED import io.github.wulkanowy.data.enums.MessageFolder.TRASHED import io.github.wulkanowy.data.mappers.mapFromEntities import io.github.wulkanowy.data.mappers.mapToEntities +import io.github.wulkanowy.data.networkBoundResource +import io.github.wulkanowy.data.onResourceError +import io.github.wulkanowy.data.onResourceSuccess import io.github.wulkanowy.data.pojos.MessageDraft +import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.domain.messages.GetMailboxByStudentUseCase import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.Folder @@ -25,7 +33,6 @@ import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.sync.Mutex -import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import timber.log.Timber @@ -97,7 +104,7 @@ class MessageRepository @Inject constructor( shouldFetch = { checkNotNull(it) { "This message no longer exist!" } Timber.d("Message content in db empty: ${it.message.content.isBlank()}") - it.message.unread || it.message.content.isBlank() + (it.message.unread && markAsRead) || it.message.content.isBlank() }, query = { messagesDb.loadMessageWithAttachment(message.messageGlobalKey) @@ -113,7 +120,10 @@ class MessageRepository @Inject constructor( messagesDb.updateAll( listOf(old.message.apply { id = message.id - unread = !markAsRead + unread = when { + markAsRead -> false + else -> unread + } sender = new.sender recipients = new.recipients.singleOrNull() ?: "Wielu adresatów" content = content.ifBlank { new.content } @@ -123,7 +133,7 @@ class MessageRepository @Inject constructor( items = new.attachments.mapToEntities(message.messageGlobalKey), ) - Timber.d("Message ${message.messageId} with blank content: ${old.message.content.isBlank()}, marked as read") + Timber.d("Message ${message.messageId} with blank content: ${old.message.content.isBlank()}, marked as read: $markAsRead") } ) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index 348a4054b..1b4893401 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -343,6 +343,12 @@ class PreferencesRepository @Inject constructor( ) } + var isIncognitoMode: Boolean + get() = getBoolean(R.string.pref_key_incognito_moge, R.bool.pref_default_incognito_mode) + set(value) = sharedPref.edit { + putBoolean(context.getString(R.string.pref_key_incognito_moge), value) + } + var installationId: String get() = sharedPref.getString(PREF_KEY_INSTALLATION_ID, null).orEmpty() private set(value) = sharedPref.edit { putString(PREF_KEY_INSTALLATION_ID, value) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt index 4317fb7fa..02bc13a16 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt @@ -11,7 +11,9 @@ import androidx.core.view.updateMargins import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R -import io.github.wulkanowy.data.enums.MessageFolder.* +import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED +import io.github.wulkanowy.data.enums.MessageFolder.SENT +import io.github.wulkanowy.data.enums.MessageFolder.TRASHED import io.github.wulkanowy.databinding.FragmentMessageBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter @@ -49,6 +51,7 @@ class MessageFragment : BaseFragment(R.layout.fragment_m override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentMessageBinding.bind(view) + messageContainer = binding.messageViewPager presenter.onAttachView(this) } @@ -95,6 +98,10 @@ class MessageFragment : BaseFragment(R.layout.fragment_m binding.messageProgress.visibility = if (show) VISIBLE else INVISIBLE } + override fun showMessage(messageId: Int) { + showMessage(getString(messageId)) + } + override fun showNewMessage(show: Boolean) { binding.openSendMessageButton.run { if (show) show() else hide() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt index cf6bad19e..37a2d422a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt @@ -1,5 +1,7 @@ package io.github.wulkanowy.ui.modules.message +import io.github.wulkanowy.R +import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler @@ -9,7 +11,8 @@ import javax.inject.Inject class MessagePresenter @Inject constructor( errorHandler: ErrorHandler, - studentRepository: StudentRepository + studentRepository: StudentRepository, + private val preferencesRepository: PreferencesRepository, ) : BasePresenter(errorHandler, studentRepository) { override fun onAttachView(view: MessageView) { @@ -19,6 +22,14 @@ class MessagePresenter @Inject constructor( Timber.i("Message view was initialized") loadData() } + + showIncognitoModeReminderMessage() + } + + private fun showIncognitoModeReminderMessage() { + if (preferencesRepository.isIncognitoMode) { + view?.showMessage(R.string.message_incognito_mode_on) + } } fun onPageSelected(index: Int) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageView.kt index def4a2751..7fdc6e181 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageView.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.ui.modules.message +import androidx.annotation.StringRes import io.github.wulkanowy.ui.base.BaseView interface MessageView : BaseView { @@ -12,6 +13,8 @@ interface MessageView : BaseView { fun showProgress(show: Boolean) + fun showMessage(@StringRes messageId: Int) + fun showNewMessage(show: Boolean) fun showTabLayout(show: Boolean) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt index 6c54d9fcb..3ed685cd7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt @@ -12,6 +12,7 @@ import android.view.View.VISIBLE import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient +import androidx.annotation.StringRes import androidx.core.content.getSystemService import androidx.core.os.bundleOf import androidx.recyclerview.widget.LinearLayoutManager @@ -164,6 +165,10 @@ class MessagePreviewFragment : binding.messagePreviewErrorRetry.setOnClickListener { callback() } } + override fun showMessage(@StringRes messageId: Int) { + showMessage(getString(messageId)) + } + override fun openMessageReply(message: Message?) { context?.let { it.startActivity(SendMessageActivity.getStartIntent(it, message, true)) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt index 56f23b6fa..cd7b72843 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt @@ -2,11 +2,13 @@ package io.github.wulkanowy.ui.modules.message.preview import android.annotation.SuppressLint import androidx.core.text.parseAsHtml +import io.github.wulkanowy.R import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.MessageAttachment import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.data.repositories.MessageRepository +import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler @@ -20,6 +22,7 @@ class MessagePreviewPresenter @Inject constructor( errorHandler: ErrorHandler, studentRepository: StudentRepository, private val messageRepository: MessageRepository, + private val preferencesRepository: PreferencesRepository, private val analytics: AnalyticsHelper ) : BasePresenter(errorHandler, studentRepository) { @@ -54,7 +57,11 @@ class MessagePreviewPresenter @Inject constructor( private fun loadData(messageToLoad: Message) { flatResourceFlow { val student = studentRepository.getCurrentStudent() - messageRepository.getMessage(student, messageToLoad, true) + messageRepository.getMessage( + student = student, + message = messageToLoad, + markAsRead = !preferencesRepository.isIncognitoMode, + ) } .logResourceStatus("message ${messageToLoad.messageId} preview") .onResourceData { @@ -65,6 +72,10 @@ class MessagePreviewPresenter @Inject constructor( setMessageWithAttachment(it) showContent(true) initOptions() + + if (preferencesRepository.isIncognitoMode && it.message.unread) { + showMessage(R.string.message_incognito_description) + } } } else { view?.run { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt index c5a947939..7f5f140b2 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.ui.modules.message.preview +import androidx.annotation.StringRes import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.MessageWithAttachment import io.github.wulkanowy.ui.base.BaseView @@ -43,4 +44,6 @@ interface MessagePreviewView : BaseView { fun popView() fun printDocument(html: String, jobName: String) + + fun showMessage(@StringRes messageId: Int) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt index 592cbd604..4364e8681 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt @@ -7,6 +7,7 @@ import android.view.MenuItem import android.view.View import android.view.View.* import android.widget.CompoundButton +import androidx.annotation.StringRes import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.SearchView import androidx.core.os.bundleOf @@ -134,14 +135,20 @@ class MessageTabFragment : BaseFragment(R.layout.frag } } + @Deprecated("Deprecated in Java") @Suppress("DEPRECATION") override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu, inflater) inflater.inflate(R.menu.action_menu_message_tab, menu) - val searchView = menu.findItem(R.id.action_search).actionView as SearchView - searchView.queryHint = getString(R.string.all_search_hint) - searchView.maxWidth = Int.MAX_VALUE + initializeSearchView(menu) + } + + private fun initializeSearchView(menu: Menu) { + val searchView = (menu.findItem(R.id.action_search).actionView as SearchView).apply { + queryHint = getString(R.string.all_search_hint) + maxWidth = Int.MAX_VALUE + } searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String) = false override fun onQueryTextChange(query: String): Boolean { @@ -207,8 +214,8 @@ class MessageTabFragment : BaseFragment(R.layout.frag binding.messageTabSwipe.isRefreshing = show } - override fun showMessagesDeleted() { - showMessage(getString(R.string.message_messages_deleted)) + override fun showMessage(@StringRes messageId: Int) { + showMessage(getString(messageId)) } override fun notifyParentShowNewMessage(show: Boolean) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt index ec92e9c20..90f93b145 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.ui.modules.message.tab +import io.github.wulkanowy.R import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.Mailbox import io.github.wulkanowy.data.db.entities.Message @@ -26,7 +27,7 @@ class MessageTabPresenter @Inject constructor( errorHandler: ErrorHandler, studentRepository: StudentRepository, private val messageRepository: MessageRepository, - private val analytics: AnalyticsHelper + private val analytics: AnalyticsHelper, ) : BasePresenter(errorHandler, studentRepository) { lateinit var folder: MessageFolder @@ -135,7 +136,7 @@ class MessageTabPresenter @Inject constructor( messageRepository.deleteMessages(student, selectedMailbox, messageList) } .onFailure(errorHandler::dispatch) - .onSuccess { view?.showMessagesDeleted() } + .onSuccess { view?.showMessage(R.string.message_messages_deleted) } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabView.kt index 6ece6621b..247af4342 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabView.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.ui.modules.message.tab +import androidx.annotation.StringRes import io.github.wulkanowy.data.db.entities.Mailbox import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.ui.base.BaseView @@ -26,7 +27,7 @@ interface MessageTabView : BaseView { fun showEmpty(show: Boolean) - fun showMessagesDeleted() + fun showMessage(@StringRes messageId: Int) fun showErrorView(show: Boolean) diff --git a/app/src/main/res/values/preferences_defaults.xml b/app/src/main/res/values/preferences_defaults.xml index 6c81100d9..fefd9b13c 100644 --- a/app/src/main/res/values/preferences_defaults.xml +++ b/app/src/main/res/values/preferences_defaults.xml @@ -38,4 +38,5 @@ false false + false diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index 716639c0f..e7fa542a3 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -39,5 +39,6 @@ ads_privacy_policy ads_consent_data_processing ads_over_eighteen + incognito_mode appearance_menu_order diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 98c316cb0..9dc7e7969 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -339,6 +339,8 @@ Messages deleted Choose mailbox + Incognito mode is on + Thanks to incognito mode sender is not notified when you read the message @@ -728,6 +730,9 @@ Value of the minus Reply with message history Show arithmetic average when no weights provided + Incognito mode + Do not inform about reading the message + Support Privacy Policy Agreements diff --git a/app/src/main/res/xml/scheme_preferences_advanced.xml b/app/src/main/res/xml/scheme_preferences_advanced.xml index 95f6f383a..8185de81c 100644 --- a/app/src/main/res/xml/scheme_preferences_advanced.xml +++ b/app/src/main/res/xml/scheme_preferences_advanced.xml @@ -53,5 +53,12 @@ app:key="@string/pref_key_fill_message_content" app:singleLineTitle="false" app:title="@string/pref_other_fill_message_content" /> + From 7f6a13a9ee8cae095855b9798b4220b5bd127299 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 17:35:29 +0000 Subject: [PATCH 801/869] Bump coroutines from 1.7.2 to 1.7.3 (#2267) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index f8603cc88..e188d709f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -187,7 +187,7 @@ ext { room = "2.5.2" chucker = "3.5.2" mockk = "1.13.5" - coroutines = "1.7.2" + coroutines = "1.7.3" } dependencies { From fc2adff997dd24f2fc75d3cf7bcc60a564bf3716 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 17:35:38 +0000 Subject: [PATCH 802/869] Bump androidx.fragment:fragment-ktx from 1.6.0 to 1.6.1 (#2269) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index e188d709f..d43b667cd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -202,7 +202,7 @@ dependencies { implementation 'androidx.core:core-splashscreen:1.0.1' implementation "androidx.activity:activity-ktx:1.7.2" implementation "androidx.appcompat:appcompat:1.6.1" - implementation "androidx.fragment:fragment-ktx:1.6.0" + implementation "androidx.fragment:fragment-ktx:1.6.1" implementation "androidx.annotation:annotation:1.6.0" implementation "androidx.preference:preference-ktx:1.2.0" From 0f129109ba25cace9a0e09cc4ed7d26668daa5ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 19:00:17 +0000 Subject: [PATCH 803/869] Bump com.android.tools.build:gradle from 8.0.2 to 8.1.0 (#2266) --- app/build.gradle | 26 ++++++++++++------------ build.gradle | 6 +++--- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d43b667cd..74ef02bc2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,3 +1,6 @@ +import com.github.triplet.gradle.androidpublisher.ReleaseStatus +import ru.cian.huawei.publish.ReleaseNote + apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlinx-serialization' @@ -17,7 +20,7 @@ apply from: 'hooks.gradle' android { namespace 'io.github.wulkanowy' - compileSdkVersion 33 + compileSdk 33 defaultConfig { applicationId "io.github.wulkanowy" @@ -78,7 +81,7 @@ android { } } - flavorDimensions "platform" + flavorDimensions += "platform" productFlavors { hms { @@ -117,20 +120,20 @@ android { } } - testOptions.unitTests { - includeAndroidResources = true + testOptions { + unitTests.includeAndroidResources = true // workaround HMS test errors https://github.com/robolectric/robolectric/issues/2750 - all { jvmArgs '-noverify' } + unitTests.all { jvmArgs '-noverify' } } compileOptions { coreLibraryDesugaringEnabled true - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "11" + jvmTarget = "17" freeCompilerArgs += ["-opt-in=kotlin.RequiresOptIn", "-Xjvm-default=all"] } @@ -152,14 +155,11 @@ kapt { ksp { arg("room.schemaLocation", "$projectDir/schemas".toString()) } -kotlin { - jvmToolchain(11) -} play { defaultToAppBundles = false track = 'production' - releaseStatus = com.github.triplet.gradle.androidpublisher.ReleaseStatus.IN_PROGRESS + releaseStatus = ReleaseStatus.IN_PROGRESS userFraction = 0.25d updatePriority = 1 enabled.set(false) @@ -172,7 +172,7 @@ huaweiPublish { buildFormat = "aab" deployType = "publish" releaseNotes = [ - new ru.cian.huawei.publish.ReleaseNote( + new ReleaseNote( "pl-PL", "$projectDir/src/main/play/release-notes/pl-PL/default.txt" ) diff --git a/build.gradle b/build.gradle index 9584caaca..c5a6f5984 100644 --- a/build.gradle +++ b/build.gradle @@ -14,12 +14,12 @@ buildscript { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$kotlin_version-1.0.11" - classpath 'com.android.tools.build:gradle:8.0.2' + classpath 'com.android.tools.build:gradle:8.1.0' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.15' classpath 'com.huawei.agconnect:agcp:1.9.1.300' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.7' - classpath "com.github.triplet.gradle:play-publisher:3.6.0" + classpath "com.github.triplet.gradle:play-publisher:3.8.4" classpath "ru.cian:huawei-publish-gradle-plugin:1.4.0" classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.3.0.3225" classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0" @@ -37,6 +37,6 @@ allprojects { } } -task clean(type: Delete) { +tasks.register('clean', Delete) { delete rootProject.buildDir } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8707e8b50..9b0a13f0f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 50326c7a48644d6f5997df130d8d88a1496d7188 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 19:00:37 +0000 Subject: [PATCH 804/869] Bump androidx.recyclerview:recyclerview from 1.3.0 to 1.3.1 (#2268) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 74ef02bc2..197ea3569 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -206,7 +206,7 @@ dependencies { implementation "androidx.annotation:annotation:1.6.0" implementation "androidx.preference:preference-ktx:1.2.0" - implementation "androidx.recyclerview:recyclerview:1.3.0" + implementation "androidx.recyclerview:recyclerview:1.3.1" implementation "androidx.viewpager2:viewpager2:1.1.0-beta02" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.constraintlayout:constraintlayout:2.1.4" From 74820f9571cb9cf6259f45b73409552277ae975a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Mon, 31 Jul 2023 21:32:07 +0200 Subject: [PATCH 805/869] New Crowdin updates (#2265) --- app/src/main/res/values-cs/strings.xml | 6 +++++- app/src/main/res/values-da-rDK/strings.xml | 4 ++++ app/src/main/res/values-de/strings.xml | 4 ++++ app/src/main/res/values-es-rES/strings.xml | 4 ++++ app/src/main/res/values-it-rIT/strings.xml | 4 ++++ app/src/main/res/values-pl/strings.xml | 4 ++++ app/src/main/res/values-ru/strings.xml | 4 ++++ app/src/main/res/values-sk/strings.xml | 6 +++++- app/src/main/res/values-uk/strings.xml | 4 ++++ 9 files changed, 38 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 964329dae..ff461d420 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -37,7 +37,7 @@ Přihlášení, číslo PESEL nebo e-mail Heslo Variace deníku UONET+ - Custom domain suffix + Vlastní přípona domény Mobile API Scraper Hybridní @@ -352,6 +352,8 @@ Zprávy odstraněné Vyberte poštovní schránku + Anonymní režim je zapnutý + Díky anonymnímu režimu není odesílatel upozorněn, když si zprávu přečtete Žádné informace o poznámkách Body @@ -738,6 +740,8 @@ Hodnota mínusu Odpovědět s historií zpráv Vypočítat aritmetický průměr, pokud žádná známka nemá váhu + Anonymní režim + Neinformovat o přečtení zprávy Podpora Ochrana osobních údajů Souhlasy diff --git a/app/src/main/res/values-da-rDK/strings.xml b/app/src/main/res/values-da-rDK/strings.xml index 5c7d02a0f..259a42647 100644 --- a/app/src/main/res/values-da-rDK/strings.xml +++ b/app/src/main/res/values-da-rDK/strings.xml @@ -310,6 +310,8 @@ Messages deleted Choose mailbox + Incognito mode is on + Thanks to incognito mode sender is not notified when you read the message No info about notes Points @@ -650,6 +652,8 @@ Value of the minus Reply with message history Show arithmetic average when no weights provided + Incognito mode + Do not inform about reading the message Support Privacy Policy Agreements diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 96423e35e..1836d0479 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -310,6 +310,8 @@ Nachrichten gelöscht Postfach auswählen + Incognito mode is on + Thanks to incognito mode sender is not notified when you read the message Keine Informationen über Eintragen Punkte @@ -650,6 +652,8 @@ Wert des Minus Antwort mit Nachrichtenhistorie Arithmetisches Mittel anzeigen, wenn keine Gewichte angegeben sind + Incognito mode + Do not inform about reading the message Unterstützung Datenschutz-Bestimmungen Vereinbarungen diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 5c7d02a0f..259a42647 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -310,6 +310,8 @@ Messages deleted Choose mailbox + Incognito mode is on + Thanks to incognito mode sender is not notified when you read the message No info about notes Points @@ -650,6 +652,8 @@ Value of the minus Reply with message history Show arithmetic average when no weights provided + Incognito mode + Do not inform about reading the message Support Privacy Policy Agreements diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml index 5c7d02a0f..259a42647 100644 --- a/app/src/main/res/values-it-rIT/strings.xml +++ b/app/src/main/res/values-it-rIT/strings.xml @@ -310,6 +310,8 @@ Messages deleted Choose mailbox + Incognito mode is on + Thanks to incognito mode sender is not notified when you read the message No info about notes Points @@ -650,6 +652,8 @@ Value of the minus Reply with message history Show arithmetic average when no weights provided + Incognito mode + Do not inform about reading the message Support Privacy Policy Agreements diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 2c7077977..0c1bbf785 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -352,6 +352,8 @@ Wiadomości zostały usunięte Wybierz skrzynkę + Tryb incognito jest włączony + Dzięki trybowi incognito nadawca nie zobaczy, że przeczytałeś tę wiadomość Brak informacji o uwagach Punkty @@ -738,6 +740,8 @@ Wartość minusa Odpowiadaj z historią wiadomości Licz średnią arytmetyczną, gdy żadna ocena nie ma wagi + Tryb incognito + Nie informuj o przeczytaniu wiadomości Wsparcie Polityka prywatności Zgody diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index eb8be0028..606971740 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -352,6 +352,8 @@ Сообщение удалено Выбрать почтовый ящик + Incognito mode is on + Thanks to incognito mode sender is not notified when you read the message Нет записей о замечаниях и свершениях Баллы @@ -738,6 +740,8 @@ Стоимость минуса Отвечать с историей сообщений Показывать среднее арифметическое при отсутствии стоимости + Incognito mode + Do not inform about reading the message Поддержка Политика приватности Соглашения diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index bcbd832ae..20d8818b3 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -37,7 +37,7 @@ Prihlásenie, číslo PESEL alebo e-mail Heslo Variácia denníka UONET+ - Custom domain suffix + Vlastná prípona domény Mobile API Scraper Hybridné @@ -352,6 +352,8 @@ Správy odstránené Vyberte poštovú schránku + Režim inkognito je zapnutý + Vďaka inkognito režimu nie je odosielateľ upozornený, keď si správu prečítate Žiadne informácie o poznámkach Body @@ -738,6 +740,8 @@ Hodnota mínusu Odpovedať s históriou správ Vypočítať aritmetický priemer, ak žiadna známka nemá váhu + Režim inkognito + Neinformovať o prečítaní správy Podpora Ochrana osobných údajov Súhlasy diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index f89e38fcd..db5c3cb0d 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -352,6 +352,8 @@ Листи видалено Вибрати поштову скриньку + Режим анонімності включено + Завдяки режиму анонімності, відправник не буде сповіщений коли ви прочитаєте повідомлення Немає інформації о зауваженнях Бали @@ -738,6 +740,8 @@ Вартість мінуса Відповісти з історією повідомлень Вилічити середню аритметичну, якщо оцінка немає вартості + Анонімний режим + Не повідомляти про прочитання повідомлення Підтримка Політика конфіденційності Угоди From 722b4e58126de65c05dcee6c048cd410a7db6db0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 21:19:33 +0000 Subject: [PATCH 806/869] Bump androidx.preference:preference-ktx from 1.2.0 to 1.2.1 (#2274) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 197ea3569..8ff342070 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -205,7 +205,7 @@ dependencies { implementation "androidx.fragment:fragment-ktx:1.6.1" implementation "androidx.annotation:annotation:1.6.0" - implementation "androidx.preference:preference-ktx:1.2.0" + implementation "androidx.preference:preference-ktx:1.2.1" implementation "androidx.recyclerview:recyclerview:1.3.1" implementation "androidx.viewpager2:viewpager2:1.1.0-beta02" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" From c4396036ce7790fb4c35714393b6235be3c36512 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 21:19:49 +0000 Subject: [PATCH 807/869] Bump com.google.firebase:firebase-bom from 32.2.0 to 32.2.2 (#2271) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 8ff342070..5f148144a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -247,7 +247,7 @@ dependencies { implementation 'com.fredporciuncula:flow-preferences:1.9.1' implementation 'org.apache.commons:commons-text:1.10.0' - playImplementation platform('com.google.firebase:firebase-bom:32.2.0') + playImplementation platform('com.google.firebase:firebase-bom:32.2.2') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' From e21c17ea99005a578d47f101b020ce1faa9b9985 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 21:20:05 +0000 Subject: [PATCH 808/869] Bump com.google.firebase:firebase-crashlytics-gradle from 2.9.7 to 2.9.8 (#2270) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c5a6f5984..2b52c068b 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ buildscript { classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.15' classpath 'com.huawei.agconnect:agcp:1.9.1.300' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.7' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.8' classpath "com.github.triplet.gradle:play-publisher:3.8.4" classpath "ru.cian:huawei-publish-gradle-plugin:1.4.0" classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.3.0.3225" From 8fbe341607467ea821e3b58b38193af2fe7fc639 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 21:20:19 +0000 Subject: [PATCH 809/869] Bump com.huawei.hms:hianalytics from 6.10.0.302 to 6.10.0.303 (#2272) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 5f148144a..824c8376d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -256,7 +256,7 @@ dependencies { playImplementation 'com.google.android.play:core-ktx:1.8.1' playImplementation 'com.google.android.gms:play-services-ads:22.2.0' - hmsImplementation 'com.huawei.hms:hianalytics:6.10.0.302' + hmsImplementation 'com.huawei.hms:hianalytics:6.10.0.303' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.1.300' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 7d5a29d4053fecdf7e1b1c6e4cf0d7cdf554789a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 21:20:45 +0000 Subject: [PATCH 810/869] Bump org.gradle.toolchains.foojay-resolver-convention (#2276) --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index af9bb737a..16731297d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,4 @@ plugins { - id 'org.gradle.toolchains.foojay-resolver-convention' version '0.6.0' + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.7.0' } include ':app' From 024ca897084bf1b939afb747ec4be1220d89ace8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 21:39:04 +0000 Subject: [PATCH 811/869] Bump mockk from 1.13.5 to 1.13.7 (#2275) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 824c8376d..37b165c17 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -186,7 +186,7 @@ ext { android_hilt = "1.0.0" room = "2.5.2" chucker = "3.5.2" - mockk = "1.13.5" + mockk = "1.13.7" coroutines = "1.7.3" } From 533157709b3db6ea03827b75660a1c77761cac7f Mon Sep 17 00:00:00 2001 From: Antoni Paduch <70513486+janAte1@users.noreply.github.com> Date: Tue, 22 Aug 2023 23:47:12 +0200 Subject: [PATCH 812/869] Add option to show empty tiles in the timetable (#2236) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mikołaj Pich --- app/src/main/assets/contributors.json | 4 + .../wulkanowy/data/enums/TimetableGapsMode.kt | 11 ++ .../repositories/PreferencesRepository.kt | 9 +- .../widgets/TimetableWidgetService.kt | 12 +- .../ui/modules/timetable/TimetableAdapter.kt | 47 ++++++-- .../ui/modules/timetable/TimetableItem.kt | 6 + .../modules/timetable/TimetablePresenter.kt | 71 +++++++++--- .../timetablewidget/TimetableWidgetFactory.kt | 107 ++++++++++++++---- .../timetablewidget/TimetableWidgetItem.kt | 26 +++++ .../main/res/layout/item_timetable_empty.xml | 43 +++++++ .../layout/item_widget_timetable_empty.xml | 36 ++++++ .../main/res/values-pl/preferences_values.xml | 5 + app/src/main/res/values-pl/strings.xml | 7 ++ .../main/res/values/preferences_defaults.xml | 1 + app/src/main/res/values/preferences_keys.xml | 1 + .../main/res/values/preferences_values.xml | 11 ++ app/src/main/res/values/strings.xml | 5 + .../res/xml/scheme_preferences_appearance.xml | 8 ++ 18 files changed, 363 insertions(+), 47 deletions(-) create mode 100644 app/src/main/java/io/github/wulkanowy/data/enums/TimetableGapsMode.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetItem.kt create mode 100644 app/src/main/res/layout/item_timetable_empty.xml create mode 100644 app/src/main/res/layout/item_widget_timetable_empty.xml diff --git a/app/src/main/assets/contributors.json b/app/src/main/assets/contributors.json index b2849931a..a7629c22f 100644 --- a/app/src/main/assets/contributors.json +++ b/app/src/main/assets/contributors.json @@ -50,5 +50,9 @@ { "displayName": "Tomasz F.", "githubUsername": "Pengwius" + }, + { + "displayName": "Antoni Paduch", + "githubUsername": "janAte1" } ] diff --git a/app/src/main/java/io/github/wulkanowy/data/enums/TimetableGapsMode.kt b/app/src/main/java/io/github/wulkanowy/data/enums/TimetableGapsMode.kt new file mode 100644 index 000000000..c8310c02d --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/enums/TimetableGapsMode.kt @@ -0,0 +1,11 @@ +package io.github.wulkanowy.data.enums + +enum class TimetableGapsMode(val value: String) { + NO_GAPS("no_gaps"), + BETWEEN_LESSONS("between"), + BETWEEN_AND_BEFORE_LESSONS("before_and_between"); + + companion object { + fun getByValue(value: String) = entries.find { it.value == value } ?: BETWEEN_LESSONS + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index 1b4893401..85c74072e 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -15,7 +15,6 @@ import io.github.wulkanowy.ui.modules.grade.GradeAverageMode import io.github.wulkanowy.ui.modules.settings.appearance.menuorder.AppMenuItem import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import java.time.Instant @@ -201,6 +200,14 @@ class PreferencesRepository @Inject constructor( R.bool.pref_default_timetable_show_timers ) + val showTimetableGaps: TimetableGapsMode + get() = TimetableGapsMode.getByValue( + getString( + R.string.pref_key_timetable_show_gaps, + R.string.pref_default_timetable_show_gaps + ) + ) + val showSubjectsWithoutGrades: Boolean get() = getBoolean( R.string.pref_key_subjects_without_grades, diff --git a/app/src/main/java/io/github/wulkanowy/services/widgets/TimetableWidgetService.kt b/app/src/main/java/io/github/wulkanowy/services/widgets/TimetableWidgetService.kt index d48556fa6..ffdb07ecd 100644 --- a/app/src/main/java/io/github/wulkanowy/services/widgets/TimetableWidgetService.kt +++ b/app/src/main/java/io/github/wulkanowy/services/widgets/TimetableWidgetService.kt @@ -4,6 +4,7 @@ import android.content.Intent import android.widget.RemoteViewsService import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.data.db.SharedPrefProvider +import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.TimetableRepository @@ -26,10 +27,19 @@ class TimetableWidgetService : RemoteViewsService() { @Inject lateinit var sharedPref: SharedPrefProvider + @Inject + lateinit var prefRepository: PreferencesRepository + override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory { Timber.d("TimetableWidgetFactory created") return TimetableWidgetFactory( - timetableRepo, studentRepo, semesterRepo, sharedPref, applicationContext, intent + timetableRepository = timetableRepo, + studentRepository = studentRepo, + semesterRepository = semesterRepo, + sharedPref = sharedPref, + prefRepository = prefRepository, + context = applicationContext, + intent = intent, ) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt index d917e7d51..1201937c0 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt @@ -12,7 +12,9 @@ import androidx.recyclerview.widget.RecyclerView import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.databinding.ItemTimetableBinding +import io.github.wulkanowy.databinding.ItemTimetableEmptyBinding import io.github.wulkanowy.databinding.ItemTimetableSmallBinding +import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.toFormattedString import javax.inject.Inject @@ -29,9 +31,14 @@ class TimetableAdapter @Inject constructor() : TimetableItemType.SMALL -> SmallViewHolder( ItemTimetableSmallBinding.inflate(inflater, parent, false) ) + TimetableItemType.NORMAL -> NormalViewHolder( ItemTimetableBinding.inflate(inflater, parent, false) ) + + TimetableItemType.EMPTY -> EmptyViewHolder( + ItemTimetableEmptyBinding.inflate(inflater, parent, false) + ) } } @@ -40,12 +47,12 @@ class TimetableAdapter @Inject constructor() : position: Int, payloads: MutableList ) { - if (payloads.isEmpty()) return super.onBindViewHolder(holder, position, payloads) - - if (holder is NormalViewHolder) updateTimeLeft( - binding = holder.binding, - timeLeft = (getItem(position) as TimetableItem.Normal).timeLeft, - ) + if (payloads.isNotEmpty() && holder is NormalViewHolder) { + updateTimeLeft( + binding = holder.binding, + timeLeft = (getItem(position) as TimetableItem.Normal).timeLeft, + ) + } else super.onBindViewHolder(holder, position, payloads) } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { @@ -54,10 +61,16 @@ class TimetableAdapter @Inject constructor() : binding = holder.binding, item = getItem(position) as TimetableItem.Small, ) + is NormalViewHolder -> bindNormalView( binding = holder.binding, item = getItem(position) as TimetableItem.Normal, ) + + is EmptyViewHolder -> bindEmptyView( + binding = holder.binding, + item = getItem(position) as TimetableItem.Empty, + ) } } @@ -100,6 +113,19 @@ class TimetableAdapter @Inject constructor() : } } + private fun bindEmptyView(binding: ItemTimetableEmptyBinding, item: TimetableItem.Empty) { + with(binding) { + timetableEmptyItemNumber.text = when (item.numFrom) { + item.numTo -> item.numFrom.toString() + else -> "${item.numFrom}-${item.numTo}" + } + timetableEmptyItemSubject.text = timetableEmptyItemSubject.context.getPlural( + R.plurals.timetable_no_lesson, + item.numTo - item.numFrom + 1 + ) + } + } + private fun updateTimeLeft(binding: ItemTimetableBinding, timeLeft: TimeLeft?) { with(binding) { when { @@ -137,6 +163,7 @@ class TimetableAdapter @Inject constructor() : timetableItemTimeLeft.visibility = VISIBLE timetableItemTimeLeft.text = root.context.getString(R.string.timetable_finished) } + else -> { timetableItemTimeUntil.visibility = GONE timetableItemTimeLeft.visibility = GONE @@ -191,7 +218,8 @@ class TimetableAdapter @Inject constructor() : ) } else { timetableItemDescription.visibility = GONE - timetableItemRoom.isVisible = lesson.room.isNotBlank() || lesson.roomOld.isNotBlank() + timetableItemRoom.isVisible = + lesson.room.isNotBlank() || lesson.roomOld.isNotBlank() timetableItemGroup.isVisible = item.showGroupsInPlan && lesson.group.isNotBlank() timetableItemTeacher.visibility = VISIBLE } @@ -274,6 +302,9 @@ class TimetableAdapter @Inject constructor() : private class SmallViewHolder(val binding: ItemTimetableSmallBinding) : RecyclerView.ViewHolder(binding.root) + private class EmptyViewHolder(val binding: ItemTimetableEmptyBinding) : + RecyclerView.ViewHolder(binding.root) + companion object { private val differ = object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: TimetableItem, newItem: TimetableItem): Boolean = @@ -281,9 +312,11 @@ class TimetableAdapter @Inject constructor() : oldItem is TimetableItem.Small && newItem is TimetableItem.Small -> { oldItem.lesson.start == newItem.lesson.start } + oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal -> { oldItem.lesson.start == newItem.lesson.start } + else -> oldItem == newItem } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableItem.kt index 92716ace8..105ece38c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableItem.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableItem.kt @@ -16,6 +16,11 @@ sealed class TimetableItem(val type: TimetableItemType) { val timeLeft: TimeLeft?, val onClick: (Timetable) -> Unit, ) : TimetableItem(TimetableItemType.NORMAL) + + data class Empty( + val numFrom: Int, + val numTo: Int + ) : TimetableItem(TimetableItemType.EMPTY) } data class TimeLeft( @@ -27,4 +32,5 @@ data class TimeLeft( enum class TimetableItemType { SMALL, NORMAL, + EMPTY } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt index d06874082..0f8395ded 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt @@ -1,23 +1,44 @@ package io.github.wulkanowy.ui.modules.timetable -import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.Timetable +import io.github.wulkanowy.data.enums.TimetableGapsMode.BETWEEN_AND_BEFORE_LESSONS +import io.github.wulkanowy.data.enums.TimetableGapsMode.NO_GAPS import io.github.wulkanowy.data.enums.TimetableMode +import io.github.wulkanowy.data.flatResourceFlow +import io.github.wulkanowy.data.logResourceStatus +import io.github.wulkanowy.data.onResourceData +import io.github.wulkanowy.data.onResourceError +import io.github.wulkanowy.data.onResourceIntermediate +import io.github.wulkanowy.data.onResourceNotLoading +import io.github.wulkanowy.data.onResourceSuccess import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.TimetableRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler -import io.github.wulkanowy.utils.* +import io.github.wulkanowy.utils.AnalyticsHelper +import io.github.wulkanowy.utils.capitalise +import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday +import io.github.wulkanowy.utils.isHolidays +import io.github.wulkanowy.utils.isJustFinished +import io.github.wulkanowy.utils.isShowTimeUntil +import io.github.wulkanowy.utils.left +import io.github.wulkanowy.utils.nextOrSameSchoolDay +import io.github.wulkanowy.utils.nextSchoolDay +import io.github.wulkanowy.utils.previousSchoolDay +import io.github.wulkanowy.utils.toFormattedString +import io.github.wulkanowy.utils.until import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onEach import timber.log.Timber import java.time.Instant import java.time.LocalDate -import java.time.LocalDate.* -import java.util.* +import java.time.LocalDate.now +import java.time.LocalDate.of +import java.time.LocalDate.ofEpochDay +import java.util.Timer import javax.inject.Inject import kotlin.concurrent.timer @@ -192,16 +213,38 @@ class TimetablePresenter @Inject constructor( compareBy({ item -> item.number }, { item -> !item.isStudentPlan }) ) - return filteredItems.mapIndexed { i, it -> - if (it.isStudentPlan) TimetableItem.Normal( - lesson = it, - showGroupsInPlan = prefRepository.showGroupsInPlan, - timeLeft = filteredItems.getTimeLeftForLesson(it, i), - onClick = ::onTimetableItemSelected - ) else TimetableItem.Small( - lesson = it, - onClick = ::onTimetableItemSelected - ) + var prevNum = when (prefRepository.showTimetableGaps) { + BETWEEN_AND_BEFORE_LESSONS -> 0 + else -> null + } + return buildList { + filteredItems.forEachIndexed { i, it -> + if (prefRepository.showTimetableGaps != NO_GAPS && prevNum != null && it.number > prevNum!! + 1) { + val emptyLesson = TimetableItem.Empty( + numFrom = prevNum!! + 1, + numTo = it.number - 1 + ) + add(emptyLesson) + } + + if (it.isStudentPlan) { + val normalLesson = TimetableItem.Normal( + lesson = it, + showGroupsInPlan = prefRepository.showGroupsInPlan, + timeLeft = filteredItems.getTimeLeftForLesson(it, i), + onClick = ::onTimetableItemSelected + ) + add(normalLesson) + } else { + val smallLesson = TimetableItem.Small( + lesson = it, + onClick = ::onTimetableItemSelected + ) + add(smallLesson) + } + + prevNum = it.number + } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt index d545413da..4e0578e2b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt @@ -16,6 +16,9 @@ import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Timetable +import io.github.wulkanowy.data.enums.TimetableGapsMode.BETWEEN_AND_BEFORE_LESSONS +import io.github.wulkanowy.data.enums.TimetableGapsMode.NO_GAPS +import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.TimetableRepository @@ -24,6 +27,7 @@ import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Co import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getStudentWidgetKey import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getTodayLastLessonEndDateTimeWidgetKey import io.github.wulkanowy.utils.getCompatColor +import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import kotlinx.coroutines.runBlocking import timber.log.Timber @@ -35,11 +39,12 @@ class TimetableWidgetFactory( private val studentRepository: StudentRepository, private val semesterRepository: SemesterRepository, private val sharedPref: SharedPrefProvider, + private val prefRepository: PreferencesRepository, private val context: Context, private val intent: Intent? ) : RemoteViewsService.RemoteViewsFactory { - private var lessons = emptyList() + private var items = emptyList() private var timetableCanceledColor: Int? = null @@ -47,18 +52,13 @@ class TimetableWidgetFactory( private var timetableChangeColor: Int? = null - private var lastSyncInstant: Instant? = null - override fun getLoadingView() = null override fun hasStableIds() = true - override fun getCount() = when { - lessons.isEmpty() -> 0 - else -> lessons.size + 1 - } + override fun getCount() = items.size - override fun getViewTypeCount() = 2 + override fun getViewTypeCount() = 3 override fun getItemId(position: Int) = position.toLong() @@ -75,9 +75,10 @@ class TimetableWidgetFactory( runBlocking { val student = getStudent(studentId) ?: return@runBlocking val semester = semesterRepository.getCurrentSemester(student) - lessons = getLessons(student, semester, date) - lastSyncInstant = - timetableRepository.getLastRefreshTimestamp(semester, date, date) + items = createItems( + lessons = getLessons(student, semester, date), + lastSync = timetableRepository.getLastRefreshTimestamp(semester, date, date) + ) if (date == LocalDate.now()) { updateTodayLastLessonEnd(appWidgetId) } @@ -101,8 +102,33 @@ class TimetableWidgetFactory( return lessons.sortedBy { it.number } } + private fun createItems( + lessons: List, + lastSync: Instant?, + ): List { + var prevNum = when (prefRepository.showTimetableGaps) { + BETWEEN_AND_BEFORE_LESSONS -> 0 + else -> null + } + return buildList { + lessons.forEach { + if (prefRepository.showTimetableGaps != NO_GAPS && prevNum != null && it.number > prevNum!! + 1) { + val emptyItem = TimetableWidgetItem.Empty( + numFrom = prevNum!! + 1, + numTo = it.number - 1 + ) + add(emptyItem) + } + add(TimetableWidgetItem.Normal(it)) + prevNum = it.number + } + add(TimetableWidgetItem.Synchronized(lastSync ?: Instant.MIN)) + } + } + private fun updateTodayLastLessonEnd(appWidgetId: Int) { - val todayLastLessonEnd = lessons.maxOfOrNull { it.end } ?: return + val todayLastLessonEnd = items.filterIsInstance() + .maxOfOrNull { it.lesson.end } ?: return val key = getTodayLastLessonEndDateTimeWidgetKey(appWidgetId) sharedPref.putLong(key, todayLastLessonEnd.epochSecond, true) } @@ -112,15 +138,15 @@ class TimetableWidgetFactory( } override fun getViewAt(position: Int): RemoteViews? { - if (position == lessons.size) { - val synchronizationInstant = lastSyncInstant ?: Instant.MIN - val synchronizationText = getSynchronizationInfoText(synchronizationInstant) - return RemoteViews(context.packageName, R.layout.item_widget_timetable_footer).apply { - setTextViewText(R.id.timetableWidgetSynchronizationTime, synchronizationText) - } + return when (val item = items.getOrNull(position) ?: return null) { + is TimetableWidgetItem.Normal -> getNormalItemRemoteView(item) + is TimetableWidgetItem.Empty -> getEmptyItemRemoteView(item) + is TimetableWidgetItem.Synchronized -> getSynchronizedItemRemoteView(item) } + } - val lesson = lessons.getOrNull(position) ?: return null + private fun getNormalItemRemoteView(item: TimetableWidgetItem.Normal): RemoteViews { + val lesson = item.lesson val lessonStartTime = lesson.start.toFormattedString(TIME_FORMAT_STYLE) val lessonEndTime = lesson.end.toFormattedString(TIME_FORMAT_STYLE) @@ -130,30 +156,63 @@ class TimetableWidgetFactory( setTextViewText(R.id.timetableWidgetItemTimeStart, lessonStartTime) setTextViewText(R.id.timetableWidgetItemTimeFinish, lessonEndTime) setTextViewText(R.id.timetableWidgetItemSubject, lesson.subject) + setTextViewText(R.id.timetableWidgetItemTeacher, lesson.teacher) setTextViewText(R.id.timetableWidgetItemDescription, lesson.info) setOnClickFillInIntent(R.id.timetableWidgetItemContainer, Intent()) } - updateTheme() clearLessonStyles(remoteViews) - if (lesson.room.isBlank()) { remoteViews.setViewVisibility(R.id.timetableWidgetItemRoom, GONE) } else { remoteViews.setTextViewText(R.id.timetableWidgetItemRoom, lesson.room) } - when { lesson.canceled -> applyCancelledLessonStyles(remoteViews) lesson.changes or lesson.info.isNotBlank() -> applyChangedLessonStyles( - remoteViews, lesson + remoteViews = remoteViews, + lesson = lesson, ) } - return remoteViews } + private fun getEmptyItemRemoteView(item: TimetableWidgetItem.Empty): RemoteViews { + return RemoteViews( + context.packageName, + R.layout.item_widget_timetable_empty + ).apply { + setTextViewText( + R.id.timetableWidgetEmptyItemNumber, + when (item.numFrom) { + item.numTo -> item.numFrom.toString() + else -> "${item.numFrom}-${item.numTo}" + } + ) + setTextViewText( + R.id.timetableWidgetEmptyItemText, + context.getPlural( + R.plurals.timetable_no_lesson, + item.numTo - item.numFrom + 1 + ) + ) + setOnClickFillInIntent(R.id.timetableWidgetEmptyItemContainer, Intent()) + } + } + + private fun getSynchronizedItemRemoteView(item: TimetableWidgetItem.Synchronized): RemoteViews { + return RemoteViews( + context.packageName, + R.layout.item_widget_timetable_footer + ).apply { + setTextViewText( + R.id.timetableWidgetSynchronizationTime, + getSynchronizationInfoText(item.timestamp) + ) + } + } + private fun updateTheme() { when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { Configuration.UI_MODE_NIGHT_YES -> { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetItem.kt new file mode 100644 index 000000000..166b1a8fb --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetItem.kt @@ -0,0 +1,26 @@ +package io.github.wulkanowy.ui.modules.timetablewidget + +import io.github.wulkanowy.data.db.entities.Timetable +import java.time.Instant + +sealed class TimetableWidgetItem(val type: TimetableWidgetItemType) { + + data class Normal( + val lesson: Timetable, + ) : TimetableWidgetItem(TimetableWidgetItemType.NORMAL) + + data class Empty( + val numFrom: Int, + val numTo: Int + ) : TimetableWidgetItem(TimetableWidgetItemType.EMPTY) + + data class Synchronized( + val timestamp: Instant, + ) : TimetableWidgetItem(TimetableWidgetItemType.SYNCHRONIZED) +} + +enum class TimetableWidgetItemType { + NORMAL, + EMPTY, + SYNCHRONIZED, +} diff --git a/app/src/main/res/layout/item_timetable_empty.xml b/app/src/main/res/layout/item_timetable_empty.xml new file mode 100644 index 000000000..12fddb752 --- /dev/null +++ b/app/src/main/res/layout/item_timetable_empty.xml @@ -0,0 +1,43 @@ + + + + + + + diff --git a/app/src/main/res/layout/item_widget_timetable_empty.xml b/app/src/main/res/layout/item_widget_timetable_empty.xml new file mode 100644 index 000000000..a48b3645e --- /dev/null +++ b/app/src/main/res/layout/item_widget_timetable_empty.xml @@ -0,0 +1,36 @@ + + + + + + + + diff --git a/app/src/main/res/values-pl/preferences_values.xml b/app/src/main/res/values-pl/preferences_values.xml index 456005747..8872b7ab1 100644 --- a/app/src/main/res/values-pl/preferences_values.xml +++ b/app/src/main/res/values-pl/preferences_values.xml @@ -51,6 +51,11 @@ Średnia ze średnich z obu semestrów Średnia wszystkich ocen z całego roku + + Nie pokauj + Tylko między lekcjami + Przed i między lekcjami + Szczęśliwy numerek Nieprzeczytane wiadomości diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 0c1bbf785..a2b5510ec 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -185,6 +185,12 @@ Zmiana sali z %1$s na %2$s Zmiana nauczyciela z %1$s na %2$s Zmiana przedmiotu z %1$s na %2$s + + Brak lekcji + Brak lekcji + Brak lekcji + Brak lekcji + Zmiana planu lekcji Zmiany planu lekcji @@ -700,6 +706,7 @@ Rozwijanie ocen Oznaczaj bieżącą lekcję Pokazuj grupę obok przedmiotu + Pokazuj puste kafelki gdzie nie ma lekcji Pokazuj listę wykresów w ocenach klasy Pokazuj przedmioty bez ocen Schemat kolorów ocen diff --git a/app/src/main/res/values/preferences_defaults.xml b/app/src/main/res/values/preferences_defaults.xml index fefd9b13c..8d69f25c8 100644 --- a/app/src/main/res/values/preferences_defaults.xml +++ b/app/src/main/res/values/preferences_defaults.xml @@ -23,6 +23,7 @@ no alphabetic false + between false false 0 diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index e7fa542a3..c48381e8c 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -28,6 +28,7 @@ show_whole_class_plan show_groups_in_plan timetable_show_timers + timetable_show_gaps subjects_without_grades optional_arithmetic_average message_draft diff --git a/app/src/main/res/values/preferences_values.xml b/app/src/main/res/values/preferences_values.xml index 312f0b878..f56707c89 100644 --- a/app/src/main/res/values/preferences_values.xml +++ b/app/src/main/res/values/preferences_values.xml @@ -123,6 +123,17 @@ all_year + + Don\'t show + Only between lessons + Before and between lessons + + + no_gaps + between + before_and_between + + Lucky number Unread messages diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9dc7e7969..ce277bdce 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -186,6 +186,10 @@ Change of room from %1$s to %2$s Change of teacher from %1$s to %2$s Change of subject from %1$s to %2$s + + No lesson + No lessons + Timetable change Timetable changes @@ -690,6 +694,7 @@ Grades expanding Mark current lesson Show groups next to subjects + Show empty tiles where there\'s no lesson Show chart list in class grades Show subjects without grades Grades color scheme diff --git a/app/src/main/res/xml/scheme_preferences_appearance.xml b/app/src/main/res/xml/scheme_preferences_appearance.xml index 62216c760..7177d396c 100644 --- a/app/src/main/res/xml/scheme_preferences_appearance.xml +++ b/app/src/main/res/xml/scheme_preferences_appearance.xml @@ -111,5 +111,13 @@ app:title="@string/pref_view_timetable_show_whole_class" app:useSimpleSummaryProvider="true" /> --> + From 2e2b13384a214fed9599a713fe7cbeedf0eb9cf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Wed, 23 Aug 2023 12:24:17 +0200 Subject: [PATCH 813/869] Try to switch to next school year before it starts (#2278) --- app/build.gradle | 2 +- .../data/repositories/SemesterRepository.kt | 4 +-- .../wulkanowy/utils/SemesterExtension.kt | 17 ++++++++-- .../repositories/SemesterRepositoryTest.kt | 23 +++++++------ .../utils/SemesterExtensionKtTest.kt | 34 +++++++++++++++++++ 5 files changed, 64 insertions(+), 16 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 37b165c17..136c54303 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -191,7 +191,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.0.8' + implementation 'io.github.wulkanowy:sdk:2.0.9-SNAPSHOT' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt index 92bb37081..dd44df70f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt @@ -41,7 +41,7 @@ class SemesterRepository @Inject constructor( val isRefreshOnModeChangeRequired = when { Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE -> { - semesters.firstOrNull { it.isCurrent }?.let { + semesters.firstOrNull { it.isCurrent() }?.let { 0 == it.diaryId && 0 == it.kindergartenDiaryId } == true } @@ -49,7 +49,7 @@ class SemesterRepository @Inject constructor( } val isRefreshOnNoCurrentAppropriate = - refreshOnNoCurrent && !semesters.any { semester -> semester.isCurrent } + refreshOnNoCurrent && !semesters.any { semester -> semester.isCurrent() } return forceRefresh || isNoSemesters || isRefreshOnModeChangeRequired || isRefreshOnNoCurrentAppropriate } diff --git a/app/src/main/java/io/github/wulkanowy/utils/SemesterExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/SemesterExtension.kt index 380d6bf6e..e3b8a3b4c 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/SemesterExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/SemesterExtension.kt @@ -1,16 +1,27 @@ package io.github.wulkanowy.utils import io.github.wulkanowy.data.db.entities.Semester +import java.time.LocalDate import java.time.LocalDate.now +import java.time.Month -inline val Semester.isCurrent: Boolean - get() = now() in start..end +fun Semester.isCurrent(now: LocalDate = now()): Boolean { + val shiftedStart = if (start.month == Month.SEPTEMBER) { + start.minusDays(3) + } else start + + val shiftedEnd = if (end.month == Month.AUGUST || end.month == Month.SEPTEMBER) { + end.minusDays(3) + } else end + + return now in shiftedStart..shiftedEnd +} fun List.getCurrentOrLast(): Semester { if (isEmpty()) throw RuntimeException("Empty semester list") // when there is only one current semester - singleOrNull { it.isCurrent }?.let { return it } + singleOrNull { it.isCurrent() }?.let { return it } // when there is more than one current semester - find one with higher id singleOrNull { semester -> semester.semesterId == maxByOrNull { it.semesterId }?.semesterId }?.let { return it } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/SemesterRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/SemesterRepositoryTest.kt index d8256869f..31098d2ef 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/SemesterRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/SemesterRepositoryTest.kt @@ -15,6 +15,7 @@ import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.SpyK import io.mockk.just import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Before @@ -81,15 +82,15 @@ class SemesterRepositoryTest { } @Test - fun getSemesters_invalidDiary_scrapper() { + fun getSemesters_invalidDiary_scrapper() = runTest { val badSemesters = listOf( - getSemesterPojo(0, 1, now().minusMonths(6), now().minusMonths(3)), - getSemesterPojo(0, 2, now().minusMonths(3), now()) + getSemesterPojo(0, 2, now().minusMonths(6), now()), + getSemesterPojo(0, 2, now(), now().plusMonths(6)), ) val goodSemesters = listOf( - getSemesterPojo(1, 1, now().minusMonths(6), now().minusMonths(3)), - getSemesterPojo(1, 2, now().minusMonths(3), now()) + getSemesterPojo(1, 2, now().minusMonths(6), now()), + getSemesterPojo(2, 3, now(), now().plusMonths(6)), ) coEvery { semesterDb.loadAll(student.studentId, student.classId) } returnsMany listOf( @@ -101,7 +102,9 @@ class SemesterRepositoryTest { coEvery { semesterDb.deleteAll(any()) } just Runs coEvery { semesterDb.insertSemesters(any()) } returns listOf() - val items = runBlocking { semesterRepository.getSemesters(student.copy(loginMode = Sdk.Mode.SCRAPPER.name)) } + val items = semesterRepository.getSemesters( + student = student.copy(loginMode = Sdk.Mode.SCRAPPER.name) + ) assertEquals(2, items.size) assertNotEquals(0, items[0].diaryId) } @@ -188,15 +191,15 @@ class SemesterRepositoryTest { } @Test - fun getSemesters_doubleCurrent_refreshOnNoCurrent() { + fun getSemesters_doubleCurrent_refreshOnNoCurrent() = runTest { val semesters = listOf( - getSemesterEntity(1, 1, now(), now()), - getSemesterEntity(1, 2, now(), now()) + getSemesterEntity(1, 1, now().minusMonths(1), now().plusMonths(1)), + getSemesterEntity(1, 2, now().minusMonths(1), now().plusMonths(1)) ) coEvery { semesterDb.loadAll(student.studentId, student.classId) } returns semesters - val items = runBlocking { semesterRepository.getSemesters(student, refreshOnNoCurrent = true) } + val items = semesterRepository.getSemesters(student, refreshOnNoCurrent = true) assertEquals(2, items.size) } diff --git a/app/src/test/java/io/github/wulkanowy/utils/SemesterExtensionKtTest.kt b/app/src/test/java/io/github/wulkanowy/utils/SemesterExtensionKtTest.kt index b7d3ecc94..e8ba8a876 100644 --- a/app/src/test/java/io/github/wulkanowy/utils/SemesterExtensionKtTest.kt +++ b/app/src/test/java/io/github/wulkanowy/utils/SemesterExtensionKtTest.kt @@ -8,6 +8,40 @@ import kotlin.test.assertEquals class SemesterExtensionKtTest { + @Test + fun `check is first semester is current`() { + val first = getSemesterEntity( + semesterName = 1, + start = LocalDate.of(2023, 9, 1), + end = LocalDate.of(2024, 1, 31), + ) + + // first boundary - school-year start + assertEquals(false, first.isCurrent(LocalDate.of(2023, 8, 28))) + assertEquals(true, first.isCurrent(LocalDate.of(2023, 8, 29))) + + // second boundary + assertEquals(true, first.isCurrent(LocalDate.of(2024, 1, 31))) + assertEquals(false, first.isCurrent(LocalDate.of(2024, 2, 1))) + } + + @Test + fun `check is second semester is current`() { + val second = getSemesterEntity( + semesterName = 2, + start = LocalDate.of(2024, 2, 1), + end = LocalDate.of(2024, 9, 1), + ) + + // first boundary + assertEquals(false, second.isCurrent(LocalDate.of(2024, 1, 31))) + assertEquals(true, second.isCurrent(LocalDate.of(2024, 2, 1))) + + // second boundary - school-year end + assertEquals(true, second.isCurrent(LocalDate.of(2024, 8, 29))) + assertEquals(false, second.isCurrent(LocalDate.of(2024, 8, 30))) + } + @Test(expected = IllegalArgumentException::class) fun `get current semester when current is doubled`() { val semesters = listOf( From fbce9e58d034fe5616736d06bd4e5235f573d593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Wed, 23 Aug 2023 19:46:53 +0200 Subject: [PATCH 814/869] New Crowdin updates (#2277) --- app/src/main/res/values-cs/preferences_values.xml | 5 +++++ app/src/main/res/values-cs/strings.xml | 7 +++++++ app/src/main/res/values-da-rDK/preferences_values.xml | 5 +++++ app/src/main/res/values-da-rDK/strings.xml | 5 +++++ app/src/main/res/values-de/preferences_values.xml | 5 +++++ app/src/main/res/values-de/strings.xml | 5 +++++ app/src/main/res/values-es-rES/preferences_values.xml | 5 +++++ app/src/main/res/values-es-rES/strings.xml | 5 +++++ app/src/main/res/values-it-rIT/preferences_values.xml | 5 +++++ app/src/main/res/values-it-rIT/strings.xml | 5 +++++ app/src/main/res/values-ru/preferences_values.xml | 5 +++++ app/src/main/res/values-ru/strings.xml | 7 +++++++ app/src/main/res/values-sk/preferences_values.xml | 5 +++++ app/src/main/res/values-sk/strings.xml | 7 +++++++ app/src/main/res/values-uk/preferences_values.xml | 5 +++++ app/src/main/res/values-uk/strings.xml | 7 +++++++ 16 files changed, 88 insertions(+) diff --git a/app/src/main/res/values-cs/preferences_values.xml b/app/src/main/res/values-cs/preferences_values.xml index 23073adf1..2cf402631 100644 --- a/app/src/main/res/values-cs/preferences_values.xml +++ b/app/src/main/res/values-cs/preferences_values.xml @@ -51,6 +51,11 @@ Průměr z průměrů z obou semestrů Průměr známek z celého roku + + Don\'t show + Only between lessons + Before and between lessons + Šťastné číslo Nepřečtené zprávy diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index ff461d420..a63a0aa12 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -185,6 +185,12 @@ Změna učebny z %1$s na %2$s Změna učitele z %1$s na %2$s Změna předmětu z %1$s na %2$s + + No lesson + No lessons + No lessons + No lessons + Změna plánu lekcí Změny plánu lekcí @@ -700,6 +706,7 @@ Rozvíjení známek Označit aktuální lekci Zobrazit skupiny vedle předmětů + Show empty tiles where there\'s no lesson Zobrazit seznam grafů v známkách třídy Zobrazit předměty bez známek Známky barevné schéma diff --git a/app/src/main/res/values-da-rDK/preferences_values.xml b/app/src/main/res/values-da-rDK/preferences_values.xml index ac2b6e9e5..5aff12dec 100644 --- a/app/src/main/res/values-da-rDK/preferences_values.xml +++ b/app/src/main/res/values-da-rDK/preferences_values.xml @@ -51,6 +51,11 @@ Average of averages from both semesters Average of grades from the whole year + + Don\'t show + Only between lessons + Before and between lessons + Lucky number Unread messages diff --git a/app/src/main/res/values-da-rDK/strings.xml b/app/src/main/res/values-da-rDK/strings.xml index 259a42647..2abf1a4a6 100644 --- a/app/src/main/res/values-da-rDK/strings.xml +++ b/app/src/main/res/values-da-rDK/strings.xml @@ -171,6 +171,10 @@ Change of room from %1$s to %2$s Change of teacher from %1$s to %2$s Change of subject from %1$s to %2$s + + No lesson + No lessons + Timetable change Timetable changes @@ -612,6 +616,7 @@ Grades expanding Mark current lesson Show groups next to subjects + Show empty tiles where there\'s no lesson Show chart list in class grades Show subjects without grades Grades color scheme diff --git a/app/src/main/res/values-de/preferences_values.xml b/app/src/main/res/values-de/preferences_values.xml index d9cac1959..d1001c74b 100644 --- a/app/src/main/res/values-de/preferences_values.xml +++ b/app/src/main/res/values-de/preferences_values.xml @@ -51,6 +51,11 @@ Durchschnittswert der Durchschnittswerte beider Semester Durchschnitt der Noten aus dem ganzen Jahr + + Don\'t show + Only between lessons + Before and between lessons + Glückszahl Ungelesene Nachrichten diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 1836d0479..f08a504a5 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -171,6 +171,10 @@ Änderung des Raumes von %1$s zu %2$s Wechsel des Lehrers von %1$s zu %2$s Thema von %1$s zu %2$s wechseln + + No lesson + No lessons + Änderung des Zeitplans Änderungen des Zeitplans @@ -612,6 +616,7 @@ Steigende Sorten Aktuelle Lektion markieren Gruppen neben Schulfächen anzeigen + Show empty tiles where there\'s no lesson Liste der Diagramme in Klassenbewertungen anzeigen Schulfächer ohne Noten anzeigen Farbschema der Noten diff --git a/app/src/main/res/values-es-rES/preferences_values.xml b/app/src/main/res/values-es-rES/preferences_values.xml index ac2b6e9e5..5aff12dec 100644 --- a/app/src/main/res/values-es-rES/preferences_values.xml +++ b/app/src/main/res/values-es-rES/preferences_values.xml @@ -51,6 +51,11 @@ Average of averages from both semesters Average of grades from the whole year + + Don\'t show + Only between lessons + Before and between lessons + Lucky number Unread messages diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 259a42647..2abf1a4a6 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -171,6 +171,10 @@ Change of room from %1$s to %2$s Change of teacher from %1$s to %2$s Change of subject from %1$s to %2$s + + No lesson + No lessons + Timetable change Timetable changes @@ -612,6 +616,7 @@ Grades expanding Mark current lesson Show groups next to subjects + Show empty tiles where there\'s no lesson Show chart list in class grades Show subjects without grades Grades color scheme diff --git a/app/src/main/res/values-it-rIT/preferences_values.xml b/app/src/main/res/values-it-rIT/preferences_values.xml index ac2b6e9e5..5aff12dec 100644 --- a/app/src/main/res/values-it-rIT/preferences_values.xml +++ b/app/src/main/res/values-it-rIT/preferences_values.xml @@ -51,6 +51,11 @@ Average of averages from both semesters Average of grades from the whole year + + Don\'t show + Only between lessons + Before and between lessons + Lucky number Unread messages diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml index 259a42647..2abf1a4a6 100644 --- a/app/src/main/res/values-it-rIT/strings.xml +++ b/app/src/main/res/values-it-rIT/strings.xml @@ -171,6 +171,10 @@ Change of room from %1$s to %2$s Change of teacher from %1$s to %2$s Change of subject from %1$s to %2$s + + No lesson + No lessons + Timetable change Timetable changes @@ -612,6 +616,7 @@ Grades expanding Mark current lesson Show groups next to subjects + Show empty tiles where there\'s no lesson Show chart list in class grades Show subjects without grades Grades color scheme diff --git a/app/src/main/res/values-ru/preferences_values.xml b/app/src/main/res/values-ru/preferences_values.xml index 8a8c260da..df3629c02 100644 --- a/app/src/main/res/values-ru/preferences_values.xml +++ b/app/src/main/res/values-ru/preferences_values.xml @@ -51,6 +51,11 @@ Средняя из средних оценок семестров Средняя из оценок со всего года + + Don\'t show + Only between lessons + Before and between lessons + Счастливый номер Непрочитанные письма diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 606971740..d075dac6c 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -185,6 +185,12 @@ Аудитория изменена с %1$s на %2$s Учитель изменён с %1$s на %2$s Тема изменена с %1$s на %2$s + + No lesson + No lessons + No lessons + No lessons + Изменение расписания Изменения расписания @@ -700,6 +706,7 @@ Разворачивание оценок Отметить текущий урок Показать группы рядом с темами + Show empty tiles where there\'s no lesson Показывать диаграммы в оценках класса Показать предметы без оценок Цветовая схема оценок diff --git a/app/src/main/res/values-sk/preferences_values.xml b/app/src/main/res/values-sk/preferences_values.xml index e4331315a..fd393394f 100644 --- a/app/src/main/res/values-sk/preferences_values.xml +++ b/app/src/main/res/values-sk/preferences_values.xml @@ -51,6 +51,11 @@ Priemer z priemerov z oboch semestrov Priemer známok z celého roka + + Don\'t show + Only between lessons + Before and between lessons + Šťastné číslo Neprečítané správy diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 20d8818b3..d1990e35e 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -185,6 +185,12 @@ Zmena učebne z %1$s na %2$s Zmena učiteľa z %1$s na %2$s Zmena predmetu z %1$s na %2$s + + No lesson + No lessons + No lessons + No lessons + Zmena plánu lekcií Zmeny plánu lekcií @@ -700,6 +706,7 @@ Rozvijanie známok Označiť aktuálne lekciu Zobraziť skupiny vedľa predmetov + Show empty tiles where there\'s no lesson Zobraziť zoznam grafov v známkach triedy Zobraziť predmety bez známok Známky farebnú schému diff --git a/app/src/main/res/values-uk/preferences_values.xml b/app/src/main/res/values-uk/preferences_values.xml index 44acd18e9..55cf905bb 100644 --- a/app/src/main/res/values-uk/preferences_values.xml +++ b/app/src/main/res/values-uk/preferences_values.xml @@ -51,6 +51,11 @@ Середнє значення з обох семестрів Середня оцінка з цілого року + + Don\'t show + Only between lessons + Before and between lessons + Щасливий номер Непрочитані листи diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index db5c3cb0d..876562d31 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -185,6 +185,12 @@ Зміна аудіторії з %1$s на %2$s Зміна вчителя з %1$s на %2$s Зміна теми з %1$s на %2$s + + No lesson + No lessons + No lessons + No lessons + Зміна у розкладі Зміни у розкладі @@ -700,6 +706,7 @@ Розгортання оцінок Позначити поточний урок Показувати групи поруч з темами + Show empty tiles where there\'s no lesson Показувати діаграми в оцінках класу Показати предмети без оцінок Схема кольорів оцінок From 3dfc55c4d196cb767704e8f6f73ac1e25f611df8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Thu, 24 Aug 2023 11:33:40 +0200 Subject: [PATCH 815/869] Add admin messages to login screen (#2280) --- app/build.gradle | 2 +- .../57.json | 2443 +++++++++++++++++ .../java/io/github/wulkanowy/data/Resource.kt | 2 +- .../github/wulkanowy/data/db/AppDatabase.kt | 3 +- .../io/github/wulkanowy/data/db/Converters.kt | 6 + .../data/db/entities/AdminMessage.kt | 4 +- .../data/db/migrations/Migration57.kt | 10 + .../wulkanowy/data/enums/MessageType.kt | 9 + .../repositories/AdminMessageRepository.kt | 45 +- .../GetAppropriateAdminMessageUseCase.kt | 64 + .../dashboard/DashboardItemMoveCallback.kt | 3 +- .../modules/dashboard/DashboardPresenter.kt | 34 +- .../dashboard/adapters/DashboardAdapter.kt | 45 +- .../viewholders/AdminMessageViewHolder.kt | 52 + .../modules/login/form/LoginFormFragment.kt | 15 + .../modules/login/form/LoginFormPresenter.kt | 40 +- .../ui/modules/login/form/LoginFormView.kt | 5 + .../main/res/layout/fragment_login_form.xml | 15 +- .../login/form/LoginFormPresenterTest.kt | 10 + 19 files changed, 2722 insertions(+), 85 deletions(-) create mode 100644 app/schemas/io.github.wulkanowy.data.db.AppDatabase/57.json create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration57.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/enums/MessageType.kt create mode 100644 app/src/main/java/io/github/wulkanowy/domain/adminmessage/GetAppropriateAdminMessageUseCase.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/viewholders/AdminMessageViewHolder.kt diff --git a/app/build.gradle b/app/build.gradle index 136c54303..d0ae8894c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,7 +27,7 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 33 - versionCode 130 + versionCode 131 // todo: already bumped for 2.1.0 version versionName "2.0.8" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/57.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/57.json new file mode 100644 index 000000000..2eff12231 --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/57.json @@ -0,0 +1,2443 @@ +{ + "formatVersion": 1, + "database": { + "version": 57, + "identityHash": "d15dbe7d7e4d7df98ec98d9a3a4b5fcd", + "entities": [ + { + "tableName": "Students", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrapper_base_url` TEXT NOT NULL, `scrapper_domain_suffix` TEXT NOT NULL DEFAULT '', `mobile_base_url` TEXT NOT NULL, `login_type` TEXT NOT NULL, `login_mode` TEXT NOT NULL, `certificate_key` TEXT NOT NULL, `private_key` TEXT NOT NULL, `is_parent` INTEGER NOT NULL, `email` TEXT NOT NULL, `password` TEXT NOT NULL, `symbol` TEXT NOT NULL, `student_id` INTEGER NOT NULL, `user_login_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `student_name` TEXT NOT NULL, `school_id` TEXT NOT NULL, `school_short` TEXT NOT NULL, `school_name` TEXT NOT NULL, `class_name` TEXT NOT NULL, `class_id` INTEGER NOT NULL, `is_current` INTEGER NOT NULL, `registration_date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `nick` TEXT NOT NULL, `avatar_color` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "scrapperBaseUrl", + "columnName": "scrapper_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scrapperDomainSuffix", + "columnName": "scrapper_domain_suffix", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "mobileBaseUrl", + "columnName": "mobile_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginType", + "columnName": "login_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginMode", + "columnName": "login_mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificateKey", + "columnName": "certificate_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "private_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isParent", + "columnName": "is_parent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "user_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "student_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolSymbol", + "columnName": "school_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "school_short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolName", + "columnName": "school_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "className", + "columnName": "class_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCurrent", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registrationDate", + "columnName": "registration_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nick", + "columnName": "nick", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarColor", + "columnName": "avatar_color", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Students_email_symbol_student_id_school_id_class_id", + "unique": true, + "columnNames": [ + "email", + "symbol", + "student_id", + "school_id", + "class_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `kindergarten_diary_id` INTEGER NOT NULL DEFAULT 0, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "kindergartenDiaryId", + "columnName": "kindergarten_diary_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "diaryName", + "columnName": "diary_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolYear", + "columnName": "school_year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterName", + "columnName": "semester_name", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "kindergarten_diary_id", + "semester_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `kindergarten_diary_id`, `semester_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Exams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Timetable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `number` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `subjectOld` TEXT NOT NULL, `group` TEXT NOT NULL, `room` TEXT NOT NULL, `roomOld` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacherOld` TEXT NOT NULL, `info` TEXT NOT NULL, `student_plan` INTEGER NOT NULL, `changes` INTEGER NOT NULL, `canceled` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectOld", + "columnName": "subjectOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "room", + "columnName": "room", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roomOld", + "columnName": "roomOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherOld", + "columnName": "teacherOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isStudentPlan", + "columnName": "student_plan", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "changes", + "columnName": "changes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canceled", + "columnName": "canceled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Attendance", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `time_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `excusable` INTEGER NOT NULL, `excuse_status` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeId", + "columnName": "time_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excusable", + "columnName": "excusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excuseStatus", + "columnName": "excuse_status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AttendanceSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `subject_id` INTEGER NOT NULL, `month` INTEGER NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `absence_excused` INTEGER NOT NULL, `absence_for_school_reasons` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `lateness_excused` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subject_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "month", + "columnName": "month", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceExcused", + "columnName": "absence_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceForSchoolReasons", + "columnName": "absence_for_school_reasons", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latenessExcused", + "columnName": "lateness_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Grades", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `entry` TEXT NOT NULL, `value` REAL NOT NULL, `modifier` REAL NOT NULL, `comment` TEXT NOT NULL, `color` TEXT NOT NULL, `grade_symbol` TEXT NOT NULL, `description` TEXT NOT NULL, `weight` TEXT NOT NULL, `weightValue` REAL NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entry", + "columnName": "entry", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "modifier", + "columnName": "modifier", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gradeSymbol", + "columnName": "grade_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weightValue", + "columnName": "weightValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `subject` TEXT NOT NULL, `predicted_grade` TEXT NOT NULL, `final_grade` TEXT NOT NULL, `proposed_points` TEXT NOT NULL, `final_points` TEXT NOT NULL, `points_sum` TEXT NOT NULL, `average` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_predicted_grade_notified` INTEGER NOT NULL, `is_final_grade_notified` INTEGER NOT NULL, `predicted_grade_last_change` INTEGER NOT NULL, `final_grade_last_change` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "predictedGrade", + "columnName": "predicted_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalGrade", + "columnName": "final_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "proposedPoints", + "columnName": "proposed_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalPoints", + "columnName": "final_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pointsSum", + "columnName": "points_sum", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "average", + "columnName": "average", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPredictedGradeNotified", + "columnName": "is_predicted_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFinalGradeNotified", + "columnName": "is_final_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "predictedGradeLastChange", + "columnName": "predicted_grade_last_change", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "finalGradeLastChange", + "columnName": "final_grade_last_change", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradePartialStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `class_average` TEXT NOT NULL, `student_average` TEXT NOT NULL, `class_amounts` TEXT NOT NULL, `student_amounts` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAverage", + "columnName": "class_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAverage", + "columnName": "student_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAmounts", + "columnName": "class_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAmounts", + "columnName": "student_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesPointsStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `others` REAL NOT NULL, `student` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "others", + "columnName": "others", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "student", + "columnName": "student", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradeSemesterStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `amounts` TEXT NOT NULL, `student_grade` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amounts", + "columnName": "amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentGrade", + "columnName": "student_grade", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `message_global_key` TEXT NOT NULL, `mailbox_key` TEXT NOT NULL, `message_id` INTEGER NOT NULL, `correspondents` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `read_by` INTEGER, `unread_by` INTEGER, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `content` TEXT NOT NULL, `sender` TEXT, `recipients` TEXT)", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageGlobalKey", + "columnName": "message_global_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mailboxKey", + "columnName": "mailbox_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "correspondents", + "columnName": "correspondents", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasAttachments", + "columnName": "has_attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recipients", + "columnName": "recipients", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`message_global_key` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`message_global_key`, `url`, `filename`))", + "fields": [ + { + "fieldPath": "messageGlobalKey", + "columnName": "message_global_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "message_global_key", + "url", + "filename" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `category` TEXT NOT NULL, `category_type` INTEGER NOT NULL, `is_points_show` INTEGER NOT NULL, `points` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryType", + "columnName": "category_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPointsShow", + "columnName": "is_points_show", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Homework", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `attachments` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_done` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL, `is_added_by_user` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "is_done", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Subjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "LuckyNumbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `lucky_number` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "luckyNumber", + "columnName": "lucky_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CompletedLesson", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `topic` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `substitution` TEXT NOT NULL, `absence` TEXT NOT NULL, `resources` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "topic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "substitution", + "columnName": "substitution", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "resources", + "columnName": "resources", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Mailboxes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`globalKey` TEXT NOT NULL, `email` TEXT NOT NULL, `symbol` TEXT NOT NULL, `schoolId` TEXT NOT NULL, `fullName` TEXT NOT NULL, `userName` TEXT NOT NULL, `studentName` TEXT NOT NULL, `schoolNameShort` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`globalKey`))", + "fields": [ + { + "fieldPath": "globalKey", + "columnName": "globalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolId", + "columnName": "schoolId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "studentName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolNameShort", + "columnName": "schoolNameShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "globalKey" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mailboxGlobalKey` TEXT NOT NULL, `studentMailboxGlobalKey` TEXT NOT NULL, `fullName` TEXT NOT NULL, `userName` TEXT NOT NULL, `schoolShortName` TEXT NOT NULL, `type` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "mailboxGlobalKey", + "columnName": "mailboxGlobalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentMailboxGlobalKey", + "columnName": "studentMailboxGlobalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "schoolShortName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MobileDevices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_login_id` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Teachers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `short_name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "School", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `contact` TEXT NOT NULL, `headmaster` TEXT NOT NULL, `pedagogue` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contact", + "columnName": "contact", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headmaster", + "columnName": "headmaster", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pedagogue", + "columnName": "pedagogue", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Conferences", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `subject` TEXT NOT NULL, `agenda` TEXT NOT NULL, `present_on_conference` TEXT NOT NULL, `conference_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agenda", + "columnName": "agenda", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presentOnConference", + "columnName": "present_on_conference", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conference_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableAdditional", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `repeat_id` BLOB DEFAULT NULL, `is_added_by_user` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatId", + "columnName": "repeat_id", + "affinity": "BLOB", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StudentInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `full_name` TEXT NOT NULL, `first_name` TEXT NOT NULL, `second_name` TEXT NOT NULL, `surname` TEXT NOT NULL, `birth_date` INTEGER NOT NULL, `birth_place` TEXT NOT NULL, `gender` TEXT NOT NULL, `has_polish_citizenship` INTEGER NOT NULL, `family_name` TEXT NOT NULL, `parents_names` TEXT NOT NULL, `address` TEXT NOT NULL, `registered_address` TEXT NOT NULL, `correspondence_address` TEXT NOT NULL, `phone_number` TEXT NOT NULL, `cell_phone_number` TEXT NOT NULL, `email` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `first_guardian_full_name` TEXT, `first_guardian_kinship` TEXT, `first_guardian_address` TEXT, `first_guardian_phones` TEXT, `first_guardian_email` TEXT, `second_guardian_full_name` TEXT, `second_guardian_kinship` TEXT, `second_guardian_address` TEXT, `second_guardian_phones` TEXT, `second_guardian_email` TEXT)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "full_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondName", + "columnName": "second_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "surname", + "columnName": "surname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "birthDate", + "columnName": "birth_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthPlace", + "columnName": "birth_place", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gender", + "columnName": "gender", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPolishCitizenship", + "columnName": "has_polish_citizenship", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "familyName", + "columnName": "family_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentsNames", + "columnName": "parents_names", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "registeredAddress", + "columnName": "registered_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "correspondenceAddress", + "columnName": "correspondence_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cellPhoneNumber", + "columnName": "cell_phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstGuardian.fullName", + "columnName": "first_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.kinship", + "columnName": "first_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.address", + "columnName": "first_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.phones", + "columnName": "first_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.email", + "columnName": "first_guardian_email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.fullName", + "columnName": "second_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.kinship", + "columnName": "second_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.address", + "columnName": "second_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.phones", + "columnName": "second_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.email", + "columnName": "second_guardian_email", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableHeaders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SchoolAnnouncements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_login_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `type` TEXT NOT NULL, `destination` TEXT NOT NULL DEFAULT '{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}', `date` INTEGER NOT NULL, `data` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "destination", + "columnName": "destination", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}'" + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AdminMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `version_name` INTEGER, `version_max` INTEGER, `target_register_host` TEXT, `target_flavor` TEXT, `destination_url` TEXT, `priority` TEXT NOT NULL, `types` TEXT NOT NULL DEFAULT '[]', `is_dismissible` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "versionMin", + "columnName": "version_name", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMax", + "columnName": "version_max", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "targetRegisterHost", + "columnName": "target_register_host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "targetFlavor", + "columnName": "target_flavor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "destinationUrl", + "columnName": "destination_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "types", + "columnName": "types", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'[]'" + }, + { + "fieldPath": "isDismissible", + "columnName": "is_dismissible", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "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, 'd15dbe7d7e4d7df98ec98d9a3a4b5fcd')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/Resource.kt b/app/src/main/java/io/github/wulkanowy/data/Resource.kt index 6b611e477..2c5bf0ea9 100644 --- a/app/src/main/java/io/github/wulkanowy/data/Resource.kt +++ b/app/src/main/java/io/github/wulkanowy/data/Resource.kt @@ -148,7 +148,7 @@ inline fun networkBoundResource( crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit, crossinline onFetchFailed: (Throwable) -> Unit = { }, crossinline shouldFetch: (ResultType) -> Boolean = { true }, - crossinline mapResult: (ResultType) -> T + crossinline mapResult: (ResultType) -> T, ) = flow { emit(Resource.Loading()) diff --git a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt index 882a70167..48a2942c9 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt @@ -50,6 +50,7 @@ import javax.inject.Singleton AutoMigration(from = 51, to = 52), AutoMigration(from = 54, to = 55, spec = Migration55::class), AutoMigration(from = 55, to = 56), + AutoMigration(from = 56, to = 57, spec = Migration57::class), ], version = AppDatabase.VERSION_SCHEMA, exportSchema = true @@ -58,7 +59,7 @@ import javax.inject.Singleton abstract class AppDatabase : RoomDatabase() { companion object { - const val VERSION_SCHEMA = 56 + const val VERSION_SCHEMA = 57 fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( Migration2(), diff --git a/app/src/main/java/io/github/wulkanowy/data/db/Converters.kt b/app/src/main/java/io/github/wulkanowy/data/db/Converters.kt index 9d3beae1f..7bc8d12a2 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/Converters.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/Converters.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.data.db import androidx.room.TypeConverter +import io.github.wulkanowy.data.enums.MessageType import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.utils.toTimestamp import kotlinx.serialization.SerializationException @@ -68,4 +69,9 @@ class Converters { @TypeConverter fun stringToDestination(destination: String): Destination = json.decodeFromString(destination) + @TypeConverter + fun messageTypesToString(types: List): String = json.encodeToString(types) + + @TypeConverter + fun stringToMessageTypes(text: String): List = json.decodeFromString(text) } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt index 97fec69b7..875c2a3a5 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt @@ -3,6 +3,7 @@ package io.github.wulkanowy.data.db.entities import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey +import io.github.wulkanowy.data.enums.MessageType import kotlinx.serialization.Serializable @Serializable @@ -33,7 +34,8 @@ data class AdminMessage( val priority: String, - val type: String, + @ColumnInfo(name = "types", defaultValue = "[]") + val types: List = emptyList(), @ColumnInfo(name = "is_dismissible") val isDismissible: Boolean = false diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration57.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration57.kt new file mode 100644 index 000000000..2fc8718f9 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration57.kt @@ -0,0 +1,10 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.DeleteColumn +import androidx.room.migration.AutoMigrationSpec + +@DeleteColumn( + tableName = "AdminMessages", + columnName = "type", +) +class Migration57 : AutoMigrationSpec diff --git a/app/src/main/java/io/github/wulkanowy/data/enums/MessageType.kt b/app/src/main/java/io/github/wulkanowy/data/enums/MessageType.kt new file mode 100644 index 000000000..531684e4e --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/enums/MessageType.kt @@ -0,0 +1,9 @@ +package io.github.wulkanowy.data.enums + +enum class MessageType { + GENERAL_MESSAGE, + DASHBOARD_MESSAGE, + LOGIN_MESSAGE, + PASS_RESET_MESSAGE, + ERROR_OVERRIDE, +} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt index c9655b722..b831ee755 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt @@ -1,10 +1,11 @@ package io.github.wulkanowy.data.repositories +import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.api.AdminMessageService import io.github.wulkanowy.data.db.dao.AdminMessageDao -import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.data.networkBoundResource -import io.github.wulkanowy.utils.AppInfo +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -13,34 +14,20 @@ import javax.inject.Singleton class AdminMessageRepository @Inject constructor( private val adminMessageService: AdminMessageService, private val adminMessageDao: AdminMessageDao, - private val appInfo: AppInfo ) { + private val saveFetchResultMutex = Mutex() - suspend fun getAdminMessages(student: Student) = networkBoundResource( - mutex = saveFetchResultMutex, - isResultEmpty = { it == null }, - query = { adminMessageDao.loadAll() }, - fetch = { adminMessageService.getAdminMessages() }, - shouldFetch = { true }, - saveFetchResult = { oldItems, newItems -> - adminMessageDao.removeOldAndSaveNew(oldItems, newItems) - }, - showSavedOnLoading = false, - mapResult = { adminMessages -> - adminMessages.filter { adminMessage -> - val isCorrectRegister = adminMessage.targetRegisterHost?.let { - student.scrapperBaseUrl.contains(it, true) - } ?: true - val isCorrectFlavor = - adminMessage.targetFlavor?.equals(appInfo.buildFlavor, true) ?: true - val isCorrectMaxVersion = - adminMessage.versionMax?.let { it >= appInfo.versionCode } ?: true - val isCorrectMinVersion = - adminMessage.versionMin?.let { it <= appInfo.versionCode } ?: true - - isCorrectRegister && isCorrectFlavor && isCorrectMaxVersion && isCorrectMinVersion - }.maxByOrNull { it.id } - } - ) + fun getAdminMessages(): Flow>> = + networkBoundResource( + mutex = saveFetchResultMutex, + isResultEmpty = { false }, + query = { adminMessageDao.loadAll() }, + fetch = { adminMessageService.getAdminMessages() }, + shouldFetch = { true }, + saveFetchResult = { oldItems, newItems -> + adminMessageDao.removeOldAndSaveNew(oldItems, newItems) + }, + showSavedOnLoading = false, + ) } diff --git a/app/src/main/java/io/github/wulkanowy/domain/adminmessage/GetAppropriateAdminMessageUseCase.kt b/app/src/main/java/io/github/wulkanowy/domain/adminmessage/GetAppropriateAdminMessageUseCase.kt new file mode 100644 index 000000000..b55bf899d --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/domain/adminmessage/GetAppropriateAdminMessageUseCase.kt @@ -0,0 +1,64 @@ +package io.github.wulkanowy.domain.adminmessage + +import io.github.wulkanowy.data.Resource +import io.github.wulkanowy.data.db.entities.AdminMessage +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.enums.MessageType +import io.github.wulkanowy.data.mapResourceData +import io.github.wulkanowy.data.repositories.AdminMessageRepository +import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.utils.AppInfo +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class GetAppropriateAdminMessageUseCase @Inject constructor( + private val adminMessageRepository: AdminMessageRepository, + private val preferencesRepository: PreferencesRepository, + private val appInfo: AppInfo +) { + + operator fun invoke(student: Student, type: MessageType): Flow> { + return invoke(student.scrapperBaseUrl, type) + } + + operator fun invoke(scrapperBaseUrl: String, type: MessageType): Flow> { + return adminMessageRepository.getAdminMessages().mapResourceData { adminMessages -> + adminMessages + .asSequence() + .filter { it.isNotDismissed() } + .filter { it.isVersionMatch() } + .filter { it.isRegisterHostMatch(scrapperBaseUrl) } + .filter { it.isFlavorMatch() } + .filter { it.isTypeMatch(type) } + .maxByOrNull { it.id } + } + } + + private fun AdminMessage.isNotDismissed(): Boolean { + return id !in preferencesRepository.dismissedAdminMessageIds + } + + private fun AdminMessage.isRegisterHostMatch(scrapperBaseUrl: String): Boolean { + return targetRegisterHost?.let { + scrapperBaseUrl.contains(it, true) + } ?: true + } + + private fun AdminMessage.isFlavorMatch(): Boolean { + return targetFlavor?.equals(appInfo.buildFlavor, true) ?: true + } + + private fun AdminMessage.isVersionMatch(): Boolean { + val isCorrectMaxVersion = versionMax?.let { it >= appInfo.versionCode } ?: true + val isCorrectMinVersion = versionMin?.let { it <= appInfo.versionCode } ?: true + + return isCorrectMaxVersion && isCorrectMinVersion + } + + private fun AdminMessage.isTypeMatch(messageType: MessageType): Boolean { + if (messageType in types) return true + if (MessageType.GENERAL_MESSAGE in types) return true + + return false + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt index 9c15acc35..f033b5947 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt @@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.dashboard import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import io.github.wulkanowy.ui.modules.dashboard.adapters.DashboardAdapter +import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder import java.util.* class DashboardItemMoveCallback( @@ -55,5 +56,5 @@ class DashboardItemMoveCallback( } private val RecyclerView.ViewHolder.isAdminMessageOrAccountItem: Boolean - get() = this is DashboardAdapter.AdminMessageViewHolder || this is DashboardAdapter.AccountViewHolder + get() = this is AdminMessageViewHolder || this is DashboardAdapter.AccountViewHolder } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt index ac2c896dc..ecf084c68 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt @@ -5,7 +5,9 @@ import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.enums.MessageFolder +import io.github.wulkanowy.data.enums.MessageType import io.github.wulkanowy.data.repositories.* +import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.AdsHelper @@ -32,7 +34,7 @@ class DashboardPresenter @Inject constructor( private val conferenceRepository: ConferenceRepository, private val preferencesRepository: PreferencesRepository, private val schoolAnnouncementRepository: SchoolAnnouncementRepository, - private val adminMessageRepository: AdminMessageRepository, + private val getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase, private val adsHelper: AdsHelper ) : BasePresenter(errorHandler, studentRepository) { @@ -159,19 +161,23 @@ class DashboardPresenter @Inject constructor( DashboardItem.Type.ACCOUNT -> { updateData(DashboardItem.Account(student), forceRefresh) } + DashboardItem.Type.HORIZONTAL_GROUP -> { loadHorizontalGroup(student, forceRefresh) } + DashboardItem.Type.LESSONS -> loadLessons(student, forceRefresh) DashboardItem.Type.GRADES -> loadGrades(student, forceRefresh) DashboardItem.Type.HOMEWORK -> loadHomework(student, forceRefresh) DashboardItem.Type.ANNOUNCEMENTS -> { loadSchoolAnnouncements(student, forceRefresh) } + DashboardItem.Type.EXAMS -> loadExams(student, forceRefresh) DashboardItem.Type.CONFERENCES -> { loadConferences(student, forceRefresh) } + DashboardItem.Type.ADS -> loadAds(forceRefresh) DashboardItem.Type.ADMIN_MESSAGE -> loadAdminMessage(student, forceRefresh) } @@ -355,6 +361,7 @@ class DashboardPresenter @Inject constructor( firstLoadedItemList += DashboardItem.Type.GRADES } } + is Resource.Success -> { Timber.i("Loading dashboard grades result: Success") updateData( @@ -365,6 +372,7 @@ class DashboardPresenter @Inject constructor( forceRefresh ) } + is Resource.Error -> { Timber.i("Loading dashboard grades result: An exception occurred") errorHandler.dispatch(it.error) @@ -402,12 +410,14 @@ class DashboardPresenter @Inject constructor( firstLoadedItemList += DashboardItem.Type.LESSONS } } + is Resource.Success -> { Timber.i("Loading dashboard lessons result: Success") updateData( DashboardItem.Lessons(it.data), forceRefresh ) } + is Resource.Error -> { Timber.i("Loading dashboard lessons result: An exception occurred") errorHandler.dispatch(it.error) @@ -457,10 +467,12 @@ class DashboardPresenter @Inject constructor( firstLoadedItemList += DashboardItem.Type.HOMEWORK } } + is Resource.Success -> { Timber.i("Loading dashboard homework result: Success") updateData(DashboardItem.Homework(it.data), forceRefresh) } + is Resource.Error -> { Timber.i("Loading dashboard homework result: An exception occurred") errorHandler.dispatch(it.error) @@ -489,10 +501,12 @@ class DashboardPresenter @Inject constructor( firstLoadedItemList += DashboardItem.Type.ANNOUNCEMENTS } } + is Resource.Success -> { Timber.i("Loading dashboard announcements result: Success") updateData(DashboardItem.Announcements(it.data), forceRefresh) } + is Resource.Error -> { Timber.i("Loading dashboard announcements result: An exception occurred") errorHandler.dispatch(it.error) @@ -530,10 +544,12 @@ class DashboardPresenter @Inject constructor( firstLoadedItemList += DashboardItem.Type.EXAMS } } + is Resource.Success -> { Timber.i("Loading dashboard exams result: Success") updateData(DashboardItem.Exams(it.data), forceRefresh) } + is Resource.Error -> { Timber.i("Loading dashboard exams result: An exception occurred") errorHandler.dispatch(it.error) @@ -569,10 +585,12 @@ class DashboardPresenter @Inject constructor( firstLoadedItemList += DashboardItem.Type.CONFERENCES } } + is Resource.Success -> { Timber.i("Loading dashboard conferences result: Success") updateData(DashboardItem.Conferences(it.data), forceRefresh) } + is Resource.Error -> { Timber.i("Loading dashboard conferences result: An exception occurred") errorHandler.dispatch(it.error) @@ -584,12 +602,12 @@ class DashboardPresenter @Inject constructor( } private fun loadAdminMessage(student: Student, forceRefresh: Boolean) { - flatResourceFlow { adminMessageRepository.getAdminMessages(student) } - .filter { - val data = it.dataOrNull ?: return@filter true - val isDismissed = data.id in preferencesRepository.dismissedAdminMessageIds - !isDismissed - } + flatResourceFlow { + getAppropriateAdminMessageUseCase( + student = student, + type = MessageType.DASHBOARD_MESSAGE, + ) + } .onEach { when (it) { is Resource.Loading -> { @@ -597,6 +615,7 @@ class DashboardPresenter @Inject constructor( if (forceRefresh) return@onEach updateData(DashboardItem.AdminMessages(), forceRefresh) } + is Resource.Success -> { Timber.i("Loading dashboard admin message result: Success") updateData( @@ -604,6 +623,7 @@ class DashboardPresenter @Inject constructor( forceRefresh = forceRefresh ) } + is Resource.Error -> { Timber.i("Loading dashboard admin message result: An exception occurred") Timber.e(it.error) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardAdapter.kt index 4ad4e9d67..7c74cae80 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/adapters/DashboardAdapter.kt @@ -1,8 +1,6 @@ package io.github.wulkanowy.ui.modules.dashboard.adapters import android.annotation.SuppressLint -import android.content.res.ColorStateList -import android.graphics.Color import android.graphics.Typeface import android.os.Handler import android.os.Looper @@ -24,6 +22,7 @@ import io.github.wulkanowy.data.db.entities.TimetableHeader import io.github.wulkanowy.data.enums.GradeColorTheme import io.github.wulkanowy.databinding.* import io.github.wulkanowy.ui.modules.dashboard.DashboardItem +import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder import io.github.wulkanowy.utils.* import timber.log.Timber import java.time.Duration @@ -109,7 +108,9 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter AdminMessageViewHolder( - ItemDashboardAdminMessageBinding.inflate(inflater, parent, false) + ItemDashboardAdminMessageBinding.inflate(inflater, parent, false), + onAdminMessageDismissClickListener = onAdminMessageDismissClickListener, + onAdminMessageClickListener = onAdminMessageClickListener, ) DashboardItem.Type.ADS.ordinal -> AdsViewHolder( ItemDashboardAdsBinding.inflate(inflater, parent, false) @@ -128,7 +129,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter bindAnnouncementsViewHolder(holder, position) is ExamsViewHolder -> bindExamsViewHolder(holder, position) is ConferencesViewHolder -> bindConferencesViewHolder(holder, position) - is AdminMessageViewHolder -> bindAdminMessage(holder, position) + is AdminMessageViewHolder -> holder.bind((items[position] as DashboardItem.AdminMessages).adminMessage) is AdsViewHolder -> bindAdsViewHolder(holder, position) } } @@ -733,39 +734,6 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter { - context.getThemeAttrColor(R.attr.colorMessageHigh) to - context.getThemeAttrColor(R.attr.colorOnMessageHigh) - } - "MEDIUM" -> { - context.getThemeAttrColor(R.attr.colorMessageMedium) to Color.BLACK - } - else -> null to context.getThemeAttrColor(R.attr.colorOnSurface) - } - - with(adminMessageViewHolder.binding) { - dashboardAdminMessageItemTitle.text = item.title - dashboardAdminMessageItemTitle.setTextColor(textColor) - dashboardAdminMessageItemDescription.text = item.content - dashboardAdminMessageItemDescription.setTextColor(textColor) - dashboardAdminMessageItemIcon.setColorFilter(textColor) - dashboardAdminMessageItemDismiss.isVisible = item.isDismissible - dashboardAdminMessageItemDismiss.setTextColor(textColor) - dashboardAdminMessageItemDismiss.setOnClickListener { - onAdminMessageDismissClickListener(item) - } - - root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) }) - item.destinationUrl?.let { url -> - root.setOnClickListener { onAdminMessageClickListener(url) } - } - } - } - private fun bindAdsViewHolder(adsViewHolder: AdsViewHolder, position: Int) { val item = (items[position] as DashboardItem.Ads).adBanner ?: return val binding = adsViewHolder.binding @@ -819,9 +787,6 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter Unit, + private val onAdminMessageClickListener: (String?) -> Unit, +) : RecyclerView.ViewHolder(binding.root) { + + fun bind(item: AdminMessage?) { + item ?: return + + val context = binding.root.context + val (backgroundColor, textColor) = when (item.priority) { + "HIGH" -> { + context.getThemeAttrColor(R.attr.colorMessageHigh) to + context.getThemeAttrColor(R.attr.colorOnMessageHigh) + } + "MEDIUM" -> { + context.getThemeAttrColor(R.attr.colorMessageMedium) to Color.BLACK + } + else -> null to context.getThemeAttrColor(R.attr.colorOnSurface) + } + + with(binding) { + dashboardAdminMessageItemTitle.text = item.title + dashboardAdminMessageItemTitle.setTextColor(textColor) + dashboardAdminMessageItemDescription.text = item.content + dashboardAdminMessageItemDescription.setTextColor(textColor) + dashboardAdminMessageItemIcon.setColorFilter(textColor) + dashboardAdminMessageItemDismiss.isVisible = item.isDismissible + dashboardAdminMessageItemDismiss.setTextColor(textColor) + dashboardAdminMessageItemDismiss.setOnClickListener { + onAdminMessageDismissClickListener(item) + } + + root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) }) + item.destinationUrl?.let { url -> + root.setOnClickListener { onAdminMessageClickListener(url) } + } + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt index 1085ff509..ff7fd864b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt @@ -9,10 +9,12 @@ import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.databinding.FragmentLoginFormBinding import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.utils.* @@ -207,6 +209,19 @@ class LoginFormFragment : BaseFragment(R.layout.fragme binding.loginFormContainer.visibility = if (show) VISIBLE else GONE } + override fun showAdminMessage(message: AdminMessage?) { + AdminMessageViewHolder( + binding = binding.loginFormMessage, + onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed, + onAdminMessageClickListener = presenter::onAdminMessageSelected, + ).bind(message) + binding.loginFormMessage.root.isVisible = message != null + } + + override fun openInternetBrowser(url: String) { + requireContext().openInternetBrowser(url) + } + override fun showDomainSuffixInput(show: Boolean) { binding.loginFormDomainSuffixLayout.isVisible = show } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt index 85f428415..4e0404d9b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt @@ -1,13 +1,19 @@ package io.github.wulkanowy.ui.modules.login.form import androidx.core.net.toUri +import io.github.wulkanowy.data.db.entities.AdminMessage +import io.github.wulkanowy.data.enums.MessageType +import io.github.wulkanowy.data.flatResourceFlow import io.github.wulkanowy.data.logResourceStatus +import io.github.wulkanowy.data.onResourceData import io.github.wulkanowy.data.onResourceError import io.github.wulkanowy.data.onResourceLoading import io.github.wulkanowy.data.onResourceNotLoading import io.github.wulkanowy.data.onResourceSuccess +import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.resourceFlow +import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginErrorHandler @@ -22,7 +28,9 @@ class LoginFormPresenter @Inject constructor( studentRepository: StudentRepository, private val loginErrorHandler: LoginErrorHandler, private val appInfo: AppInfo, - private val analytics: AnalyticsHelper + private val analytics: AnalyticsHelper, + private val getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase, + private val preferencesRepository: PreferencesRepository, ) : BasePresenter(loginErrorHandler, studentRepository) { private var lastError: Throwable? = null @@ -41,6 +49,31 @@ class LoginFormPresenter @Inject constructor( Timber.i("Entered wrong username or password") } } + + reloadAdminMessage() + } + + private fun reloadAdminMessage() { + flatResourceFlow { + getAppropriateAdminMessageUseCase( + scrapperBaseUrl = view?.formHostValue.orEmpty(), + type = MessageType.LOGIN_MESSAGE, + ) + } + .logResourceStatus("load login admin message") + .onResourceData { view?.showAdminMessage(it) } + .onResourceError { view?.showAdminMessage(null) } + .launch() + } + + fun onAdminMessageSelected(url: String?) { + url?.let { view?.openInternetBrowser(it) } + } + + fun onAdminMessageDismissed(adminMessage: AdminMessage) { + preferencesRepository.dismissedAdminMessageIds += adminMessage.id + + view?.showAdminMessage(null) } fun onPrivacyLinkClick() { @@ -63,6 +96,7 @@ class LoginFormPresenter @Inject constructor( } updateCustomDomainSuffixVisibility() updateUsernameLabel() + reloadAdminMessage() } } @@ -103,7 +137,9 @@ class LoginFormPresenter @Inject constructor( val email = view?.formUsernameValue.orEmpty().trim() val password = view?.formPassValue.orEmpty().trim() val host = view?.formHostValue.orEmpty().trim() - val domainSuffix = view?.formDomainSuffix.orEmpty().trim() + val domainSuffix = view?.formDomainSuffix.orEmpty().trim().takeIf { + "customSuffix" in host + }.orEmpty() val symbol = view?.formHostSymbol.orEmpty().trim() if (!validateCredentials(email, password, host)) return diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt index 5fb260620..5b4dcadfe 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.ui.modules.login.form +import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.modules.login.LoginData @@ -58,6 +59,10 @@ interface LoginFormView : BaseView { fun showContent(show: Boolean) + fun showAdminMessage(message: AdminMessage?) + + fun openInternetBrowser(url: String) + fun showDomainSuffixInput(show: Boolean) fun showOtherOptionsButton(show: Boolean) diff --git a/app/src/main/res/layout/fragment_login_form.xml b/app/src/main/res/layout/fragment_login_form.xml index 39f3146c8..fc5e5f35e 100644 --- a/app/src/main/res/layout/fragment_login_form.xml +++ b/app/src/main/res/layout/fragment_login_form.xml @@ -105,6 +105,18 @@ android:background="?android:attr/listDivider" /> + + Date: Fri, 25 Aug 2023 00:01:36 +0200 Subject: [PATCH 816/869] Version 2.1.0 --- app/build.gradle | 8 ++++---- app/src/main/play/release-notes/pl-PL/default.txt | 9 +++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d0ae8894c..b09078a9e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,8 +27,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 33 - versionCode 131 // todo: already bumped for 2.1.0 version - versionName "2.0.8" + versionCode 131 + versionName "2.1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" @@ -161,7 +161,7 @@ play { track = 'production' releaseStatus = ReleaseStatus.IN_PROGRESS userFraction = 0.25d - updatePriority = 1 + updatePriority = 3 enabled.set(false) } @@ -191,7 +191,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.0.9-SNAPSHOT' + implementation 'io.github.wulkanowy:sdk:2.1.0' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index e881cfdaf..aa934ce98 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,7 +1,8 @@ -Wersja 2.0.8 +Wersja 2.1.0 -— poprawiliśmy wyświetlanie kilku rodzajów zmian w planie lekcji -— dodaliśmy limit znaków w okienku usprawiedliwiania -— naprawiliśmy wyświetlanie frekwencji w szkołach, gdzie działa już system eduOne (ciągle jednak brak opcji usprawiedliwiania) +— dodaliśmy tryb incognito w wiadomościach +— dodaliśmy wyświetlanie pustych lekcji (okienek) w planie lekcji +— poprawiliśmy widżet planu lekcji (będzie teraz trochę bardziej kompaktowy) +— zmieniliśmy datę rozpoczęcia roku szkolnego na 3 dni przed 1 września (sorry) Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases From 10f9812495f83ec7be0ffcea927b9eb95bdd9adc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Sep 2023 17:04:34 +0000 Subject: [PATCH 817/869] Bump com.android.tools.build:gradle from 8.1.0 to 8.1.1 (#2286) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2b52c068b..7adbb4694 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ buildscript { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$kotlin_version-1.0.11" - classpath 'com.android.tools.build:gradle:8.1.0' + classpath 'com.android.tools.build:gradle:8.1.1' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.15' classpath 'com.huawei.agconnect:agcp:1.9.1.300' From 5238e4d187ec314fc180630e0ba1ad687065a7da Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Sep 2023 17:04:57 +0000 Subject: [PATCH 818/869] Bump com.google.android.gms:play-services-ads from 22.2.0 to 22.3.0 (#2285) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index b09078a9e..7d8cc08ba 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -254,7 +254,7 @@ dependencies { playImplementation 'com.google.firebase:firebase-config-ktx' playImplementation 'com.google.android.play:core:1.10.3' playImplementation 'com.google.android.play:core-ktx:1.8.1' - playImplementation 'com.google.android.gms:play-services-ads:22.2.0' + playImplementation 'com.google.android.gms:play-services-ads:22.3.0' hmsImplementation 'com.huawei.hms:hianalytics:6.10.0.303' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.1.300' From aff56a83118e33259039d66c6d89bbfb4e2796f0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Sep 2023 17:05:12 +0000 Subject: [PATCH 819/869] Bump com.google.firebase:firebase-bom from 32.2.2 to 32.2.3 (#2284) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 7d8cc08ba..03c6c837c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -247,7 +247,7 @@ dependencies { implementation 'com.fredporciuncula:flow-preferences:1.9.1' implementation 'org.apache.commons:commons-text:1.10.0' - playImplementation platform('com.google.firebase:firebase-bom:32.2.2') + playImplementation platform('com.google.firebase:firebase-bom:32.2.3') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' From a77b3d4cd73961f0b17c01dd3866f95fc4b176b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Sep 2023 17:05:29 +0000 Subject: [PATCH 820/869] Bump com.google.firebase:firebase-crashlytics-gradle from 2.9.8 to 2.9.9 (#2283) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 7adbb4694..3f4d6d73b 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ buildscript { classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.15' classpath 'com.huawei.agconnect:agcp:1.9.1.300' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.8' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9' classpath "com.github.triplet.gradle:play-publisher:3.8.4" classpath "ru.cian:huawei-publish-gradle-plugin:1.4.0" classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.3.0.3225" From 50a177d18c01f1ecbdc04169034a0e49780cae9e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Sep 2023 17:05:47 +0000 Subject: [PATCH 821/869] Bump org.jetbrains.kotlinx:kotlinx-serialization-json (#2282) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 03c6c837c..b578b4cdd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -195,7 +195,7 @@ dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' - implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" implementation "androidx.core:core-ktx:1.10.1" From c82e6ae95bb25845a15fc067a5858a43c3b39f62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Fri, 1 Sep 2023 19:06:48 +0200 Subject: [PATCH 822/869] New Crowdin updates (#2287) --- app/src/main/res/values-uk/preferences_values.xml | 6 +++--- app/src/main/res/values-uk/strings.xml | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/res/values-uk/preferences_values.xml b/app/src/main/res/values-uk/preferences_values.xml index 55cf905bb..c02efb54a 100644 --- a/app/src/main/res/values-uk/preferences_values.xml +++ b/app/src/main/res/values-uk/preferences_values.xml @@ -52,9 +52,9 @@ Середня оцінка з цілого року - Don\'t show - Only between lessons - Before and between lessons + Не показувати + Тільки між уроками + Перед і між уроками Щасливий номер diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 876562d31..d01dcfbaf 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -186,10 +186,10 @@ Зміна вчителя з %1$s на %2$s Зміна теми з %1$s на %2$s - No lesson - No lessons - No lessons - No lessons + Немає уроку + Немає уроків + Немає уроків + Немає уроків Зміна у розкладі @@ -706,7 +706,7 @@ Розгортання оцінок Позначити поточний урок Показувати групи поруч з темами - Show empty tiles where there\'s no lesson + Показувати порожні плитки там, де немає уроків Показувати діаграми в оцінках класу Показати предмети без оцінок Схема кольорів оцінок From 8cc69728aac101c6821e08ef062c0470fcbc40f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Sep 2023 17:29:02 +0000 Subject: [PATCH 823/869] Bump kotlin_version from 1.9.0 to 1.9.10 (#2281) --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 3f4d6d73b..ba792ef1e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - kotlin_version = '1.9.0' + kotlin_version = '1.9.10' about_libraries = '10.8.3' hilt_version = "2.47" } @@ -13,7 +13,7 @@ buildscript { dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" - classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$kotlin_version-1.0.11" + classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$kotlin_version-1.0.13" classpath 'com.android.tools.build:gradle:8.1.1' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.15' From 8478b8b7ed08043a2f75e7d298f6e2f6dcfa6b70 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Sep 2023 18:13:24 +0000 Subject: [PATCH 824/869] Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin (#2291) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ba792ef1e..b1e35375f 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ buildscript { classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9' classpath "com.github.triplet.gradle:play-publisher:3.8.4" classpath "ru.cian:huawei-publish-gradle-plugin:1.4.0" - classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.3.0.3225" + classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.3.1.3277" classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0" classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries" } From 017d46e5dbb3e828074d22ab311787d390fc98ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Sep 2023 18:13:39 +0000 Subject: [PATCH 825/869] Bump hilt_version from 2.47 to 2.48 (#2290) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b1e35375f..51f4be8f7 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { kotlin_version = '1.9.10' about_libraries = '10.8.3' - hilt_version = "2.47" + hilt_version = "2.48" } repositories { mavenCentral() From 0a402378099d02629187a04cb6761a16bfb56ee9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Sep 2023 18:30:02 +0000 Subject: [PATCH 826/869] Bump com.github.bastienpaulfr:Treessence from 1.0.5 to 1.1.2 (#2289) --- app/build.gradle | 2 +- app/src/hms/java/io/github/wulkanowy/utils/CrashLogUtils.kt | 4 ++-- app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt | 2 +- app/src/main/java/io/github/wulkanowy/utils/LoggerUtils.kt | 2 +- .../play/java/io/github/wulkanowy/utils/CrashlyticsUtils.kt | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index b578b4cdd..fb03ca162 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -239,7 +239,7 @@ dependencies { implementation "com.jakewharton.timber:timber:5.0.1" implementation "at.favre.lib:slf4j-timber:1.0.1" - implementation 'com.github.bastienpaulfr:Treessence:1.0.5' + implementation 'com.github.bastienpaulfr:Treessence:1.1.2' implementation "com.mikepenz:aboutlibraries-core:$about_libraries" implementation "io.coil-kt:coil:2.4.0" implementation "io.github.wulkanowy:AppKillerManager:3.0.1" diff --git a/app/src/hms/java/io/github/wulkanowy/utils/CrashLogUtils.kt b/app/src/hms/java/io/github/wulkanowy/utils/CrashLogUtils.kt index 377e83666..2b1f1d30e 100644 --- a/app/src/hms/java/io/github/wulkanowy/utils/CrashLogUtils.kt +++ b/app/src/hms/java/io/github/wulkanowy/utils/CrashLogUtils.kt @@ -2,8 +2,8 @@ package io.github.wulkanowy.utils import android.util.Log import com.huawei.agconnect.crash.AGConnectCrash -import fr.bipi.tressence.base.FormatterPriorityTree -import fr.bipi.tressence.common.StackTraceRecorder +import fr.bipi.treessence.base.FormatterPriorityTree +import fr.bipi.treessence.common.StackTraceRecorder class CrashLogTree : FormatterPriorityTree(Log.VERBOSE) { diff --git a/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt b/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt index a39a38745..dc1061018 100644 --- a/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt +++ b/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt @@ -6,7 +6,7 @@ import androidx.hilt.work.HiltWorkerFactory import androidx.work.Configuration import com.yariksoffice.lingver.Lingver import dagger.hilt.android.HiltAndroidApp -import fr.bipi.tressence.file.FileLoggerTree +import fr.bipi.treessence.file.FileLoggerTree import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.ui.base.ThemeManager import io.github.wulkanowy.utils.* diff --git a/app/src/main/java/io/github/wulkanowy/utils/LoggerUtils.kt b/app/src/main/java/io/github/wulkanowy/utils/LoggerUtils.kt index 1e9f49a66..00ed2c110 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/LoggerUtils.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/LoggerUtils.kt @@ -7,7 +7,7 @@ import android.os.Bundle import android.view.View import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager -import fr.bipi.tressence.common.filters.Filter +import fr.bipi.treessence.common.filters.Filter import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException import timber.log.Timber diff --git a/app/src/play/java/io/github/wulkanowy/utils/CrashlyticsUtils.kt b/app/src/play/java/io/github/wulkanowy/utils/CrashlyticsUtils.kt index f980bc4bb..2e5fad622 100644 --- a/app/src/play/java/io/github/wulkanowy/utils/CrashlyticsUtils.kt +++ b/app/src/play/java/io/github/wulkanowy/utils/CrashlyticsUtils.kt @@ -2,8 +2,8 @@ package io.github.wulkanowy.utils import android.util.Log import com.google.firebase.crashlytics.FirebaseCrashlytics -import fr.bipi.tressence.base.FormatterPriorityTree -import fr.bipi.tressence.common.StackTraceRecorder +import fr.bipi.treessence.base.FormatterPriorityTree +import fr.bipi.treessence.common.StackTraceRecorder class CrashLogTree : FormatterPriorityTree(Log.VERBOSE) { From db02f0c1e12d815a1d0f619ebe99816a18654811 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Sep 2023 20:32:56 +0000 Subject: [PATCH 827/869] Bump com.google.android.gms:play-services-ads from 22.3.0 to 22.4.0 (#2301) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index fb03ca162..d1fbfd16a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -254,7 +254,7 @@ dependencies { playImplementation 'com.google.firebase:firebase-config-ktx' playImplementation 'com.google.android.play:core:1.10.3' playImplementation 'com.google.android.play:core-ktx:1.8.1' - playImplementation 'com.google.android.gms:play-services-ads:22.3.0' + playImplementation 'com.google.android.gms:play-services-ads:22.4.0' hmsImplementation 'com.huawei.hms:hianalytics:6.10.0.303' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.1.300' From 05a804832bd993f3afc5b588ba1eb292548c1a82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Sep 2023 20:33:10 +0000 Subject: [PATCH 828/869] Bump com.google.gms:google-services from 4.3.15 to 4.4.0 (#2300) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 51f4be8f7..8aec83fbb 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ buildscript { classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$kotlin_version-1.0.13" classpath 'com.android.tools.build:gradle:8.1.1' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" - classpath 'com.google.gms:google-services:4.3.15' + classpath 'com.google.gms:google-services:4.4.0' classpath 'com.huawei.agconnect:agcp:1.9.1.300' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9' classpath "com.github.triplet.gradle:play-publisher:3.8.4" From 81d8f7ea4826441f9d5b632d83889fadf949a6ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Sep 2023 20:33:30 +0000 Subject: [PATCH 829/869] Bump androidx.lifecycle:lifecycle-livedata-ktx from 2.6.1 to 2.6.2 (#2295) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index d1fbfd16a..46c6ed719 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -219,7 +219,7 @@ dependencies { implementation "androidx.work:work-runtime-ktx:$work_manager" playImplementation "androidx.work:work-gcm:$work_manager" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.2" implementation "androidx.room:room-runtime:$room" implementation "androidx.room:room-ktx:$room" From 09d16cf6d8b5f6016fdeadf6da3ec26b632d518a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Sep 2023 20:33:50 +0000 Subject: [PATCH 830/869] Bump androidx.annotation:annotation from 1.6.0 to 1.7.0 (#2293) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 46c6ed719..5b4eeacd1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -203,7 +203,7 @@ dependencies { implementation "androidx.activity:activity-ktx:1.7.2" implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.fragment:fragment-ktx:1.6.1" - implementation "androidx.annotation:annotation:1.6.0" + implementation "androidx.annotation:annotation:1.7.0" implementation "androidx.preference:preference-ktx:1.2.1" implementation "androidx.recyclerview:recyclerview:1.3.1" From aabd7345c1521066666f376fdc0c5c0e9b1a65ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Sep 2023 20:49:25 +0000 Subject: [PATCH 831/869] Bump com.google.firebase:firebase-bom from 32.2.3 to 32.3.1 (#2299) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 5b4eeacd1..6def2c133 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -247,7 +247,7 @@ dependencies { implementation 'com.fredporciuncula:flow-preferences:1.9.1' implementation 'org.apache.commons:commons-text:1.10.0' - playImplementation platform('com.google.firebase:firebase-bom:32.2.3') + playImplementation platform('com.google.firebase:firebase-bom:32.3.1') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' From 1d8d71709ff84c04bb36ae43d52bd186aebcaf7f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Sep 2023 20:49:46 +0000 Subject: [PATCH 832/869] Bump com.huawei.agconnect:agconnect-crash from 1.9.1.300 to 1.9.1.301 (#2298) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 6def2c133..57755d743 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -257,7 +257,7 @@ dependencies { playImplementation 'com.google.android.gms:play-services-ads:22.4.0' hmsImplementation 'com.huawei.hms:hianalytics:6.10.0.303' - hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.1.300' + hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.1.301' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From ff2aa6f195990be56bdc87a115075a5ef57e7bb3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Sep 2023 20:59:38 +0000 Subject: [PATCH 833/869] Bump com.huawei.agconnect:agcp from 1.9.1.300 to 1.9.1.301 (#2297) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 8aec83fbb..e7543d972 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { classpath 'com.android.tools.build:gradle:8.1.1' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.4.0' - classpath 'com.huawei.agconnect:agcp:1.9.1.300' + classpath 'com.huawei.agconnect:agcp:1.9.1.301' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9' classpath "com.github.triplet.gradle:play-publisher:3.8.4" classpath "ru.cian:huawei-publish-gradle-plugin:1.4.0" From c4a3da93ca41cb78806ddfd6aff1ab42e72739c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Sep 2023 21:00:06 +0000 Subject: [PATCH 834/869] Bump com.huawei.hms:hianalytics from 6.10.0.303 to 6.12.0.300 (#2294) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 57755d743..191a574e9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -256,7 +256,7 @@ dependencies { playImplementation 'com.google.android.play:core-ktx:1.8.1' playImplementation 'com.google.android.gms:play-services-ads:22.4.0' - hmsImplementation 'com.huawei.hms:hianalytics:6.10.0.303' + hmsImplementation 'com.huawei.hms:hianalytics:6.12.0.300' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.1.301' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From afd0c8513a034a89bb240f0592d442793b1edb7f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 17:43:16 +0000 Subject: [PATCH 835/869] Bump mockk from 1.13.7 to 1.13.8 (#2305) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 191a574e9..abe1a4b18 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -186,7 +186,7 @@ ext { android_hilt = "1.0.0" room = "2.5.2" chucker = "3.5.2" - mockk = "1.13.7" + mockk = "1.13.8" coroutines = "1.7.3" } From 646b4a149d64b55ac9699d62332831ad65c8a32e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 17:43:41 +0000 Subject: [PATCH 836/869] Bump about_libraries from 10.8.3 to 10.9.0 (#2304) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e7543d972..651e9ca16 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { kotlin_version = '1.9.10' - about_libraries = '10.8.3' + about_libraries = '10.9.0' hilt_version = "2.48" } repositories { From 0fa197d5204a5a56b51da2a497dd5241bc60c5cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Mon, 25 Sep 2023 19:43:57 +0200 Subject: [PATCH 837/869] New Crowdin updates (#2303) --- app/src/main/res/values-cs/preferences_values.xml | 6 +++--- app/src/main/res/values-cs/strings.xml | 10 +++++----- app/src/main/res/values-sk/preferences_values.xml | 6 +++--- app/src/main/res/values-sk/strings.xml | 10 +++++----- app/src/main/res/values-uk/strings.xml | 2 +- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/app/src/main/res/values-cs/preferences_values.xml b/app/src/main/res/values-cs/preferences_values.xml index 2cf402631..1590c47ab 100644 --- a/app/src/main/res/values-cs/preferences_values.xml +++ b/app/src/main/res/values-cs/preferences_values.xml @@ -52,9 +52,9 @@ Průměr známek z celého roku - Don\'t show - Only between lessons - Before and between lessons + Nezobrazovat + Pouze mezi lekcemi + Před a mezi lekcemi Šťastné číslo diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index a63a0aa12..e9e049326 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -186,10 +186,10 @@ Změna učitele z %1$s na %2$s Změna předmětu z %1$s na %2$s - No lesson - No lessons - No lessons - No lessons + Žádné lekce + Žádné lekce + Žádné lekce + Žádné lekce Změna plánu lekcí @@ -706,7 +706,7 @@ Rozvíjení známek Označit aktuální lekci Zobrazit skupiny vedle předmětů - Show empty tiles where there\'s no lesson + Zobrazit prázdné dlaždice, kde není žádná lekce Zobrazit seznam grafů v známkách třídy Zobrazit předměty bez známek Známky barevné schéma diff --git a/app/src/main/res/values-sk/preferences_values.xml b/app/src/main/res/values-sk/preferences_values.xml index fd393394f..6cd221540 100644 --- a/app/src/main/res/values-sk/preferences_values.xml +++ b/app/src/main/res/values-sk/preferences_values.xml @@ -52,9 +52,9 @@ Priemer známok z celého roka - Don\'t show - Only between lessons - Before and between lessons + Nezobrazovať + Iba medzi lekciami + Pred a medzi lekciami Šťastné číslo diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index d1990e35e..d059927eb 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -186,10 +186,10 @@ Zmena učiteľa z %1$s na %2$s Zmena predmetu z %1$s na %2$s - No lesson - No lessons - No lessons - No lessons + Žiadne lekcie + Žiadne lekcie + Žiadne lekcie + Žiadne lekcie Zmena plánu lekcií @@ -706,7 +706,7 @@ Rozvijanie známok Označiť aktuálne lekciu Zobraziť skupiny vedľa predmetov - Show empty tiles where there\'s no lesson + Zobraziť prázdne dlaždice, kde nie je žiadne lekcie Zobraziť zoznam grafov v známkach triedy Zobraziť predmety bez známok Známky farebnú schému diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index d01dcfbaf..b46b9afe9 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -55,7 +55,7 @@ Використовуйте призначений логін замість адреси e-mail Використовуйте призначений логін або адресу e-mail в @%1$s Неправильний symbol - Студента не знайдено. Перевірте symbol та обранний тип щоденника UONET+ + Студента не знайдено. Перевірте symbol та обраний тип щоденника UONET+ Цього учня вже авторизовано Symbol можно знайти на сторінці щоденника у Uczeń → Dostęp Mobilny → Wygeneruj kod dostępu.\n\nПереконайтеся, що ви вказали відповідний щоденник у полі Тип щоденника UONET+ на першому екрані логування Виберіть учнів для авторизації в додатку From 95b4d53fac498f81ef82d74b0836763fbd43c9bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Mon, 25 Sep 2023 19:44:13 +0200 Subject: [PATCH 838/869] Add schools API integration (#2302) --- app/build.gradle | 3 + .../github/wulkanowy/utils/IntegrityHelper.kt | 11 +++ .../github/wulkanowy/utils/IntegrityHelper.kt | 11 +++ .../io/github/wulkanowy/data/DataModule.kt | 17 ++++- .../wulkanowy/data/api/SchoolsService.kt | 14 ++++ .../github/wulkanowy/data/pojos/LoginEvent.kt | 21 ++++++ .../data/repositories/SchoolsRepository.kt | 68 +++++++++++++++++++ .../LoginStudentSelectPresenter.kt | 29 +++++--- .../java/io/github/wulkanowy/utils/AppInfo.kt | 3 +- .../github/wulkanowy/utils/IntegrityHelper.kt | 27 ++++++++ .../LoginStudentSelectPresenterTest.kt | 6 ++ 11 files changed, 195 insertions(+), 15 deletions(-) create mode 100644 app/src/fdroid/java/io/github/wulkanowy/utils/IntegrityHelper.kt create mode 100644 app/src/hms/java/io/github/wulkanowy/utils/IntegrityHelper.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/api/SchoolsService.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/pojos/LoginEvent.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/repositories/SchoolsRepository.kt create mode 100644 app/src/play/java/io/github/wulkanowy/utils/IntegrityHelper.kt diff --git a/app/build.gradle b/app/build.gradle index abe1a4b18..d85d0a3d8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -69,6 +69,7 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' signingConfig signingConfigs.release buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\"" + buildConfigField "String", "SCHOOLS_BASE_URL", '"https://schools.wulkanowy.net.pl"' } debug { minifyEnabled false @@ -78,6 +79,7 @@ android { versionNameSuffix "-dev" ext.enableCrashlytics = project.hasProperty("enableFirebase") buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\"" + buildConfigField "String", "SCHOOLS_BASE_URL", '"https://schools.wulkanowy.net.pl"' } } @@ -255,6 +257,7 @@ dependencies { playImplementation 'com.google.android.play:core:1.10.3' playImplementation 'com.google.android.play:core-ktx:1.8.1' playImplementation 'com.google.android.gms:play-services-ads:22.4.0' + playImplementation "com.google.android.play:integrity:1.2.0" hmsImplementation 'com.huawei.hms:hianalytics:6.12.0.300' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.1.301' diff --git a/app/src/fdroid/java/io/github/wulkanowy/utils/IntegrityHelper.kt b/app/src/fdroid/java/io/github/wulkanowy/utils/IntegrityHelper.kt new file mode 100644 index 000000000..7af68058c --- /dev/null +++ b/app/src/fdroid/java/io/github/wulkanowy/utils/IntegrityHelper.kt @@ -0,0 +1,11 @@ +package io.github.wulkanowy.utils + +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class IntegrityHelper @Inject constructor() { + + @Suppress("UNUSED_PARAMETER") + fun getIntegrityToken(requestId: String): String? = null +} diff --git a/app/src/hms/java/io/github/wulkanowy/utils/IntegrityHelper.kt b/app/src/hms/java/io/github/wulkanowy/utils/IntegrityHelper.kt new file mode 100644 index 000000000..7af68058c --- /dev/null +++ b/app/src/hms/java/io/github/wulkanowy/utils/IntegrityHelper.kt @@ -0,0 +1,11 @@ +package io.github.wulkanowy.utils + +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class IntegrityHelper @Inject constructor() { + + @Suppress("UNUSED_PARAMETER") + fun getIntegrityToken(requestId: String): String? = null +} diff --git a/app/src/main/java/io/github/wulkanowy/data/DataModule.kt b/app/src/main/java/io/github/wulkanowy/data/DataModule.kt index c9e4990f9..bea3f7064 100644 --- a/app/src/main/java/io/github/wulkanowy/data/DataModule.kt +++ b/app/src/main/java/io/github/wulkanowy/data/DataModule.kt @@ -14,6 +14,7 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import io.github.wulkanowy.data.api.AdminMessageService +import io.github.wulkanowy.data.api.SchoolsService import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.repositories.PreferencesRepository @@ -82,19 +83,29 @@ internal class DataModule { @Singleton @Provides - fun provideRetrofit( + fun provideAdminMessageService( okHttpClient: OkHttpClient, json: Json, appInfo: AppInfo - ): Retrofit = Retrofit.Builder() + ): AdminMessageService = Retrofit.Builder() .baseUrl(appInfo.messagesBaseUrl) .client(okHttpClient) .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) .build() + .create() @Singleton @Provides - fun provideAdminMessageService(retrofit: Retrofit): AdminMessageService = retrofit.create() + fun provideSchoolsService( + okHttpClient: OkHttpClient, + json: Json, + appInfo: AppInfo, + ): SchoolsService = Retrofit.Builder() + .baseUrl(appInfo.schoolsBaseUrl) + .client(okHttpClient) + .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) + .build() + .create() @Singleton @Provides diff --git a/app/src/main/java/io/github/wulkanowy/data/api/SchoolsService.kt b/app/src/main/java/io/github/wulkanowy/data/api/SchoolsService.kt new file mode 100644 index 000000000..a7da9b63c --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/api/SchoolsService.kt @@ -0,0 +1,14 @@ +package io.github.wulkanowy.data.api + +import io.github.wulkanowy.data.pojos.IntegrityRequest +import io.github.wulkanowy.data.pojos.LoginEvent +import retrofit2.http.Body +import retrofit2.http.POST +import javax.inject.Singleton + +@Singleton +interface SchoolsService { + + @POST("/log/loginEvent") + suspend fun logLoginEvent(@Body request: IntegrityRequest) +} diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/LoginEvent.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/LoginEvent.kt new file mode 100644 index 000000000..c2b4d2ded --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/pojos/LoginEvent.kt @@ -0,0 +1,21 @@ +package io.github.wulkanowy.data.pojos + +import kotlinx.serialization.Serializable + +@Serializable +data class LoginEvent( + val uuid: String, + val schoolName: String, + val schoolShort: String, + val schoolAddress: String, + val scraperBaseUrl: String, + val symbol: String, + val schoolId: String, + val loginType: String, +) + +@Serializable +data class IntegrityRequest( + val tokenString: String, + val data: T, +) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolsRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolsRepository.kt new file mode 100644 index 000000000..9c6429343 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolsRepository.kt @@ -0,0 +1,68 @@ +package io.github.wulkanowy.data.repositories + +import io.github.wulkanowy.data.api.SchoolsService +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.StudentWithSemesters +import io.github.wulkanowy.data.pojos.IntegrityRequest +import io.github.wulkanowy.data.pojos.LoginEvent +import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.ui.modules.login.LoginData +import io.github.wulkanowy.utils.IntegrityHelper +import io.github.wulkanowy.utils.getCurrentOrLast +import io.github.wulkanowy.utils.init +import kotlinx.coroutines.withTimeout +import timber.log.Timber +import java.util.UUID +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.time.Duration.Companion.seconds + +@Singleton +class SchoolsRepository @Inject constructor( + private val integrityHelper: IntegrityHelper, + private val schoolsService: SchoolsService, + private val sdk: Sdk, +) { + + suspend fun logSchoolLogin(loginData: LoginData, students: List) { + students.forEach { + runCatching { + withTimeout(10.seconds) { + logLogin(loginData, it.student, it.semesters.getCurrentOrLast()) + } + } + .onFailure { Timber.e(it) } + } + } + + private suspend fun logLogin(loginData: LoginData, student: Student, semester: Semester) { + val requestId = UUID.randomUUID().toString() + val token = integrityHelper.getIntegrityToken(requestId) ?: return + + val schoolInfo = sdk + .init(student.copy(password = loginData.password)) + .switchDiary( + diaryId = semester.diaryId, + kindergartenDiaryId = semester.kindergartenDiaryId, + schoolYear = semester.schoolYear + ) + .getSchool() + + schoolsService.logLoginEvent( + IntegrityRequest( + tokenString = token, + data = LoginEvent( + uuid = requestId, + schoolAddress = schoolInfo.address, + schoolName = schoolInfo.name, + schoolShort = student.schoolShortName, + scraperBaseUrl = student.scrapperBaseUrl, + loginType = student.loginType, + symbol = student.symbol, + schoolId = student.schoolSymbol, + ) + ) + ) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt index 70862e828..0da86e56e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt @@ -9,6 +9,7 @@ import io.github.wulkanowy.data.pojos.RegisterStudent import io.github.wulkanowy.data.pojos.RegisterSymbol import io.github.wulkanowy.data.pojos.RegisterUnit import io.github.wulkanowy.data.pojos.RegisterUser +import io.github.wulkanowy.data.repositories.SchoolsRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.sdk.scrapper.login.AccountPermissionException @@ -26,6 +27,7 @@ import javax.inject.Inject class LoginStudentSelectPresenter @Inject constructor( studentRepository: StudentRepository, + private val schoolsRepository: SchoolsRepository, private val loginErrorHandler: LoginErrorHandler, private val syncManager: SyncManager, private val analytics: AnalyticsHelper, @@ -236,17 +238,20 @@ class LoginStudentSelectPresenter @Inject constructor( } private fun registerStudents(students: List) { - val studentsWithSemesters = students - .filterIsInstance().map { item -> - item.student.mapToStudentWithSemesters( - user = registerUser, - symbol = item.symbol, - scrapperDomainSuffix = loginData.domainSuffix, - unit = item.unit, - colors = appInfo.defaultColorsForAvatar, - ) - } - resourceFlow { studentRepository.saveStudents(studentsWithSemesters) } + val filteredStudents = students.filterIsInstance() + val studentsWithSemesters = filteredStudents.map { item -> + item.student.mapToStudentWithSemesters( + user = registerUser, + symbol = item.symbol, + scrapperDomainSuffix = loginData.domainSuffix, + unit = item.unit, + colors = appInfo.defaultColorsForAvatar, + ) + } + resourceFlow { + studentRepository.saveStudents(studentsWithSemesters) + schoolsRepository.logSchoolLogin(loginData, studentsWithSemesters) + } .logResourceStatus("registration") .onEach { when (it) { @@ -254,11 +259,13 @@ class LoginStudentSelectPresenter @Inject constructor( showProgress(true) showContent(false) } + is Resource.Success -> { syncManager.startOneTimeSyncWorker(quiet = true) view?.navigateToNext() logRegisterEvent(studentsWithSemesters) } + is Resource.Error -> { view?.apply { showProgress(false) diff --git a/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt b/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt index 962e5b208..e16db53c6 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt @@ -25,7 +25,8 @@ open class AppInfo @Inject constructor() { open val systemModel: String get() = Build.MODEL - open val messagesBaseUrl = BuildConfig.MESSAGES_BASE_URL + open val messagesBaseUrl: String = BuildConfig.MESSAGES_BASE_URL + open val schoolsBaseUrl: String = BuildConfig.SCHOOLS_BASE_URL @Suppress("DEPRECATION") open val systemLanguage: String diff --git a/app/src/play/java/io/github/wulkanowy/utils/IntegrityHelper.kt b/app/src/play/java/io/github/wulkanowy/utils/IntegrityHelper.kt new file mode 100644 index 000000000..41df0487a --- /dev/null +++ b/app/src/play/java/io/github/wulkanowy/utils/IntegrityHelper.kt @@ -0,0 +1,27 @@ +package io.github.wulkanowy.utils + +import android.content.Context +import com.google.android.play.core.integrity.IntegrityManagerFactory +import com.google.android.play.core.integrity.IntegrityTokenRequest +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.tasks.await +import java.util.UUID +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class IntegrityHelper @Inject constructor( + @ApplicationContext private val context: Context, +) { + + suspend fun getIntegrityToken(nonce: String): String? { + val integrityManager = IntegrityManagerFactory.create(context) + + val integrityTokenResponse = integrityManager.requestIntegrityToken( + IntegrityTokenRequest.builder() + .setNonce(nonce) + .build() + ) + return integrityTokenResponse.await().token() + } +} diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt index 06aabec72..fad6436d8 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt @@ -5,6 +5,7 @@ import io.github.wulkanowy.data.pojos.RegisterStudent import io.github.wulkanowy.data.pojos.RegisterSymbol import io.github.wulkanowy.data.pojos.RegisterUnit import io.github.wulkanowy.data.pojos.RegisterUser +import io.github.wulkanowy.data.repositories.SchoolsRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.scrapper.Scrapper @@ -40,6 +41,9 @@ class LoginStudentSelectPresenterTest { @MockK lateinit var studentRepository: StudentRepository + @MockK + lateinit var schoolsRepository: SchoolsRepository + @MockK(relaxed = true) lateinit var analytics: AnalyticsHelper @@ -110,6 +114,7 @@ class LoginStudentSelectPresenterTest { clearMocks(studentRepository, loginStudentSelectView) coEvery { studentRepository.getSavedStudents(false) } returns emptyList() + coEvery { schoolsRepository.logSchoolLogin(any(), any()) } just Runs every { loginStudentSelectView.initView() } just Runs every { loginStudentSelectView.symbols } returns emptyMap() @@ -120,6 +125,7 @@ class LoginStudentSelectPresenterTest { presenter = LoginStudentSelectPresenter( studentRepository = studentRepository, + schoolsRepository = schoolsRepository, loginErrorHandler = errorHandler, syncManager = syncManager, analytics = analytics, From 4d3b16ec8073285f724448f4e73dbe5b75b9e786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Tue, 26 Sep 2023 21:01:25 +0200 Subject: [PATCH 839/869] Fix password toggle icon tint after clearing error (#2309) --- .../wulkanowy/ui/modules/login/form/LoginFormFragment.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt index ff7fd864b..aa2da3114 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt @@ -184,7 +184,9 @@ class LoginFormFragment : BaseFragment(R.layout.fragme override fun clearPassError() { binding.loginFormPassLayout.error = null - binding.loginFormPassLayout.setEndIconTintList(null) + binding.loginFormPassLayout.setEndIconTintList( + requireContext().getAttrColorStateList(R.attr.colorOnSurface) + ) binding.loginFormErrorBox.isVisible = false } From 183544646846be7f6fd1c160825b3a07e8c4e39d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Tue, 26 Sep 2023 21:01:59 +0200 Subject: [PATCH 840/869] Fix password reset related issues (#2308) * Fix login hint in password reset field * Don't hide first password reset button * Change recover button label --- .../wulkanowy/ui/modules/login/form/LoginFormFragment.kt | 3 +-- .../ui/modules/login/recover/LoginRecoverPresenter.kt | 4 ++-- app/src/main/res/values/strings.xml | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt index aa2da3114..0a2ea5d6d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt @@ -238,8 +238,7 @@ class LoginFormFragment : BaseFragment(R.layout.fragme } override fun showContact(show: Boolean) { - binding.loginFormContact.visibility = if (show) VISIBLE else GONE - binding.loginFormRecoverLink.visibility = if (show) GONE else VISIBLE + binding.loginFormContact.isVisible = show } override fun openPrivacyPolicyPage() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverPresenter.kt index 3d0493012..a424df40d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverPresenter.kt @@ -46,7 +46,7 @@ class LoginRecoverPresenter @Inject constructor( fun updateFields() { view?.run { - setUsernameHint(if ("standard" in recoverHostValue) emailHintString else loginPeselEmailHintString) + setUsernameHint(if ("email" in recoverHostValue) emailHintString else loginPeselEmailHintString) } } @@ -92,7 +92,7 @@ class LoginRecoverPresenter @Inject constructor( isCorrect = false } - if ("standard" in host && "@" !in username) { + if ("email" in host && "@" !in username) { view?.setUsernameError(view?.invalidEmailString.orEmpty()) isCorrect = false } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ce277bdce..7da41b3d4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -76,7 +76,7 @@ Zgłoszenie: Problemy z logowaniem Informacje o aplikacji:\n\nUrządzenie: %1$s\nWersja SDK: %2$s\nWersja aplikacji: %3$s\nDodatkowe informacje: %4$s\nIdentyfikator instalacji: %5$s\nOstatni błąd: %6$s\n\nNazwa szkoły i miejscowość: Make sure you select the correct UONET+ register variation! - I forgot my password + Reset password Recover your account Recover Student is already signed in From 26a95ecb995f701cd9eb7184499c7467c4c4527d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Tue, 26 Sep 2023 21:02:36 +0200 Subject: [PATCH 841/869] Auto select students for login (#2307) * Auto select students for login * Add sign in icon to sign in button on student select screen --- .../LoginStudentSelectPresenter.kt | 25 ++++++++++++++++++- app/src/main/res/drawable/ic_login.xml | 5 ++++ .../layout/fragment_login_student_select.xml | 2 ++ 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/drawable/ic_login.xml diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt index 0da86e56e..4e5cd5108 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt @@ -21,6 +21,7 @@ import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.ifNullOrBlank +import io.github.wulkanowy.utils.isCurrent import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -73,7 +74,14 @@ class LoginStudentSelectPresenter @Inject constructor( students = it.dataOrNull.orEmpty() when (it) { is Resource.Loading -> Timber.d("Login student select students load started") - is Resource.Success -> refreshItems() + is Resource.Success -> { + getStudentsWithCurrentlyActiveSemesters() + selectedStudents.clear() + selectedStudents.addAll(getStudentsWithCurrentlyActiveSemesters()) + view?.enableSignIn(selectedStudents.isNotEmpty()) + refreshItems() + } + is Resource.Error -> { errorHandler.dispatch(it.error) lastError = it.error @@ -83,6 +91,21 @@ class LoginStudentSelectPresenter @Inject constructor( }.launch() } + private fun getStudentsWithCurrentlyActiveSemesters(): List { + val students = registerUser.symbols.flatMap { symbol -> + symbol.schools.flatMap { unit -> + unit.students.map { + createStudentItem(it, symbol, unit, students) + } + } + } + return students.filter { student -> + student.student.semesters.any { semester -> + semester.isCurrent() + } + } + } + private fun createItems(): List = buildList { val notEmptySymbols = registerUser.symbols.filter { it.schools.isNotEmpty() } val emptySymbols = registerUser.symbols.filter { it.schools.isEmpty() } diff --git a/app/src/main/res/drawable/ic_login.xml b/app/src/main/res/drawable/ic_login.xml new file mode 100644 index 000000000..57aef3dca --- /dev/null +++ b/app/src/main/res/drawable/ic_login.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/fragment_login_student_select.xml b/app/src/main/res/layout/fragment_login_student_select.xml index c47b9ae35..04c808857 100644 --- a/app/src/main/res/layout/fragment_login_student_select.xml +++ b/app/src/main/res/layout/fragment_login_student_select.xml @@ -56,6 +56,8 @@ android:layout_marginBottom="32dp" android:enabled="false" android:text="@string/login_sign_in" + app:icon="@drawable/ic_login" + app:iconGravity="end" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/loginStudentSelectRecycler" /> From 58d5196ac9847ff7db7033e9df95c8febf27b9ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Tue, 26 Sep 2023 22:25:23 +0200 Subject: [PATCH 842/869] Improve symbol input field (#2312) --- .../ui/modules/login/symbol/LoginSymbolFragment.kt | 7 +++++++ .../ui/modules/login/symbol/LoginSymbolPresenter.kt | 11 +++++++++++ .../ui/modules/login/symbol/LoginSymbolView.kt | 2 ++ app/src/main/res/layout/fragment_login_symbol.xml | 4 +++- app/src/main/res/values/strings.xml | 4 +++- 5 files changed, 26 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt index 692aaeb76..679576599 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt @@ -100,6 +100,13 @@ class LoginSymbolFragment : } } + override fun setErrorSymbolDefinitelyInvalid() { + with(binding.loginSymbolNameLayout) { + requestFocus() + error = getString(R.string.login_invalid_symbol_definitely) + } + } + override fun setErrorSymbolRequire() { setErrorSymbol(getString(R.string.error_field_required)) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt index 91fe1ac3c..01855fcec 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt @@ -53,6 +53,10 @@ class LoginSymbolPresenter @Inject constructor( view?.setErrorSymbolRequire() return } + if (isFormDefinitelyInvalid()) { + view?.setErrorSymbolDefinitelyInvalid() + return + } loginData = loginData.copy( symbol = view?.symbolValue?.getNormalizedSymbol(), @@ -130,6 +134,13 @@ class LoginSymbolPresenter @Inject constructor( }.launch("login") } + private fun isFormDefinitelyInvalid(): Boolean { + val definitelyInvalidSymbols = listOf("vulcan", "uonet", "wulkanowy", "standardowa") + val normalizedSymbol = view?.symbolValue.orEmpty().getNormalizedSymbol() + + return normalizedSymbol in definitelyInvalidSymbols + } + fun onFaqClick() { view?.openFaqPage() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolView.kt index 6585c00f4..3c10d5744 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolView.kt @@ -18,6 +18,8 @@ interface LoginSymbolView : BaseView { fun setErrorSymbolInvalid() + fun setErrorSymbolDefinitelyInvalid() + fun setErrorSymbolRequire() fun setErrorSymbol(message: String) diff --git a/app/src/main/res/layout/fragment_login_symbol.xml b/app/src/main/res/layout/fragment_login_symbol.xml index d928d65fd..37f4c731a 100644 --- a/app/src/main/res/layout/fragment_login_symbol.xml +++ b/app/src/main/res/layout/fragment_login_symbol.xml @@ -141,7 +141,9 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/loginSymbolHelper"> + app:layout_constraintTop_toBottomOf="@+id/loginSymbolHelper" + app:placeholderText="@string/login_symbol_placeholder" + app:placeholderTextColor="?colorTertiary"> Token PIN Symbol + E.g. \"lodz\" or \"powiatjaroslawski\" Sign in Password too short Login details are incorrect @@ -59,7 +60,8 @@ Invalid email Use the assigned login instead of email Use the assigned login or email in @%1$s - Invalid symbol + Invalid symbol. If you cannot find it, please contact the school + Don\'t make this up! If you cannot find it, please contact the school Student not found. Validate the symbol and the chosen variation of the UONET+ register Selected student is already logged in The symbol can be found on the register page in Uczeń → Dostęp Mobilny → Wygeneruj kod dostępu.\n\nMake sure that you have set the appropriate register variant in the UONET+ register variant field on the first login screen From 711de0f77f0954d78b70c2e7d5689aa8bada7919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Tue, 26 Sep 2023 22:26:19 +0200 Subject: [PATCH 843/869] Fix average calculation when there is no real semesters available (#2310) --- .../github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt index 2d63aae4d..ec4bd8e8c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt @@ -58,7 +58,7 @@ class GradeAverageProvider @Inject constructor( when (params.gradeAverageMode) { ONE_SEMESTER -> getGradeSubjects( student = student, - semester = semesters.single { it.semesterId == semesterId }, + semester = semesters.first { it.semesterId == semesterId }, forceRefresh = forceRefresh, params = params, ) From fca69e723427dbb67f90cc32bb8be09dad99903a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Tue, 26 Sep 2023 22:27:08 +0200 Subject: [PATCH 844/869] Add form dialog to login e-mail support (#2306) --- .../modules/login/form/LoginFormFragment.kt | 19 +-- .../modules/login/form/LoginFormPresenter.kt | 41 ++++-- .../ui/modules/login/form/LoginFormView.kt | 3 +- .../LoginStudentSelectFragment.kt | 20 +-- .../LoginStudentSelectPresenter.kt | 33 ++--- .../studentselect/LoginStudentSelectView.kt | 3 +- .../login/support/LoginSupportDialog.kt | 133 ++++++++++++++++++ .../modules/login/support/LoginSupportInfo.kt | 12 ++ .../login/symbol/LoginSymbolFragment.kt | 25 ++-- .../login/symbol/LoginSymbolPresenter.kt | 19 ++- .../modules/login/symbol/LoginSymbolView.kt | 3 +- .../wulkanowy/ui/modules/main/MainActivity.kt | 1 - .../main/res/layout/dialog_login_support.xml | 92 ++++++++++++ .../main/res/layout/fragment_dashboard.xml | 5 +- app/src/main/res/values/strings.xml | 8 +- app/src/main/res/values/styles.xml | 1 - 16 files changed, 322 insertions(+), 96 deletions(-) create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/login/support/LoginSupportDialog.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/login/support/LoginSupportInfo.kt create mode 100644 app/src/main/res/layout/dialog_login_support.xml diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt index 0a2ea5d6d..8e9b86fa3 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt @@ -17,6 +17,8 @@ import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.LoginData +import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog +import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo import io.github.wulkanowy.utils.* import javax.inject.Inject @@ -282,20 +284,7 @@ class LoginFormFragment : BaseFragment(R.layout.fragme presenter.updateCustomDomainSuffixVisibility() } - override fun openEmail(lastError: String) { - context?.openEmailClient( - chooserTitle = requireContext().getString(R.string.login_email_intent_title), - email = "wulkanowyinc@gmail.com", - subject = requireContext().getString(R.string.login_email_subject), - body = requireContext().getString( - R.string.login_email_text, - "${appInfo.systemManufacturer} ${appInfo.systemModel}", - appInfo.systemVersion.toString(), - "${appInfo.versionName}-${appInfo.buildFlavor}", - "$formHostValue/$formHostSymbol", - preferencesRepository.installationId, - lastError - ) - ) + override fun openEmail(supportInfo: LoginSupportInfo) { + LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog") } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt index 4e0404d9b..ad535c382 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt @@ -17,6 +17,7 @@ import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginErrorHandler +import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.ifNullOrBlank @@ -133,7 +134,7 @@ class LoginFormPresenter @Inject constructor( } } - fun onSignInClick() { + private fun getLoginData(): LoginData { val email = view?.formUsernameValue.orEmpty().trim() val password = view?.formPassValue.orEmpty().trim() val host = view?.formHostValue.orEmpty().trim() @@ -142,15 +143,27 @@ class LoginFormPresenter @Inject constructor( }.orEmpty() val symbol = view?.formHostSymbol.orEmpty().trim() - if (!validateCredentials(email, password, host)) return + return LoginData( + login = email, + password = password, + baseUrl = host, + domainSuffix = domainSuffix, + symbol = symbol + ) + } + + fun onSignInClick() { + val loginData = getLoginData() + + if (!validateCredentials(loginData.login, loginData.password, loginData.baseUrl)) return resourceFlow { studentRepository.getUserSubjectsFromScrapper( - email = email, - password = password, - scrapperBaseUrl = host, - domainSuffix = domainSuffix, - symbol = symbol + email = loginData.login, + password = loginData.password, + scrapperBaseUrl = loginData.baseUrl, + domainSuffix = loginData.domainSuffix, + symbol = loginData.symbol.orEmpty(), ) } .logResourceStatus("login") @@ -162,7 +175,6 @@ class LoginFormPresenter @Inject constructor( } } .onResourceSuccess { - val loginData = LoginData(email, password, host, domainSuffix, symbol) when (it.symbols.size) { 0 -> view?.navigateToSymbol(loginData) else -> view?.navigateToStudentSelect(loginData, it) @@ -170,7 +182,7 @@ class LoginFormPresenter @Inject constructor( analytics.logEvent( "registration_form", "success" to true, - "scrapperBaseUrl" to host, + "scrapperBaseUrl" to loginData.baseUrl, "error" to "No error" ) } @@ -187,7 +199,7 @@ class LoginFormPresenter @Inject constructor( analytics.logEvent( "registration_form", "success" to false, - "scrapperBaseUrl" to host, + "scrapperBaseUrl" to loginData.baseUrl, "error" to it.message.ifNullOrBlank { "No message" } ) } @@ -199,7 +211,14 @@ class LoginFormPresenter @Inject constructor( } fun onEmailClick() { - view?.openEmail(lastError?.message.ifNullOrBlank { "none" }) + view?.openEmail( + LoginSupportInfo( + loginData = getLoginData(), + lastErrorMessage = lastError?.message, + registerUser = null, + enteredSymbol = null, + ) + ) } fun onRecoverClick() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt index 5b4dcadfe..f2b7b1003 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt @@ -4,6 +4,7 @@ import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.modules.login.LoginData +import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo interface LoginFormView : BaseView { @@ -79,7 +80,7 @@ interface LoginFormView : BaseView { fun openFaqPage() - fun openEmail(lastError: String) + fun openEmail(supportInfo: LoginSupportInfo) fun openAdvancedLogin() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt index c33d12faa..06efd8d98 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt @@ -10,8 +10,11 @@ import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.databinding.FragmentLoginStudentSelectBinding import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.auth.AuthDialog import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.LoginData +import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog +import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.openEmailClient import io.github.wulkanowy.utils.openInternetBrowser @@ -106,21 +109,8 @@ class LoginStudentSelectFragment : context?.openInternetBrowser("https://discord.gg/vccAQBr", ::showMessage) } - override fun openEmail(lastError: String) { - context?.openEmailClient( - chooserTitle = requireContext().getString(R.string.login_email_intent_title), - email = "wulkanowyinc@gmail.com", - subject = requireContext().getString(R.string.login_email_subject), - body = requireContext().getString( - R.string.login_email_text, - "${appInfo.systemManufacturer} ${appInfo.systemModel}", - appInfo.systemVersion.toString(), - "${appInfo.versionName}-${appInfo.buildFlavor}", - "Select users to log in", - preferencesRepository.installationId, - lastError - ) - ) + override fun openEmail(supportInfo: LoginSupportInfo) { + LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog") } override fun onDestroyView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt index 4e5cd5108..cc9e422ed 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt @@ -12,15 +12,14 @@ import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.data.repositories.SchoolsRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.resourceFlow -import io.github.wulkanowy.sdk.scrapper.login.AccountPermissionException import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginErrorHandler +import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AppInfo -import io.github.wulkanowy.utils.ifNullOrBlank import io.github.wulkanowy.utils.isCurrent import kotlinx.coroutines.flow.onEach import timber.log.Timber @@ -311,28 +310,14 @@ class LoginStudentSelectPresenter @Inject constructor( } private fun onEmailClick() { - view?.openEmail(lastError?.message.ifNullOrBlank { - loginData.baseUrl + "/" + loginData.symbol + "\n" + registerUser.symbols.filterNot { - (it.error is AccountPermissionException || it.error is InvalidSymbolException) && it.symbol != loginData.symbol - }.joinToString(";\n") { symbol -> - buildString { - append(" -") - append(symbol.symbol) - append("(${symbol.error?.message?.let { it.take(46) + "..." } ?: symbol.schools.size})") - if (symbol.schools.isNotEmpty()) { - append(": ") - } - append(symbol.schools.joinToString(", ") { unit -> - buildString { - append(unit.schoolShortName) - append("(${unit.error?.message?.let { it.take(46) + "..." } ?: unit.students.size})") - } - }) - } - } + "\nPozostałe: " + registerUser.symbols.filter { - it.error is AccountPermissionException || it.error is InvalidSymbolException - }.joinToString(", ") { it.symbol } - }) + view?.openEmail( + LoginSupportInfo( + loginData = loginData, + registerUser = registerUser, + lastErrorMessage = lastError?.message, + enteredSymbol = loginData.symbol, + ) + ) } private fun logRegisterEvent( diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectView.kt index 39f312bf3..b69700f17 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectView.kt @@ -2,6 +2,7 @@ package io.github.wulkanowy.ui.modules.login.studentselect import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.modules.login.LoginData +import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo interface LoginStudentSelectView : BaseView { @@ -23,5 +24,5 @@ interface LoginStudentSelectView : BaseView { fun openDiscordInvite() - fun openEmail(lastError: String) + fun openEmail(supportInfo: LoginSupportInfo) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/support/LoginSupportDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/support/LoginSupportDialog.kt new file mode 100644 index 000000000..d2b1d2ce6 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/support/LoginSupportDialog.kt @@ -0,0 +1,133 @@ +package io.github.wulkanowy.ui.modules.login.support + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.os.bundleOf +import androidx.core.widget.doOnTextChanged +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.databinding.DialogLoginSupportBinding +import io.github.wulkanowy.sdk.scrapper.login.AccountPermissionException +import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException +import io.github.wulkanowy.ui.base.BaseDialogFragment +import io.github.wulkanowy.utils.AppInfo +import io.github.wulkanowy.utils.openEmailClient +import io.github.wulkanowy.utils.serializable +import javax.inject.Inject + +@AndroidEntryPoint +class LoginSupportDialog : BaseDialogFragment() { + + @Inject + lateinit var appInfo: AppInfo + + @Inject + lateinit var preferencesRepository: PreferencesRepository + + private lateinit var supportInfo: LoginSupportInfo + + companion object { + private const val ARGUMENT_KEY = "info" + + fun newInstance(info: LoginSupportInfo) = LoginSupportDialog().apply { + arguments = bundleOf(ARGUMENT_KEY to info) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NO_FRAME, R.style.WulkanowyTheme_NoActionBar) + supportInfo = requireArguments().serializable(ARGUMENT_KEY) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val binding = DialogLoginSupportBinding.inflate(inflater) + .apply { binding = this } + binding.dialogLoginSupportToolbar.setNavigationOnClickListener { dismiss() } + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + with(binding) { + dialogLoginSupportSchoolInput.doOnTextChanged { _, _, _, _ -> + with(dialogLoginSupportSchoolLayout) { + isErrorEnabled = false + error = null + } + } + dialogLoginSupportSubmit.setOnClickListener { + if (dialogLoginSupportSchoolInput.text.isNullOrBlank()) { + with(dialogLoginSupportSchoolLayout) { + isErrorEnabled = true + error = getString(R.string.error_field_required) + } + } else { + onSubmitClick() + dismiss() + } + } + } + } + + private fun onSubmitClick() { + with(binding) { + context?.openEmailClient( + chooserTitle = requireContext().getString(R.string.login_email_intent_title), + email = "wulkanowyinc@gmail.com", + subject = requireContext().getString(R.string.login_email_subject), + body = requireContext().getString( + R.string.login_email_text, + "${appInfo.systemManufacturer} ${appInfo.systemModel}", + appInfo.systemVersion.toString(), + "${appInfo.versionName}-${appInfo.buildFlavor}", + supportInfo.loginData.baseUrl + "/" + supportInfo.loginData.symbol, + preferencesRepository.installationId, + getLastErrorFromStudentSelectScreen(), + dialogLoginSupportSchoolInput.text.takeIf { !it.isNullOrBlank() } + ?: return@with, + dialogLoginSupportAdditionalInput.text, + ) + ) + } + } + + private fun getLastErrorFromStudentSelectScreen(): String { + if (!supportInfo.lastErrorMessage.isNullOrBlank()) { + return supportInfo.lastErrorMessage!! + } + if (supportInfo.registerUser?.symbols.isNullOrEmpty()) { + return "" + } + + return "\n" + supportInfo.registerUser?.symbols?.filterNot { + (it.error is AccountPermissionException || it.error is InvalidSymbolException) && + it.symbol != supportInfo.enteredSymbol + }?.joinToString(";\n") { symbol -> + buildString { + append(" -") + append(symbol.symbol) + append("(${symbol.error?.message?.let { it.take(46) + "..." } ?: symbol.schools.size})") + if (symbol.schools.isNotEmpty()) { + append(": ") + } + append(symbol.schools.joinToString(", ") { unit -> + buildString { + append(unit.schoolShortName) + append("(${unit.error?.message?.let { it.take(46) + "..." } ?: unit.students.size})") + } + }) + } + } + "\nPozostałe: " + supportInfo.registerUser?.symbols?.filter { + it.error is AccountPermissionException || it.error is InvalidSymbolException + }?.joinToString(", ") { it.symbol } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/support/LoginSupportInfo.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/support/LoginSupportInfo.kt new file mode 100644 index 000000000..3f101dd63 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/support/LoginSupportInfo.kt @@ -0,0 +1,12 @@ +package io.github.wulkanowy.ui.modules.login.support + +import io.github.wulkanowy.data.pojos.RegisterUser +import io.github.wulkanowy.ui.modules.login.LoginData +import java.io.Serializable + +data class LoginSupportInfo( + val loginData: LoginData, + val registerUser: RegisterUser?, + val lastErrorMessage: String?, + val enteredSymbol: String?, +) : Serializable diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt index 679576599..23ebffe9d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt @@ -18,7 +18,13 @@ import io.github.wulkanowy.databinding.FragmentLoginSymbolBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.LoginData -import io.github.wulkanowy.utils.* +import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog +import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo +import io.github.wulkanowy.utils.AppInfo +import io.github.wulkanowy.utils.hideSoftInput +import io.github.wulkanowy.utils.openInternetBrowser +import io.github.wulkanowy.utils.serializable +import io.github.wulkanowy.utils.showSoftInput import javax.inject.Inject @AndroidEntryPoint @@ -170,20 +176,7 @@ class LoginSymbolFragment : ) } - override fun openEmail(host: String, lastError: String) { - context?.openEmailClient( - chooserTitle = requireContext().getString(R.string.login_email_intent_title), - email = "wulkanowyinc@gmail.com", - subject = requireContext().getString(R.string.login_email_subject), - body = requireContext().getString( - R.string.login_email_text, - "${appInfo.systemManufacturer} ${appInfo.systemModel}", - appInfo.systemVersion.toString(), - "${appInfo.versionName}-${appInfo.buildFlavor}", - "$host/${binding.loginSymbolName.text}", - preferencesRepository.installationId, - lastError - ) - ) + override fun openSupportDialog(supportInfo: LoginSupportInfo) { + LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog") } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt index 01855fcec..02bfde5d7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt @@ -11,6 +11,7 @@ import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginErrorHandler +import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.ifNullOrBlank import kotlinx.coroutines.flow.onEach @@ -78,6 +79,7 @@ class LoginSymbolPresenter @Inject constructor( showProgress(true) showContent(false) } + is Resource.Success -> { when (user.data.symbols.size) { 0 -> { @@ -87,6 +89,7 @@ class LoginSymbolPresenter @Inject constructor( showContact(true) } } + else -> { val enteredSymbolDetails = user.data.symbols .firstOrNull() @@ -111,6 +114,7 @@ class LoginSymbolPresenter @Inject constructor( "error" to "No error" ) } + is Resource.Error -> { Timber.i("Login with symbol result: An exception occurred") analytics.logEvent( @@ -146,12 +150,13 @@ class LoginSymbolPresenter @Inject constructor( } fun onEmailClick() { - view?.openEmail(loginData.baseUrl, lastError?.message.ifNullOrBlank { - registerUser?.symbols?.flatMap { symbol -> - symbol.schools.map { it.error?.message } + symbol.error?.message - }?.filterNotNull()?.distinct()?.joinToString(";") { - it.take(46) + "..." - } ?: "blank" - }) + view?.openSupportDialog( + LoginSupportInfo( + loginData = loginData, + registerUser = registerUser, + lastErrorMessage = lastError?.message, + enteredSymbol = view?.symbolValue, + ) + ) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolView.kt index 3c10d5744..ace12f780 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolView.kt @@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.login.symbol import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.modules.login.LoginData +import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo interface LoginSymbolView : BaseView { @@ -42,5 +43,5 @@ interface LoginSymbolView : BaseView { fun openFaqPage() - fun openEmail(host: String, lastError: String) + fun openSupportDialog(supportInfo: LoginSupportInfo) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt index 091080a55..178d6e94b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt @@ -119,7 +119,6 @@ class MainActivity : BaseActivity(), MainVie //https://developer.android.com/guide/playcore/in-app-updates#status_callback @Deprecated("Deprecated in Java") - @Suppress("DEPRECATION") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) updateHelper.onActivityResult(requestCode, resultCode) diff --git a/app/src/main/res/layout/dialog_login_support.xml b/app/src/main/res/layout/dialog_login_support.xml new file mode 100644 index 000000000..c04d78120 --- /dev/null +++ b/app/src/main/res/layout/dialog_login_support.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_dashboard.xml b/app/src/main/res/layout/fragment_dashboard.xml index 30f85446a..348602d77 100644 --- a/app/src/main/res/layout/fragment_dashboard.xml +++ b/app/src/main/res/layout/fragment_dashboard.xml @@ -26,7 +26,8 @@ android:layout_height="match_parent" android:clipToPadding="false" android:paddingTop="6dp" - android:paddingBottom="6dp" /> + android:paddingBottom="6dp" + tools:listitem="@layout/item_dashboard_grades" /> - \ No newline at end of file + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index df40fe707..08e3ebe61 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -76,7 +76,7 @@ Discord Send email Zgłoszenie: Problemy z logowaniem - Informacje o aplikacji:\n\nUrządzenie: %1$s\nWersja SDK: %2$s\nWersja aplikacji: %3$s\nDodatkowe informacje: %4$s\nIdentyfikator instalacji: %5$s\nOstatni błąd: %6$s\n\nNazwa szkoły i miejscowość: + Informacje o aplikacji:\n\nUrządzenie: %1$s\nWersja SDK: %2$s\nWersja aplikacji: %3$s\nDodatkowe informacje: %4$s\nIdentyfikator instalacji: %5$s\nOstatni błąd: %6$s\n\nNazwa szkoły i miejscowość: %7$s\nDodatkowe informacje: %8$s Make sure you select the correct UONET+ register variation! Reset password Recover your account @@ -86,6 +86,12 @@ Other search locations No active students found Enter a different symbol + Get help + Full school name (required) + Np. ZSTiO Jarosław lub SP nr 99 w Łodzi + Additional information in Polish (optional) + Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\" + Submit diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 603e22abc..a0023dda1 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -82,7 +82,6 @@ From 4d085f826652b7fd9a04fa1ddbf8cae3d3a97a4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Tue, 26 Sep 2023 23:04:27 +0200 Subject: [PATCH 845/869] New Crowdin updates (#2311) --- app/src/main/res/values-cs/strings.xml | 12 ++++++++++-- app/src/main/res/values-da-rDK/strings.xml | 12 ++++++++++-- app/src/main/res/values-de/strings.xml | 12 ++++++++++-- app/src/main/res/values-es-rES/strings.xml | 12 ++++++++++-- app/src/main/res/values-it-rIT/strings.xml | 12 ++++++++++-- app/src/main/res/values-pl/strings.xml | 12 ++++++++++-- app/src/main/res/values-ru/strings.xml | 12 ++++++++++-- app/src/main/res/values-sk/strings.xml | 12 ++++++++++-- app/src/main/res/values-uk/strings.xml | 12 ++++++++++-- 9 files changed, 90 insertions(+), 18 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index e9e049326..a365942a8 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -44,6 +44,7 @@ Token PIN Symbol + E.g. \"lodz\" or \"powiatjaroslawski\" Přihlásit Toto heslo je příliš krátké Přihlašovací údaje jsou nesprávné @@ -54,7 +55,8 @@ Neplatný e-mail Místo e-mailu použijte přiřazené přihlašovací údaje Použijte přiřazené přihlašovací nebo e-mail v @%1$s - Neplatný symbol + Invalid symbol. If you cannot find it, please contact the school + Don\'t make this up! If you cannot find it, please contact the school Žák nebyl nalezen. Zkontrolujte správnost symbolu a vybrané varianty deníku UONET+ Vybraný žák je už přihlášen Symbol najdete na stránce deníku v  Uczeń→ Dostęp Mobilny → Wygeneruj kod dostępu.\n\nUjistěte se, že jste nastavili správnou variantu deníku v poli Variace deníku UONET+ na první přihlašovací obrazovce @@ -69,7 +71,7 @@ Discord Poslat e-mail Ujistěte se, že jste vybrali správnou variantu deníku UONET+! - Zapomněl jsem své heslo + Reset password Obnovte svůj účet Obnovit Žák je už přihlášen @@ -77,6 +79,12 @@ Jiná místa vyhledávání Nebyli nalezeni žádní aktivní žáci Zadejte jiný symbol + Get help + Full school name (required) + Np. ZSTiO Jarosław lub SP nr 99 w Łodzi + Additional information in Polish (optional) + Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\" + Submit Povolit oznámení Povolit upozornění, abyste nezmeškali zprávu od učitele nebo o nové známce diff --git a/app/src/main/res/values-da-rDK/strings.xml b/app/src/main/res/values-da-rDK/strings.xml index 2abf1a4a6..c8846684b 100644 --- a/app/src/main/res/values-da-rDK/strings.xml +++ b/app/src/main/res/values-da-rDK/strings.xml @@ -44,6 +44,7 @@ Token PIN Symbol + E.g. \"lodz\" or \"powiatjaroslawski\" Sign in Password too short Login details are incorrect @@ -54,7 +55,8 @@ Invalid email Use the assigned login instead of email Use the assigned login or email in @%1$s - Invalid symbol + Invalid symbol. If you cannot find it, please contact the school + Don\'t make this up! If you cannot find it, please contact the school Student not found. Validate the symbol and the chosen variation of the UONET+ register Selected student is already logged in The symbol can be found on the register page in Uczeń → Dostęp Mobilny → Wygeneruj kod dostępu.\n\nMake sure that you have set the appropriate register variant in the UONET+ register variant field on the first login screen @@ -69,7 +71,7 @@ Discord Send email Make sure you select the correct UONET+ register variation! - I forgot my password + Reset password Recover your account Recover Student is already signed in @@ -77,6 +79,12 @@ Other search locations No active students found Enter a different symbol + Get help + Full school name (required) + Np. ZSTiO Jarosław lub SP nr 99 w Łodzi + Additional information in Polish (optional) + Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\" + Submit Enable notifications Enable notifications so you don\'t miss message from teacher or new grade diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index f08a504a5..a909a6b80 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -44,6 +44,7 @@ Token PIN Symbol + E.g. \"lodz\" or \"powiatjaroslawski\" Anmelden Passwort ist zu kurz Anmeldedaten sind falsch @@ -54,7 +55,8 @@ Ungültige email Den zugewiesenen Login anstelle von email verwenden Benutze den zugewiesenen Login oder E-Mail in @%1$s - Ungültige symbol + Invalid symbol. If you cannot find it, please contact the school + Don\'t make this up! If you cannot find it, please contact the school Schüler nicht gefunden. Überprüfen Sie das Symbol und die gewählte Variation des UONET+ Registers Ausgewählter Student ist bereits angemeldet. Das Symbol kann auf der Registerseite in Student → Tost Möbeln → Registrieren Sie Ihr Mobilgerätgefunden werden.\n\nStellen Sie sicher, dass Sie die entsprechende Registervariante im Feld UONET+ Registervariante auf dem vorherigen Bildschirm festgelegt haben @@ -69,7 +71,7 @@ Discord email senden Stellen Sie sicher, dass Sie die richtige UONET+ Registervariation wählen! - Ich habe mein Passwort vergessen. + Reset password Ihr Konto wiederherstellen Wiederherstellen Student ist bereits angemeldet @@ -77,6 +79,12 @@ Andere Suchorte Keine aktiven Schüler gefunden Geben Sie ein anderes Symbol ein + Get help + Full school name (required) + Np. ZSTiO Jarosław lub SP nr 99 w Łodzi + Additional information in Polish (optional) + Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\" + Submit Benachrichtigungen aktivieren Aktivieren Sie Benachrichtigungen, damit Sie keine Nachricht vom Lehrer oder eine neue Klasse verpassen diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 2abf1a4a6..c8846684b 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -44,6 +44,7 @@ Token PIN Symbol + E.g. \"lodz\" or \"powiatjaroslawski\" Sign in Password too short Login details are incorrect @@ -54,7 +55,8 @@ Invalid email Use the assigned login instead of email Use the assigned login or email in @%1$s - Invalid symbol + Invalid symbol. If you cannot find it, please contact the school + Don\'t make this up! If you cannot find it, please contact the school Student not found. Validate the symbol and the chosen variation of the UONET+ register Selected student is already logged in The symbol can be found on the register page in Uczeń → Dostęp Mobilny → Wygeneruj kod dostępu.\n\nMake sure that you have set the appropriate register variant in the UONET+ register variant field on the first login screen @@ -69,7 +71,7 @@ Discord Send email Make sure you select the correct UONET+ register variation! - I forgot my password + Reset password Recover your account Recover Student is already signed in @@ -77,6 +79,12 @@ Other search locations No active students found Enter a different symbol + Get help + Full school name (required) + Np. ZSTiO Jarosław lub SP nr 99 w Łodzi + Additional information in Polish (optional) + Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\" + Submit Enable notifications Enable notifications so you don\'t miss message from teacher or new grade diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml index 2abf1a4a6..c8846684b 100644 --- a/app/src/main/res/values-it-rIT/strings.xml +++ b/app/src/main/res/values-it-rIT/strings.xml @@ -44,6 +44,7 @@ Token PIN Symbol + E.g. \"lodz\" or \"powiatjaroslawski\" Sign in Password too short Login details are incorrect @@ -54,7 +55,8 @@ Invalid email Use the assigned login instead of email Use the assigned login or email in @%1$s - Invalid symbol + Invalid symbol. If you cannot find it, please contact the school + Don\'t make this up! If you cannot find it, please contact the school Student not found. Validate the symbol and the chosen variation of the UONET+ register Selected student is already logged in The symbol can be found on the register page in Uczeń → Dostęp Mobilny → Wygeneruj kod dostępu.\n\nMake sure that you have set the appropriate register variant in the UONET+ register variant field on the first login screen @@ -69,7 +71,7 @@ Discord Send email Make sure you select the correct UONET+ register variation! - I forgot my password + Reset password Recover your account Recover Student is already signed in @@ -77,6 +79,12 @@ Other search locations No active students found Enter a different symbol + Get help + Full school name (required) + Np. ZSTiO Jarosław lub SP nr 99 w Łodzi + Additional information in Polish (optional) + Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\" + Submit Enable notifications Enable notifications so you don\'t miss message from teacher or new grade diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index a2b5510ec..4ed2facc8 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -44,6 +44,7 @@ Token PIN Symbol + Np. \"lodz\" czy \"powiatjaroslawski\" Zaloguj To hasło jest za krótkie Dane logowania są niepoprawne @@ -54,7 +55,8 @@ Nieprawidłowy adres e-mail Użyj loginu zamiast adresu e-mail Użyj loginu lub adresu e-mail w @%1$s - Nieprawidłowy symbol + Nieprawidłowy symbol. Jeśli nie możesz go znaleźć, skontaktuj się ze szkołą + Nie zmyślaj! Jeśli nie możesz znaleźć symbolu, skontaktuj się ze szkołą Nie znaleziono ucznia. Sprawdź poprawność symbolu i wybranej odmiany dziennika UONET+ Wybrany uczeń jest już zalogowany Symbol można znaleźć na stronie dziennika w Uczeń→ Dostęp Mobilny → Wygeneruj kod dostępu.\n\nUpewnij się, że ustawiłeś odpowiednią odmianę dziennika w polu Odmiana dziennika UONET+ na pierwszym ekranie logowania @@ -69,7 +71,7 @@ Discord Wyślij wiadomość e-mail Upewnij się, że została wybrana odpowiednia odmiana dziennika UONET+! - Nie pamiętam hasła + Zresetuj hasło Przywróć swoje konto Przywróć Uczeń jest już zalogowany @@ -77,6 +79,12 @@ Inne lokalizacje wyszukiwania Nie znaleziono aktywnych uczniów Wprowadź inny symbol + Uzyskaj pomoc + Pełna nazwa szkoły (wymagana) + Np. ZSTiO Jarosław lub SP nr 99 w Łodzi + Dodatkowe informacje (po polsku) (nieobowiązkowo) + Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\" + Wyślij Włącz powiadomienia Włącz powiadomienia, aby nie przegapić wiadomości od nauczyciela lub nowej oceny diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index d075dac6c..841fa9f90 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -44,6 +44,7 @@ Token PIN Symbol + E.g. \"lodz\" or \"powiatjaroslawski\" Войти Пароль слишком короткий Данные для входа указаны неверно @@ -54,7 +55,8 @@ Неверный e-mail Используйте назначенный логин вместо e-mail Используйте назначенный логин или email в @%1$s - Неверный symbol + Invalid symbol. If you cannot find it, please contact the school + Don\'t make this up! If you cannot find it, please contact the school Ученик не найден. Проверьте symbol и выбранный тип дненика UONET+ Данный ученик уже авторизован Symbol можно найти на странице регистрации в  Uczeń → Dostęp Mobilny → Wygeneruj kod dostępu.\n\nУбедитесь, что вы выбрали соответствующий тип дневника в поле Тип дневника UONET+ на первом экране входа @@ -69,7 +71,7 @@ Discord Отправить письмо Убедитесь, что вы выбрали правильный тип дневника UONET+ - Забыли пароль? + Reset password Восстановите свой аккаунт Восстановить Ученик уже авторизован @@ -77,6 +79,12 @@ Другие места поиска Не найдено активных учеников Введите другой symbol + Get help + Full school name (required) + Np. ZSTiO Jarosław lub SP nr 99 w Łodzi + Additional information in Polish (optional) + Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\" + Submit Включить уведомления Включить уведомления, чтобы вы не пропустили сообщение от учителя или новую оценку diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index d059927eb..018806b68 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -44,6 +44,7 @@ Token PIN Symbol + E.g. \"lodz\" or \"powiatjaroslawski\" Prihlásiť Toto heslo je príliš krátke Prihlasovacie údaje sú nesprávne @@ -54,7 +55,8 @@ Neplatný e-mail Namiesto e-mailu použite priradené prihlasovacie údaje Použite priradené prihlasovacie alebo e-mail v @%1$s - Neplatný symbol + Invalid symbol. If you cannot find it, please contact the school + Don\'t make this up! If you cannot find it, please contact the school Žiak nebol nájdený. Skontrolujte správnosť symbolu a vybrané varianty denníka UONET+ Vybraný žiak už je prihlásený Symbol nájdete na stránke denníka v  Uczeń→ Dostęp Mobilny → Wygeneruj kod dostępu.\n\nUistite sa, že ste nastavili správny variant denníka v poli Variácia denníka UONET+ na prvej prihlasovacej obrazovke @@ -69,7 +71,7 @@ Discord Poslať e-mail Uistite sa, že ste vybrali správny variant denníka UONET+! - Zabudol som heslo + Reset password Obnovte svoj účet Obnoviť Žiak je už prihlásený @@ -77,6 +79,12 @@ Iné miesta vyhľadávania Neboli nájdení žiadni aktívni žiaci Zadajte iný symbol + Get help + Full school name (required) + Np. ZSTiO Jarosław lub SP nr 99 w Łodzi + Additional information in Polish (optional) + Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\" + Submit Povoliť oznámenia Povoliť oznámenia, aby ste nezmeškali správu od učiteľa alebo o novej známke diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index b46b9afe9..a05634cff 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -44,6 +44,7 @@ Token PIN Symbol + E.g. \"lodz\" or \"powiatjaroslawski\" Увійти Занадто короткий пароль Вказані невірні дані @@ -54,7 +55,8 @@ Недійсна адреса e-mail Використовуйте призначений логін замість адреси e-mail Використовуйте призначений логін або адресу e-mail в @%1$s - Неправильний symbol + Invalid symbol. If you cannot find it, please contact the school + Don\'t make this up! If you cannot find it, please contact the school Студента не знайдено. Перевірте symbol та обраний тип щоденника UONET+ Цього учня вже авторизовано Symbol можно знайти на сторінці щоденника у Uczeń → Dostęp Mobilny → Wygeneruj kod dostępu.\n\nПереконайтеся, що ви вказали відповідний щоденник у полі Тип щоденника UONET+ на першому екрані логування @@ -69,7 +71,7 @@ Discord Надіслати електронний лист Переконайтеся, що ви вибрали правильний тип щоденника UONET+! - Забули пароль? + Reset password Відновіть свій обліковий запис Відновити Учня вже авторизовано @@ -77,6 +79,12 @@ Інші розташування пошуку Активних учнів не знайдено Введіть інший symbol + Get help + Full school name (required) + Np. ZSTiO Jarosław lub SP nr 99 w Łodzi + Additional information in Polish (optional) + Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\" + Submit Увімкнути сповіщення Увімкнути сповіщення, щоб не пропустити лист від вчителя або нову оцінку From 4a2bf539f0f4bc0f0a21e3e733b227e787e130ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Tue, 26 Sep 2023 23:13:32 +0200 Subject: [PATCH 846/869] Version 2.2.0 --- app/build.gradle | 11 +++++------ app/src/main/play/release-notes/pl-PL/default.txt | 7 ++----- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d85d0a3d8..ec3ee4f34 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,12 +27,11 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 33 - versionCode 131 - versionName "2.1.0" + versionCode 132 + versionName "2.2.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" - manifestPlaceholders = [ firebase_enabled: project.hasProperty("enableFirebase"), admob_project_id: "" @@ -162,8 +161,8 @@ play { defaultToAppBundles = false track = 'production' releaseStatus = ReleaseStatus.IN_PROGRESS - userFraction = 0.25d - updatePriority = 3 + userFraction = 0.01d + updatePriority = 1 enabled.set(false) } @@ -193,7 +192,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.1.0' + implementation 'io.github.wulkanowy:sdk:2.2.0' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index aa934ce98..898256212 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,8 +1,5 @@ -Wersja 2.1.0 +Wersja 2.2.0 -— dodaliśmy tryb incognito w wiadomościach -— dodaliśmy wyświetlanie pustych lekcji (okienek) w planie lekcji -— poprawiliśmy widżet planu lekcji (będzie teraz trochę bardziej kompaktowy) -— zmieniliśmy datę rozpoczęcia roku szkolnego na 3 dni przed 1 września (sorry) +Dokonaliśmy drobnych usprawnień w obszarze logowania, które powinny pomóc Wam i nam Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases From d1d665bbdffb27be240a3e1383095485a0623891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Mon, 2 Oct 2023 12:20:34 +0200 Subject: [PATCH 847/869] Fix student auto selection if there is already active some students logged (#2314) --- .github/workflows/deploy-store.yml | 4 ++-- .github/workflows/deploy-test.yml | 4 ++-- .github/workflows/test.yml | 6 +++--- .../login/studentselect/LoginStudentSelectPresenter.kt | 10 ++++++---- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.github/workflows/deploy-store.yml b/.github/workflows/deploy-store.yml index e8a220ddf..e8237a381 100644 --- a/.github/workflows/deploy-store.yml +++ b/.github/workflows/deploy-store.yml @@ -13,7 +13,7 @@ jobs: environment: google-play steps: - uses: actions/checkout@v3 - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: 17 @@ -49,7 +49,7 @@ jobs: environment: app-gallery steps: - uses: actions/checkout@v3 - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: 17 diff --git a/.github/workflows/deploy-test.yml b/.github/workflows/deploy-test.yml index f2e9f016f..c4f55e6af 100644 --- a/.github/workflows/deploy-test.yml +++ b/.github/workflows/deploy-test.yml @@ -19,7 +19,7 @@ jobs: environment: app-center steps: - uses: actions/checkout@v3 - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: 17 @@ -89,7 +89,7 @@ jobs: if: github.event_name != 'pull_request_target' steps: - uses: actions/checkout@v3 - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: 17 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bc4b36470..6d50c45d5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: - uses: fkirc/skip-duplicate-actions@master - uses: actions/checkout@v3 - uses: gradle/wrapper-validation-action@v1 - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: 17 @@ -45,7 +45,7 @@ jobs: - uses: fkirc/skip-duplicate-actions@master - uses: actions/checkout@v3 - uses: gradle/wrapper-validation-action@v1 - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: 17 @@ -71,7 +71,7 @@ jobs: - uses: fkirc/skip-duplicate-actions@master - uses: actions/checkout@v3 - uses: gradle/wrapper-validation-action@v1 - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: 17 diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt index cc9e422ed..6cbdfbb85 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt @@ -98,11 +98,13 @@ class LoginStudentSelectPresenter @Inject constructor( } } } - return students.filter { student -> - student.student.semesters.any { semester -> - semester.isCurrent() + return students + .filter { it.isEnabled } + .filter { student -> + student.student.semesters.any { semester -> + semester.isCurrent() + } } - } } private fun createItems(): List = buildList { From c04b3e40d256ad04bde58d35f212b6173a7fe3f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Mon, 2 Oct 2023 12:21:04 +0200 Subject: [PATCH 848/869] Add negative e-mail validation in school input on support dialog (#2315) --- .../login/support/LoginSupportDialog.kt | 38 +++++++++++++------ app/src/main/res/values/strings.xml | 3 +- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/support/LoginSupportDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/support/LoginSupportDialog.kt index d2b1d2ce6..fcf7f51c9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/support/LoginSupportDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/support/LoginSupportDialog.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.ui.modules.login.support import android.os.Bundle +import android.util.Patterns import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -64,21 +65,36 @@ class LoginSupportDialog : BaseDialogFragment() { error = null } } - dialogLoginSupportSubmit.setOnClickListener { - if (dialogLoginSupportSchoolInput.text.isNullOrBlank()) { - with(dialogLoginSupportSchoolLayout) { - isErrorEnabled = true - error = getString(R.string.error_field_required) - } - } else { - onSubmitClick() - dismiss() - } - } + dialogLoginSupportSubmit.setOnClickListener { onSubmitClick() } } } private fun onSubmitClick() { + when { + binding.dialogLoginSupportSchoolInput.text.isNullOrBlank() -> { + with(binding.dialogLoginSupportSchoolLayout) { + isErrorEnabled = true + error = getString(R.string.error_field_required) + } + } + + Patterns.EMAIL_ADDRESS.matcher( + binding.dialogLoginSupportSchoolInput.text.toString() + ).matches() -> { + with(binding.dialogLoginSupportSchoolLayout) { + isErrorEnabled = true + error = getString(R.string.login_support_school_invalid) + } + } + + else -> { + openEmailClientWithFilledTemplate() + dismiss() + } + } + } + + private fun openEmailClientWithFilledTemplate() { with(binding) { context?.openEmailClient( chooserTitle = requireContext().getString(R.string.login_email_intent_title), diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 08e3ebe61..0ba9c9970 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -87,8 +87,9 @@ No active students found Enter a different symbol Get help - Full school name (required) + Full school name with the town (required) Np. ZSTiO Jarosław lub SP nr 99 w Łodzi + Enter correct name of the school Additional information in Polish (optional) Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\" Submit From 2cdd322ed4b5616b224b8cc9a46e4f53fcdfab1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Mon, 2 Oct 2023 12:22:02 +0200 Subject: [PATCH 849/869] Add missing class_id colum in JOIN clause of students with semesters (#2317) --- .../wulkanowy/data/db/dao/StudentDao.kt | 9 +++---- .../data/db/entities/StudentWithSemesters.kt | 5 ---- .../data/repositories/StudentRepository.kt | 24 ++++++++++++------- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt index a2f0abac6..d7847c240 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.data.db.dao import androidx.room.* +import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentName import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar @@ -33,12 +34,12 @@ abstract class StudentDao { abstract suspend fun loadAll(): List @Transaction - @Query("SELECT * FROM Students") - abstract suspend fun loadStudentsWithSemesters(): List + @Query("SELECT * FROM Students JOIN Semesters ON Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id") + abstract suspend fun loadStudentsWithSemesters(): Map> @Transaction - @Query("SELECT * FROM Students WHERE id = :id") - abstract suspend fun loadStudentWithSemestersById(id: Long): StudentWithSemesters? + @Query("SELECT * FROM Students JOIN Semesters ON Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id WHERE Students.id = :id") + abstract suspend fun loadStudentWithSemestersById(id: Long): Map> @Query("UPDATE Students SET is_current = 1 WHERE id = :id") abstract suspend fun updateCurrent(id: Long) diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/StudentWithSemesters.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/StudentWithSemesters.kt index 9362a954e..f9869d4e2 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/StudentWithSemesters.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/StudentWithSemesters.kt @@ -1,13 +1,8 @@ package io.github.wulkanowy.data.db.entities -import androidx.room.Embedded -import androidx.room.Relation import java.io.Serializable data class StudentWithSemesters( - @Embedded val student: Student, - - @Relation(parentColumn = "student_id", entityColumn = "student_id") val semesters: List ) : Serializable diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt index 42d1eb84e..2e04224aa 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt @@ -62,20 +62,28 @@ class StudentRepository @Inject constructor( .getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol) .mapToPojo(password) - suspend fun getSavedStudents(decryptPass: Boolean = true) = - studentDb.loadStudentsWithSemesters() - .map { - it.apply { + suspend fun getSavedStudents(decryptPass: Boolean = true): List { + return studentDb.loadStudentsWithSemesters().map { (student, semesters) -> + StudentWithSemesters( + student = student.apply { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { student.password = withContext(dispatchers.io) { decrypt(student.password) } } - } - } + }, + semesters = semesters, + ) + } + } - suspend fun getSavedStudentById(id: Long, decryptPass: Boolean = true) = - studentDb.loadStudentWithSemestersById(id)?.apply { + suspend fun getSavedStudentById(id: Long, decryptPass: Boolean = true): StudentWithSemesters? = + studentDb.loadStudentWithSemestersById(id).let { res -> + StudentWithSemesters( + student = res.keys.firstOrNull() ?: return null, + semesters = res.values.first(), + ) + }.apply { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { student.password = withContext(dispatchers.io) { decrypt(student.password) From 693ce8217d1ed871659de3009de1615ab47978af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Tue, 3 Oct 2023 00:48:37 +0200 Subject: [PATCH 850/869] New Crowdin updates (#2313) --- app/src/main/res/values-cs/strings.xml | 21 ++++++++++--------- app/src/main/res/values-da-rDK/strings.xml | 3 ++- app/src/main/res/values-de/strings.xml | 3 ++- app/src/main/res/values-es-rES/strings.xml | 3 ++- app/src/main/res/values-it-rIT/strings.xml | 3 ++- .../main/res/values-pl/preferences_values.xml | 2 +- app/src/main/res/values-pl/strings.xml | 3 ++- app/src/main/res/values-ru/strings.xml | 3 ++- app/src/main/res/values-sk/strings.xml | 21 ++++++++++--------- app/src/main/res/values-uk/strings.xml | 21 ++++++++++--------- 10 files changed, 46 insertions(+), 37 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index a365942a8..1749548e1 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -44,7 +44,7 @@ Token PIN Symbol - E.g. \"lodz\" or \"powiatjaroslawski\" + Např. „lodz“ nebo „powiatjaroslawski“ Přihlásit Toto heslo je příliš krátké Přihlašovací údaje jsou nesprávné @@ -55,8 +55,8 @@ Neplatný e-mail Místo e-mailu použijte přiřazené přihlašovací údaje Použijte přiřazené přihlašovací nebo e-mail v @%1$s - Invalid symbol. If you cannot find it, please contact the school - Don\'t make this up! If you cannot find it, please contact the school + Neplatný symbol. Pokud jej nemůžete najít, kontaktujte školu + Nevymýšlejte si! Pokud symbol nemůžete najít, kontaktujte školu Žák nebyl nalezen. Zkontrolujte správnost symbolu a vybrané varianty deníku UONET+ Vybraný žák je už přihlášen Symbol najdete na stránce deníku v  Uczeń→ Dostęp Mobilny → Wygeneruj kod dostępu.\n\nUjistěte se, že jste nastavili správnou variantu deníku v poli Variace deníku UONET+ na první přihlašovací obrazovce @@ -71,7 +71,7 @@ Discord Poslat e-mail Ujistěte se, že jste vybrali správnou variantu deníku UONET+! - Reset password + Obnovit heslo Obnovte svůj účet Obnovit Žák je už přihlášen @@ -79,12 +79,13 @@ Jiná místa vyhledávání Nebyli nalezeni žádní aktivní žáci Zadejte jiný symbol - Get help - Full school name (required) - Np. ZSTiO Jarosław lub SP nr 99 w Łodzi - Additional information in Polish (optional) - Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\" - Submit + Získat pomoc + Celý název školy s městem (povinný) + Např. ZSTiO Jarosław nebo SP nr 99 w Łodzi + Zadejte správný název školy + Dodatečné informace v polštině (volitelné) + Např. „Ostatnio zmieniłem szkołę i…“ (Nedávno jsem změnil školu a…) nebo „Jestem rodzicem i nie widzę drugiego dziecka…“ (Jsem rodič a nevidím žádné další dítě…) + Odeslat Povolit oznámení Povolit upozornění, abyste nezmeškali zprávu od učitele nebo o nové známce diff --git a/app/src/main/res/values-da-rDK/strings.xml b/app/src/main/res/values-da-rDK/strings.xml index c8846684b..500fba1f3 100644 --- a/app/src/main/res/values-da-rDK/strings.xml +++ b/app/src/main/res/values-da-rDK/strings.xml @@ -80,8 +80,9 @@ No active students found Enter a different symbol Get help - Full school name (required) + Full school name with the town (required) Np. ZSTiO Jarosław lub SP nr 99 w Łodzi + Enter correct name of the school Additional information in Polish (optional) Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\" Submit diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index a909a6b80..d8cc92984 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -80,8 +80,9 @@ Keine aktiven Schüler gefunden Geben Sie ein anderes Symbol ein Get help - Full school name (required) + Full school name with the town (required) Np. ZSTiO Jarosław lub SP nr 99 w Łodzi + Enter correct name of the school Additional information in Polish (optional) Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\" Submit diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index c8846684b..500fba1f3 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -80,8 +80,9 @@ No active students found Enter a different symbol Get help - Full school name (required) + Full school name with the town (required) Np. ZSTiO Jarosław lub SP nr 99 w Łodzi + Enter correct name of the school Additional information in Polish (optional) Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\" Submit diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml index c8846684b..500fba1f3 100644 --- a/app/src/main/res/values-it-rIT/strings.xml +++ b/app/src/main/res/values-it-rIT/strings.xml @@ -80,8 +80,9 @@ No active students found Enter a different symbol Get help - Full school name (required) + Full school name with the town (required) Np. ZSTiO Jarosław lub SP nr 99 w Łodzi + Enter correct name of the school Additional information in Polish (optional) Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\" Submit diff --git a/app/src/main/res/values-pl/preferences_values.xml b/app/src/main/res/values-pl/preferences_values.xml index 8872b7ab1..2f2432e98 100644 --- a/app/src/main/res/values-pl/preferences_values.xml +++ b/app/src/main/res/values-pl/preferences_values.xml @@ -52,7 +52,7 @@ Średnia wszystkich ocen z całego roku - Nie pokauj + Nie pokazuj Tylko między lekcjami Przed i między lekcjami diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 4ed2facc8..9375aeb39 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -80,8 +80,9 @@ Nie znaleziono aktywnych uczniów Wprowadź inny symbol Uzyskaj pomoc - Pełna nazwa szkoły (wymagana) + Pełna nazwa szkoły z miastem (wymagana) Np. ZSTiO Jarosław lub SP nr 99 w Łodzi + Wprowadź poprawną nazwę szkoły Dodatkowe informacje (po polsku) (nieobowiązkowo) Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\" Wyślij diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 841fa9f90..a6e45d445 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -80,8 +80,9 @@ Не найдено активных учеников Введите другой symbol Get help - Full school name (required) + Full school name with the town (required) Np. ZSTiO Jarosław lub SP nr 99 w Łodzi + Enter correct name of the school Additional information in Polish (optional) Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\" Submit diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 018806b68..ed06e023e 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -44,7 +44,7 @@ Token PIN Symbol - E.g. \"lodz\" or \"powiatjaroslawski\" + Napr. „lodz“ alebo „powiatjaroslawski“ Prihlásiť Toto heslo je príliš krátke Prihlasovacie údaje sú nesprávne @@ -55,8 +55,8 @@ Neplatný e-mail Namiesto e-mailu použite priradené prihlasovacie údaje Použite priradené prihlasovacie alebo e-mail v @%1$s - Invalid symbol. If you cannot find it, please contact the school - Don\'t make this up! If you cannot find it, please contact the school + Neplatný symbol. Pokiaľ ho nemôžete nájsť, kontaktujte školu + Nevymýšľajte si! Pokiaľ symbol nemôžete nájsť, kontaktujte školu Žiak nebol nájdený. Skontrolujte správnosť symbolu a vybrané varianty denníka UONET+ Vybraný žiak už je prihlásený Symbol nájdete na stránke denníka v  Uczeń→ Dostęp Mobilny → Wygeneruj kod dostępu.\n\nUistite sa, že ste nastavili správny variant denníka v poli Variácia denníka UONET+ na prvej prihlasovacej obrazovke @@ -71,7 +71,7 @@ Discord Poslať e-mail Uistite sa, že ste vybrali správny variant denníka UONET+! - Reset password + Obnoviť heslo Obnovte svoj účet Obnoviť Žiak je už prihlásený @@ -79,12 +79,13 @@ Iné miesta vyhľadávania Neboli nájdení žiadni aktívni žiaci Zadajte iný symbol - Get help - Full school name (required) - Np. ZSTiO Jarosław lub SP nr 99 w Łodzi - Additional information in Polish (optional) - Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\" - Submit + Získať pomoc + Celý názov školy s mestom (povinný) + Napr. ZSTiO Jarosław alebo SP nr 99 w Łodzi + Zadajte správny názov školy + Dodatočné informácie v poľštine (voliteľné) + Napr. „Ostatnio zmieniłem szkołę i…“ (Nedávno som zmenil školu a…) alebo „Jestem rodzicem i nie widzę drugiego dziecka…“ (Som rodič a nevidím žiadne ďalšie dieťa…) + Odoslať Povoliť oznámenia Povoliť oznámenia, aby ste nezmeškali správu od učiteľa alebo o novej známke diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index a05634cff..6a79263f7 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -44,7 +44,7 @@ Token PIN Symbol - E.g. \"lodz\" or \"powiatjaroslawski\" + Напр. \"lodz\" чи \"powiatjaroslawski\" Увійти Занадто короткий пароль Вказані невірні дані @@ -55,8 +55,8 @@ Недійсна адреса e-mail Використовуйте призначений логін замість адреси e-mail Використовуйте призначений логін або адресу e-mail в @%1$s - Invalid symbol. If you cannot find it, please contact the school - Don\'t make this up! If you cannot find it, please contact the school + Некоректний символ. Якщо ви не можете знайти його, будь ласка, зв\'яжіться зі школою + Не вигадуйте! Якщо ви не можете знайти його, будь ласка, зв\'яжіться зі школою Студента не знайдено. Перевірте symbol та обраний тип щоденника UONET+ Цього учня вже авторизовано Symbol можно знайти на сторінці щоденника у Uczeń → Dostęp Mobilny → Wygeneruj kod dostępu.\n\nПереконайтеся, що ви вказали відповідний щоденник у полі Тип щоденника UONET+ на першому екрані логування @@ -71,7 +71,7 @@ Discord Надіслати електронний лист Переконайтеся, що ви вибрали правильний тип щоденника UONET+! - Reset password + Скинути пароль Відновіть свій обліковий запис Відновити Учня вже авторизовано @@ -79,12 +79,13 @@ Інші розташування пошуку Активних учнів не знайдено Введіть інший symbol - Get help - Full school name (required) - Np. ZSTiO Jarosław lub SP nr 99 w Łodzi - Additional information in Polish (optional) - Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\" - Submit + Отримати допомогу + Повна назва школи з містом (обов\'язково) + Напр. ZSTiO Jarosław lub SP nr 99 w Łodzi + Введіть правильну назву школи + Додаткова інформація польською мовою (за бажанням) + Напр. \"Я нещодавно змінив школу і...\" або \"Я батько і не бачу другу дитину...\" + Надіслати Увімкнути сповіщення Увімкнути сповіщення, щоб не пропустити лист від вчителя або нову оцінку From 3212efe21ed7e9cf3718cef23f606fdce9894a63 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 22:49:05 +0000 Subject: [PATCH 851/869] Bump com.android.tools.build:gradle from 8.1.1 to 8.1.2 (#2320) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 651e9ca16..9293d1239 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ buildscript { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$kotlin_version-1.0.13" - classpath 'com.android.tools.build:gradle:8.1.1' + classpath 'com.android.tools.build:gradle:8.1.2' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.4.0' classpath 'com.huawei.agconnect:agcp:1.9.1.301' From c8332a064208f646f341f11dd3a5d139ff11b6f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 22:49:26 +0000 Subject: [PATCH 852/869] Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin (#2321) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 9293d1239..33eefd889 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ buildscript { classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9' classpath "com.github.triplet.gradle:play-publisher:3.8.4" classpath "ru.cian:huawei-publish-gradle-plugin:1.4.0" - classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.3.1.3277" + classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.4.0.3356" classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0" classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries" } From fc5ad16cb761dce29810c753af4edd3ed01bc799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Tue, 3 Oct 2023 01:14:10 +0200 Subject: [PATCH 853/869] Version 2.2.1 --- app/build.gradle | 8 ++++---- app/src/main/play/release-notes/pl-PL/default.txt | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ec3ee4f34..08070c5d7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,8 +27,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 33 - versionCode 132 - versionName "2.2.0" + versionCode 133 + versionName "2.2.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" @@ -162,7 +162,7 @@ play { track = 'production' releaseStatus = ReleaseStatus.IN_PROGRESS userFraction = 0.01d - updatePriority = 1 + updatePriority = 3 enabled.set(false) } @@ -192,7 +192,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.2.0' + implementation 'io.github.wulkanowy:sdk:2.2.1' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index 898256212..dc78c1e3d 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,5 +1,6 @@ -Wersja 2.2.0 +Wersja 2.2.1 -Dokonaliśmy drobnych usprawnień w obszarze logowania, które powinny pomóc Wam i nam +– dokonaliśmy kilka poprawek na ekranie logowania +– naprawiliśmy przypadek z błędnym wyświetlaniem starej klasy ucznia po zalogowaniu się na konto z nowej klasy Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases From 2f5577cc541c458596c5c90aac451ab0f337a2fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Fri, 6 Oct 2023 10:07:55 +0200 Subject: [PATCH 854/869] Update SDK to 34 (#2322) --- app/build.gradle | 8 +++---- app/jacoco.gradle | 22 +++++++++--------- .../settings/advanced/AdvancedFragment.kt | 2 +- .../settings/advanced/AdvancedPresenter.kt | 3 ++- .../settings/appearance/AppearanceFragment.kt | 2 +- .../appearance/AppearancePresenter.kt | 3 ++- .../notifications/NotificationsFragment.kt | 2 +- .../notifications/NotificationsPresenter.kt | 3 ++- .../ui/modules/settings/sync/SyncFragment.kt | 2 +- .../ui/modules/settings/sync/SyncPresenter.kt | 6 ++++- build.gradle | 4 ++-- gradle/wrapper/gradle-wrapper.jar | Bin 62076 -> 63375 bytes gradle/wrapper/gradle-wrapper.properties | 3 ++- gradlew | 5 +++- settings.gradle | 3 --- 15 files changed, 38 insertions(+), 30 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 08070c5d7..b3ebb5cbe 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,13 +20,13 @@ apply from: 'hooks.gradle' android { namespace 'io.github.wulkanowy' - compileSdk 33 + compileSdk 34 defaultConfig { applicationId "io.github.wulkanowy" testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 - targetSdkVersion 33 + targetSdkVersion 34 versionCode 133 versionName "2.2.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -199,9 +199,9 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" - implementation "androidx.core:core-ktx:1.10.1" + implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.core:core-splashscreen:1.0.1' - implementation "androidx.activity:activity-ktx:1.7.2" + implementation "androidx.activity:activity-ktx:1.8.0" implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.fragment:fragment-ktx:1.6.1" implementation "androidx.annotation:annotation:1.7.0" diff --git a/app/jacoco.gradle b/app/jacoco.gradle index f253673e6..434b9218b 100644 --- a/app/jacoco.gradle +++ b/app/jacoco.gradle @@ -1,16 +1,16 @@ apply plugin: "jacoco" jacoco { - toolVersion "0.8.7" + toolVersion "0.8.10" reportsDirectory.set(file("$buildDir/reports")) } -tasks.withType(Test) { +tasks.withType(Test).configureEach { jacoco.includeNoLocationClasses = true jacoco.excludes = ['jdk.internal.*'] } -task jacocoTestReport(type: JacocoReport) { +tasks.register('jacocoTestReport', JacocoReport) { group = "Reporting" description = "Generate Jacoco coverage reports" @@ -33,19 +33,19 @@ task jacocoTestReport(type: JacocoReport) { '**/*_Factory.*'] classDirectories.setFrom(fileTree( - dir: "$buildDir/intermediates/classes/debug", - excludes: excludes + dir: "$buildDir/intermediates/classes/debug", + excludes: excludes ) + fileTree( - dir: "$buildDir/tmp/kotlin-classes/fdroidDebug", - excludes: excludes + dir: "$buildDir/tmp/kotlin-classes/fdroidDebug", + excludes: excludes )) sourceDirectories.setFrom(files([ - "src/main/java", - "src/fdroid/java" + "src/main/java", + "src/fdroid/java" ])) executionData.setFrom(fileTree( - dir: project.projectDir, - includes: ["**/*.exec", "**/*.ec"] + dir: project.projectDir, + includes: ["**/*.exec", "**/*.ec"] )) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt index 41e9e8b1a..1b8d1a8fa 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt @@ -35,7 +35,7 @@ class AdvancedFragment : PreferenceFragmentCompat(), setPreferencesFromResource(R.xml.scheme_preferences_advanced, rootKey) } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { presenter.onSharedPreferenceChanged(key) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedPresenter.kt index d38f841fe..4bc24594a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedPresenter.kt @@ -18,7 +18,8 @@ class AdvancedPresenter @Inject constructor( Timber.i("Settings advanced view was initialized") } - fun onSharedPreferenceChanged(key: String) { + fun onSharedPreferenceChanged(key: String?) { + key ?: return Timber.i("Change settings $key") analytics.logEvent("setting_changed", "name" to key) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt index 493ab5d7f..70dd694cc 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt @@ -39,7 +39,7 @@ class AppearanceFragment : PreferenceFragmentCompat(), setPreferencesFromResource(R.xml.scheme_preferences_appearance, rootKey) } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { presenter.onSharedPreferenceChanged(key) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearancePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearancePresenter.kt index 14592a6cc..d3410ea84 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearancePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearancePresenter.kt @@ -22,7 +22,8 @@ class AppearancePresenter @Inject constructor( Timber.i("Settings appearance view was initialized") } - fun onSharedPreferenceChanged(key: String) { + fun onSharedPreferenceChanged(key: String?) { + key ?: return Timber.i("Change settings $key") preferencesRepository.apply { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt index 35c1faa45..af4c4e6ae 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt @@ -114,7 +114,7 @@ class NotificationsFragment : PreferenceFragmentCompat(), setPreferencesFromResource(R.xml.scheme_preferences_notifications, rootKey) } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { presenter.onSharedPreferenceChanged(key) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt index 232b03480..b2938cf65 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsPresenter.kt @@ -38,7 +38,8 @@ class NotificationsPresenter @Inject constructor( Timber.i("Settings notifications view was initialized") } - fun onSharedPreferenceChanged(key: String) { + fun onSharedPreferenceChanged(key: String?) { + key ?: return Timber.i("Change settings $key") preferencesRepository.apply { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt index df2e1348a..f48abe9ba 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt @@ -52,7 +52,7 @@ class SyncFragment : PreferenceFragmentCompat(), setPreferencesFromResource(R.xml.scheme_preferences_sync, rootKey) } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { presenter.onSharedPreferenceChanged(key) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncPresenter.kt index 1ecb4a6ef..594e097af 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncPresenter.kt @@ -31,7 +31,8 @@ class SyncPresenter @Inject constructor( setSyncDateInView() } - fun onSharedPreferenceChanged(key: String) { + fun onSharedPreferenceChanged(key: String?) { + key ?: return Timber.i("Change settings $key") preferencesRepository.apply { @@ -52,10 +53,12 @@ class SyncPresenter @Inject constructor( Timber.i("Setting sync now started") analytics.logEvent("sync_now", "status" to "started") } + WorkInfo.State.SUCCEEDED -> { showMessage(syncSuccessString) analytics.logEvent("sync_now", "status" to "success") } + WorkInfo.State.FAILED -> { showError( syncFailedString, @@ -66,6 +69,7 @@ class SyncPresenter @Inject constructor( ) analytics.logEvent("sync_now", "status" to "failed") } + else -> Timber.d("Sync now state: ${workInfo?.state}") } if (workInfo?.state?.isFinished == true) { diff --git a/build.gradle b/build.gradle index 33eefd889..baa1f9b67 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,8 @@ buildscript { ext { kotlin_version = '1.9.10' - about_libraries = '10.9.0' - hilt_version = "2.48" + about_libraries = '10.9.1' + hilt_version = '2.48.1' } repositories { mavenCentral() diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index c1962a79e29d3e0ab67b14947c167a862655af9b..033e24c4cdf41af1ab109bc7f253b2b887023340 100644 GIT binary patch delta 16170 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/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. diff --git a/settings.gradle b/settings.gradle index 16731297d..e7b4def49 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1 @@ -plugins { - id 'org.gradle.toolchains.foojay-resolver-convention' version '0.7.0' -} include ':app' From 9098e74065ebe4821694370e5c0fc8c404d52089 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 14 Oct 2023 19:17:56 +0000 Subject: [PATCH 855/869] Bump com.google.android.material:material from 1.9.0 to 1.10.0 (#2325) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index b3ebb5cbe..a591d2b2e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -212,7 +212,7 @@ dependencies { implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.constraintlayout:constraintlayout:2.1.4" implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0" - implementation "com.google.android.material:material:1.9.0" + implementation "com.google.android.material:material:1.10.0" implementation "com.github.wulkanowy:material-chips-input:2.3.1" implementation "com.github.PhilJay:MPAndroidChart:v3.1.0" implementation 'com.github.lopspower:CircularImageView:4.3.0' From 516922d5aa53717d5a964161497a55e09524fa9d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 14 Oct 2023 19:18:28 +0000 Subject: [PATCH 856/869] Bump org.sonarsource.scanner.gradle:sonarqube-gradle-plugin (#2324) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index baa1f9b67..3fafd2d24 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ buildscript { classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9' classpath "com.github.triplet.gradle:play-publisher:3.8.4" classpath "ru.cian:huawei-publish-gradle-plugin:1.4.0" - classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.4.0.3356" + classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.4.1.3373" classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0" classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries" } From 5dffbdadfa030f74f62c24b3157ca9c6b65f5ad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Mon, 23 Oct 2023 13:04:42 +0200 Subject: [PATCH 857/869] Points statistics improvements (#2328) --- .../grade/statistics/GradeStatisticsAdapter.kt | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsAdapter.kt index 3fce8d57e..e5f1eba0c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsAdapter.kt @@ -22,6 +22,8 @@ import io.github.wulkanowy.databinding.ItemGradeStatisticsHeaderBinding import io.github.wulkanowy.databinding.ItemGradeStatisticsPieBinding import io.github.wulkanowy.utils.getThemeAttrColor import javax.inject.Inject +import kotlin.math.max +import kotlin.math.roundToInt class GradeStatisticsAdapter @Inject constructor() : RecyclerView.Adapter() { @@ -269,7 +271,7 @@ class GradeStatisticsAdapter @Inject constructor() : valueTextSize = 12f valueTextColor = binding.root.context.getThemeAttrColor(android.R.attr.textColorPrimary) valueFormatter = object : ValueFormatter() { - override fun getBarLabel(barEntry: BarEntry) = "${barEntry.y}%" + override fun getBarLabel(barEntry: BarEntry) = "${barEntry.y}" } colors = gradePointsColors } @@ -304,15 +306,20 @@ class GradeStatisticsAdapter @Inject constructor() : } xAxis.setDrawLabels(false) xAxis.setDrawGridLines(false) + + val yMaxFromValues = (max(points.others, points.student)).roundToInt() + 30f + val yMaxFromValuesWithMargin = ((yMaxFromValues / 10.0).roundToInt() * 10).toFloat() + val yMax = yMaxFromValuesWithMargin.coerceAtLeast(100f) + val yLabelCount = (yMax / 10).toInt() + 1 with(axisLeft) { axisMinimum = 0f - axisMaximum = 100f - labelCount = 11 + axisMaximum = yMax + labelCount = yLabelCount } with(axisRight) { axisMinimum = 0f - axisMaximum = 100f - labelCount = 11 + axisMaximum = yMax + labelCount = yLabelCount } invalidate() } From 9d62410530b6dbc707634790f49857412d36db8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Mon, 23 Oct 2023 13:05:05 +0200 Subject: [PATCH 858/869] Sort teachers by name in school and teachers tab (#2327) --- .../ui/modules/schoolandteachers/teacher/TeacherPresenter.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherPresenter.kt index e2af05c92..fef06328d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/teacher/TeacherPresenter.kt @@ -58,7 +58,10 @@ class TeacherPresenter @Inject constructor( .logResourceStatus("load teachers data") .onResourceData { view?.run { - updateData(it.filter { item -> item.name.isNotBlank() }) + updateData(it + .filter { item -> item.name.isNotBlank() } + .sortedBy { it.name } + ) showContent(it.isNotEmpty()) showEmpty(it.isEmpty()) showErrorView(false) From 83527d91f3283ad64566189ee86785775f4430a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Mon, 23 Oct 2023 13:05:46 +0200 Subject: [PATCH 859/869] Allow direct access to weekend from day navigation when there is any lesson during weekend (#2326) --- .../modules/attendance/AttendanceFragment.kt | 8 +- .../modules/attendance/AttendancePresenter.kt | 97 +++++++++++----- .../ui/modules/attendance/AttendanceView.kt | 2 + .../modules/dashboard/DashboardPresenter.kt | 2 +- .../ui/modules/timetable/TimetableFragment.kt | 9 +- .../modules/timetable/TimetablePresenter.kt | 107 +++++++++++++----- .../ui/modules/timetable/TimetableView.kt | 2 + .../main/res/layout/fragment_attendance.xml | 4 +- .../main/res/layout/fragment_timetable.xml | 4 +- 9 files changed, 172 insertions(+), 63 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt index a73c2606e..6e842b4d7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt @@ -148,6 +148,10 @@ class AttendanceFragment : BaseFragment(R.layout.frag binding.attendanceNavDate.text = date } + override fun showNavigation(show: Boolean) { + binding.attendanceNavContainer.isVisible = show + } + override fun clearData() { with(attendanceAdapter) { items = emptyList() @@ -281,7 +285,9 @@ class AttendanceFragment : BaseFragment(R.layout.frag override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay()) + presenter.currentDate?.let { + outState.putLong(SAVED_DATE_KEY, it.toEpochDay()) + } } override fun onDestroyView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt index 26bfaf19f..f66479daf 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt @@ -3,10 +3,14 @@ package io.github.wulkanowy.ui.modules.attendance import android.annotation.SuppressLint import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.Attendance +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.repositories.AttendanceRepository import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.data.repositories.TimetableRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.* @@ -14,6 +18,7 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onEach import timber.log.Timber +import java.time.DayOfWeek import java.time.LocalDate import java.time.LocalDate.now import java.time.LocalDate.ofEpochDay @@ -28,9 +33,10 @@ class AttendancePresenter @Inject constructor( private val analytics: AnalyticsHelper ) : BasePresenter(errorHandler, studentRepository) { - private var baseDate: LocalDate = now().previousOrSameSchoolDay + private var initialDate: LocalDate? = null + private var isWeekendHasLessons: Boolean = false - lateinit var currentDate: LocalDate + var currentDate: LocalDate? = null private set private lateinit var lastError: Throwable @@ -44,27 +50,34 @@ class AttendancePresenter @Inject constructor( view.initView() Timber.i("Attendance view was initialized") errorHandler.showErrorMessage = ::showErrorViewOnError - reloadView(ofEpochDay(date ?: baseDate.toEpochDay())) + currentDate = date?.let(::ofEpochDay) loadData() - if (currentDate.isHolidays) setBaseDateOnHolidays() } fun onPreviousDay() { + val date = if (isWeekendHasLessons) { + currentDate?.minusDays(1) + } else currentDate?.previousSchoolDay + view?.finishActionMode() attendanceToExcuseList.clear() - reloadView(currentDate.previousSchoolDay) + reloadView(date ?: return) loadData() } fun onNextDay() { + val date = if (isWeekendHasLessons) { + currentDate?.plusDays(1) + } else currentDate?.nextSchoolDay + view?.finishActionMode() attendanceToExcuseList.clear() - reloadView(currentDate.nextSchoolDay) + reloadView(date ?: return) loadData() } fun onPickDate() { - view?.showDatePickerDialog(currentDate) + view?.showDatePickerDialog(currentDate ?: return) } fun onDateSet(year: Int, month: Int, day: Int) { @@ -93,10 +106,8 @@ class AttendancePresenter @Inject constructor( Timber.i("Attendance view is reselected") view?.let { view -> if (view.currentStackSize == 1) { - baseDate = now().previousOrSameSchoolDay - - if (currentDate != baseDate) { - reloadView(baseDate) + if (currentDate != initialDate) { + reloadView(initialDate ?: return) loadData() } else if (!view.isViewEmpty) { view.resetView() @@ -188,19 +199,6 @@ class AttendancePresenter @Inject constructor( return true } - private fun setBaseDateOnHolidays() { - flow { - val student = studentRepository.getCurrentStudent() - emit(semesterRepository.getCurrentSemester(student)) - }.catch { - Timber.i("Loading semester result: An exception occurred") - }.onEach { - baseDate = baseDate.getLastSchoolDayIfHoliday(it.schoolYear) - currentDate = baseDate - reloadNavigation() - }.launch("holidays") - } - private fun loadData(forceRefresh: Boolean = false) { Timber.i("Loading attendance data started") @@ -211,11 +209,13 @@ class AttendancePresenter @Inject constructor( isParent = student.isParent val semester = semesterRepository.getCurrentSemester(student) + + checkInitialAndCurrentDate(student, semester) attendanceRepository.getAttendance( student = student, semester = semester, - start = currentDate, - end = currentDate, + start = currentDate ?: now(), + end = currentDate ?: now(), forceRefresh = forceRefresh ) } @@ -231,6 +231,8 @@ class AttendancePresenter @Inject constructor( }.sortedBy { item -> item.number } } .onResourceData { + isWeekendHasLessons = isWeekendHasLessons || isWeekendHasLessons(it) + view?.run { enableSwipe(true) showProgress(false) @@ -238,6 +240,7 @@ class AttendancePresenter @Inject constructor( showEmpty(it.isEmpty()) showContent(it.isNotEmpty()) updateData(it) + reloadNavigation() } } .onResourceIntermediate { view?.showRefresh(true) } @@ -263,6 +266,43 @@ class AttendancePresenter @Inject constructor( .launch() } + private suspend fun checkInitialAndCurrentDate(student: Student, semester: Semester) { + if (initialDate == null) { + val lessons = attendanceRepository.getAttendance( + student = student, + semester = semester, + start = now().monday, + end = now().sunday, + forceRefresh = false, + ).toFirstResult().dataOrNull.orEmpty() + isWeekendHasLessons = isWeekendHasLessons(lessons) + initialDate = getInitialDate(semester) + } + + if (currentDate == null) { + currentDate = initialDate + } + } + + private fun isWeekendHasLessons( + lessons: List, + ): Boolean = lessons.any { + it.date.dayOfWeek in listOf( + DayOfWeek.SATURDAY, + DayOfWeek.SUNDAY, + ) + } + + private fun getInitialDate(semester: Semester): LocalDate { + val now = now() + + return when { + now.isHolidays -> now.getLastSchoolDayIfHoliday(semester.schoolYear) + isWeekendHasLessons -> now + else -> now.previousOrSameSchoolDay + } + } + private fun excuseAbsence(reason: String?, toExcuseList: List) { resourceFlow { val student = studentRepository.getCurrentStudent() @@ -311,7 +351,7 @@ class AttendancePresenter @Inject constructor( private fun reloadView(date: LocalDate) { currentDate = date - Timber.i("Reload attendance view with the date ${currentDate.toFormattedString()}") + Timber.i("Reload attendance view with the date ${currentDate?.toFormattedString()}") view?.apply { showProgress(true) enableSwipe(false) @@ -326,10 +366,13 @@ class AttendancePresenter @Inject constructor( @SuppressLint("DefaultLocale") private fun reloadNavigation() { + val currentDate = currentDate ?: return + view?.apply { showPreButton(!currentDate.minusDays(1).isHolidays) showNextButton(!currentDate.plusDays(1).isHolidays) updateNavigationDay(currentDate.toFormattedString("EEEE, dd.MM").capitalise()) + showNavigation(true) } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt index b0123065a..2629c217e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt @@ -40,6 +40,8 @@ interface AttendanceView : BaseView { fun showContent(show: Boolean) + fun showNavigation(show: Boolean) + fun showPreButton(show: Boolean) fun showNextButton(show: Boolean) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt index ecf084c68..ae451ae15 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt @@ -386,7 +386,7 @@ class DashboardPresenter @Inject constructor( private fun loadLessons(student: Student, forceRefresh: Boolean) { flatResourceFlow { val semester = semesterRepository.getCurrentSemester(student) - val date = LocalDate.now().nextOrSameSchoolDay + val date = LocalDate.now() timetableRepository.getTimetable( student = student, diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt index ebc16239f..0e6459110 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt @@ -9,6 +9,7 @@ import android.view.View.GONE import android.view.View.VISIBLE import androidx.core.os.bundleOf import androidx.core.text.parseAsHtml +import androidx.core.view.isVisible import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -160,6 +161,10 @@ class TimetableFragment : BaseFragment(R.layout.fragme binding.timetableRecycler.visibility = if (show) VISIBLE else GONE } + override fun showNavigation(show: Boolean) { + binding.timetableNavContainer.isVisible = true + } + override fun showPreButton(show: Boolean) { binding.timetablePreviousButton.visibility = if (show) VISIBLE else View.INVISIBLE } @@ -193,7 +198,9 @@ class TimetableFragment : BaseFragment(R.layout.fragme override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay()) + presenter.currentDate?.toEpochDay()?.let { + outState.putLong(SAVED_DATE_KEY, it) + } } override fun onDestroyView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt index 0f8395ded..f99970482 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt @@ -1,5 +1,8 @@ package io.github.wulkanowy.ui.modules.timetable +import io.github.wulkanowy.data.dataOrNull +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.enums.TimetableGapsMode.BETWEEN_AND_BEFORE_LESSONS import io.github.wulkanowy.data.enums.TimetableGapsMode.NO_GAPS @@ -15,6 +18,8 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.TimetableRepository +import io.github.wulkanowy.data.toFirstResult +import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper @@ -24,15 +29,16 @@ import io.github.wulkanowy.utils.isHolidays import io.github.wulkanowy.utils.isJustFinished import io.github.wulkanowy.utils.isShowTimeUntil import io.github.wulkanowy.utils.left +import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.nextOrSameSchoolDay import io.github.wulkanowy.utils.nextSchoolDay import io.github.wulkanowy.utils.previousSchoolDay +import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.until -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.firstOrNull import timber.log.Timber +import java.time.DayOfWeek import java.time.Instant import java.time.LocalDate import java.time.LocalDate.now @@ -51,9 +57,10 @@ class TimetablePresenter @Inject constructor( private val analytics: AnalyticsHelper, ) : BasePresenter(errorHandler, studentRepository) { - private var baseDate: LocalDate = now().nextOrSameSchoolDay + private var initialDate: LocalDate? = null + private var isWeekendHasLessons: Boolean = false - lateinit var currentDate: LocalDate + var currentDate: LocalDate? = null private set private lateinit var lastError: Throwable @@ -65,23 +72,30 @@ class TimetablePresenter @Inject constructor( view.initView() Timber.i("Timetable was initialized") errorHandler.showErrorMessage = ::showErrorViewOnError - reloadView(ofEpochDay(date ?: baseDate.toEpochDay())) + currentDate = date?.let(::ofEpochDay) loadData() - if (currentDate.isHolidays) setBaseDateOnHolidays() } fun onPreviousDay() { - reloadView(currentDate.previousSchoolDay) + val date = if (isWeekendHasLessons) { + currentDate?.minusDays(1) + } else currentDate?.previousSchoolDay + + reloadView(date ?: return) loadData() } fun onNextDay() { - reloadView(currentDate.nextSchoolDay) + val date = if (isWeekendHasLessons) { + currentDate?.plusDays(1) + } else currentDate?.nextSchoolDay + + reloadView(date ?: return) loadData() } fun onPickDate() { - view?.showDatePickerDialog(currentDate) + view?.showDatePickerDialog(currentDate ?: return) } fun onDateSet(year: Int, month: Int, day: Int) { @@ -110,10 +124,8 @@ class TimetablePresenter @Inject constructor( Timber.i("Timetable view is reselected") view?.let { view -> if (view.currentStackSize == 1) { - baseDate = now().nextOrSameSchoolDay - - if (currentDate != baseDate) { - reloadView(baseDate) + if (currentDate != initialDate) { + reloadView(initialDate ?: return) loadData() } else if (!view.isViewEmpty) { view.resetView() @@ -134,34 +146,25 @@ class TimetablePresenter @Inject constructor( return true } - private fun setBaseDateOnHolidays() { - flow { - val student = studentRepository.getCurrentStudent() - emit(semesterRepository.getCurrentSemester(student)) - }.catch { - Timber.i("Loading semester result: An exception occurred") - }.onEach { - baseDate = baseDate.getLastSchoolDayIfHoliday(it.schoolYear) - currentDate = baseDate - reloadNavigation() - }.launch("holidays") - } - private fun loadData(forceRefresh: Boolean = false) { flatResourceFlow { val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student) + + checkInitialAndCurrentDate(student, semester) timetableRepository.getTimetable( student = student, semester = semester, - start = currentDate, - end = currentDate, + start = currentDate ?: now(), + end = currentDate ?: now(), forceRefresh = forceRefresh, timetableType = TimetableRepository.TimetableType.NORMAL ) } .logResourceStatus("load timetable data") .onResourceData { + isWeekendHasLessons = isWeekendHasLessons || isWeekendHasLessons(it.lessons) + view?.run { enableSwipe(true) showProgress(false) @@ -169,7 +172,8 @@ class TimetablePresenter @Inject constructor( showContent(it.lessons.isNotEmpty()) showEmpty(it.lessons.isEmpty()) updateData(it.lessons) - setDayHeaderMessage(it.headers.singleOrNull { header -> header.date == currentDate }?.content) + setDayHeaderMessage(it.headers.find { header -> header.date == currentDate }?.content) + reloadNavigation() } } .onResourceIntermediate { view?.showRefresh(true) } @@ -191,6 +195,44 @@ class TimetablePresenter @Inject constructor( .launch() } + private suspend fun checkInitialAndCurrentDate(student: Student, semester: Semester) { + if (initialDate == null) { + val lessons = timetableRepository.getTimetable( + student = student, + semester = semester, + start = now().monday, + end = now().sunday, + forceRefresh = false, + timetableType = TimetableRepository.TimetableType.NORMAL + ).toFirstResult().dataOrNull?.lessons.orEmpty() + isWeekendHasLessons = isWeekendHasLessons(lessons) + initialDate = getInitialDate(semester) + } + + if (currentDate == null) { + currentDate = initialDate + } + } + + private fun isWeekendHasLessons( + lessons: List, + ): Boolean = lessons.any { + it.date.dayOfWeek in listOf( + DayOfWeek.SATURDAY, + DayOfWeek.SUNDAY, + ) + } + + private fun getInitialDate(semester: Semester): LocalDate { + val now = now() + + return when { + now.isHolidays -> now.getLastSchoolDayIfHoliday(semester.schoolYear) + isWeekendHasLessons -> now + else -> now.nextOrSameSchoolDay + } + } + private fun updateData(lessons: List) { tickTimer?.cancel() @@ -285,7 +327,7 @@ class TimetablePresenter @Inject constructor( private fun reloadView(date: LocalDate) { currentDate = date - Timber.i("Reload timetable view with the date ${currentDate.toFormattedString()}") + Timber.i("Reload timetable view with the date ${currentDate?.toFormattedString()}") view?.apply { showProgress(true) enableSwipe(false) @@ -298,10 +340,13 @@ class TimetablePresenter @Inject constructor( } private fun reloadNavigation() { + val currentDate = currentDate ?: return + view?.apply { showPreButton(!currentDate.minusDays(1).isHolidays) showNextButton(!currentDate.plusDays(1).isHolidays) updateNavigationDay(currentDate.toFormattedString("EEEE, dd.MM").capitalise()) + showNavigation(true) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt index 8cfb26204..40190d51f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt @@ -36,6 +36,8 @@ interface TimetableView : BaseView { fun showContent(show: Boolean) + fun showNavigation(show: Boolean) + fun showPreButton(show: Boolean) fun showNextButton(show: Boolean) diff --git a/app/src/main/res/layout/fragment_attendance.xml b/app/src/main/res/layout/fragment_attendance.xml index 14331120c..078daf610 100644 --- a/app/src/main/res/layout/fragment_attendance.xml +++ b/app/src/main/res/layout/fragment_attendance.xml @@ -128,7 +128,9 @@ android:layout_gravity="bottom" android:gravity="center" android:orientation="horizontal" - tools:ignore="UnusedAttribute"> + android:visibility="gone" + tools:ignore="UnusedAttribute" + tools:visibility="visible"> + android:visibility="gone" + tools:ignore="UnusedAttribute" + tools:visibility="visible"> Date: Mon, 23 Oct 2023 16:47:03 +0000 Subject: [PATCH 860/869] Bump androidx.recyclerview:recyclerview from 1.3.1 to 1.3.2 (#2332) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index a591d2b2e..2d86ff376 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -207,7 +207,7 @@ dependencies { implementation "androidx.annotation:annotation:1.7.0" implementation "androidx.preference:preference-ktx:1.2.1" - implementation "androidx.recyclerview:recyclerview:1.3.1" + implementation "androidx.recyclerview:recyclerview:1.3.2" implementation "androidx.viewpager2:viewpager2:1.1.0-beta02" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.constraintlayout:constraintlayout:2.1.4" From 0e1c20a9527bfa7d1c82d7d851c5d1753b1e8400 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 16:48:23 +0000 Subject: [PATCH 861/869] Bump room from 2.5.2 to 2.6.0 (#2329) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 2d86ff376..97b526220 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -185,7 +185,7 @@ huaweiPublish { ext { work_manager = "2.8.1" android_hilt = "1.0.0" - room = "2.5.2" + room = "2.6.0" chucker = "3.5.2" mockk = "1.13.8" coroutines = "1.7.3" From 3d76d41b55278ba21e6d7297b75695ad223f09f3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 16:49:58 +0000 Subject: [PATCH 862/869] Bump com.squareup.okhttp3:logging-interceptor from 4.11.0 to 4.12.0 (#2330) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 97b526220..47ef3cde0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -236,7 +236,7 @@ dependencies { implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0" - implementation "com.squareup.okhttp3:logging-interceptor:4.11.0" + implementation "com.squareup.okhttp3:logging-interceptor:4.12.0" implementation "com.jakewharton.timber:timber:5.0.1" implementation "at.favre.lib:slf4j-timber:1.0.1" From a4a191700e5b449efa177125db3caad07421ad8c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 17:28:38 +0000 Subject: [PATCH 863/869] Bump com.google.firebase:firebase-bom from 32.3.1 to 32.4.0 (#2331) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 47ef3cde0..066b3736d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -248,7 +248,7 @@ dependencies { implementation 'com.fredporciuncula:flow-preferences:1.9.1' implementation 'org.apache.commons:commons-text:1.10.0' - playImplementation platform('com.google.firebase:firebase-bom:32.3.1') + playImplementation platform('com.google.firebase:firebase-bom:32.4.0') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' From fcea2218b53e06f19a31c5a2f777edb676fa529c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Mon, 23 Oct 2023 19:56:46 +0200 Subject: [PATCH 864/869] Version 2.2.2 --- app/build.gradle | 6 +++--- app/src/main/play/release-notes/pl-PL/default.txt | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 066b3736d..42a1f8f74 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,8 +27,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 34 - versionCode 133 - versionName "2.2.1" + versionCode 134 + versionName "2.2.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" @@ -192,7 +192,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.2.1' + implementation 'io.github.wulkanowy:sdk:2.2.2' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index dc78c1e3d..2d8d4ce7f 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,6 +1,7 @@ -Wersja 2.2.1 +Wersja 2.2.2 -– dokonaliśmy kilka poprawek na ekranie logowania -– naprawiliśmy przypadek z błędnym wyświetlaniem starej klasy ucznia po zalogowaniu się na konto z nowej klasy +— dodaliśmy możliwość łatwego wejścia w sobotę i niedziele w planie lekcji przy użyciu strzałek +— poprawiliśmy wsparcie dla statystyk ocen z systemem punktowym +— poprawiliśmy sortowanie nauczycieli w widoku Szkoła i nauczyciele Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases From 762d4b13931768247bf5b29768dbbaef13c4f7c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Thu, 26 Oct 2023 10:06:54 +0200 Subject: [PATCH 865/869] Timetable timers fixes (#2333) --- .../wulkanowy/data/repositories/PreferencesRepository.kt | 6 ------ .../wulkanowy/ui/modules/timetable/TimetableAdapter.kt | 2 +- .../wulkanowy/ui/modules/timetable/TimetablePresenter.kt | 8 ++++++-- app/src/main/res/values-cs/strings.xml | 1 - app/src/main/res/values-da-rDK/strings.xml | 1 - app/src/main/res/values-de/strings.xml | 1 - app/src/main/res/values-es-rES/strings.xml | 1 - app/src/main/res/values-it-rIT/strings.xml | 1 - app/src/main/res/values-pl/strings.xml | 1 - app/src/main/res/values-ru/strings.xml | 1 - app/src/main/res/values-sk/strings.xml | 1 - app/src/main/res/values-uk/strings.xml | 1 - app/src/main/res/values/preferences_defaults.xml | 1 - app/src/main/res/values/preferences_keys.xml | 1 - app/src/main/res/values/strings.xml | 1 - app/src/main/res/xml/scheme_preferences_appearance.xml | 5 ----- 16 files changed, 7 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index 85c74072e..495415f9f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -194,12 +194,6 @@ class PreferencesRepository @Inject constructor( ) ) - val showTimetableTimers: Boolean - get() = getBoolean( - R.string.pref_key_timetable_show_timers, - R.bool.pref_default_timetable_show_timers - ) - val showTimetableGaps: TimetableGapsMode get() = TimetableGapsMode.getByValue( getString( diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt index 1201937c0..a957ef798 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt @@ -325,7 +325,7 @@ class TimetableAdapter @Inject constructor() : override fun getChangePayload(oldItem: TimetableItem, newItem: TimetableItem): Any? { return if (oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal) { - if (oldItem.lesson == newItem.lesson && oldItem.timeLeft != newItem.timeLeft) { + if (oldItem.lesson == newItem.lesson && oldItem.showGroupsInPlan == newItem.showGroupsInPlan && oldItem.timeLeft != newItem.timeLeft) { "time_left" } else super.getChangePayload(oldItem, newItem) } else super.getChangePayload(oldItem, newItem) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt index f99970482..6b442d1c2 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt @@ -1,5 +1,7 @@ package io.github.wulkanowy.ui.modules.timetable +import android.os.Handler +import android.os.Looper import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student @@ -236,11 +238,13 @@ class TimetablePresenter @Inject constructor( private fun updateData(lessons: List) { tickTimer?.cancel() - if (!prefRepository.showTimetableTimers) { + if (currentDate != now()) { view?.updateData(createItems(lessons)) } else { tickTimer = timer(period = 2_000) { - view?.updateData(createItems(lessons)) + Handler(Looper.getMainLooper()).post { + view?.updateData(createItems(lessons)) + } } } } diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 1749548e1..3af494fa4 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -713,7 +713,6 @@ Zobrazit přítomnost Motiv Rozvíjení známek - Označit aktuální lekci Zobrazit skupiny vedle předmětů Zobrazit prázdné dlaždice, kde není žádná lekce Zobrazit seznam grafů v známkách třídy diff --git a/app/src/main/res/values-da-rDK/strings.xml b/app/src/main/res/values-da-rDK/strings.xml index 500fba1f3..a89b83769 100644 --- a/app/src/main/res/values-da-rDK/strings.xml +++ b/app/src/main/res/values-da-rDK/strings.xml @@ -623,7 +623,6 @@ Show presence Theme Grades expanding - Mark current lesson Show groups next to subjects Show empty tiles where there\'s no lesson Show chart list in class grades diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d8cc92984..4c9aa2d28 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -623,7 +623,6 @@ Anwesendheit zeigen Thema Steigende Sorten - Aktuelle Lektion markieren Gruppen neben Schulfächen anzeigen Show empty tiles where there\'s no lesson Liste der Diagramme in Klassenbewertungen anzeigen diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 500fba1f3..a89b83769 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -623,7 +623,6 @@ Show presence Theme Grades expanding - Mark current lesson Show groups next to subjects Show empty tiles where there\'s no lesson Show chart list in class grades diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml index 500fba1f3..a89b83769 100644 --- a/app/src/main/res/values-it-rIT/strings.xml +++ b/app/src/main/res/values-it-rIT/strings.xml @@ -623,7 +623,6 @@ Show presence Theme Grades expanding - Mark current lesson Show groups next to subjects Show empty tiles where there\'s no lesson Show chart list in class grades diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 9375aeb39..31b9ce32c 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -713,7 +713,6 @@ Pokazuj obecność Motyw Rozwijanie ocen - Oznaczaj bieżącą lekcję Pokazuj grupę obok przedmiotu Pokazuj puste kafelki gdzie nie ma lekcji Pokazuj listę wykresów w ocenach klasy diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index a6e45d445..89a94a59d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -713,7 +713,6 @@ Показывать присутствия Тема Разворачивание оценок - Отметить текущий урок Показать группы рядом с темами Show empty tiles where there\'s no lesson Показывать диаграммы в оценках класса diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index ed06e023e..3cde8152c 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -713,7 +713,6 @@ Zobraziť prítomnosť Motív Rozvijanie známok - Označiť aktuálne lekciu Zobraziť skupiny vedľa predmetov Zobraziť prázdne dlaždice, kde nie je žiadne lekcie Zobraziť zoznam grafov v známkach triedy diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 6a79263f7..20a917aa4 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -713,7 +713,6 @@ Показувати присутність Тема Розгортання оцінок - Позначити поточний урок Показувати групи поруч з темами Показувати порожні плитки там, де немає уроків Показувати діаграми в оцінках класу diff --git a/app/src/main/res/values/preferences_defaults.xml b/app/src/main/res/values/preferences_defaults.xml index 8d69f25c8..8e6fc7d66 100644 --- a/app/src/main/res/values/preferences_defaults.xml +++ b/app/src/main/res/values/preferences_defaults.xml @@ -22,7 +22,6 @@ false no alphabetic - false between false false diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index c48381e8c..5afffb649 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -27,7 +27,6 @@ grade_sorting_mode show_whole_class_plan show_groups_in_plan - timetable_show_timers timetable_show_gaps subjects_without_grades optional_arithmetic_average diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0ba9c9970..5cad09d0b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -701,7 +701,6 @@ Show presence Theme Grades expanding - Mark current lesson Show groups next to subjects Show empty tiles where there\'s no lesson Show chart list in class grades diff --git a/app/src/main/res/xml/scheme_preferences_appearance.xml b/app/src/main/res/xml/scheme_preferences_appearance.xml index 7177d396c..9c02a4910 100644 --- a/app/src/main/res/xml/scheme_preferences_appearance.xml +++ b/app/src/main/res/xml/scheme_preferences_appearance.xml @@ -90,11 +90,6 @@ android:layout_height="wrap_content" app:iconSpaceReserved="false" app:title="@string/pref_timetable_appearance_view"> - Date: Thu, 26 Oct 2023 14:01:45 +0200 Subject: [PATCH 866/869] New Crowdin updates (#2334) From eef3464d0bc79233a996d711903a8159079bc7ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Thu, 26 Oct 2023 18:31:51 +0200 Subject: [PATCH 867/869] Version 2.2.3 --- app/build.gradle | 6 +++--- app/src/main/play/release-notes/pl-PL/default.txt | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 42a1f8f74..afa4ac160 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,8 +27,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 34 - versionCode 134 - versionName "2.2.2" + versionCode 135 + versionName "2.2.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" @@ -192,7 +192,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.2.2' + implementation 'io.github.wulkanowy:sdk:2.2.3' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index 2d8d4ce7f..dfc045462 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,6 +1,6 @@ -Wersja 2.2.2 +Wersja 2.2.3 -— dodaliśmy możliwość łatwego wejścia w sobotę i niedziele w planie lekcji przy użyciu strzałek +— ułatwiliśmy przełączenie dnia na weekend w planie lekcji przy użyciu strzałek — poprawiliśmy wsparcie dla statystyk ocen z systemem punktowym — poprawiliśmy sortowanie nauczycieli w widoku Szkoła i nauczyciele From 1dbaa8bfdc327b11ceedaa24fd3f2449355247d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Fri, 27 Oct 2023 14:09:42 +0200 Subject: [PATCH 868/869] Migrate to separate app-update and review artifacts from play:core (#2336) --- app/build.gradle | 4 +- .../wulkanowy/utils/InAppUpdateHelper.kt | 13 ++++ .../io/github/wulkanowy/utils/UpdateHelper.kt | 17 ------ .../wulkanowy/utils/InAppUpdateHelper.kt | 13 ++++ .../io/github/wulkanowy/utils/UpdateHelper.kt | 17 ------ .../ui/modules/login/LoginActivity.kt | 18 ++---- .../wulkanowy/ui/modules/main/MainActivity.kt | 15 ++--- .../{UpdateHelper.kt => InAppUpdateHelper.kt} | 59 +++++++++++-------- 8 files changed, 71 insertions(+), 85 deletions(-) create mode 100644 app/src/fdroid/java/io/github/wulkanowy/utils/InAppUpdateHelper.kt delete mode 100644 app/src/fdroid/java/io/github/wulkanowy/utils/UpdateHelper.kt create mode 100644 app/src/hms/java/io/github/wulkanowy/utils/InAppUpdateHelper.kt delete mode 100644 app/src/hms/java/io/github/wulkanowy/utils/UpdateHelper.kt rename app/src/play/java/io/github/wulkanowy/utils/{UpdateHelper.kt => InAppUpdateHelper.kt} (76%) diff --git a/app/build.gradle b/app/build.gradle index afa4ac160..88becb9a2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -253,10 +253,10 @@ dependencies { playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' playImplementation 'com.google.firebase:firebase-config-ktx' - playImplementation 'com.google.android.play:core:1.10.3' - playImplementation 'com.google.android.play:core-ktx:1.8.1' playImplementation 'com.google.android.gms:play-services-ads:22.4.0' playImplementation "com.google.android.play:integrity:1.2.0" + playImplementation 'com.google.android.play:app-update-ktx:2.1.0' + playImplementation 'com.google.android.play:review-ktx:2.0.1' hmsImplementation 'com.huawei.hms:hianalytics:6.12.0.300' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.1.301' diff --git a/app/src/fdroid/java/io/github/wulkanowy/utils/InAppUpdateHelper.kt b/app/src/fdroid/java/io/github/wulkanowy/utils/InAppUpdateHelper.kt new file mode 100644 index 000000000..51b22ec74 --- /dev/null +++ b/app/src/fdroid/java/io/github/wulkanowy/utils/InAppUpdateHelper.kt @@ -0,0 +1,13 @@ +package io.github.wulkanowy.utils + +import android.view.View +import javax.inject.Inject + +class InAppUpdateHelper @Inject constructor() { + + lateinit var messageContainer: View + + fun checkAndInstallUpdates() {} + + fun onResume() {} +} diff --git a/app/src/fdroid/java/io/github/wulkanowy/utils/UpdateHelper.kt b/app/src/fdroid/java/io/github/wulkanowy/utils/UpdateHelper.kt deleted file mode 100644 index 3abab9629..000000000 --- a/app/src/fdroid/java/io/github/wulkanowy/utils/UpdateHelper.kt +++ /dev/null @@ -1,17 +0,0 @@ -package io.github.wulkanowy.utils - -import android.app.Activity -import android.view.View -import javax.inject.Inject - -@Suppress("UNUSED_PARAMETER") -class UpdateHelper @Inject constructor() { - - lateinit var messageContainer: View - - fun checkAndInstallUpdates(activity: Activity) {} - - fun onActivityResult(requestCode: Int, resultCode: Int) {} - - fun onResume(activity: Activity) {} -} diff --git a/app/src/hms/java/io/github/wulkanowy/utils/InAppUpdateHelper.kt b/app/src/hms/java/io/github/wulkanowy/utils/InAppUpdateHelper.kt new file mode 100644 index 000000000..51b22ec74 --- /dev/null +++ b/app/src/hms/java/io/github/wulkanowy/utils/InAppUpdateHelper.kt @@ -0,0 +1,13 @@ +package io.github.wulkanowy.utils + +import android.view.View +import javax.inject.Inject + +class InAppUpdateHelper @Inject constructor() { + + lateinit var messageContainer: View + + fun checkAndInstallUpdates() {} + + fun onResume() {} +} diff --git a/app/src/hms/java/io/github/wulkanowy/utils/UpdateHelper.kt b/app/src/hms/java/io/github/wulkanowy/utils/UpdateHelper.kt deleted file mode 100644 index 3abab9629..000000000 --- a/app/src/hms/java/io/github/wulkanowy/utils/UpdateHelper.kt +++ /dev/null @@ -1,17 +0,0 @@ -package io.github.wulkanowy.utils - -import android.app.Activity -import android.view.View -import javax.inject.Inject - -@Suppress("UNUSED_PARAMETER") -class UpdateHelper @Inject constructor() { - - lateinit var messageContainer: View - - fun checkAndInstallUpdates(activity: Activity) {} - - fun onActivityResult(requestCode: Int, resultCode: Int) {} - - fun onResume(activity: Activity) {} -} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt index c17c92efd..88f295788 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt @@ -23,7 +23,7 @@ import io.github.wulkanowy.ui.modules.login.symbol.LoginSymbolFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.notifications.NotificationsFragment import io.github.wulkanowy.utils.AppInfo -import io.github.wulkanowy.utils.UpdateHelper +import io.github.wulkanowy.utils.InAppUpdateHelper import javax.inject.Inject @AndroidEntryPoint @@ -33,7 +33,7 @@ class LoginActivity : BaseActivity(), Logi override lateinit var presenter: LoginPresenter @Inject - lateinit var updateHelper: UpdateHelper + lateinit var inAppUpdateHelper: InAppUpdateHelper @Inject lateinit var appInfo: AppInfo @@ -47,10 +47,10 @@ class LoginActivity : BaseActivity(), Logi setContentView(ActivityLoginBinding.inflate(layoutInflater).apply { binding = this }.root) setSupportActionBar(binding.loginToolbar) messageContainer = binding.loginContainer - updateHelper.messageContainer = binding.loginContainer + inAppUpdateHelper.messageContainer = binding.loginContainer presenter.onAttachView(this) - updateHelper.checkAndInstallUpdates(this) + inAppUpdateHelper.checkAndInstallUpdates() if (savedInstanceState == null) { openFragment(LoginFormFragment.newInstance(), clearBackStack = true) @@ -117,14 +117,6 @@ class LoginActivity : BaseActivity(), Logi override fun onResume() { super.onResume() - updateHelper.onResume(this) - } - - //https://developer.android.com/guide/playcore/in-app-updates#status_callback - @Deprecated("Deprecated in Java") - @Suppress("DEPRECATION") - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - updateHelper.onActivityResult(requestCode, resultCode) + inAppUpdateHelper.onResume() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt index 178d6e94b..25ab73bca 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt @@ -45,7 +45,7 @@ class MainActivity : BaseActivity(), MainVie lateinit var analytics: AnalyticsHelper @Inject - lateinit var updateHelper: UpdateHelper + lateinit var inAppUpdateHelper: InAppUpdateHelper @Inject lateinit var inAppReviewHelper: InAppReviewHelper @@ -100,7 +100,7 @@ class MainActivity : BaseActivity(), MainVie this.savedInstanceState = savedInstanceState messageContainer = binding.mainMessageContainer messageAnchor = binding.mainMessageContainer - updateHelper.messageContainer = binding.mainFragmentContainer + inAppUpdateHelper.messageContainer = binding.mainFragmentContainer onBackCallback = onBackPressedDispatcher.addCallback(this, enabled = false) { presenter.onBackPressed() } @@ -109,19 +109,12 @@ class MainActivity : BaseActivity(), MainVie ?.takeIf { savedInstanceState == null } presenter.onAttachView(this, destination) - updateHelper.checkAndInstallUpdates(this) + inAppUpdateHelper.checkAndInstallUpdates() } override fun onResume() { super.onResume() - updateHelper.onResume(this) - } - - //https://developer.android.com/guide/playcore/in-app-updates#status_callback - @Deprecated("Deprecated in Java") - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - updateHelper.onActivityResult(requestCode, resultCode) + inAppUpdateHelper.onResume() } override fun onCreateOptionsMenu(menu: Menu): Boolean { diff --git a/app/src/play/java/io/github/wulkanowy/utils/UpdateHelper.kt b/app/src/play/java/io/github/wulkanowy/utils/InAppUpdateHelper.kt similarity index 76% rename from app/src/play/java/io/github/wulkanowy/utils/UpdateHelper.kt rename to app/src/play/java/io/github/wulkanowy/utils/InAppUpdateHelper.kt index 6772237e2..d89dfd5f8 100644 --- a/app/src/play/java/io/github/wulkanowy/utils/UpdateHelper.kt +++ b/app/src/play/java/io/github/wulkanowy/utils/InAppUpdateHelper.kt @@ -3,12 +3,15 @@ package io.github.wulkanowy.utils import android.app.Activity import android.app.Activity.RESULT_OK import android.content.Context -import android.content.IntentSender import android.view.View import android.widget.Toast +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity import com.google.android.material.snackbar.Snackbar import com.google.android.play.core.appupdate.AppUpdateInfo import com.google.android.play.core.appupdate.AppUpdateManagerFactory +import com.google.android.play.core.appupdate.AppUpdateOptions import com.google.android.play.core.install.InstallStateUpdatedListener import com.google.android.play.core.install.model.AppUpdateType.FLEXIBLE import com.google.android.play.core.install.model.AppUpdateType.IMMEDIATE @@ -20,15 +23,16 @@ import com.google.android.play.core.ktx.isFlexibleUpdateAllowed import com.google.android.play.core.ktx.isImmediateUpdateAllowed import com.google.android.play.core.ktx.updatePriority import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.scopes.ActivityScoped import io.github.wulkanowy.R import timber.log.Timber import javax.inject.Inject -import javax.inject.Singleton -@Singleton -class UpdateHelper @Inject constructor( +@ActivityScoped +class InAppUpdateHelper @Inject constructor( @ApplicationContext private val context: Context, private val analyticsHelper: AnalyticsHelper, + activity: Activity ) { lateinit var messageContainer: View @@ -39,6 +43,7 @@ class UpdateHelper @Inject constructor( when (state.installStatus()) { PENDING -> Toast.makeText(context, R.string.update_download_started, Toast.LENGTH_SHORT) .show() + DOWNLOADED -> popupSnackBarForCompleteUpdate() else -> Timber.d("Update state: ${state.installStatus()}") } @@ -70,45 +75,55 @@ class UpdateHelper @Inject constructor( return updateAvailability() == UPDATE_AVAILABLE && isFlexibleUpdateAllowed && isUpdatePriorityAllowUpdate } - fun checkAndInstallUpdates(activity: Activity) { + private val activityResultLauncher = (activity as AppCompatActivity).registerForActivityResult( + ActivityResultContracts.StartIntentSenderForResult(), + ::onActivityResult + ) + + fun checkAndInstallUpdates() { Timber.d("Checking for updates...") appUpdateManager.appUpdateInfo.addOnSuccessListener { appUpdateInfo -> when { appUpdateInfo.isImmediateUpdateAvailable -> { - startUpdate(activity, appUpdateInfo, IMMEDIATE) + startUpdate(appUpdateInfo, IMMEDIATE) } + appUpdateInfo.isFlexibleUpdateAvailable -> { appUpdateManager.registerListener(flexibleUpdateListener) - startUpdate(activity, appUpdateInfo, FLEXIBLE) + startUpdate(appUpdateInfo, FLEXIBLE) } + else -> Timber.d("No update available") } } } - private fun startUpdate(activity: Activity, appUpdateInfo: AppUpdateInfo, updateType: Int) { + private fun startUpdate(appUpdateInfo: AppUpdateInfo, updateType: Int) { Timber.d("Start update ($updateType): $appUpdateInfo") + try { appUpdateManager.startUpdateFlowForResult( - appUpdateInfo, updateType, activity, IN_APP_UPDATE_REQUEST_CODE + appUpdateInfo, + activityResultLauncher, + AppUpdateOptions.defaultOptions(updateType) ) - } catch (e: IntentSender.SendIntentException) { - Timber.i("Update failed! Duplicated PendingIntent") + } catch (e: Exception) { + Timber.e(e, "Update failed!") } } - fun onActivityResult(requestCode: Int, resultCode: Int) { - if (requestCode == IN_APP_UPDATE_REQUEST_CODE) { - if (resultCode != RESULT_OK) { - Timber.i("Update failed! Result code: $resultCode") - Toast.makeText(context, R.string.update_failed, Toast.LENGTH_LONG).show() - } + private fun onActivityResult(activityResult: ActivityResult) { + val resultCode = activityResult.resultCode - analyticsHelper.logEvent("inapp_update", "code" to resultCode) + if (resultCode != RESULT_OK) { + Timber.i("Update failed! Result code: $resultCode") + Toast.makeText(context, R.string.update_failed, Toast.LENGTH_LONG).show() } + + analyticsHelper.logEvent("inapp_update", "code" to resultCode) } - fun onResume(activity: Activity) { + fun onResume() { appUpdateManager.appUpdateInfo.addOnSuccessListener { info -> Timber.d("InAppUpdate.onResume() listener: $info") @@ -116,7 +131,6 @@ class UpdateHelper @Inject constructor( DOWNLOADED == info.installStatus() -> popupSnackBarForCompleteUpdate() DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS == info.updateAvailability() -> { startUpdate( - activity = activity, appUpdateInfo = info, updateType = if (info.isImmediateUpdateAvailable) IMMEDIATE else FLEXIBLE ) @@ -139,9 +153,4 @@ class UpdateHelper @Inject constructor( show() } } - - private companion object { - - private const val IN_APP_UPDATE_REQUEST_CODE = 1721 - } } From 124b6dfd79bcb5a4898219a7e7db5f5622437c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Fri, 27 Oct 2023 14:49:41 +0200 Subject: [PATCH 869/869] Version 2.2.4 --- app/build.gradle | 8 ++++---- app/src/main/play/release-notes/pl-PL/default.txt | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 88becb9a2..0fcedc68e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,8 +27,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 34 - versionCode 135 - versionName "2.2.3" + versionCode 136 + versionName "2.2.4" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" @@ -162,7 +162,7 @@ play { track = 'production' releaseStatus = ReleaseStatus.IN_PROGRESS userFraction = 0.01d - updatePriority = 3 + updatePriority = 0 enabled.set(false) } @@ -192,7 +192,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.2.3' + implementation 'io.github.wulkanowy:sdk:2.2.4' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index dfc045462..907221d79 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,4 +1,4 @@ -Wersja 2.2.3 +Wersja 2.2.4 — ułatwiliśmy przełączenie dnia na weekend w planie lekcji przy użyciu strzałek — poprawiliśmy wsparcie dla statystyk ocen z systemem punktowym