1
0
Fork 1

Compare commits

..

No commits in common. "1.4.3" and "1.2.1" have entirely different histories.
1.4.3 ... 1.2.1

340 changed files with 2580 additions and 18872 deletions

View file

@ -28,17 +28,15 @@ jobs:
SERVICES_ENCRYPT_KEY: ${{ secrets.SERVICES_ENCRYPT_KEY }} SERVICES_ENCRYPT_KEY: ${{ secrets.SERVICES_ENCRYPT_KEY }}
run: | run: |
gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/google-services.json.gpg 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 gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg
- name: Upload apk to google play - name: Upload apk to google play
env: env:
PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }}
PLAY_KEY_ALIAS: ${{ secrets.PLAY_KEY_ALIAS }} PLAY_KEY_ALIAS: ${{ secrets.PLAY_KEY_ALIAS }}
PLAY_KEY_PASSWORD: ${{ secrets.PLAY_KEY_PASSWORD }} PLAY_KEY_PASSWORD: ${{ secrets.PLAY_KEY_PASSWORD }}
ANDROID_PUBLISHER_CREDENTIALS: ${{ secrets.ANDROID_PUBLISHER_CREDENTIALS }} PLAY_SERVICE_ACCOUNT_EMAIL: ${{ secrets.PLAY_SERVICE_ACCOUNT_EMAIL }}
ADMOB_PROJECT_ID: ${{ secrets.ADMOB_PROJECT_ID }} PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }}
SINGLE_SUPPORT_AD_ID: ${{ secrets.SINGLE_SUPPORT_AD_ID }} run: ./gradlew publishPlayRelease -PenableFirebase --stacktrace;
SET_BUILD_TIMESTAMP: ${{ secrets.SET_BUILD_TIMESTAMP }}
run: ./gradlew publishPlayReleaseApps -PenableFirebase --stacktrace;
deploy-app-gallery: deploy-app-gallery:
name: Deploy to AppGallery name: Deploy to AppGallery
@ -62,6 +60,7 @@ jobs:
SERVICES_ENCRYPT_KEY: ${{ secrets.SERVICES_ENCRYPT_KEY }} SERVICES_ENCRYPT_KEY: ${{ secrets.SERVICES_ENCRYPT_KEY }}
run: | run: |
gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/agconnect-services.json.gpg 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 gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg
- name: Prepare credentials - name: Prepare credentials
env: env:
@ -69,8 +68,7 @@ jobs:
run: echo $AGC_CREDENTIALS > ./app/src/release/agconnect-credentials.json run: echo $AGC_CREDENTIALS > ./app/src/release/agconnect-credentials.json
- name: Build and publish HMS version - name: Build and publish HMS version
env: env:
PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }}
PLAY_KEY_ALIAS: ${{ secrets.PLAY_KEY_ALIAS }} PLAY_KEY_ALIAS: ${{ secrets.PLAY_KEY_ALIAS }}
PLAY_KEY_PASSWORD: ${{ secrets.PLAY_KEY_PASSWORD }} PLAY_KEY_PASSWORD: ${{ secrets.PLAY_KEY_PASSWORD }}
SET_BUILD_TIMESTAMP: ${{ secrets.SET_BUILD_TIMESTAMP }} PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }}
run: ./gradlew bundleHmsRelease --stacktrace && ./gradlew publishHuaweiAppGalleryHmsRelease --stacktrace run: ./gradlew assembleHmsRelease --stacktrace && ./gradlew publishHuaweiAppGalleryHmsRelease --stacktrace

View file

@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier same "printed page" as the copyright notice for easier
identification within third-party archives. identification within third-party archives.
Copyright 2021 Wulkanowy Copyright 2019 Wulkanowy
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View file

@ -1,78 +0,0 @@
[English version of README](README.en.md)
[Deutsche Version von README](README.de.md)
[Polska wersja README](README.md)
[Slovenská verzia README](README.sk.md)
# Wulkanowy
[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/wulkanowy/wulkanowy/Tests/develop?style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions)
[![Codecov](https://img.shields.io/codecov/c/github/wulkanowy/wulkanowy/master.svg?style=flat-square)](https://codecov.io/gh/wulkanowy/wulkanowy)
[![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr)
[![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/)
[![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases)
Neoficiální klient deníku VULCAN UONET+ pro žáka a rodiče
## Funkce
* přihlášení pomocí emailu a hesla
* funkce z webové stránky deníku:
* známky
* statistiky známek
* frekvence
* procento frekvence
* zkoušky
* plán lekce
* dokončené lekce
* zprávy
* domácí úkoly
* poznámky
* šťastné číslo
* další lekce
* školní setkání
* informace o žáku a škole
* výpočet průměru nezávisle na preferencích školy
* upozornění, např. o nových známkách
* podpora více účtů s možností přejmenování žáků
* tmavý a černý (AMOLED) motiv
* offline režim
* žádné reklamy
## Stáhnout
Aktuální verzi si můžete stáhnout z Google Play, F-Droid nebo Huawei AppGallery
[<img src="https://play.google.com/intl/cs-CZ/badges/images/generic/cs_badge_web_generic.png"
alt="Nyní na Google Play"
height="80">](https://play.google.com/store/apps/details?id=io.github.wulkanowy)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Stáhnout s F-Droid"
height="80">](https://f-droid.org/packages/io.github.wulkanowy/)
[<img src="https://i.imgur.com/baTGiDP.png"
alt="Objevuj v AppGallery"
height="80">](https://appgallery.cloud.huawei.com/ag/n/app/C101440411?channelId=Badge&id=1b3f7fbb700849a9be0dba6b520b2282&s=EB1D3BF9ED9D1564D869B7B94B18016D3CABFCA5AEFB8E29F675FA04E0DC131D&detailType=0&v=)
Můžete si také stáhnout [vývojovou verzi](https://wulkanowy.github.io/#download), která zahrnuje nové funkce připravované pro příští vydání
## Postaveno s
* [Wulkanowy SDK](https://github.com/wulkanowy/sdk)
* [Kotlin Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html)
* [Hilt](https://dagger.dev/hilt/)
* [Room](https://developer.android.com/topic/libraries/architecture/room)
* [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)
## Spolupráce
Přispějte do projektu vytvořením PR nebo odesláním issue na GitHub.
Pro zájemce o překlad aplikace do různých jazyků poskytujeme Crowdin:
https://crowdin.com/project/wulkanowy2
## Licence
Tento projekt je licencován pod licencí Apache License 2.0 - podrobnosti v souboru [LICENSE](LICENSE)

View file

@ -1,74 +0,0 @@
[Polska wersja README](README.md)
[English version of README](README.en.md)
# Wulkanowy
[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/wulkanowy/wulkanowy/Tests/develop?style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions)
[![Codecov](https://img.shields.io/codecov/c/github/wulkanowy/wulkanowy/master.svg?style=flat-square)](https://codecov.io/gh/wulkanowy/wulkanowy)
[![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr)
[![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/)
[![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases)
Inoffizieller Android VULCAN UONET+ Registrierungsclient für Schüler und ihre Eltern
## Merkmale
* Einloggen mit E-Mail und Passwort
* Funktionen von der Registerwebsite:
* Noten
* Notenstatistik
* Anwesenheit
* Prozentsatz der Anwesenheit
* Prüfungen
* Stundenplan
* Unterricht abgeschlossen
* Nachrichten
* Hausaufgaben
* Anmerkungen
* Glückszahl
* Zusätzliche Lektionen
* Schulkonferenzen
* Schüler- und Schulinformationen
* Berechnung des Durchschnitts unabhängig von den Präferenzen der Schule
* Benachrichtigungen, z. B. über eine neue Note
* Unterstützung für mehrere Konten mit der Möglichkeit, den Namen des Schülers zu ändern
* dunkles und schwarzes (AMOLED) Thema
* Offline-Modus
* keine Werbung
## Herunterladen
Die aktuelle Version können Sie von der Google Play, F-Droid oder Huawei AppGallery store herunterladen
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
alt="Get it on Google Play"
height="80">](https://play.google.com/store/apps/details?id=io.github.wulkanowy)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/packages/io.github.wulkanowy/)
[<img src="appgallery_badge.png"
alt="Explore it on AppGallery"
height="80">](https://appgallery.cloud.huawei.com/ag/n/app/C101440411?channelId=Badge&id=1b3f7fbb700849a9be0dba6b520b2282&s=EB1D3BF9ED9D1564D869B7B94B18016D3CABFCA5AEFB8E29F675FA04E0DC131D&detailType=0&v=)
Sie können auch ein [Entwicklungsversion herunterladen](https://wulkanowy.github.io/#download) das beinhaltet neue Funktionen, die für die nächste Version vorbereitet werden
## Gebaut mit
* [Wulkanowy SDK](https://github.com/wulkanowy/sdk)
* [Kotlin Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html)
* [Hilt](https://dagger.dev/hilt/)
* [Room](https://developer.android.com/topic/libraries/architecture/room)
* [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)
## Beitragen
Bitte tragen Sie zum Projekt bei, indem Sie entweder eine PR erstellen oder ein Issue auf GitHub einreichen.
Für Personen, die daran interessiert sind, die Anwendung in verschiedene Sprachen zu übersetzen, bieten wir Crowdin
https://crowdin.com/project/wulkanowy2
## Lizenz
Dieses Projekt ist unter der Apache License 2.0 lizenziert - siehe die [LIZENZ](LICENSE) Datei für Details

View file

@ -1,11 +1,5 @@
[Polska wersja README](README.md) [Polska wersja README](README.md)
[Deutsche Version von README](README.de.md)
[Česká verze README](README.cs.md)
[Slovenská verzia README](README.sk.md)
# Wulkanowy # Wulkanowy
[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/wulkanowy/wulkanowy/Tests/develop?style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions) [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/wulkanowy/wulkanowy/Tests/develop?style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions)

View file

@ -1,11 +1,5 @@
[English version of README](README.en.md) [English version of README](README.en.md)
[Deutsche Version von README](README.de.md)
[Česká verze README](README.cs.md)
[Slovenská verzia README](README.sk.md)
# Wulkanowy # Wulkanowy
[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/wulkanowy/wulkanowy/Tests/develop?style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions) [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/wulkanowy/wulkanowy/Tests/develop?style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions)

View file

@ -1,78 +0,0 @@
[English version of README](README.en.md)
[Deutsche Version von README](README.de.md)
[Polska wersja README](README.md)
[Česká verze README](README.cs.md)
# Wulkanowy
[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/wulkanowy/wulkanowy/Tests/develop?style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions)
[![Codecov](https://img.shields.io/codecov/c/github/wulkanowy/wulkanowy/master.svg?style=flat-square)](https://codecov.io/gh/wulkanowy/wulkanowy)
[![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr)
[![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/)
[![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases)
Neoficiálny klient denníka VULCAN UONET+ pre žiaka a rodičov
## Funkcie
* prihlásenie pomocou emailu a hesla
* funkcie z webovej stránky denníka:
* známky
* štatistiky známok
* frekvencia
* percento frekvencie
* skúšky
* plán lekcie
* dokončené lekcie
* správy
* domáce úlohy
* poznámky
* šťastné číslo
* ďalšie lekcie
* školské stretnutie
* informácie o žiakovi a škole
* výpočet priemeru nezávisle od preferencií školy
* upozornenia, napr. o nových známkach
* podpora viacerých účtov s možnosťou premenovania žiakov
* tmavý a čierny (AMOLED) motív
* offline režim
* žiadne reklamy
## Stiahnuť
Aktuálnu verziu si môžete stiahnuť z Google Play, F-Droid alebo Huawei AppGallery
[<img src="https://play.google.com/intl/sk/badges/images/generic/sk_badge_web_generic.png"
alt="Nyní na Google Play"
height="80">](https://play.google.com/store/apps/details?id=io.github.wulkanowy)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Stiahnuť s F-Droid"
height="80">](https://f-droid.org/packages/io.github.wulkanowy/)
[<img src="https://i.imgur.com/sX8UyAw.png"
alt="Objavíte v AppGallery"
height="80">](https://appgallery.cloud.huawei.com/ag/n/app/C101440411?channelId=Badge&id=1b3f7fbb700849a9be0dba6b520b2282&s=EB1D3BF9ED9D1564D869B7B94B18016D3CABFCA5AEFB8E29F675FA04E0DC131D&detailType=0&v=)
Môžete si tiež stiahnuť [vývojovú verziu](https://wulkanowy.github.io/#download), ktorá zahrňuje nové funkcie pripravované pre budúce vydanie
## Postavené s
* [Wulkanowy SDK](https://github.com/wulkanowy/sdk)
* [Kotlin Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html)
* [Hilt](https://dagger.dev/hilt/)
* [Room](https://developer.android.com/topic/libraries/architecture/room)
* [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)
## Spolupráca
Prispejte do projektu vytvorením PR alebo odoslaním issue na GitHub.
Pre záujemcov o preklad aplikácie do rôznych jazykov poskytujeme Crowdin:
https://crowdin.com/project/wulkanowy2
## Licencia
Tento projekt je licencovaný pod licenciou Apache License 2.0 - podrobnosti v súbore [LICENSE](LICENSE)

View file

@ -1,6 +1,5 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin' apply plugin: 'dagger.hilt.android.plugin'
@ -15,22 +14,23 @@ apply from: 'sonarqube.gradle'
apply from: 'hooks.gradle' apply from: 'hooks.gradle'
android { android {
compileSdkVersion 31 compileSdkVersion 30
defaultConfig { defaultConfig {
applicationId "io.github.wulkanowy" applicationId "io.github.wulkanowy"
testApplicationId "io.github.tests.wulkanowy" testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 31 targetSdkVersion 30
versionCode 101 versionCode 94
versionName "1.4.3" versionName "1.2.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
resValue "string", "app_name", "Wulkanowy" resValue "string", "app_name", "Wulkanowy"
buildConfigField "long", "BUILD_TIMESTAMP", String.valueOf(System.currentTimeMillis())
manifestPlaceholders = [ manifestPlaceholders = [
firebase_enabled: project.hasProperty("enableFirebase"), firebase_enabled: project.hasProperty("enableFirebase")
admob_project_id: ""
] ]
javaCompileOptions { javaCompileOptions {
annotationProcessorOptions { annotationProcessorOptions {
@ -40,14 +40,6 @@ android {
] ]
} }
} }
buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null"
if (System.env.SET_BUILD_TIMESTAMP) {
buildConfigField "long", "BUILD_TIMESTAMP", String.valueOf(System.currentTimeMillis())
} else {
buildConfigField "long", "BUILD_TIMESTAMP", "1486235849000"
}
} }
sourceSets { sourceSets {
@ -70,14 +62,12 @@ android {
shrinkResources true shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release signingConfig signingConfigs.release
buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\""
} }
debug { debug {
resValue "string", "app_name", "Wulkanowy DEV" resValue "string", "app_name", "Wulkanowy DEV " + defaultConfig.versionCode
applicationIdSuffix ".dev" applicationIdSuffix ".dev"
versionNameSuffix "-dev" versionNameSuffix "-dev"
ext.enableCrashlytics = project.hasProperty("enableFirebase") ext.enableCrashlytics = project.hasProperty("enableFirebase")
buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\""
} }
} }
@ -86,38 +76,30 @@ android {
productFlavors { productFlavors {
hms { hms {
dimension "platform" dimension "platform"
manifestPlaceholders = [install_channel: "AppGallery"] manifestPlaceholders = [
install_channel: "AppGallery"
]
} }
play { play {
dimension "platform" dimension "platform"
manifestPlaceholders = [ 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 { fdroid {
dimension "platform" dimension "platform"
manifestPlaceholders = [install_channel: "F-Droid"] manifestPlaceholders = [
install_channel: "F-Droid"
]
} }
} }
playConfigs {
play { enabled.set(true) }
}
buildFeatures { buildFeatures {
viewBinding true viewBinding true
} }
bundle {
language {
enableSplit = false
}
}
testOptions.unitTests { testOptions.unitTests {
includeAndroidResources = true includeAndroidResources = true
} }
@ -148,61 +130,61 @@ kapt {
} }
play { play {
serviceAccountEmail = System.getenv("PLAY_SERVICE_ACCOUNT_EMAIL") ?: "jan@fakelog.cf"
serviceAccountCredentials = file('key.p12')
defaultToAppBundles = false defaultToAppBundles = false
track = 'beta' track = 'production'
updatePriority = 1 updatePriority = 3
enabled.set(false)
} }
huaweiPublish { huaweiPublish {
instances { instances {
hmsRelease { hmsRelease {
credentialsPath = "$rootDir/app/src/release/agconnect-credentials.json" credentialsPath = "$rootDir/app/src/release/agconnect-credentials.json"
buildFormat = "aab" buildFormat = "apk"
deployType = "draft" deployType = "draft"
} }
} }
} }
ext { ext {
work_manager = "2.7.1" work_manager = "2.5.0"
android_hilt = "1.0.0" android_hilt = "1.0.0"
room = "2.3.0" room = "2.3.0"
chucker = "3.5.2" chucker = "3.5.2"
mockk = "1.12.1" mockk = "1.12.0"
coroutines = "1.5.2" moshi = "1.12.0"
} }
dependencies { dependencies {
implementation "io.github.wulkanowy:sdk:1.4.3" implementation "io.github.wulkanowy:sdk:1.2.1"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
implementation "androidx.core:core-ktx:1.7.0" 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.activity:activity-ktx:1.4.0" implementation "androidx.appcompat:appcompat:1.3.1"
implementation "androidx.appcompat:appcompat:1.4.0" implementation "androidx.appcompat:appcompat-resources:1.3.1"
implementation "androidx.fragment:fragment-ktx:1.4.0" implementation "androidx.fragment:fragment-ktx:1.3.6"
implementation "androidx.annotation:annotation:1.3.0" implementation "androidx.annotation:annotation:1.2.0"
implementation "androidx.preference:preference-ktx:1.1.1" implementation "androidx.preference:preference-ktx:1.1.1"
implementation "androidx.recyclerview:recyclerview:1.2.1" implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation "androidx.viewpager2:viewpager2:1.1.0-beta01" implementation "androidx.viewpager:viewpager:1.0.0"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:2.1.2" implementation "androidx.constraintlayout:constraintlayout:2.1.0"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0" implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0"
implementation "com.google.android.material:material:1.4.0" implementation "com.google.android.material:material:1.4.0"
implementation "com.github.wulkanowy:material-chips-input:2.3.1" implementation "com.github.wulkanowy:material-chips-input:2.2.0"
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0" implementation "com.github.PhilJay:MPAndroidChart:v3.1.0"
implementation 'com.github.lopspower:CircularImageView:4.2.0' implementation 'com.github.lopspower:CircularImageView:4.2.0'
implementation "androidx.work:work-runtime-ktx:$work_manager" implementation "androidx.work:work-runtime-ktx:$work_manager"
playImplementation "androidx.work:work-gcm:$work_manager" playImplementation "androidx.work:work-gcm:$work_manager"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0" implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
implementation "androidx.room:room-runtime:$room" implementation "androidx.room:room-runtime:$room"
implementation "androidx.room:room-ktx:$room" implementation "androidx.room:room-ktx:$room"
@ -216,41 +198,40 @@ dependencies {
implementation 'com.github.ncapdevi:FragNav:3.3.0' implementation 'com.github.ncapdevi:FragNav:3.3.0'
implementation "com.github.YarikSOffice:lingver:1.3.0" implementation "com.github.YarikSOffice:lingver:1.3.0"
implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation "com.squareup.moshi:moshi:$moshi"
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0" implementation "com.squareup.moshi:moshi-adapters:$moshi"
implementation "com.squareup.okhttp3:logging-interceptor:4.9.3" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi"
implementation "com.jakewharton.timber:timber:5.0.1" implementation "com.jakewharton.timber:timber:5.0.1"
implementation "at.favre.lib:slf4j-timber:1.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.0.4'
implementation "com.mikepenz:aboutlibraries-core:$about_libraries" implementation "com.mikepenz:aboutlibraries-core:$about_libraries"
implementation "io.coil-kt:coil:1.4.0" implementation "io.coil-kt:coil:1.3.2"
implementation "io.github.wulkanowy:AppKillerManager:3.0.0" implementation "io.github.wulkanowy:AppKillerManager:3.0.0"
implementation 'me.xdrop:fuzzywuzzy:1.3.1' implementation 'me.xdrop:fuzzywuzzy:1.3.1'
implementation 'com.fredporciuncula:flow-preferences:1.6.0' implementation 'com.fredporciuncula:flow-preferences:1.5.0'
playImplementation platform('com.google.firebase:firebase-bom:29.0.0') playImplementation platform('com.google.firebase:firebase-bom:28.4.0')
playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-analytics-ktx'
playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-messaging:'
playImplementation 'com.google.firebase:firebase-crashlytics:' playImplementation 'com.google.firebase:firebase-crashlytics:'
playImplementation 'com.google.android.play:core:1.10.2' playImplementation 'com.google.android.play:core:1.10.1'
playImplementation 'com.google.android.play:core-ktx:1.8.1' playImplementation 'com.google.android.play:core-ktx:1.8.1'
playImplementation 'com.google.android.gms:play-services-ads:20.5.0'
hmsImplementation 'com.huawei.hms:hianalytics:6.3.2.300' hmsImplementation 'com.huawei.hms:hianalytics:6.2.0.301'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.2.200' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.0.300'
releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker"
debugImplementation "com.github.ChuckerTeam.Chucker:library:$chucker" debugImplementation "com.github.ChuckerTeam.Chucker:library:$chucker"
debugImplementation 'com.github.amitshekhariitbhu.Android-Debug-Database:debug-db:1.0.6' debugImplementation 'com.github.amitshekhariitbhu.Android-Debug-Database:debug-db:v1.0.6'
testImplementation "junit:junit:4.13.2" testImplementation "junit:junit:4.13.2"
testImplementation "io.mockk:mockk:$mockk" testImplementation "io.mockk:mockk:$mockk"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines" testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2'
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
testImplementation 'org.robolectric:robolectric:4.7.2' testImplementation 'org.robolectric:robolectric:4.6.1'
testImplementation "androidx.test:runner:1.4.0" testImplementation "androidx.test:runner:1.4.0"
testImplementation "androidx.test.ext:junit:1.1.3" testImplementation "androidx.test.ext:junit:1.1.3"
testImplementation "androidx.test:core:1.4.0" testImplementation "androidx.test:core:1.4.0"

BIN
app/key.p12.gpg Normal file

Binary file not shown.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,6 @@
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<queries> <queries>
<intent> <intent>
@ -39,14 +38,13 @@
android:allowBackup="false" android:allowBackup="false"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="false" android:supportsRtl="false"
android:theme="@style/WulkanowyTheme" android:theme="@style/WulkanowyTheme"
android:usesCleartextTraffic="true"
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute"> tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
<activity <activity
android:name=".ui.modules.splash.SplashActivity" android:name=".ui.modules.splash.SplashActivity"
android:exported="true"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:theme="@style/WulkanowyTheme.SplashScreen" android:theme="@style/WulkanowyTheme.SplashScreen"
tools:ignore="LockedOrientationActivity"> tools:ignore="LockedOrientationActivity">
@ -76,7 +74,6 @@
<activity <activity
android:name=".ui.modules.timetablewidget.TimetableWidgetConfigureActivity" android:name=".ui.modules.timetablewidget.TimetableWidgetConfigureActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true"
android:noHistory="true" android:noHistory="true"
android:theme="@style/WulkanowyTheme.WidgetAccountSwitcher"> android:theme="@style/WulkanowyTheme.WidgetAccountSwitcher">
<intent-filter> <intent-filter>
@ -86,7 +83,6 @@
<activity <activity
android:name=".ui.modules.luckynumberwidget.LuckyNumberWidgetConfigureActivity" android:name=".ui.modules.luckynumberwidget.LuckyNumberWidgetConfigureActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true"
android:noHistory="true" android:noHistory="true"
android:theme="@style/WulkanowyTheme.WidgetAccountSwitcher"> android:theme="@style/WulkanowyTheme.WidgetAccountSwitcher">
<intent-filter> <intent-filter>
@ -97,23 +93,6 @@
<service <service
android:name=".services.widgets.TimetableWidgetService" android:name=".services.widgets.TimetableWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" /> android:permission="android.permission.BIND_REMOTEVIEWS" />
<service
android:name=".services.piggyback.VulcanNotificationListenerService"
android:exported="true"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
<service
android:name=".services.messaging.AppMessagingService"
android:exported="false"
tools:ignore="MissingClass">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<receiver <receiver
android:name=".ui.modules.timetablewidget.TimetableWidgetProvider" android:name=".ui.modules.timetablewidget.TimetableWidgetProvider"
@ -128,7 +107,6 @@
</receiver> </receiver>
<receiver <receiver
android:name=".ui.modules.luckynumberwidget.LuckyNumberWidgetProvider" android:name=".ui.modules.luckynumberwidget.LuckyNumberWidgetProvider"
android:exported="true"
android:label="@string/lucky_number_title"> android:label="@string/lucky_number_title">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@ -141,9 +119,11 @@
<receiver android:name=".services.alarm.TimetableNotificationReceiver" /> <receiver android:name=".services.alarm.TimetableNotificationReceiver" />
<provider <provider
android:name="androidx.startup.InitializationProvider" android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.androidx-startup" android:authorities="${applicationId}.workmanager-init"
android:exported="false"
tools:node="remove" /> tools:node="remove" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider" android:authorities="${applicationId}.fileprovider"
@ -154,44 +134,44 @@
android:resource="@xml/provider_paths" /> android:resource="@xml/provider_paths" />
</provider> </provider>
<meta-data
android:name="install_channel"
android:value="${install_channel}" />
<!-- workaround for https://github.com/firebase/firebase-android-sdk/issues/473 enabled:false --> <!-- workaround for https://github.com/firebase/firebase-android-sdk/issues/473 enabled:false -->
<!-- https://firebase.googleblog.com/2017/03/take-control-of-your-firebase-init-on.html --> <!-- https://firebase.googleblog.com/2017/03/take-control-of-your-firebase-init-on.html -->
<provider <provider
android:name="com.google.firebase.provider.FirebaseInitProvider" android:name="com.google.firebase.provider.FirebaseInitProvider"
android:authorities="${applicationId}.firebaseinitprovider" android:authorities="${applicationId}.firebaseinitprovider"
android:enabled="${firebase_enabled}" android:enabled="${firebase_enabled}"
android:exported="false" android:exported="false" />
tools:ignore="MissingClass" />
<meta-data
android:name="install_channel"
android:value="${install_channel}" />
<meta-data <meta-data
android:name="firebase_analytics_collection_enabled" android:name="firebase_analytics_collection_enabled"
android:value="${firebase_enabled}" /> android:value="${firebase_enabled}" />
<meta-data <meta-data
android:name="google_analytics_adid_collection_enabled" android:name="google_analytics_adid_collection_enabled"
android:value="${firebase_enabled}" /> android:value="${firebase_enabled}" />
<meta-data <meta-data
android:name="firebase_crashlytics_collection_enabled" android:name="firebase_crashlytics_collection_enabled"
android:value="${firebase_enabled}" /> android:value="${firebase_enabled}" />
<meta-data <meta-data
android:name="firebase_messaging_auto_init_enabled" android:name="firebase_messaging_auto_init_enabled"
android:value="${firebase_enabled}" /> android:value="${firebase_enabled}" />
<meta-data <meta-data
android:name="firebase_inapp_messaging_auto_data_collection_enabled" android:name="firebase_inapp_messaging_auto_data_collection_enabled"
android:value="${firebase_enabled}" /> android:value="${firebase_enabled}" />
<meta-data <meta-data
android:name="com.google.firebase.messaging.default_notification_icon" android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_stat_all" /> android:resource="@drawable/ic_stat_all" />
<meta-data <meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id" android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="push_channel" /> android:value="push_channel" />
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="${admob_project_id}" />
<meta-data
android:name="com.google.android.gms.ads.DELAY_APP_MEASUREMENT_INIT"
android:value="true" />
</application> </application>
</manifest> </manifest>

View file

@ -1,10 +1,12 @@
package io.github.wulkanowy package io.github.wulkanowy
import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import android.util.Log.DEBUG import android.util.Log.DEBUG
import android.util.Log.INFO import android.util.Log.INFO
import android.util.Log.VERBOSE import android.util.Log.VERBOSE
import android.webkit.WebView import android.webkit.WebView
import androidx.fragment.app.FragmentManager
import androidx.hilt.work.HiltWorkerFactory import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration import androidx.work.Configuration
import com.yariksoffice.lingver.Lingver import com.yariksoffice.lingver.Lingver
@ -39,8 +41,10 @@ class WulkanowyApp : Application(), Configuration.Provider {
@Inject @Inject
lateinit var analyticsHelper: AnalyticsHelper lateinit var analyticsHelper: AnalyticsHelper
@SuppressLint("UnsafeOptInUsageWarning")
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
FragmentManager.enableNewStateManager(false)
initializeAppLanguage() initializeAppLanguage()
themeManager.applyDefaultTheme() themeManager.applyDefaultTheme()
initLogging() initLogging()

View file

@ -2,100 +2,62 @@ package io.github.wulkanowy.data
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.AssetManager
import android.content.res.Resources
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.chuckerteam.chucker.api.ChuckerCollector import com.chuckerteam.chucker.api.ChuckerCollector
import com.chuckerteam.chucker.api.ChuckerInterceptor import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.chuckerteam.chucker.api.RetentionManager import com.chuckerteam.chucker.api.RetentionManager
import com.squareup.moshi.Moshi
import com.fredporciuncula.flow.preferences.FlowSharedPreferences import com.fredporciuncula.flow.preferences.FlowSharedPreferences
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent 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.AppDatabase
import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.AppInfo
import kotlinx.coroutines.ExperimentalCoroutinesApi 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 timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
internal class DataModule { internal class RepositoryModule {
@Singleton @Singleton
@Provides @Provides
fun provideSdk(chuckerInterceptor: ChuckerInterceptor) = fun provideSdk(chuckerCollector: ChuckerCollector, @ApplicationContext context: Context): Sdk {
Sdk().apply { return Sdk().apply {
androidVersion = android.os.Build.VERSION.RELEASE androidVersion = android.os.Build.VERSION.RELEASE
buildTag = android.os.Build.MODEL buildTag = android.os.Build.MODEL
setSimpleHttpLogger { Timber.d(it) } setSimpleHttpLogger { Timber.d(it) }
// for debug only // for debug only
addInterceptor(chuckerInterceptor, network = true) addInterceptor(
ChuckerInterceptor.Builder(context)
.collector(chuckerCollector)
.alwaysReadResponseBody(true)
.build(), network = true
)
} }
}
@Singleton @Singleton
@Provides @Provides
fun provideChuckerCollector( fun provideChuckerCollector(
@ApplicationContext context: Context, @ApplicationContext context: Context,
prefRepository: PreferencesRepository prefRepository: PreferencesRepository
) = ChuckerCollector( ): ChuckerCollector {
context = context, return ChuckerCollector(
showNotification = prefRepository.isDebugNotificationEnable, context = context,
retentionPeriod = RetentionManager.Period.ONE_HOUR 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 @Singleton
@Provides @Provides
@ -105,6 +67,14 @@ internal class DataModule {
appInfo: AppInfo appInfo: AppInfo
) = AppDatabase.newInstance(context, sharedPrefProvider, 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 @Singleton
@Provides @Provides
fun provideSharedPref(@ApplicationContext context: Context): SharedPreferences = fun provideSharedPref(@ApplicationContext context: Context): SharedPreferences =
@ -118,9 +88,7 @@ internal class DataModule {
@Singleton @Singleton
@Provides @Provides
fun provideJson() = Json { fun provideMoshi() = Moshi.Builder().build()
ignoreUnknownKeys = true
}
@Singleton @Singleton
@Provides @Provides
@ -234,12 +202,4 @@ internal class DataModule {
@Singleton @Singleton
@Provides @Provides
fun provideSchoolAnnouncementDao(database: AppDatabase) = database.schoolAnnouncementDao fun provideSchoolAnnouncementDao(database: AppDatabase) = database.schoolAnnouncementDao
@Singleton
@Provides
fun provideNotificationDao(database: AppDatabase) = database.notificationDao
@Singleton
@Provides
fun provideAdminMessageDao(database: AppDatabase) = database.adminMessagesDao
} }

View file

@ -1,12 +0,0 @@
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<AdminMessage>
}

View file

@ -6,11 +6,11 @@ import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.RoomDatabase.JournalMode.TRUNCATE import androidx.room.RoomDatabase.JournalMode.TRUNCATE
import androidx.room.TypeConverters 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.AttendanceDao
import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao
import io.github.wulkanowy.data.db.dao.CompletedLessonsDao import io.github.wulkanowy.data.db.dao.CompletedLessonsDao
import io.github.wulkanowy.data.db.dao.ConferenceDao 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.ExamDao
import io.github.wulkanowy.data.db.dao.GradeDao import io.github.wulkanowy.data.db.dao.GradeDao
import io.github.wulkanowy.data.db.dao.GradePartialStatisticsDao import io.github.wulkanowy.data.db.dao.GradePartialStatisticsDao
@ -23,10 +23,8 @@ import io.github.wulkanowy.data.db.dao.MessageAttachmentDao
import io.github.wulkanowy.data.db.dao.MessagesDao import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.dao.MobileDeviceDao import io.github.wulkanowy.data.db.dao.MobileDeviceDao
import io.github.wulkanowy.data.db.dao.NoteDao 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.RecipientDao
import io.github.wulkanowy.data.db.dao.ReportingUnitDao 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.SchoolDao
import io.github.wulkanowy.data.db.dao.SemesterDao import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao import io.github.wulkanowy.data.db.dao.StudentDao
@ -36,11 +34,11 @@ import io.github.wulkanowy.data.db.dao.TeacherDao
import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao
import io.github.wulkanowy.data.db.dao.TimetableDao import io.github.wulkanowy.data.db.dao.TimetableDao
import io.github.wulkanowy.data.db.dao.TimetableHeaderDao 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.Attendance
import io.github.wulkanowy.data.db.entities.AttendanceSummary import io.github.wulkanowy.data.db.entities.AttendanceSummary
import io.github.wulkanowy.data.db.entities.CompletedLesson import io.github.wulkanowy.data.db.entities.CompletedLesson
import io.github.wulkanowy.data.db.entities.Conference 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.Exam
import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradePartialStatistics import io.github.wulkanowy.data.db.entities.GradePartialStatistics
@ -53,11 +51,9 @@ import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageAttachment import io.github.wulkanowy.data.db.entities.MessageAttachment
import io.github.wulkanowy.data.db.entities.MobileDevice import io.github.wulkanowy.data.db.entities.MobileDevice
import io.github.wulkanowy.data.db.entities.Note 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.Recipient
import io.github.wulkanowy.data.db.entities.ReportingUnit import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.github.wulkanowy.data.db.entities.School 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.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentInfo import io.github.wulkanowy.data.db.entities.StudentInfo
@ -99,11 +95,6 @@ import io.github.wulkanowy.data.db.migrations.Migration37
import io.github.wulkanowy.data.db.migrations.Migration38 import io.github.wulkanowy.data.db.migrations.Migration38
import io.github.wulkanowy.data.db.migrations.Migration39 import io.github.wulkanowy.data.db.migrations.Migration39
import io.github.wulkanowy.data.db.migrations.Migration4 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.Migration44
import io.github.wulkanowy.data.db.migrations.Migration5 import io.github.wulkanowy.data.db.migrations.Migration5
import io.github.wulkanowy.data.db.migrations.Migration6 import io.github.wulkanowy.data.db.migrations.Migration6
import io.github.wulkanowy.data.db.migrations.Migration7 import io.github.wulkanowy.data.db.migrations.Migration7
@ -143,8 +134,6 @@ import javax.inject.Singleton
StudentInfo::class, StudentInfo::class,
TimetableHeader::class, TimetableHeader::class,
SchoolAnnouncement::class, SchoolAnnouncement::class,
Notification::class,
AdminMessage::class
], ],
version = AppDatabase.VERSION_SCHEMA, version = AppDatabase.VERSION_SCHEMA,
exportSchema = true exportSchema = true
@ -153,7 +142,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
companion object { companion object {
const val VERSION_SCHEMA = 44 const val VERSION_SCHEMA = 39
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(), Migration2(),
@ -194,11 +183,6 @@ abstract class AppDatabase : RoomDatabase() {
Migration37(), Migration37(),
Migration38(), Migration38(),
Migration39(), Migration39(),
Migration40(),
Migration41(sharedPrefProvider),
Migration42(),
Migration43(),
Migration44()
) )
fun newInstance( fun newInstance(
@ -268,8 +252,4 @@ abstract class AppDatabase : RoomDatabase() {
abstract val timetableHeaderDao: TimetableHeaderDao abstract val timetableHeaderDao: TimetableHeaderDao
abstract val schoolAnnouncementDao: SchoolAnnouncementDao abstract val schoolAnnouncementDao: SchoolAnnouncementDao
abstract val notificationDao: NotificationDao
abstract val adminMessagesDao: AdminMessageDao
} }

View file

@ -1,10 +1,9 @@
package io.github.wulkanowy.data.db package io.github.wulkanowy.data.db
import androidx.room.TypeConverter import androidx.room.TypeConverter
import kotlinx.serialization.SerializationException import com.squareup.moshi.Moshi
import kotlinx.serialization.decodeFromString import com.squareup.moshi.Types
import kotlinx.serialization.encodeToString import io.github.wulkanowy.data.db.adapters.PairAdapterFactory
import kotlinx.serialization.json.Json
import java.time.Instant import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
@ -14,7 +13,15 @@ import java.util.Date
class Converters { class Converters {
private val json = Json private val moshi by lazy { Moshi.Builder().add(PairAdapterFactory).build() }
private val integerListAdapter by lazy {
moshi.adapter<List<Int>>(Types.newParameterizedType(List::class.java, Integer::class.java))
}
private val stringListPairAdapter by lazy {
moshi.adapter<List<Pair<String, String>>>(Types.newParameterizedType(List::class.java, Pair::class.java, String::class.java, String::class.java))
}
@TypeConverter @TypeConverter
fun timestampToDate(value: Long?): LocalDate? = value?.run { fun timestampToDate(value: Long?): LocalDate? = value?.run {
@ -44,25 +51,21 @@ class Converters {
@TypeConverter @TypeConverter
fun intListToJson(list: List<Int>): String { fun intListToJson(list: List<Int>): String {
return json.encodeToString(list) return integerListAdapter.toJson(list)
} }
@TypeConverter @TypeConverter
fun jsonToIntList(value: String): List<Int> { fun jsonToIntList(value: String): List<Int> {
return json.decodeFromString(value) return integerListAdapter.fromJson(value).orEmpty()
} }
@TypeConverter @TypeConverter
fun stringPairListToJson(list: List<Pair<String, String>>): String { fun stringPairListToJson(list: List<Pair<String, String>>): String {
return json.encodeToString(list) return stringListPairAdapter.toJson(list)
} }
@TypeConverter @TypeConverter
fun jsonToStringPairList(value: String): List<Pair<String, String>> { fun jsonToStringPairList(value: String): List<Pair<String, String>> {
return try { return stringListPairAdapter.fromJson(value).orEmpty()
json.decodeFromString(value)
} catch (e: SerializationException) {
emptyList() // handle errors from old gson Pair serialized data
}
} }
} }

View file

@ -22,14 +22,11 @@ class SharedPrefProvider @Inject constructor(
fun getString(key: String) = sharedPref.getString(key, null) fun getString(key: String) = sharedPref.getString(key, null)
fun getString(key: String, defaultValue: String): String = fun getString(key: String, defaultValue: String): String = sharedPref.getString(key, defaultValue) ?: defaultValue
sharedPref.getString(key, defaultValue) ?: defaultValue
fun getBoolean(key: String, defaultValue: Boolean): Boolean = fun getBoolean(key: String, defaultValue: Boolean): Boolean = sharedPref.getBoolean(key, defaultValue)
sharedPref.getBoolean(key, defaultValue)
fun putBoolean(key: String, value: Boolean, sync: Boolean = false) = fun putBoolean(key: String, value: Boolean, sync: Boolean = false) = sharedPref.edit(sync) { putBoolean(key, value) }
sharedPref.edit(sync) { putBoolean(key, value) }
fun putString(key: String, value: String?, sync: Boolean = false) { fun putString(key: String, value: String?, sync: Boolean = false) {
sharedPref.edit(sync) { putString(key, value) } sharedPref.edit(sync) { putString(key, value) }

View file

@ -0,0 +1,68 @@
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<out Annotation>, 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<List<Map<String, String>>>(listType)
val mapType = Types.newParameterizedType(MutableMap::class.java, String::class.java, String::class.java)
val mapAdapter = moshi.adapter<Map<String, String>>(mapType)
return PairAdapter(listAdapter, mapAdapter)
}
private class PairAdapter(
private val listAdapter: JsonAdapter<List<Map<String, String>>>,
private val mapAdapter: JsonAdapter<Map<String, String>>,
) : JsonAdapter<List<Pair<String, String>>>() {
override fun toJson(writer: JsonWriter, value: List<Pair<String, String>>?) {
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<Pair<String, String>>? {
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<Pair<String, String>>? {
val map = mapAdapter.fromJson(reader) ?: return null
return map.entries.map {
it.key to it.value
}
}
private fun deserializeGsonPair(reader: JsonReader): List<Pair<String, String>>? {
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()
}
}
}
}

View file

@ -1,25 +0,0 @@
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<AdminMessage> {
@Query("SELECT * FROM AdminMessages")
abstract fun loadAll(): Flow<List<AdminMessage>>
@Transaction
open suspend fun removeOldAndSaveNew(
oldMessages: List<AdminMessage>,
newMessages: List<AdminMessage>
) {
deleteAll(oldMessages)
insertAll(newMessages)
}
}

View file

@ -11,11 +11,6 @@ import javax.inject.Singleton
@Dao @Dao
interface AttendanceDao : BaseDao<Attendance> { interface AttendanceDao : BaseDao<Attendance> {
@Query("SELECT * FROM Attendance WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :start AND date <= :end") @Query("SELECT * FROM Attendance WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :from AND date <= :end")
fun loadAll( fun loadAll(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): Flow<List<Attendance>>
diaryId: Int,
studentId: Int,
start: LocalDate,
end: LocalDate
): Flow<List<Attendance>>
} }

View file

@ -1,15 +0,0 @@
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<Notification> {
@Query("SELECT * FROM Notifications WHERE student_id = :studentId OR student_id = -1")
fun loadAll(studentId: Long): Flow<List<Notification>>
}

View file

@ -14,39 +14,33 @@ import javax.inject.Singleton
@Singleton @Singleton
@Dao @Dao
abstract class StudentDao { interface StudentDao {
@Insert(onConflict = ABORT) @Insert(onConflict = ABORT)
abstract suspend fun insertAll(student: List<Student>): List<Long> suspend fun insertAll(student: List<Student>): List<Long>
@Delete @Delete
abstract suspend fun delete(student: Student) suspend fun delete(student: Student)
@Update(entity = Student::class) @Update(entity = Student::class)
abstract suspend fun update(studentNickAndAvatar: StudentNickAndAvatar) suspend fun update(studentNickAndAvatar: StudentNickAndAvatar)
@Query("SELECT * FROM Students WHERE is_current = 1") @Query("SELECT * FROM Students WHERE is_current = 1")
abstract suspend fun loadCurrent(): Student? suspend fun loadCurrent(): Student?
@Query("SELECT * FROM Students WHERE id = :id") @Query("SELECT * FROM Students WHERE id = :id")
abstract suspend fun loadById(id: Long): Student? suspend fun loadById(id: Long): Student?
@Query("SELECT * FROM Students") @Query("SELECT * FROM Students")
abstract suspend fun loadAll(): List<Student> suspend fun loadAll(): List<Student>
@Transaction @Transaction
@Query("SELECT * FROM Students") @Query("SELECT * FROM Students")
abstract suspend fun loadStudentsWithSemesters(): List<StudentWithSemesters> suspend fun loadStudentsWithSemesters(): List<StudentWithSemesters>
@Query("UPDATE Students SET is_current = 1 WHERE id = :id") @Query("UPDATE Students SET is_current = 1 WHERE id = :id")
abstract suspend fun updateCurrent(id: Long) suspend fun updateCurrent(id: Long)
@Query("UPDATE Students SET is_current = 0") @Query("UPDATE Students SET is_current = 0")
abstract suspend fun resetCurrent() suspend fun resetCurrent()
@Transaction
open suspend fun switchCurrent(id: Long) {
resetCurrent()
updateCurrent(id)
}
} }

View file

@ -1,40 +0,0 @@
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,
@ColumnInfo(name = "is_dismissible")
val isDismissible: Boolean = false
)

View file

@ -47,7 +47,4 @@ data class Attendance(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
var id: Long = 0 var id: Long = 0
@ColumnInfo(name = "is_notified")
var isNotified: Boolean = true
} }

View file

@ -40,7 +40,4 @@ data class Homework(
@ColumnInfo(name = "is_notified") @ColumnInfo(name = "is_notified")
var isNotified: Boolean = true var isNotified: Boolean = true
@ColumnInfo(name = "is_added_by_user")
var isAddedByUser: Boolean = false
} }

View file

@ -1,27 +0,0 @@
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
}

View file

@ -3,9 +3,10 @@ package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.squareup.moshi.JsonClass
import java.io.Serializable import java.io.Serializable
@kotlinx.serialization.Serializable @JsonClass(generateAdapter = true)
@Entity(tableName = "Recipients") @Entity(tableName = "Recipients")
data class Recipient( data class Recipient(

View file

@ -50,7 +50,4 @@ data class Timetable(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
var id: Long = 0 var id: Long = 0
@ColumnInfo(name = "is_notified")
var isNotified: Boolean = true
} }

View file

@ -1,23 +0,0 @@
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
)
"""
)
}
}

View file

@ -1,21 +0,0 @@
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(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")
}
}

View file

@ -1,24 +0,0 @@
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`))"""
)
}
}

View file

@ -1,12 +0,0 @@
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")
}
}

View file

@ -1,11 +0,0 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration44 : Migration(43, 44) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE AdminMessages ADD COLUMN is_dismissible INTEGER NOT NULL DEFAULT 0")
}
}

View file

@ -1,8 +1,8 @@
package io.github.wulkanowy.data.pojos package io.github.wulkanowy.data.pojos
import kotlinx.serialization.Serializable import com.squareup.moshi.JsonClass
@Serializable @JsonClass(generateAdapter = true)
class Contributor( class Contributor(
val displayName: String, val displayName: String,
val githubUsername: String val githubUsername: String

View file

@ -1,9 +1,9 @@
package io.github.wulkanowy.data.pojos package io.github.wulkanowy.data.pojos
import com.squareup.moshi.JsonClass
import io.github.wulkanowy.ui.modules.message.send.RecipientChipItem import io.github.wulkanowy.ui.modules.message.send.RecipientChipItem
import kotlinx.serialization.Serializable
@Serializable @JsonClass(generateAdapter = true)
data class MessageDraft( data class MessageDraft(
val recipients: List<RecipientChipItem>, val recipients: List<RecipientChipItem>,
val subject: String, val subject: String,

View file

@ -0,0 +1,36 @@
package io.github.wulkanowy.data.pojos
import androidx.annotation.DrawableRes
import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
import io.github.wulkanowy.services.sync.notifications.NotificationType
import io.github.wulkanowy.ui.modules.main.MainView
sealed interface Notification {
val type: NotificationType
val startMenu: MainView.Section
val icon: Int
val titleStringRes: Int
val contentStringRes: Int
}
data class MultipleNotifications(
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<String>,
) : Notification
data class OneNotification(
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<String>,
) : Notification

View file

@ -1,19 +0,0 @@
package io.github.wulkanowy.data.pojos
import android.content.Intent
import io.github.wulkanowy.services.sync.notifications.NotificationType
data class NotificationData(
val intentToStart: Intent,
val title: String,
val content: String
)
data class GroupNotificationData(
val notificationDataList: List<NotificationData>,
val title: String,
val content: String,
val intentToStart: Intent,
val type: NotificationType
)

View file

@ -1,52 +0,0 @@
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.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(cacheKey) || 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 }
}
)
}

View file

@ -1,27 +1,25 @@
package io.github.wulkanowy.data.repositories package io.github.wulkanowy.data.repositories
import android.content.Context import android.content.res.AssetManager
import dagger.hilt.android.qualifiers.ApplicationContext import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import io.github.wulkanowy.data.pojos.Contributor import io.github.wulkanowy.data.pojos.Contributor
import io.github.wulkanowy.utils.DispatchersProvider import io.github.wulkanowy.utils.DispatchersProvider
import kotlinx.coroutines.withContext 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.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class AppCreatorRepository @Inject constructor( class AppCreatorRepository @Inject constructor(
@ApplicationContext private val context: Context, private val assets: AssetManager,
private val dispatchers: DispatchersProvider, private val dispatchers: DispatchersProvider
private val json: Json,
) { ) {
@OptIn(ExperimentalSerializationApi::class)
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
suspend fun getAppCreators() = withContext(dispatchers.io) { suspend fun getAppCreators() = withContext(dispatchers.backgroundThread) {
val inputStream = context.assets.open("contributors.json").buffered() val moshi = Moshi.Builder().build()
json.decodeFromStream<List<Contributor>>(inputStream) val type = Types.newParameterizedType(List::class.java, Contributor::class.java)
val adapter = moshi.adapter<List<Contributor>>(type)
adapter.fromJson(assets.open("contributors.json").bufferedReader().use { it.readText() })
} }
} }

View file

@ -14,7 +14,6 @@ import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
@ -33,24 +32,10 @@ class AttendanceRepository @Inject constructor(
private val cacheKey = "attendance" private val cacheKey = "attendance"
fun getAttendance( fun getAttendance(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource(
student: Student,
semester: Semester,
start: LocalDate,
end: LocalDate,
forceRefresh: Boolean,
notify: Boolean = false,
) = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
shouldFetch = { shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) },
val isExpired = refreshHelper.shouldBeRefreshed( query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday) },
key = getRefreshKey(cacheKey, semester, start, end)
)
it.isEmpty() || forceRefresh || isExpired
},
query = {
attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday)
},
fetch = { fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
.getAttendance(start.monday, end.sunday, semester.semesterId) .getAttendance(start.monday, end.sunday, semester.semesterId)
@ -58,39 +43,19 @@ class AttendanceRepository @Inject constructor(
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
attendanceDb.deleteAll(old uniqueSubtract new) attendanceDb.deleteAll(old uniqueSubtract new)
val attendanceToAdd = (new uniqueSubtract old).map { newAttendance -> attendanceDb.insertAll(new uniqueSubtract old)
newAttendance.apply { if (notify) isNotified = false }
}
attendanceDb.insertAll(attendanceToAdd)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
}, },
filterResult = { it.filter { item -> item.date in start..end } } filterResult = { it.filter { item -> item.date in start..end } }
) )
fun getAttendanceFromDatabase( suspend fun excuseForAbsence(student: Student, semester: Semester, absenceList: List<Attendance>, reason: String? = null) {
semester: Semester, sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).excuseForAbsence(absenceList.map { attendance ->
start: LocalDate,
end: LocalDate
): Flow<List<Attendance>> {
return attendanceDb.loadAll(semester.diaryId, semester.studentId, start, end)
}
suspend fun updateTimetable(timetable: List<Attendance>) {
return attendanceDb.updateAll(timetable)
}
suspend fun excuseForAbsence(
student: Student, semester: Semester,
absenceList: List<Attendance>, reason: String? = null
) {
val items = absenceList.map { attendance ->
Absent( Absent(
date = LocalDateTime.of(attendance.date, LocalTime.of(0, 0)), date = LocalDateTime.of(attendance.date, LocalTime.of(0, 0)),
timeId = attendance.timeId timeId = attendance.timeId
) )
} }, reason)
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
.excuseForAbsence(items, reason)
} }
} }

View file

@ -29,12 +29,12 @@ class AttendanceSummaryRepository @Inject constructor(
student: Student, student: Student,
semester: Semester, semester: Semester,
subjectId: Int, subjectId: Int,
forceRefresh: Boolean, forceRefresh: Boolean
) = networkBoundResource( ) = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
shouldFetch = { shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester)) it.isEmpty() || forceRefresh
it.isEmpty() || forceRefresh || isExpired || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester))
}, },
query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, subjectId) }, query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, subjectId) },
fetch = { fetch = {

View file

@ -28,28 +28,10 @@ class CompletedLessonsRepository @Inject constructor(
private val cacheKey = "completed" private val cacheKey = "completed"
fun getCompletedLessons( fun getCompletedLessons(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource(
student: Student,
semester: Semester,
start: LocalDate,
end: LocalDate,
forceRefresh: Boolean,
) = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
shouldFetch = { shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) },
val isExpired = refreshHelper.shouldBeRefreshed( query = { completedLessonsDb.loadAll(semester.studentId, semester.diaryId, start.monday, end.sunday) },
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 = { fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
.getCompletedLessons(start.monday, end.sunday) .getCompletedLessons(start.monday, end.sunday)

View file

@ -35,12 +35,12 @@ class ConferenceRepository @Inject constructor(
semester: Semester, semester: Semester,
forceRefresh: Boolean, forceRefresh: Boolean,
notify: Boolean = false, notify: Boolean = false,
startDate: LocalDateTime = LocalDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC), startDate: LocalDateTime = LocalDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC)
) = networkBoundResource( ) = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
shouldFetch = { shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester)) it.isEmpty() || forceRefresh
it.isEmpty() || forceRefresh || isExpired || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester))
}, },
query = { query = {
conferenceDb.loadAll(semester.diaryId, student.studentId, startDate) conferenceDb.loadAll(semester.diaryId, student.studentId, startDate)

View file

@ -36,14 +36,14 @@ class ExamRepository @Inject constructor(
start: LocalDate, start: LocalDate,
end: LocalDate, end: LocalDate,
forceRefresh: Boolean, forceRefresh: Boolean,
notify: Boolean = false, notify: Boolean = false
) = networkBoundResource( ) = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
shouldFetch = { shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed( val isShouldBeRefreshed = refreshHelper.isShouldBeRefreshed(
key = getRefreshKey(cacheKey, semester, start, end) key = getRefreshKey(cacheKey, semester, start, end)
) )
it.isEmpty() || forceRefresh || isExpired it.isEmpty() || forceRefresh || isShouldBeRefreshed
}, },
query = { query = {
examDb.loadAll( examDb.loadAll(

View file

@ -37,12 +37,13 @@ class GradeRepository @Inject constructor(
student: Student, student: Student,
semester: Semester, semester: Semester,
forceRefresh: Boolean, forceRefresh: Boolean,
notify: Boolean = false, notify: Boolean = false
) = networkBoundResource( ) = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
shouldFetch = { (details, summaries) -> shouldFetch = { (details, summaries) ->
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester)) val isShouldBeRefreshed =
details.isEmpty() || summaries.isEmpty() || forceRefresh || isExpired refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester))
details.isEmpty() || summaries.isEmpty() || forceRefresh || isShouldBeRefreshed
}, },
query = { query = {
val detailsFlow = gradeDb.loadAll(semester.semesterId, semester.studentId) val detailsFlow = gradeDb.loadAll(semester.semesterId, semester.studentId)
@ -70,8 +71,8 @@ class GradeRepository @Inject constructor(
newDetails: List<Grade>, newDetails: List<Grade>,
notify: Boolean notify: Boolean
) { ) {
val notifyBreakDate = oldGrades.maxByOrNull {it.date } val notifyBreakDate =
?.date ?: student.registrationDate.toLocalDate() oldGrades.maxByOrNull { it.date }?.date ?: student.registrationDate.toLocalDate()
gradeDb.deleteAll(oldGrades uniqueSubtract newDetails) gradeDb.deleteAll(oldGrades uniqueSubtract newDetails)
gradeDb.insertAll((newDetails uniqueSubtract oldGrades).onEach { gradeDb.insertAll((newDetails uniqueSubtract oldGrades).onEach {
if (it.date >= notifyBreakDate) it.apply { if (it.date >= notifyBreakDate) it.apply {
@ -88,7 +89,8 @@ class GradeRepository @Inject constructor(
) { ) {
gradeSummaryDb.deleteAll(oldSummaries uniqueSubtract newSummary) gradeSummaryDb.deleteAll(oldSummaries uniqueSubtract newSummary)
gradeSummaryDb.insertAll((newSummary uniqueSubtract oldSummaries).onEach { summary -> gradeSummaryDb.insertAll((newSummary uniqueSubtract oldSummaries).onEach { summary ->
val oldSummary = oldSummaries.find { old -> old.subject == summary.subject } val oldSummary =
oldSummaries.find { oldSummary -> oldSummary.subject == summary.subject }
summary.isPredictedGradeNotified = when { summary.isPredictedGradeNotified = when {
summary.predictedGrade.isEmpty() -> true summary.predictedGrade.isEmpty() -> true
notify && oldSummary?.predictedGrade != summary.predictedGrade -> false notify && oldSummary?.predictedGrade != summary.predictedGrade -> false

View file

@ -39,19 +39,9 @@ class GradeStatisticsRepository @Inject constructor(
private val semesterCacheKey = "grade_stats_semester" private val semesterCacheKey = "grade_stats_semester"
private val pointsCacheKey = "grade_stats_points" private val pointsCacheKey = "grade_stats_points"
fun getGradesPartialStatistics( fun getGradesPartialStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource(
student: Student,
semester: Semester,
subjectName: String,
forceRefresh: Boolean,
) = networkBoundResource(
mutex = partialMutex, mutex = partialMutex,
shouldFetch = { shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(partialCacheKey, semester)) },
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(partialCacheKey, semester)
)
it.isEmpty() || forceRefresh || isExpired
},
query = { gradePartialStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, query = { gradePartialStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
fetch = { fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
@ -86,19 +76,9 @@ class GradeStatisticsRepository @Inject constructor(
} }
) )
fun getGradesSemesterStatistics( fun getGradesSemesterStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource(
student: Student,
semester: Semester,
subjectName: String,
forceRefresh: Boolean,
) = networkBoundResource(
mutex = semesterMutex, mutex = semesterMutex,
shouldFetch = { shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(semesterCacheKey, semester)) },
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(semesterCacheKey, semester)
)
it.isEmpty() || forceRefresh || isExpired
},
query = { gradeSemesterStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, query = { gradeSemesterStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
fetch = { fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
@ -114,12 +94,10 @@ class GradeStatisticsRepository @Inject constructor(
val itemsWithAverage = items.map { item -> val itemsWithAverage = items.map { item ->
item.copy().apply { item.copy().apply {
val denominator = item.amounts.sum() val denominator = item.amounts.sum()
average = if (denominator == 0) "" else { average = if (denominator == 0) "" else (item.amounts.mapIndexed { gradeValue, amount ->
(item.amounts.mapIndexed { gradeValue, amount -> (gradeValue + 1) * amount
(gradeValue + 1) * amount }.sum().toDouble() / denominator).let {
}.sum().toDouble() / denominator).let { "%.2f".format(Locale.FRANCE, it)
"%.2f".format(Locale.FRANCE, it)
}
} }
} }
} }
@ -131,9 +109,7 @@ class GradeStatisticsRepository @Inject constructor(
amounts = itemsWithAverage.map { it.amounts }.sumGradeAmounts(), amounts = itemsWithAverage.map { it.amounts }.sumGradeAmounts(),
studentGrade = 0 studentGrade = 0
).apply { ).apply {
average = itemsWithAverage.mapNotNull { average = itemsWithAverage.mapNotNull { it.average.replace(",", ".").toDoubleOrNull() }.average().let {
it.average.replace(",", ".").toDoubleOrNull()
}.average().let {
"%.2f".format(Locale.FRANCE, it) "%.2f".format(Locale.FRANCE, it)
} }
}).reversed() }).reversed()
@ -142,17 +118,9 @@ class GradeStatisticsRepository @Inject constructor(
} }
) )
fun getGradesPointsStatistics( fun getGradesPointsStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource(
student: Student,
semester: Semester,
subjectName: String,
forceRefresh: Boolean,
) = networkBoundResource(
mutex = pointsMutex, mutex = pointsMutex,
shouldFetch = { shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(pointsCacheKey, semester)) },
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(pointsCacheKey, semester))
it.isEmpty() || forceRefresh || isExpired
},
query = { gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, query = { gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
fetch = { fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)

View file

@ -30,19 +30,16 @@ class HomeworkRepository @Inject constructor(
private val cacheKey = "homework" private val cacheKey = "homework"
fun getHomework( fun getHomework(
student: Student, student: Student, semester: Semester,
semester: Semester, start: LocalDate, end: LocalDate,
start: LocalDate, forceRefresh: Boolean, notify: Boolean = false
end: LocalDate,
forceRefresh: Boolean,
notify: Boolean = false,
) = networkBoundResource( ) = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
shouldFetch = { shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed( val isShouldBeRefreshed = refreshHelper.isShouldBeRefreshed(
key = getRefreshKey(cacheKey, semester, start, end) key = getRefreshKey(cacheKey, semester, start, end)
) )
it.isEmpty() || forceRefresh || isExpired it.isEmpty() || forceRefresh || isShouldBeRefreshed
}, },
query = { query = {
homeworkDb.loadAll( homeworkDb.loadAll(
@ -61,9 +58,8 @@ class HomeworkRepository @Inject constructor(
val homeWorkToSave = (new uniqueSubtract old).onEach { val homeWorkToSave = (new uniqueSubtract old).onEach {
if (notify) it.isNotified = false if (notify) it.isNotified = false
} }
val filteredOld = old.filterNot { it.isAddedByUser }
homeworkDb.deleteAll(filteredOld uniqueSubtract new) homeworkDb.deleteAll(old uniqueSubtract new)
homeworkDb.insertAll(homeWorkToSave) homeworkDb.insertAll(homeWorkToSave)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
@ -80,8 +76,4 @@ class HomeworkRepository @Inject constructor(
homeworkDb.loadAll(semester.semesterId, semester.studentId, start.monday, end.sunday) homeworkDb.loadAll(semester.semesterId, semester.studentId, start.monday, end.sunday)
suspend fun updateHomework(homework: List<Homework>) = homeworkDb.updateAll(homework) suspend fun updateHomework(homework: List<Homework>) = homeworkDb.updateAll(homework)
suspend fun saveHomework(homework: Homework) = homeworkDb.insertAll(listOf(homework))
suspend fun deleteHomework(homework: Homework) = homeworkDb.deleteAll(listOf(homework))
} }

View file

@ -15,23 +15,24 @@ class LoggerRepository @Inject constructor(
suspend fun getLastLogLines() = getLastModified().readText().split("\n") suspend fun getLastLogLines() = getLastModified().readText().split("\n")
suspend fun getLogFiles() = withContext(dispatchers.io) { suspend fun getLogFiles() = withContext(dispatchers.backgroundThread) {
File(context.filesDir.absolutePath).listFiles(File::isFile) File(context.filesDir.absolutePath).listFiles(File::isFile)?.filter {
?.filter { it.name.endsWith(".log") }!! it.name.endsWith(".log")
}!!
} }
private suspend fun getLastModified() = withContext(dispatchers.io) { private suspend fun getLastModified(): File {
var lastModifiedTime = Long.MIN_VALUE return withContext(dispatchers.backgroundThread) {
var chosenFile: File? = null var lastModifiedTime = Long.MIN_VALUE
var chosenFile: File? = null
File(context.filesDir.absolutePath).listFiles(File::isFile) File(context.filesDir.absolutePath).listFiles(File::isFile)?.forEach { file ->
?.forEach { file ->
if (file.lastModified() > lastModifiedTime) { if (file.lastModified() > lastModifiedTime) {
lastModifiedTime = file.lastModified() lastModifiedTime = file.lastModified()
chosenFile = file chosenFile = file
} }
} }
if (chosenFile == null) throw FileNotFoundException("Log file not found")
chosenFile ?: throw FileNotFoundException("Log file not found") chosenFile!!
}
} }
} }

View file

@ -23,17 +23,11 @@ class LuckyNumberRepository @Inject constructor(
private val saveFetchResultMutex = Mutex() private val saveFetchResultMutex = Mutex()
fun getLuckyNumber( fun getLuckyNumber(student: Student, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource(
student: Student,
forceRefresh: Boolean,
notify: Boolean = false,
) = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
shouldFetch = { it == null || forceRefresh }, shouldFetch = { it == null || forceRefresh },
query = { luckyNumberDb.load(student.studentId, now()) }, query = { luckyNumberDb.load(student.studentId, now()) },
fetch = { fetch = { sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student) },
sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student)
},
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
if (new != old) { if (new != old) {
old?.let { luckyNumberDb.deleteAll(listOfNotNull(it)) } old?.let { luckyNumberDb.deleteAll(listOfNotNull(it)) }
@ -47,11 +41,9 @@ class LuckyNumberRepository @Inject constructor(
fun getLuckyNumberHistory(student: Student, start: LocalDate, end: LocalDate) = fun getLuckyNumberHistory(student: Student, start: LocalDate, end: LocalDate) =
luckyNumberDb.getAll(student.studentId, start, end) luckyNumberDb.getAll(student.studentId, start, end)
suspend fun getNotNotifiedLuckyNumber(student: Student) = suspend fun getNotNotifiedLuckyNumber(student: Student) = luckyNumberDb.load(student.studentId, now()).map {
luckyNumberDb.load(student.studentId, now()).map { if (it?.isNotified == false) it else null
if (it?.isNotified == false) it else null }.first()
}.first()
suspend fun updateLuckyNumber(luckyNumber: LuckyNumber?) = suspend fun updateLuckyNumber(luckyNumber: LuckyNumber?) = luckyNumberDb.updateAll(listOfNotNull(luckyNumber))
luckyNumberDb.updateAll(listOfNotNull(luckyNumber))
} }

View file

@ -1,6 +1,7 @@
package io.github.wulkanowy.data.repositories package io.github.wulkanowy.data.repositories
import android.content.Context import android.content.Context
import com.squareup.moshi.Moshi
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.Resource
@ -17,6 +18,7 @@ import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED
import io.github.wulkanowy.data.mappers.mapFromEntities import io.github.wulkanowy.data.mappers.mapFromEntities
import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.pojos.MessageDraft 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.Sdk
import io.github.wulkanowy.sdk.pojo.Folder import io.github.wulkanowy.sdk.pojo.Folder
import io.github.wulkanowy.sdk.pojo.SentMessage import io.github.wulkanowy.sdk.pojo.SentMessage
@ -27,9 +29,6 @@ import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import timber.log.Timber import timber.log.Timber
import java.time.LocalDateTime.now import java.time.LocalDateTime.now
import javax.inject.Inject import javax.inject.Inject
@ -43,7 +42,7 @@ class MessageRepository @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val refreshHelper: AutoRefreshHelper, private val refreshHelper: AutoRefreshHelper,
private val sharedPrefProvider: SharedPrefProvider, private val sharedPrefProvider: SharedPrefProvider,
private val json: Json, private val moshi: Moshi,
) { ) {
private val saveFetchResultMutex = Mutex() private val saveFetchResultMutex = Mutex()
@ -52,18 +51,14 @@ class MessageRepository @Inject constructor(
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
fun getMessages( fun getMessages(
student: Student, student: Student, semester: Semester,
semester: Semester, folder: MessageFolder, forceRefresh: Boolean, notify: Boolean = false
folder: MessageFolder,
forceRefresh: Boolean,
notify: Boolean = false,
): Flow<Resource<List<Message>>> = networkBoundResource( ): Flow<Resource<List<Message>>> = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
shouldFetch = { shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed( it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(
key = getRefreshKey(cacheKey, student, folder) getRefreshKey(cacheKey, student, folder)
) )
it.isEmpty() || forceRefresh || isExpired
}, },
query = { messagesDb.loadAll(student.id.toInt(), folder.id) }, query = { messagesDb.loadAll(student.id.toInt(), folder.id) },
fetch = { fetch = {
@ -82,8 +77,7 @@ class MessageRepository @Inject constructor(
) )
private fun getMessagesWithReadByChange( private fun getMessagesWithReadByChange(
old: List<Message>, old: List<Message>, new: List<Message>,
new: List<Message>,
setNotified: Boolean setNotified: Boolean
): List<Message> { ): List<Message> {
val oldMeta = old.map { Triple(it, it.readBy, it.unreadBy) } val oldMeta = old.map { Triple(it, it.readBy, it.unreadBy) }
@ -102,9 +96,7 @@ class MessageRepository @Inject constructor(
} }
fun getMessage( fun getMessage(
student: Student, student: Student, message: Message, markAsRead: Boolean = false
message: Message,
markAsRead: Boolean = false,
): Flow<Resource<MessageWithAttachment?>> = networkBoundResource( ): Flow<Resource<MessageWithAttachment?>> = networkBoundResource(
shouldFetch = { shouldFetch = {
checkNotNull(it, { "This message no longer exist!" }) checkNotNull(it, { "This message no longer exist!" })
@ -143,10 +135,8 @@ class MessageRepository @Inject constructor(
} }
suspend fun sendMessage( suspend fun sendMessage(
student: Student, student: Student, subject: String, content: String,
subject: String, recipients: List<Recipient>
content: String,
recipients: List<Recipient>,
): SentMessage = sdk.init(student).sendMessage( ): SentMessage = sdk.init(student).sendMessage(
subject = subject, subject = subject,
content = content, content = content,
@ -169,9 +159,9 @@ class MessageRepository @Inject constructor(
var draftMessage: MessageDraft? var draftMessage: MessageDraft?
get() = sharedPrefProvider.getString(context.getString(R.string.pref_key_message_send_draft)) get() = sharedPrefProvider.getString(context.getString(R.string.pref_key_message_send_draft))
?.let { json.decodeFromString(it) } ?.let { MessageDraftJsonAdapter(moshi).fromJson(it) }
set(value) = sharedPrefProvider.putString( set(value) = sharedPrefProvider.putString(
context.getString(R.string.pref_key_message_send_draft), context.getString(R.string.pref_key_message_send_draft),
value?.let { json.encodeToString(it) } value?.let { MessageDraftJsonAdapter(moshi).toJson(it) }
) )
} }

View file

@ -28,16 +28,9 @@ class MobileDeviceRepository @Inject constructor(
private val cacheKey = "devices" private val cacheKey = "devices"
fun getDevices( fun getDevices(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource(
student: Student,
semester: Semester,
forceRefresh: Boolean,
) = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
shouldFetch = { shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student)) },
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student))
it.isEmpty() || forceRefresh || isExpired
},
query = { mobileDb.loadAll(student.userLoginId.takeIf { it != 0 } ?: student.studentId) }, query = { mobileDb.loadAll(student.userLoginId.takeIf { it != 0 } ?: student.studentId) },
fetch = { fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)

View file

@ -12,6 +12,7 @@ import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -27,19 +28,9 @@ class NoteRepository @Inject constructor(
private val cacheKey = "note" private val cacheKey = "note"
fun getNotes( fun getNotes(student: Student, semester: Semester, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource(
student: Student,
semester: Semester,
forceRefresh: Boolean,
notify: Boolean = false,
) = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
shouldFetch = { shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) },
val isExpired = refreshHelper.shouldBeRefreshed(
getRefreshKey(cacheKey, semester)
)
it.isEmpty() || forceRefresh || isExpired
},
query = { noteDb.loadAll(student.studentId) }, query = { noteDb.loadAll(student.studentId) },
fetch = { fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)

View file

@ -1,17 +0,0 @@
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))
}

View file

@ -5,21 +5,21 @@ import android.content.SharedPreferences
import androidx.core.content.edit import androidx.core.content.edit
import com.fredporciuncula.flow.preferences.FlowSharedPreferences import com.fredporciuncula.flow.preferences.FlowSharedPreferences
import com.fredporciuncula.flow.preferences.Preference 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 dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.sdk.toLocalDate import io.github.wulkanowy.sdk.toLocalDate
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode 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.ui.modules.grade.GradeSortingMode
import io.github.wulkanowy.utils.toTimestamp
import io.github.wulkanowy.utils.toLocalDateTime import io.github.wulkanowy.utils.toLocalDateTime
import io.github.wulkanowy.utils.toTimestamp import io.github.wulkanowy.utils.toTimestamp
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map 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.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import javax.inject.Inject import javax.inject.Inject
@ -28,12 +28,16 @@ import javax.inject.Singleton
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@Singleton @Singleton
class PreferencesRepository @Inject constructor( class PreferencesRepository @Inject constructor(
@ApplicationContext val context: Context,
private val sharedPref: SharedPreferences, private val sharedPref: SharedPreferences,
private val flowSharedPref: FlowSharedPreferences, private val flowSharedPref: FlowSharedPreferences,
private val json: Json, @ApplicationContext val context: Context,
moshi: Moshi
) { ) {
@OptIn(ExperimentalStdlibApi::class)
private val dashboardItemsPositionAdapter: JsonAdapter<Map<DashboardItem.Type, Int>> =
moshi.adapter()
val startMenuIndex: Int val startMenuIndex: Int
get() = getString(R.string.pref_key_start_menu, R.string.pref_default_startup).toInt() get() = getString(R.string.pref_key_start_menu, R.string.pref_default_startup).toInt()
@ -57,13 +61,8 @@ class PreferencesRepository @Inject constructor(
R.bool.pref_default_grade_average_force_calc R.bool.pref_default_grade_average_force_calc
) )
val gradeExpandMode: GradeExpandMode val isGradeExpandable: Boolean
get() = GradeExpandMode.getByValue( get() = !getBoolean(R.string.pref_key_expand_grade, R.bool.pref_default_expand_grade)
getString(
R.string.pref_key_expand_grade_mode,
R.string.pref_default_expand_grade_mode
)
)
val showAllSubjectsOnStatisticsList: Boolean val showAllSubjectsOnStatisticsList: Boolean
get() = getBoolean( get() = getBoolean(
@ -103,31 +102,12 @@ class PreferencesRepository @Inject constructor(
val isUpcomingLessonsNotificationsEnableKey = val isUpcomingLessonsNotificationsEnableKey =
context.getString(R.string.pref_key_notifications_upcoming_lessons_enable) context.getString(R.string.pref_key_notifications_upcoming_lessons_enable)
var isUpcomingLessonsNotificationsEnable: Boolean val isUpcomingLessonsNotificationsEnable: Boolean
set(value) {
sharedPref.edit { putBoolean(isUpcomingLessonsNotificationsEnableKey, value) }
}
get() = getBoolean( get() = getBoolean(
isUpcomingLessonsNotificationsEnableKey, isUpcomingLessonsNotificationsEnableKey,
R.bool.pref_default_notification_upcoming_lessons_enable 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
get() = getBoolean(
R.string.pref_key_notifications_piggyback,
R.bool.pref_default_notification_piggyback
)
val isDebugNotificationEnableKey = context.getString(R.string.pref_key_notification_debug) val isDebugNotificationEnableKey = context.getString(R.string.pref_key_notification_debug)
val isDebugNotificationEnable: Boolean val isDebugNotificationEnable: Boolean
get() = getBoolean(isDebugNotificationEnableKey, R.bool.pref_default_notification_debug) get() = getBoolean(isDebugNotificationEnableKey, R.bool.pref_default_notification_debug)
@ -196,20 +176,22 @@ class PreferencesRepository @Inject constructor(
) )
var lasSyncDate: LocalDateTime var lasSyncDate: LocalDateTime
get() = getLong(R.string.pref_key_last_sync_date, R.string.pref_default_last_sync_date) get() = getLong(
.toLocalDateTime() 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() set(value) = sharedPref.edit().putLong("last_sync_date", value.toTimestamp()).apply()
var dashboardItemsPosition: Map<DashboardItem.Type, Int>? var dashboardItemsPosition: Map<DashboardItem.Type, Int>?
get() { get() {
val value = sharedPref.getString(PREF_KEY_DASHBOARD_ITEMS_POSITION, null) ?: return null val json = sharedPref.getString(PREF_KEY_DASHBOARD_ITEMS_POSITION, null) ?: return null
return json.decodeFromString(value) return dashboardItemsPositionAdapter.fromJson(json)
} }
set(value) = sharedPref.edit { set(value) = sharedPref.edit {
putString( putString(
PREF_KEY_DASHBOARD_ITEMS_POSITION, PREF_KEY_DASHBOARD_ITEMS_POSITION,
json.encodeToString(value) dashboardItemsPositionAdapter.toJson(value)
) )
} }
@ -218,7 +200,6 @@ class PreferencesRepository @Inject constructor(
.map { set -> .map { set ->
set.map { DashboardItem.Tile.valueOf(it) } set.map { DashboardItem.Tile.valueOf(it) }
.plus(DashboardItem.Tile.ACCOUNT) .plus(DashboardItem.Tile.ACCOUNT)
.plus(DashboardItem.Tile.ADMIN_MESSAGE)
.toSet() .toSet()
} }
@ -226,7 +207,6 @@ class PreferencesRepository @Inject constructor(
get() = selectedDashboardTilesPreference.get() get() = selectedDashboardTilesPreference.get()
.map { DashboardItem.Tile.valueOf(it) } .map { DashboardItem.Tile.valueOf(it) }
.plus(DashboardItem.Tile.ACCOUNT) .plus(DashboardItem.Tile.ACCOUNT)
.plus(DashboardItem.Tile.ADMIN_MESSAGE)
.toSet() .toSet()
set(value) { set(value) {
val filteredValue = value.filterNot { it == DashboardItem.Tile.ACCOUNT } val filteredValue = value.filterNot { it == DashboardItem.Tile.ACCOUNT }
@ -245,23 +225,13 @@ class PreferencesRepository @Inject constructor(
return flowSharedPref.getStringSet(prefKey, defaultSet) return flowSharedPref.getStringSet(prefKey, defaultSet)
} }
var dismissedAdminMessageIds: List<Int>
get() = sharedPref.getStringSet(PREF_KEY_ADMIN_DISMISSED_MESSAGE_IDS, emptySet())
.orEmpty()
.map { it.toInt() }
set(value) = sharedPref.edit {
putStringSet(PREF_KEY_ADMIN_DISMISSED_MESSAGE_IDS, value.map { it.toString() }.toSet())
}
var inAppReviewCount: Int var inAppReviewCount: Int
get() = sharedPref.getInt(PREF_KEY_IN_APP_REVIEW_COUNT, 0) get() = sharedPref.getInt(PREF_KEY_IN_APP_REVIEW_COUNT, 0)
set(value) = sharedPref.edit().putInt(PREF_KEY_IN_APP_REVIEW_COUNT, value).apply() set(value) = sharedPref.edit().putInt(PREF_KEY_IN_APP_REVIEW_COUNT, value).apply()
var inAppReviewDate: LocalDate? var inAppReviewDate: LocalDate?
get() = sharedPref.getLong(PREF_KEY_IN_APP_REVIEW_DATE, 0).takeIf { it != 0L } get() = sharedPref.getLong(PREF_KEY_IN_APP_REVIEW_DATE, 0).takeIf { it != 0L }?.toLocalDate()
?.toLocalDate() set(value) = sharedPref.edit().putLong(PREF_KEY_IN_APP_REVIEW_DATE, value!!.toTimestamp()).apply()
set(value) = sharedPref.edit().putLong(PREF_KEY_IN_APP_REVIEW_DATE, value!!.toTimestamp())
.apply()
var isAppReviewDone: Boolean var isAppReviewDone: Boolean
get() = sharedPref.getBoolean(PREF_KEY_IN_APP_REVIEW_DONE, false) get() = sharedPref.getBoolean(PREF_KEY_IN_APP_REVIEW_DONE, false)
@ -282,9 +252,6 @@ class PreferencesRepository @Inject constructor(
private fun getBoolean(id: String, default: Int) = private fun getBoolean(id: String, default: Int) =
sharedPref.getBoolean(id, context.resources.getBoolean(default)) sharedPref.getBoolean(id, context.resources.getBoolean(default))
private fun getBoolean(id: Int, default: Boolean) =
sharedPref.getBoolean(context.getString(id), default)
private companion object { private companion object {
private const val PREF_KEY_DASHBOARD_ITEMS_POSITION = "dashboard_items_position" private const val PREF_KEY_DASHBOARD_ITEMS_POSITION = "dashboard_items_position"
@ -294,7 +261,5 @@ class PreferencesRepository @Inject constructor(
private const val PREF_KEY_IN_APP_REVIEW_DATE = "in_app_review_date" 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" private const val PREF_KEY_IN_APP_REVIEW_DONE = "in_app_review_done"
private const val PREF_KEY_ADMIN_DISMISSED_MESSAGE_IDS = "admin_message_dismissed_ids"
} }
} }

View file

@ -7,8 +7,6 @@ import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk 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.init
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import javax.inject.Inject import javax.inject.Inject
@ -17,34 +15,26 @@ import javax.inject.Singleton
@Singleton @Singleton
class RecipientRepository @Inject constructor( class RecipientRepository @Inject constructor(
private val recipientDb: RecipientDao, 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) { suspend fun refreshRecipients(student: Student, unit: ReportingUnit, role: Int) {
val new = sdk.init(student).getRecipients(unit.unitId, role).mapToEntities(unit.studentId) val new = sdk.init(student).getRecipients(unit.unitId, role).mapToEntities(unit.studentId)
val old = recipientDb.loadAll(unit.studentId, unit.unitId, role) val old = recipientDb.loadAll(unit.studentId, unit.unitId, role)
recipientDb.deleteAll(old uniqueSubtract new) recipientDb.deleteAll(old uniqueSubtract new)
recipientDb.insertAll(new uniqueSubtract old) recipientDb.insertAll(new uniqueSubtract old)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
} }
suspend fun getRecipients(student: Student, unit: ReportingUnit, role: Int): List<Recipient> { suspend fun getRecipients(student: Student, unit: ReportingUnit, role: Int): List<Recipient> {
val cached = recipientDb.loadAll(unit.studentId, unit.unitId, role) return recipientDb.loadAll(unit.studentId, unit.unitId, role).ifEmpty {
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student))
return if (cached.isEmpty() || isExpired) {
refreshRecipients(student, unit, role) refreshRecipients(student, unit, role)
recipientDb.loadAll(unit.studentId, unit.unitId, role) recipientDb.loadAll(unit.studentId, unit.unitId, role)
} else cached }
} }
suspend fun getMessageRecipients(student: Student, message: Message): List<Recipient> { suspend fun getMessageRecipients(student: Student, message: Message): List<Recipient> {
return sdk.init(student).getMessageRecipients(message.messageId, message.senderId) return sdk.init(student).getMessageRecipients(message.messageId, message.senderId).mapToEntities(student.studentId)
.mapToEntities(student.studentId)
} }
} }

View file

@ -11,7 +11,7 @@ class RecoverRepository @Inject constructor(private val sdk: Sdk) {
return sdk.getPasswordResetCaptchaCode(host, symbol) return sdk.getPasswordResetCaptchaCode(host, symbol)
} }
suspend fun sendRecoverRequest( suspend fun sendRecoverRequest(url: String, symbol: String, email: String, reCaptchaResponse: String): String {
url: String, symbol: String, email: String, reCaptchaResponse: String return sdk.sendPasswordResetRequest(url, symbol, email, reCaptchaResponse)
): String = sdk.sendPasswordResetRequest(url, symbol, email, reCaptchaResponse) }
} }

View file

@ -2,6 +2,7 @@ package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement 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.Student
import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
@ -11,6 +12,7 @@ import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -28,15 +30,17 @@ class SchoolAnnouncementRepository @Inject constructor(
fun getSchoolAnnouncements( fun getSchoolAnnouncements(
student: Student, student: Student,
forceRefresh: Boolean, notify: Boolean = false forceRefresh: Boolean,
notify: Boolean = false
) = networkBoundResource( ) = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
shouldFetch = { shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student)) it.isEmpty() || forceRefresh
it.isEmpty() || forceRefresh || isExpired || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student))
}, },
query = { query = {
schoolAnnouncementDb.loadAll(student.studentId) schoolAnnouncementDb.loadAll(
student.studentId)
}, },
fetch = { fetch = {
sdk.init(student) sdk.init(student)
@ -53,11 +57,9 @@ class SchoolAnnouncementRepository @Inject constructor(
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
} }
) )
fun getSchoolAnnouncementFromDatabase(student: Student): Flow<List<SchoolAnnouncement>> { fun getSchoolAnnouncementFromDatabase(student: Student): Flow<List<SchoolAnnouncement>> {
return schoolAnnouncementDb.loadAll(student.studentId) return schoolAnnouncementDb.loadAll(student.studentId)
} }
suspend fun updateSchoolAnnouncement(schoolAnnouncement: List<SchoolAnnouncement>) = suspend fun updateSchoolAnnouncement(schoolAnnouncement: List<SchoolAnnouncement>) = schoolAnnouncementDb.updateAll(schoolAnnouncement)
schoolAnnouncementDb.updateAll(schoolAnnouncement)
} }

View file

@ -5,8 +5,6 @@ import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntity import io.github.wulkanowy.data.mappers.mapToEntity
import io.github.wulkanowy.sdk.Sdk 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.init
import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.networkBoundResource
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
@ -16,41 +14,29 @@ import javax.inject.Singleton
@Singleton @Singleton
class SchoolRepository @Inject constructor( class SchoolRepository @Inject constructor(
private val schoolDb: SchoolDao, private val schoolDb: SchoolDao,
private val sdk: Sdk, private val sdk: Sdk
private val refreshHelper: AutoRefreshHelper,
) { ) {
private val saveFetchResultMutex = Mutex() private val saveFetchResultMutex = Mutex()
private val cacheKey = "school_info" fun getSchoolInfo(student: Student, semester: Semester, forceRefresh: Boolean) =
networkBoundResource(
fun getSchoolInfo( mutex = saveFetchResultMutex,
student: Student, shouldFetch = { it == null || forceRefresh },
semester: Semester, query = { schoolDb.load(semester.studentId, semester.classId) },
forceRefresh: Boolean, fetch = {
) = networkBoundResource( sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).getSchool()
mutex = saveFetchResultMutex, .mapToEntity(semester)
shouldFetch = { },
val isExpired = refreshHelper.shouldBeRefreshed( saveFetchResult = { old, new ->
key = getRefreshKey(cacheKey, student) if (old != null && new != old) {
) with(schoolDb) {
it == null || forceRefresh || isExpired deleteAll(listOf(old))
}, insertAll(listOf(new))
query = { schoolDb.load(semester.studentId, semester.classId) }, }
fetch = { } else if (old == null) {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).getSchool() schoolDb.insertAll(listOf(new))
.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)) )
}
)
} }

View file

@ -26,7 +26,7 @@ class SemesterRepository @Inject constructor(
student: Student, student: Student,
forceRefresh: Boolean = false, forceRefresh: Boolean = false,
refreshOnNoCurrent: Boolean = false refreshOnNoCurrent: Boolean = false
) = withContext(dispatchers.io) { ) = withContext(dispatchers.backgroundThread) {
val semesters = semesterDb.loadAll(student.studentId, student.classId) val semesters = semesterDb.loadAll(student.studentId, student.classId)
if (isShouldFetch(student, semesters, forceRefresh, refreshOnNoCurrent)) { if (isShouldFetch(student, semesters, forceRefresh, refreshOnNoCurrent)) {
@ -64,7 +64,7 @@ class SemesterRepository @Inject constructor(
} }
suspend fun getCurrentSemester(student: Student, forceRefresh: Boolean = false) = suspend fun getCurrentSemester(student: Student, forceRefresh: Boolean = false) =
withContext(dispatchers.io) { withContext(dispatchers.backgroundThread) {
getSemesters(student, forceRefresh).getCurrentOrLast() getSemesters(student, forceRefresh).getCurrentOrLast()
} }
} }

View file

@ -19,27 +19,24 @@ class StudentInfoRepository @Inject constructor(
private val saveFetchResultMutex = Mutex() private val saveFetchResultMutex = Mutex()
fun getStudentInfo( fun getStudentInfo(student: Student, semester: Semester, forceRefresh: Boolean) =
student: Student, networkBoundResource(
semester: Semester, mutex = saveFetchResultMutex,
forceRefresh: Boolean, shouldFetch = { it == null || forceRefresh },
) = networkBoundResource( query = { studentInfoDao.loadStudentInfo(student.studentId) },
mutex = saveFetchResultMutex, fetch = {
shouldFetch = { it == null || forceRefresh }, sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
query = { studentInfoDao.loadStudentInfo(student.studentId) }, .getStudentInfo().mapToEntity(semester)
fetch = { },
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) saveFetchResult = { old, new ->
.getStudentInfo().mapToEntity(semester) if (old != null && new != old) {
}, with(studentInfoDao) {
saveFetchResult = { old, new -> deleteAll(listOf(old))
if (old != null && new != old) { insertAll(listOf(new))
with(studentInfoDao) { }
deleteAll(listOf(old)) } else if (old == null) {
insertAll(listOf(new)) studentInfoDao.insertAll(listOf(new))
} }
} else if (old == null) {
studentInfoDao.insertAll(listOf(new))
} }
} )
)
} }

View file

@ -1,9 +1,7 @@
package io.github.wulkanowy.data.repositories package io.github.wulkanowy.data.repositories
import android.content.Context import android.content.Context
import androidx.room.withTransaction
import dagger.hilt.android.qualifiers.ApplicationContext 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.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
@ -27,8 +25,7 @@ class StudentRepository @Inject constructor(
private val studentDb: StudentDao, private val studentDb: StudentDao,
private val semesterDb: SemesterDao, private val semesterDb: SemesterDao,
private val sdk: Sdk, private val sdk: Sdk,
private val appInfo: AppInfo, private val appInfo: AppInfo
private val appDatabase: AppDatabase
) { ) {
suspend fun isStudentSaved() = getSavedStudents(false).isNotEmpty() suspend fun isStudentSaved() = getSavedStudents(false).isNotEmpty()
@ -66,7 +63,7 @@ class StudentRepository @Inject constructor(
.map { .map {
it.apply { it.apply {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) {
student.password = withContext(dispatchers.io) { student.password = withContext(dispatchers.backgroundThread) {
decrypt(student.password) decrypt(student.password)
} }
} }
@ -77,7 +74,7 @@ class StudentRepository @Inject constructor(
val student = studentDb.loadById(id) ?: throw NoCurrentStudentException() 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.API) {
student.password = withContext(dispatchers.io) { student.password = withContext(dispatchers.backgroundThread) {
decrypt(student.password) decrypt(student.password)
} }
} }
@ -88,40 +85,35 @@ class StudentRepository @Inject constructor(
val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException() 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.API) {
student.password = withContext(dispatchers.io) { student.password = withContext(dispatchers.backgroundThread) {
decrypt(student.password) decrypt(student.password)
} }
} }
return student return student
} }
suspend fun saveStudents(studentsWithSemesters: List<StudentWithSemesters>) { suspend fun saveStudents(studentsWithSemesters: List<StudentWithSemesters>): List<Long> {
val semesters = studentsWithSemesters.flatMap { it.semesters } val semesters = studentsWithSemesters.flatMap { it.semesters }
val students = studentsWithSemesters.map { it.student } val students = studentsWithSemesters.map { it.student }
.map { .map {
it.apply { it.apply {
if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.API) { if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.API) {
password = withContext(dispatchers.io) { password = withContext(dispatchers.backgroundThread) {
encrypt(password, context) encrypt(password, context)
} }
} }
} }
} }
.mapIndexed { index, student ->
if (index == 0) {
student.copy(isCurrent = true).apply { avatarColor = student.avatarColor }
} else student
}
appDatabase.withTransaction { semesterDb.insertSemesters(semesters)
studentDb.resetCurrent() return studentDb.insertAll(students)
semesterDb.insertSemesters(semesters)
studentDb.insertAll(students)
}
} }
suspend fun switchStudent(studentWithSemesters: StudentWithSemesters) { suspend fun switchStudent(studentWithSemesters: StudentWithSemesters) {
studentDb.switchCurrent(studentWithSemesters.student.id) with(studentDb) {
resetCurrent()
updateCurrent(studentWithSemesters.student.id)
}
} }
suspend fun logoutStudent(student: Student) = studentDb.delete(student) suspend fun logoutStudent(student: Student) = studentDb.delete(student)

View file

@ -5,8 +5,6 @@ import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk 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.init
import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
@ -17,24 +15,14 @@ import javax.inject.Singleton
@Singleton @Singleton
class SubjectRepository @Inject constructor( class SubjectRepository @Inject constructor(
private val subjectDao: SubjectDao, private val subjectDao: SubjectDao,
private val sdk: Sdk, private val sdk: Sdk
private val refreshHelper: AutoRefreshHelper,
) { ) {
private val saveFetchResultMutex = Mutex() private val saveFetchResultMutex = Mutex()
private val cacheKey = "subjects" fun getSubjects(student: Student, semester: Semester, forceRefresh: Boolean = false) = networkBoundResource(
fun getSubjects(
student: Student,
semester: Semester,
forceRefresh: Boolean = false,
) = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
shouldFetch = { shouldFetch = { it.isEmpty() || forceRefresh },
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester))
it.isEmpty() || forceRefresh || isExpired
},
query = { subjectDao.loadAll(semester.diaryId, semester.studentId) }, query = { subjectDao.loadAll(semester.diaryId, semester.studentId) },
fetch = { fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
@ -43,8 +31,6 @@ class SubjectRepository @Inject constructor(
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
subjectDao.deleteAll(old uniqueSubtract new) subjectDao.deleteAll(old uniqueSubtract new)
subjectDao.insertAll(new uniqueSubtract old) subjectDao.insertAll(new uniqueSubtract old)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
} }
) )
} }

View file

@ -5,8 +5,6 @@ import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk 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.init
import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
@ -17,24 +15,14 @@ import javax.inject.Singleton
@Singleton @Singleton
class TeacherRepository @Inject constructor( class TeacherRepository @Inject constructor(
private val teacherDb: TeacherDao, private val teacherDb: TeacherDao,
private val sdk: Sdk, private val sdk: Sdk
private val refreshHelper: AutoRefreshHelper,
) { ) {
private val saveFetchResultMutex = Mutex() private val saveFetchResultMutex = Mutex()
private val cacheKey = "teachers" fun getTeachers(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource(
fun getTeachers(
student: Student,
semester: Semester,
forceRefresh: Boolean,
) = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
shouldFetch = { shouldFetch = { it.isEmpty() || forceRefresh },
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester))
it.isEmpty() || forceRefresh || isExpired
},
query = { teacherDb.loadAll(semester.studentId, semester.classId) }, query = { teacherDb.loadAll(semester.studentId, semester.classId) },
fetch = { fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear) sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
@ -44,8 +32,6 @@ class TeacherRepository @Inject constructor(
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
teacherDb.deleteAll(old uniqueSubtract new) teacherDb.deleteAll(old uniqueSubtract new)
teacherDb.insertAll(new uniqueSubtract old) teacherDb.insertAll(new uniqueSubtract old)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
} }
) )
} }

View file

@ -41,23 +41,18 @@ class TimetableRepository @Inject constructor(
private val cacheKey = "timetable" private val cacheKey = "timetable"
fun getTimetable( fun getTimetable(
student: Student, student: Student, semester: Semester, start: LocalDate, end: LocalDate,
semester: Semester, forceRefresh: Boolean, refreshAdditional: Boolean = false
start: LocalDate,
end: LocalDate,
forceRefresh: Boolean,
refreshAdditional: Boolean = false,
notify: Boolean = false
) = networkBoundResource( ) = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
shouldFetch = { (timetable, additional, headers) -> shouldFetch = { (timetable, additional, headers) ->
val refreshKey = getRefreshKey(cacheKey, semester, start, end) val refreshKey = getRefreshKey(cacheKey, semester, start, end)
val isExpired = refreshHelper.shouldBeRefreshed(refreshKey) val isShouldRefresh = refreshHelper.isShouldBeRefreshed(refreshKey)
val isRefreshAdditional = additional.isEmpty() && refreshAdditional val isRefreshAdditional = additional.isEmpty() && refreshAdditional
val isNoData = timetable.isEmpty() || isRefreshAdditional || headers.isEmpty() val isNoData = timetable.isEmpty() || isRefreshAdditional || headers.isEmpty()
isNoData || forceRefresh || isExpired isNoData || forceRefresh || isShouldRefresh
}, },
query = { getFullTimetableFromDatabase(student, semester, start, end) }, query = { getFullTimetableFromDatabase(student, semester, start, end) },
fetch = { fetch = {
@ -68,7 +63,7 @@ class TimetableRepository @Inject constructor(
timetableFull.mapToEntities(semester) timetableFull.mapToEntities(semester)
}, },
saveFetchResult = { timetableOld, timetableNew -> saveFetchResult = { timetableOld, timetableNew ->
refreshTimetable(student, timetableOld.lessons, timetableNew.lessons, notify) refreshTimetable(student, timetableOld.lessons, timetableNew.lessons)
refreshAdditional(timetableOld.additional, timetableNew.additional) refreshAdditional(timetableOld.additional, timetableNew.additional)
refreshDayHeaders(timetableOld.headers, timetableNew.headers) refreshDayHeaders(timetableOld.headers, timetableNew.headers)
@ -84,10 +79,8 @@ class TimetableRepository @Inject constructor(
) )
private fun getFullTimetableFromDatabase( private fun getFullTimetableFromDatabase(
student: Student, student: Student, semester: Semester,
semester: Semester, start: LocalDate, end: LocalDate
start: LocalDate,
end: LocalDate,
): Flow<TimetableFull> { ): Flow<TimetableFull> {
val timetableFlow = timetableDb.loadAll( val timetableFlow = timetableDb.loadAll(
diaryId = semester.diaryId, diaryId = semester.diaryId,
@ -118,27 +111,21 @@ class TimetableRepository @Inject constructor(
} }
} }
fun getTimetableFromDatabase(
semester: Semester,
from: LocalDate,
end: LocalDate
): Flow<List<Timetable>> {
return timetableDb.loadAll(semester.diaryId, semester.studentId, from, end)
}
suspend fun updateTimetable(timetable: List<Timetable>) {
return timetableDb.updateAll(timetable)
}
private suspend fun refreshTimetable( private suspend fun refreshTimetable(
student: Student, student: Student,
lessonsOld: List<Timetable>, lessonsOld: List<Timetable>, lessonsNew: List<Timetable>
lessonsNew: List<Timetable>,
notify: Boolean
) { ) {
val lessonsToRemove = lessonsOld uniqueSubtract lessonsNew val lessonsToRemove = lessonsOld uniqueSubtract lessonsNew
val lessonsToAdd = (lessonsNew uniqueSubtract lessonsOld).map { new -> val lessonsToAdd = (lessonsNew uniqueSubtract lessonsOld).map { new ->
new.apply { if (notify) isNotified = false } 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
} }
timetableDb.deleteAll(lessonsToRemove) timetableDb.deleteAll(lessonsToRemove)

View file

@ -15,7 +15,6 @@ import dagger.multibindings.IntoSet
import io.github.wulkanowy.services.sync.channels.Channel import io.github.wulkanowy.services.sync.channels.Channel
import io.github.wulkanowy.services.sync.channels.DebugChannel import io.github.wulkanowy.services.sync.channels.DebugChannel
import io.github.wulkanowy.services.sync.channels.LuckyNumberChannel 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.NewConferencesChannel
import io.github.wulkanowy.services.sync.channels.NewExamChannel import io.github.wulkanowy.services.sync.channels.NewExamChannel
import io.github.wulkanowy.services.sync.channels.NewGradesChannel import io.github.wulkanowy.services.sync.channels.NewGradesChannel
@ -24,7 +23,6 @@ import io.github.wulkanowy.services.sync.channels.NewMessagesChannel
import io.github.wulkanowy.services.sync.channels.NewNotesChannel import io.github.wulkanowy.services.sync.channels.NewNotesChannel
import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel
import io.github.wulkanowy.services.sync.channels.PushChannel 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.channels.UpcomingLessonsChannel
import io.github.wulkanowy.services.sync.works.AttendanceSummaryWork import io.github.wulkanowy.services.sync.works.AttendanceSummaryWork
import io.github.wulkanowy.services.sync.works.AttendanceWork import io.github.wulkanowy.services.sync.works.AttendanceWork
@ -169,12 +167,4 @@ abstract class ServicesModule {
@Binds @Binds
@IntoSet @IntoSet
abstract fun provideUpcomingLessonsChannel(channel: UpcomingLessonsChannel): Channel abstract fun provideUpcomingLessonsChannel(channel: UpcomingLessonsChannel): Channel
@Binds
@IntoSet
abstract fun provideChangeTimetableChannel(channel: TimetableChangeChannel): Channel
@Binds
@IntoSet
abstract fun provideNewAttendanceChannel(channel: NewAttendanceChannel): Channel
} }

View file

@ -1,6 +1,7 @@
package io.github.wulkanowy.services.alarm package io.github.wulkanowy.services.alarm
import android.app.PendingIntent import android.app.PendingIntent
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
@ -10,13 +11,11 @@ import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.services.HiltBroadcastReceiver import io.github.wulkanowy.services.HiltBroadcastReceiver
import io.github.wulkanowy.services.sync.channels.UpcomingLessonsChannel.Companion.CHANNEL_ID 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.splash.SplashActivity import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.PendingIntentCompat
import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.flowWithResource
import io.github.wulkanowy.utils.getCompatColor import io.github.wulkanowy.utils.getCompatColor
import io.github.wulkanowy.utils.toLocalDateTime import io.github.wulkanowy.utils.toLocalDateTime
@ -33,15 +32,12 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() {
@Inject @Inject
lateinit var studentRepository: StudentRepository lateinit var studentRepository: StudentRepository
@Inject
lateinit var preferencesRepository: PreferencesRepository
companion object { companion object {
const val NOTIFICATION_TYPE_CURRENT = 1 const val NOTIFICATION_TYPE_CURRENT = 1
const val NOTIFICATION_TYPE_UPCOMING = 2 const val NOTIFICATION_TYPE_UPCOMING = 2
const val NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION = 3 const val NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION = 3
const val NOTIFICATION_ID = 2137 const val NOTIFICATION_ID = "id"
const val STUDENT_NAME = "student_name" const val STUDENT_NAME = "student_name"
const val STUDENT_ID = "student_id" const val STUDENT_ID = "student_id"
@ -71,10 +67,10 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() {
private fun prepareNotification(context: Context, intent: Intent) { private fun prepareNotification(context: Context, intent: Intent) {
val type = intent.getIntExtra(LESSON_TYPE, 0) val type = intent.getIntExtra(LESSON_TYPE, 0)
val isPersistent = preferencesRepository.isUpcomingLessonsNotificationsPersistent val notificationId = intent.getIntExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id)
if (type == NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION) { if (type == NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION) {
return NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID) return NotificationManagerCompat.from(context).cancel(notificationId)
} }
val studentId = intent.getIntExtra(STUDENT_ID, 0) val studentId = intent.getIntExtra(STUDENT_ID, 0)
@ -91,58 +87,33 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() {
Timber.d("TimetableNotification receive: type: $type, subject: $subject, start: ${start.toLocalDateTime()}, student: $studentId") Timber.d("TimetableNotification receive: type: $type, subject: $subject, start: ${start.toLocalDateTime()}, student: $studentId")
showNotification( showNotification(context, notificationId, studentName,
context, isPersistent, studentName,
if (type == NOTIFICATION_TYPE_CURRENT) end else start, end - start, if (type == NOTIFICATION_TYPE_CURRENT) end else start, end - start,
context.getString( context.getString(if (type == NOTIFICATION_TYPE_CURRENT) R.string.timetable_now else R.string.timetable_next, "($room) $subject".removePrefix("()")),
if (type == NOTIFICATION_TYPE_CURRENT) R.string.timetable_now else R.string.timetable_next, nextSubject?.let { context.getString(R.string.timetable_later, "($nextRoom) $nextSubject".removePrefix("()")) }
"($room) $subject".removePrefix("()")
),
nextSubject?.let {
context.getString(
R.string.timetable_later,
"($nextRoom) $nextSubject".removePrefix("()")
)
}
) )
} }
private fun showNotification( private fun showNotification(context: Context, notificationId: Int, studentName: String?, countDown: Long, timeout: Long, title: String, next: String?) {
context: Context, NotificationManagerCompat.from(context).notify(notificationId, NotificationCompat.Builder(context, CHANNEL_ID)
isPersistent: Boolean, .setContentTitle(title)
studentName: String?, .setContentText(next)
countDown: Long, .setAutoCancel(false)
timeout: Long, .setOngoing(true)
title: String, .setWhen(countDown)
next: String? .apply {
) { if (Build.VERSION.SDK_INT >= N) setUsesChronometer(true)
NotificationManagerCompat.from(context) }
.notify(NOTIFICATION_ID, NotificationCompat.Builder(context, CHANNEL_ID) .setTimeoutAfter(timeout)
.setContentTitle(title) .setSmallIcon(R.drawable.ic_stat_timetable)
.setContentText(next) .setColor(context.getCompatColor(R.color.colorPrimary))
.setAutoCancel(false) .setStyle(NotificationCompat.InboxStyle().also {
.setWhen(countDown) it.setSummaryText(studentName)
.setOngoing(isPersistent) it.addLine(next)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) })
.apply { .setContentIntent(PendingIntent.getActivity(context, MainView.Section.TIMETABLE.id,
if (Build.VERSION.SDK_INT >= N) setUsesChronometer(true) MainActivity.getStartIntent(context, MainView.Section.TIMETABLE, true), FLAG_UPDATE_CURRENT))
} .build()
.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,
NOTIFICATION_ID,
SplashActivity.getStartIntent(context, Destination.Timetable()),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
)
)
.build()
)
} }
} }

View file

@ -3,9 +3,9 @@ package io.github.wulkanowy.services.alarm
import android.app.AlarmManager import android.app.AlarmManager
import android.app.AlarmManager.RTC_WAKEUP import android.app.AlarmManager.RTC_WAKEUP
import android.app.PendingIntent import android.app.PendingIntent
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import androidx.core.app.AlarmManagerCompat import androidx.core.app.AlarmManagerCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
@ -25,13 +25,12 @@ 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.NOTIFICATION_TYPE_UPCOMING
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.STUDENT_ID import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.STUDENT_ID
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.STUDENT_NAME 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.DispatchersProvider
import io.github.wulkanowy.utils.PendingIntentCompat
import io.github.wulkanowy.utils.nickOrName import io.github.wulkanowy.utils.nickOrName
import io.github.wulkanowy.utils.toTimestamp import io.github.wulkanowy.utils.toTimestamp
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.LocalDateTime.now import java.time.LocalDateTime.now
import javax.inject.Inject import javax.inject.Inject
@ -54,17 +53,14 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
suspend fun cancelScheduled(lessons: List<Timetable>, student: Student) { suspend fun cancelScheduled(lessons: List<Timetable>, student: Student) {
val studentId = student.studentId val studentId = student.studentId
withContext(dispatchersProvider.io) { withContext(dispatchersProvider.backgroundThread) {
lessons.sortedBy { it.start }.forEachIndexed { index, lesson -> lessons.sortedBy { it.start }.forEachIndexed { index, lesson ->
val upcomingTime = getUpcomingLessonTime(index, lessons, lesson) val upcomingTime = getUpcomingLessonTime(index, lessons, lesson)
cancelScheduledTo( cancelScheduledTo(
range = upcomingTime..lesson.start, upcomingTime..lesson.start,
requestCode = getRequestCode(upcomingTime, studentId) 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") Timber.d("TimetableNotification canceled: type 1 & 2, subject: ${lesson.subject}, start: ${lesson.start}, student: $studentId")
} }
@ -73,37 +69,20 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
private fun cancelScheduledTo(range: ClosedRange<LocalDateTime>, requestCode: Int) { private fun cancelScheduledTo(range: ClosedRange<LocalDateTime>, requestCode: Int) {
if (now() in range) cancelNotification() if (now() in range) cancelNotification()
alarmManager.cancel( alarmManager.cancel(
PendingIntent.getBroadcast( PendingIntent.getBroadcast(context, requestCode, Intent(), FLAG_UPDATE_CURRENT)
context,
requestCode,
Intent(),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
)
) )
} }
fun cancelNotification() = fun cancelNotification() =
NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID) NotificationManagerCompat.from(context).cancel(MainView.Section.TIMETABLE.id)
suspend fun scheduleNotifications(lessons: List<Timetable>, student: Student) { suspend fun scheduleNotifications(lessons: List<Timetable>, student: Student) {
if (!preferencesRepository.isUpcomingLessonsNotificationsEnable) { if (!preferencesRepository.isUpcomingLessonsNotificationsEnable) {
return cancelScheduled(lessons, student) return cancelScheduled(lessons, student)
} }
if (!canScheduleExactAlarms()) { withContext(dispatchersProvider.backgroundThread) {
Timber.w("Exact alarms are disabled by user")
preferencesRepository.isUpcomingLessonsNotificationsEnable = false
return
}
if (lessons.firstOrNull()?.date?.isAfter(LocalDate.now().plusDays(2)) == true) {
Timber.d("Timetable notification scheduling skipped - lessons are too far")
return
}
withContext(dispatchersProvider.io) {
lessons.groupBy { it.date } lessons.groupBy { it.date }
.map { it.value.sortedBy { lesson -> lesson.start } } .map { it.value.sortedBy { lesson -> lesson.start } }
.map { it.filter { lesson -> lesson.isStudentPlan } } .map { it.filter { lesson -> lesson.isStudentPlan } }
@ -117,26 +96,26 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
if (lesson.start > now()) { if (lesson.start > now()) {
scheduleBroadcast( scheduleBroadcast(
intent = intent, intent,
studentId = student.studentId, student.studentId,
notificationType = NOTIFICATION_TYPE_UPCOMING, NOTIFICATION_TYPE_UPCOMING,
time = getUpcomingLessonTime(index, active, lesson) getUpcomingLessonTime(index, active, lesson)
) )
} }
if (lesson.end > now()) { if (lesson.end > now()) {
scheduleBroadcast( scheduleBroadcast(
intent = intent, intent,
studentId = student.studentId, student.studentId,
notificationType = NOTIFICATION_TYPE_CURRENT, NOTIFICATION_TYPE_CURRENT,
time = lesson.start lesson.start
) )
if (active.lastIndex == index) { if (active.lastIndex == index) {
scheduleBroadcast( scheduleBroadcast(
intent = intent, intent,
studentId = student.studentId, student.studentId,
notificationType = NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION, NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION,
time = lesson.end lesson.end
) )
} }
} }
@ -164,30 +143,17 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
notificationType: Int, notificationType: Int,
time: LocalDateTime time: LocalDateTime
) { ) {
try { AlarmManagerCompat.setExactAndAllowWhileIdle(
AlarmManagerCompat.setExactAndAllowWhileIdle( alarmManager, RTC_WAKEUP, time.toTimestamp(),
alarmManager, RTC_WAKEUP, time.toTimestamp(), PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also {
PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also { it.putExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id)
it.putExtra(LESSON_TYPE, notificationType) it.putExtra(LESSON_TYPE, notificationType)
}, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE) }, FLAG_UPDATE_CURRENT)
) )
Timber.d( Timber.d(
"TimetableNotification scheduled: type: $notificationType, subject: ${ "TimetableNotification scheduled: type: $notificationType, subject: ${
intent.getStringExtra(LESSON_TITLE) intent.getStringExtra(LESSON_TITLE)
}, start: $time, student: $studentId" }, start: $time, student: $studentId"
) )
} catch (e: Throwable) {
Timber.e(e)
}
}
fun canScheduleExactAlarms(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
try {
alarmManager.canScheduleExactAlarms()
} catch (e: Throwable) {
false
}
} else true
} }
} }

View file

@ -1,24 +0,0 @@
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()
}
}
}

View file

@ -1,90 +0,0 @@
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.splash.SplashActivity
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(SplashActivity.getStartIntent(context)
.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(SplashActivity.getStartIntent(context)
.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(SplashActivity.getStartIntent(context)
.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(SplashActivity.getStartIntent(context)
.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"
}
}

View file

@ -57,24 +57,18 @@ class SyncManager @Inject constructor(
fun startPeriodicSyncWorker(restart: Boolean = false) { fun startPeriodicSyncWorker(restart: Boolean = false) {
if (preferencesRepository.isServiceEnabled && !now().isHolidays) { if (preferencesRepository.isServiceEnabled && !now().isHolidays) {
val serviceInterval = preferencesRepository.servicesInterval workManager.enqueueUniquePeriodicWork(SyncWorker::class.java.simpleName, if (restart) REPLACE else KEEP,
PeriodicWorkRequestBuilder<SyncWorker>(preferencesRepository.servicesInterval, MINUTES)
workManager.enqueueUniquePeriodicWork(
SyncWorker::class.java.simpleName, if (restart) REPLACE else KEEP,
PeriodicWorkRequestBuilder<SyncWorker>(serviceInterval, MINUTES)
.setInitialDelay(10, MINUTES) .setInitialDelay(10, MINUTES)
.setBackoffCriteria(EXPONENTIAL, 30, MINUTES) .setBackoffCriteria(EXPONENTIAL, 30, MINUTES)
.setConstraints( .setConstraints(Constraints.Builder()
Constraints.Builder() .setRequiredNetworkType(if (preferencesRepository.isServicesOnlyWifi) UNMETERED else CONNECTED)
.setRequiredNetworkType(if (preferencesRepository.isServicesOnlyWifi) UNMETERED else CONNECTED) .build())
.build() .build())
)
.build()
)
} }
} }
fun startOneTimeSyncWorker(): Flow<WorkInfo?> { fun startOneTimeSyncWorker(): Flow<WorkInfo> {
val work = OneTimeWorkRequestBuilder<SyncWorker>() val work = OneTimeWorkRequestBuilder<SyncWorker>()
.setInputData( .setInputData(
Data.Builder() Data.Builder()
@ -83,11 +77,7 @@ class SyncManager @Inject constructor(
) )
.build() .build()
workManager.enqueueUniqueWork( workManager.enqueueUniqueWork("${SyncWorker::class.java.simpleName}_one_time", ExistingWorkPolicy.REPLACE, work)
"${SyncWorker::class.java.simpleName}_one_time",
ExistingWorkPolicy.REPLACE,
work
)
return workManager.getWorkInfoByIdLiveData(work.id).asFlow() return workManager.getWorkInfoByIdLiveData(work.id).asFlow()
} }

View file

@ -19,11 +19,11 @@ import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException
import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException
import io.github.wulkanowy.services.sync.channels.DebugChannel import io.github.wulkanowy.services.sync.channels.DebugChannel
import io.github.wulkanowy.services.sync.works.Work import io.github.wulkanowy.services.sync.works.Work
import io.github.wulkanowy.utils.DispatchersProvider
import io.github.wulkanowy.utils.getCompatColor import io.github.wulkanowy.utils.getCompatColor
import kotlinx.coroutines.withContext import kotlinx.coroutines.coroutineScope
import timber.log.Timber import timber.log.Timber
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneId
import kotlin.random.Random import kotlin.random.Random
@HiltWorker @HiltWorker
@ -34,14 +34,13 @@ class SyncWorker @AssistedInject constructor(
private val semesterRepository: SemesterRepository, private val semesterRepository: SemesterRepository,
private val works: Set<@JvmSuppressWildcards Work>, private val works: Set<@JvmSuppressWildcards Work>,
private val preferencesRepository: PreferencesRepository, private val preferencesRepository: PreferencesRepository,
private val notificationManager: NotificationManagerCompat, private val notificationManager: NotificationManagerCompat
private val dispatchersProvider: DispatchersProvider
) : CoroutineWorker(appContext, workerParameters) { ) : CoroutineWorker(appContext, workerParameters) {
override suspend fun doWork() = withContext(dispatchersProvider.io) { override suspend fun doWork() = coroutineScope {
Timber.i("SyncWorker is starting") Timber.i("SyncWorker is starting")
if (!studentRepository.isCurrentStudentSet()) return@withContext Result.failure() if (!studentRepository.isCurrentStudentSet()) return@coroutineScope Result.failure()
val student = studentRepository.getCurrentStudent() val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student, true) val semester = semesterRepository.getCurrentSemester(student, true)
@ -51,12 +50,12 @@ class SyncWorker @AssistedInject constructor(
Timber.i("${work::class.java.simpleName} is starting") Timber.i("${work::class.java.simpleName} is starting")
work.doWork(student, semester) work.doWork(student, semester)
Timber.i("${work::class.java.simpleName} result: Success") Timber.i("${work::class.java.simpleName} result: Success")
preferencesRepository.lasSyncDate = LocalDateTime.now(ZoneId.systemDefault())
null null
} catch (e: Throwable) { } catch (e: Throwable) {
Timber.w("${work::class.java.simpleName} result: An exception ${e.message} occurred") Timber.w("${work::class.java.simpleName} result: An exception ${e.message} occurred")
if (e is FeatureDisabledException || e is FeatureNotAvailableException) { if (e is FeatureDisabledException || e is FeatureNotAvailableException) null
null else {
} else {
Timber.e(e) Timber.e(e)
e e
} }
@ -71,16 +70,13 @@ class SyncWorker @AssistedInject constructor(
) )
} }
exceptions.isNotEmpty() -> Result.retry() exceptions.isNotEmpty() -> Result.retry()
else -> { else -> Result.success()
preferencesRepository.lasSyncDate = LocalDateTime.now()
Result.success()
}
} }
if (preferencesRepository.isDebugNotificationEnable) notify(result) if (preferencesRepository.isDebugNotificationEnable) notify(result)
Timber.i("SyncWorker result: $result") Timber.i("SyncWorker result: $result")
return@withContext result result
} }
private fun notify(result: Result) { private fun notify(result: Result) {

View file

@ -1,36 +0,0 @@
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
})
}
}

View file

@ -1,36 +0,0 @@
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
})
}
}

View file

@ -1,180 +0,0 @@
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.GroupNotificationData
import io.github.wulkanowy.data.pojos.NotificationData
import io.github.wulkanowy.data.repositories.NotificationRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.utils.PendingIntentCompat
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 studentRepository: StudentRepository,
private val notificationRepository: NotificationRepository
) {
@SuppressLint("InlinedApi")
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)
.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 or PendingIntentCompat.FLAG_IMMUTABLE
)
)
.setContentTitle(notificationData.title)
.setContentText(notificationData.content)
.setStyle(
NotificationCompat.BigTextStyle()
.bigText(notificationData.content)
.also { builder ->
if (shouldShowStudentName()) {
builder.setSummaryText(student.nickOrName)
}
}
)
.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 or PendingIntentCompat.FLAG_IMMUTABLE
)
)
.setContentTitle(notificationData.title)
.setContentText(notificationData.content)
.setStyle(
NotificationCompat.BigTextStyle()
.bigText(notificationData.content)
.also { builder ->
if (shouldShowStudentName()) {
builder.setSummaryText(student.nickOrName)
}
}
)
.setGroup(group)
.build()
notificationManager.notify(Random.nextInt(), notification)
saveNotification(notificationData, groupNotificationData.type, student)
}
}
private suspend 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()
.also { builder ->
if (shouldShowStudentName()) {
builder.setSummaryText(student.nickOrName)
}
groupNotificationData.notificationDataList.forEach {
builder.addLine(it.content)
}
}
)
.setContentIntent(
PendingIntent.getActivity(
context,
Random.nextInt(),
groupNotificationData.intentToStart,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
)
)
.setLocalOnly(true)
.setGroup(group)
.setGroupSummary(true)
.build()
val groupId = student.id * 100 + groupNotificationData.type.ordinal
notificationManager.notify(groupId.toInt(), summaryNotification)
}
private suspend fun saveNotification(
notificationData: NotificationData,
notificationType: NotificationType,
student: Student
) {
val notificationEntity = Notification(
studentId = student.id,
title = notificationData.title,
content = notificationData.content,
type = notificationType,
date = LocalDateTime.now()
)
notificationRepository.saveNotification(notificationEntity)
}
private suspend fun shouldShowStudentName(): Boolean =
studentRepository.getSavedStudents(decryptPass = false).size > 1
}

View file

@ -0,0 +1,102 @@
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)
}
}

View file

@ -1,124 +0,0 @@
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.splash.SplashActivity
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<Timetable>, 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 = SplashActivity.getStartIntent(
context = context,
destination = Destination.Timetable(date)
)
)
}
}
.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 = SplashActivity.getStartIntent(context, Destination.Timetable()),
type = NotificationType.CHANGE_TIMETABLE
)
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
}
private fun getNotificationContents(date: LocalDate, lessons: List<Timetable>): List<String> {
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)
}
}
}
}
}
}

View file

@ -1,55 +0,0 @@
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.splash.SplashActivity
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<Attendance>, 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 = SplashActivity.getStartIntent(context, Destination.Attendance)
)
}
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 = SplashActivity.getStartIntent(context, Destination.Attendance),
type = NotificationType.NEW_ATTENDANCE
)
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
}
}

View file

@ -1,52 +1,38 @@
package io.github.wulkanowy.services.sync.notifications package io.github.wulkanowy.services.sync.notifications
import android.content.Context import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import io.github.wulkanowy.utils.getPlural
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalDateTime import java.time.LocalDateTime
import javax.inject.Inject import javax.inject.Inject
class NewConferenceNotification @Inject constructor( class NewConferenceNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager, @ApplicationContext private val context: Context,
@ApplicationContext private val context: Context notificationManager: NotificationManagerCompat,
) { ) : BaseNotification(context, notificationManager) {
suspend fun notify(items: List<Conference>, student: Student) { fun notify(items: List<Conference>, student: Student) {
val today = LocalDateTime.now() val today = LocalDateTime.now()
val lines = items.filter { !it.date.isBefore(today) } val lines = items.filter { !it.date.isBefore(today) }.map {
.map { "${it.date.toFormattedString("dd.MM")} - ${it.title}: ${it.subject}"
"${it.date.toFormattedString("dd.MM")} - ${it.title}: ${it.subject}" }.ifEmpty { return }
}
.ifEmpty { return }
val notificationDataList = lines.map { val notification = MultipleNotifications(
NotificationData( type = NotificationType.NEW_CONFERENCE,
title = context.getPlural(R.plurals.conference_notify_new_item_title, 1), icon = R.drawable.ic_more_conferences,
content = it, titleStringRes = R.plurals.conference_notify_new_item_title,
intentToStart = SplashActivity.getStartIntent(context, Destination.Conference) contentStringRes = R.plurals.conference_notify_new_items,
) summaryStringRes = R.plurals.conference_number_item,
} startMenu = MainView.Section.CONFERENCE,
lines = lines
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 = SplashActivity.getStartIntent(context, Destination.Conference),
type = NotificationType.NEW_CONFERENCE
) )
appNotificationManager.sendMultipleNotifications(groupNotificationData, student) sendNotification(notification, student)
} }
} }

View file

@ -1,52 +1,38 @@
package io.github.wulkanowy.services.sync.notifications package io.github.wulkanowy.services.sync.notifications
import android.content.Context import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import io.github.wulkanowy.utils.getPlural
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalDate import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject
class NewExamNotification @Inject constructor( class NewExamNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager, @ApplicationContext private val context: Context,
@ApplicationContext private val context: Context notificationManager: NotificationManagerCompat,
) { ) : BaseNotification(context, notificationManager) {
suspend fun notify(items: List<Exam>, student: Student) { fun notify(items: List<Exam>, student: Student) {
val today = LocalDate.now() val today = LocalDate.now()
val lines = items.filter { !it.date.isBefore(today) } val lines = items.filter { !it.date.isBefore(today) }.map {
.map { "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.description}"
"${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.description}" }.ifEmpty { return }
}
.ifEmpty { return }
val notificationDataList = lines.map { val notification = MultipleNotifications(
NotificationData( type = NotificationType.NEW_EXAM,
title = context.getPlural(R.plurals.exam_notify_new_item_title, 1), icon = R.drawable.ic_main_exam,
content = it, titleStringRes = R.plurals.exam_notify_new_item_title,
intentToStart = SplashActivity.getStartIntent(context, Destination.Exam), contentStringRes = R.plurals.exam_notify_new_item_content,
) summaryStringRes = R.plurals.exam_number_item,
} startMenu = MainView.Section.EXAM,
lines = lines
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 = SplashActivity.getStartIntent(context, Destination.Exam),
type = NotificationType.NEW_EXAM
) )
appNotificationManager.sendMultipleNotifications(groupNotificationData, student) sendNotification(notification, student)
} }
} }

View file

@ -1,88 +1,66 @@
package io.github.wulkanowy.services.sync.notifications package io.github.wulkanowy.services.sync.notifications
import android.content.Context import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import io.github.wulkanowy.utils.getPlural
import javax.inject.Inject import javax.inject.Inject
class NewGradeNotification @Inject constructor( class NewGradeNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager, @ApplicationContext private val context: Context,
@ApplicationContext private val context: Context notificationManager: NotificationManagerCompat,
) { ) : BaseNotification(context, notificationManager) {
suspend fun notifyDetails(items: List<Grade>, student: Student) { fun notifyDetails(items: List<Grade>, student: Student) {
val notificationDataList = items.map { val notification = MultipleNotifications(
NotificationData( type = NotificationType.NEW_GRADE_DETAILS,
title = context.getPlural(R.plurals.grade_new_items, 1), icon = R.drawable.ic_stat_grade,
content = "${it.subject}: ${it.entry}", titleStringRes = R.plurals.grade_new_items,
intentToStart = SplashActivity.getStartIntent(context, Destination.Grade), contentStringRes = R.plurals.grade_notify_new_items,
) summaryStringRes = R.plurals.grade_number_item,
} startMenu = MainView.Section.GRADE,
lines = items.map {
val groupNotificationData = GroupNotificationData( "${it.subject}: ${it.entry}"
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 = SplashActivity.getStartIntent(context, Destination.Grade),
type = NotificationType.NEW_GRADE_DETAILS
) )
appNotificationManager.sendMultipleNotifications(groupNotificationData, student) sendNotification(notification, student)
} }
suspend fun notifyPredicted(items: List<GradeSummary>, student: Student) { fun notifyPredicted(items: List<GradeSummary>, student: Student) {
val notificationDataList = items.map { val notification = MultipleNotifications(
NotificationData( type = NotificationType.NEW_GRADE_PREDICTED,
title = context.getPlural(R.plurals.grade_new_items_predicted, 1), icon = R.drawable.ic_stat_grade,
content = "${it.subject}: ${it.predictedGrade}", titleStringRes = R.plurals.grade_new_items_predicted,
intentToStart = SplashActivity.getStartIntent(context, Destination.Grade), contentStringRes = R.plurals.grade_notify_new_items_predicted,
) summaryStringRes = R.plurals.grade_number_item,
} startMenu = MainView.Section.GRADE,
lines = items.map {
val groupNotificationData = GroupNotificationData( "${it.subject}: ${it.predictedGrade}"
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 = SplashActivity.getStartIntent(context, Destination.Grade),
type = NotificationType.NEW_GRADE_PREDICTED
) )
appNotificationManager.sendMultipleNotifications(groupNotificationData, student) sendNotification(notification, student)
} }
suspend fun notifyFinal(items: List<GradeSummary>, student: Student) { fun notifyFinal(items: List<GradeSummary>, student: Student) {
val notificationDataList = items.map { val notification = MultipleNotifications(
NotificationData( type = NotificationType.NEW_GRADE_FINAL,
title = context.getPlural(R.plurals.grade_new_items_final, 1), icon = R.drawable.ic_stat_grade,
content = "${it.subject}: ${it.finalGrade}", titleStringRes = R.plurals.grade_new_items_final,
intentToStart = SplashActivity.getStartIntent(context, Destination.Grade), contentStringRes = R.plurals.grade_notify_new_items_final,
) summaryStringRes = R.plurals.grade_number_item,
} startMenu = MainView.Section.GRADE,
lines = items.map {
val groupNotificationData = GroupNotificationData( "${it.subject}: ${it.finalGrade}"
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 = SplashActivity.getStartIntent(context, Destination.Grade),
type = NotificationType.NEW_GRADE_FINAL
) )
appNotificationManager.sendMultipleNotifications(groupNotificationData, student) sendNotification(notification, student)
} }
} }

View file

@ -1,52 +1,38 @@
package io.github.wulkanowy.services.sync.notifications package io.github.wulkanowy.services.sync.notifications
import android.content.Context import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Homework import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import io.github.wulkanowy.utils.getPlural
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalDate import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject
class NewHomeworkNotification @Inject constructor( class NewHomeworkNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager, @ApplicationContext private val context: Context,
@ApplicationContext private val context: Context notificationManager: NotificationManagerCompat,
) { ) : BaseNotification(context, notificationManager) {
suspend fun notify(items: List<Homework>, student: Student) { fun notify(items: List<Homework>, student: Student) {
val today = LocalDate.now() val today = LocalDate.now()
val lines = items.filter { !it.date.isBefore(today) } val lines = items.filter { !it.date.isBefore(today) }.map {
.map { "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.content}"
"${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.content}" }.ifEmpty { return }
}
.ifEmpty { return }
val notificationDataList = lines.map { val notification = MultipleNotifications(
NotificationData(
title = context.getPlural(R.plurals.homework_notify_new_item_title, 1),
content = it,
intentToStart = SplashActivity.getStartIntent(context, Destination.Homework),
)
}
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 = SplashActivity.getStartIntent(context, Destination.Homework),
type = NotificationType.NEW_HOMEWORK, type = NotificationType.NEW_HOMEWORK,
notificationDataList = notificationDataList 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
) )
appNotificationManager.sendMultipleNotifications(groupNotificationData, student) sendNotification(notification, student)
} }
} }

View file

@ -1,34 +1,30 @@
package io.github.wulkanowy.services.sync.notifications package io.github.wulkanowy.services.sync.notifications
import android.content.Context import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.data.pojos.OneNotification
import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import javax.inject.Inject import javax.inject.Inject
class NewLuckyNumberNotification @Inject constructor( class NewLuckyNumberNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager, @ApplicationContext private val context: Context,
@ApplicationContext private val context: Context notificationManager: NotificationManagerCompat,
) { ) : BaseNotification(context, notificationManager) {
suspend fun notify(item: LuckyNumber, student: Student) { fun notify(item: LuckyNumber, student: Student) {
val notificationData = NotificationData( val notification = OneNotification(
title = context.getString(R.string.lucky_number_notify_new_item_title), type = NotificationType.NEW_LUCKY_NUMBER,
content = context.getString( icon = R.drawable.ic_stat_luckynumber,
R.string.lucky_number_notify_new_item, titleStringRes = R.string.lucky_number_notify_new_item_title,
item.luckyNumber.toString() contentStringRes = R.string.lucky_number_notify_new_item,
), startMenu = MainView.Section.LUCKY_NUMBER,
intentToStart = SplashActivity.getStartIntent(context, Destination.LuckyNumber) contentValues = listOf(item.luckyNumber.toString())
) )
appNotificationManager.sendSingleNotification( sendNotification(notification, student)
notificationData = notificationData,
notificationType = NotificationType.NEW_LUCKY_NUMBER,
student = student
)
} }
} }

View file

@ -1,39 +1,33 @@
package io.github.wulkanowy.services.sync.notifications package io.github.wulkanowy.services.sync.notifications
import android.content.Context import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import io.github.wulkanowy.utils.getPlural
import javax.inject.Inject import javax.inject.Inject
class NewMessageNotification @Inject constructor( class NewMessageNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager, @ApplicationContext private val context: Context,
@ApplicationContext private val context: Context notificationManager: NotificationManagerCompat,
) { ) : BaseNotification(context, notificationManager) {
suspend fun notify(items: List<Message>, student: Student) { fun notify(items: List<Message>, student: Student) {
val notificationDataList = items.map { val notification = MultipleNotifications(
NotificationData( type = NotificationType.NEW_MESSAGE,
title = context.getPlural(R.plurals.message_new_items, 1), icon = R.drawable.ic_stat_message,
content = "${it.sender}: ${it.subject}", titleStringRes = R.plurals.message_new_items,
intentToStart = SplashActivity.getStartIntent(context, Destination.Message), contentStringRes = R.plurals.message_notify_new_items,
) summaryStringRes = R.plurals.message_number_item,
} startMenu = MainView.Section.MESSAGE,
lines = items.map {
val groupNotificationData = GroupNotificationData( "${it.sender}: ${it.subject}"
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 = SplashActivity.getStartIntent(context, Destination.Message),
type = NotificationType.NEW_MESSAGE
) )
appNotificationManager.sendMultipleNotifications(groupNotificationData, student) sendNotification(notification, student)
} }
} }

View file

@ -1,46 +1,46 @@
package io.github.wulkanowy.services.sync.notifications package io.github.wulkanowy.services.sync.notifications
import android.content.Context import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Note import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.data.pojos.NotificationData
import io.github.wulkanowy.sdk.scrapper.notes.NoteCategory import io.github.wulkanowy.sdk.scrapper.notes.NoteCategory
import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import io.github.wulkanowy.utils.getPlural
import javax.inject.Inject import javax.inject.Inject
class NewNoteNotification @Inject constructor( class NewNoteNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager, @ApplicationContext private val context: Context,
@ApplicationContext private val context: Context notificationManager: NotificationManagerCompat,
) { ) : BaseNotification(context, notificationManager) {
suspend fun notify(items: List<Note>, student: Student) { fun notify(items: List<Note>, student: Student) {
val notificationDataList = items.map { val notification = MultipleNotifications(
val titleRes = when (NoteCategory.getByValue(it.categoryType)) { type = NotificationType.NEW_NOTE,
icon = R.drawable.ic_stat_note,
titleStringRes = when (NoteCategory.getByValue(items.first().categoryType)) {
NoteCategory.POSITIVE -> R.plurals.praise_new_items NoteCategory.POSITIVE -> R.plurals.praise_new_items
NoteCategory.NEUTRAL -> R.plurals.neutral_note_new_items NoteCategory.NEUTRAL -> R.plurals.neutral_note_new_items
else -> R.plurals.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 = SplashActivity.getStartIntent(context, Destination.Note),
)
}
val groupNotificationData = GroupNotificationData(
notificationDataList = notificationDataList,
intentToStart = SplashActivity.getStartIntent(context, Destination.Note),
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.sendMultipleNotifications(groupNotificationData, student) sendNotification(notification, student)
} }
} }

View file

@ -1,54 +1,33 @@
package io.github.wulkanowy.services.sync.notifications package io.github.wulkanowy.services.sync.notifications
import android.content.Context import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import io.github.wulkanowy.utils.getPlural
import javax.inject.Inject import javax.inject.Inject
class NewSchoolAnnouncementNotification @Inject constructor( class NewSchoolAnnouncementNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager, @ApplicationContext private val context: Context,
@ApplicationContext private val context: Context notificationManager: NotificationManagerCompat,
) { ) : BaseNotification(context, notificationManager) {
suspend fun notify(items: List<SchoolAnnouncement>, student: Student) { fun notify(items: List<SchoolAnnouncement>, student: Student) {
val notificationDataList = items.map { val notification = MultipleNotifications(
NotificationData(
intentToStart = SplashActivity.getStartIntent(
context = context,
destination = Destination.SchoolAnnouncement
),
title = context.getPlural(
R.plurals.school_announcement_notify_new_item_title,
1
),
content = "${it.subject}: ${it.content}"
)
}
val groupNotificationData = GroupNotificationData(
type = NotificationType.NEW_ANNOUNCEMENT, type = NotificationType.NEW_ANNOUNCEMENT,
intentToStart = SplashActivity.getStartIntent( icon = R.drawable.ic_all_about,
context = context, titleStringRes = R.plurals.school_announcement_notify_new_item_title,
destination = Destination.SchoolAnnouncement contentStringRes = R.plurals.school_announcement_notify_new_items,
), summaryStringRes = R.plurals.school_announcement_number_item,
title = context.getPlural( startMenu = MainView.Section.SCHOOL_ANNOUNCEMENT,
R.plurals.school_announcement_notify_new_item_title, lines = items.map {
items.size "${it.subject}: ${it.content}"
), }
content = context.getPlural(
R.plurals.school_announcement_notify_new_items,
items.size,
items.size
),
notificationDataList = notificationDataList
) )
appNotificationManager.sendMultipleNotifications(groupNotificationData, student) sendNotification(notification, student)
} }
} }

View file

@ -1,8 +1,6 @@
package io.github.wulkanowy.services.sync.notifications 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.LuckyNumberChannel
import io.github.wulkanowy.services.sync.channels.NewAttendanceChannel
import io.github.wulkanowy.services.sync.channels.NewConferencesChannel import io.github.wulkanowy.services.sync.channels.NewConferencesChannel
import io.github.wulkanowy.services.sync.channels.NewExamChannel import io.github.wulkanowy.services.sync.channels.NewExamChannel
import io.github.wulkanowy.services.sync.channels.NewGradesChannel import io.github.wulkanowy.services.sync.channels.NewGradesChannel
@ -10,77 +8,16 @@ import io.github.wulkanowy.services.sync.channels.NewHomeworkChannel
import io.github.wulkanowy.services.sync.channels.NewMessagesChannel import io.github.wulkanowy.services.sync.channels.NewMessagesChannel
import io.github.wulkanowy.services.sync.channels.NewNotesChannel import io.github.wulkanowy.services.sync.channels.NewNotesChannel
import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel 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( enum class NotificationType(val group: String, val channel: String) {
val group: String?, NEW_CONFERENCE("new_conferences_group", NewConferencesChannel.CHANNEL_ID),
val channel: String, NEW_EXAM("new_exam_group", NewExamChannel.CHANNEL_ID),
val icon: Int NEW_GRADE_DETAILS("new_grade_details_group", NewGradesChannel.CHANNEL_ID),
) { NEW_GRADE_PREDICTED("new_grade_predicted_group", NewGradesChannel.CHANNEL_ID),
NEW_CONFERENCE( NEW_GRADE_FINAL("new_grade_final_group", NewGradesChannel.CHANNEL_ID),
group = "new_conferences_group", NEW_HOMEWORK("new_homework_group", NewHomeworkChannel.CHANNEL_ID),
channel = NewConferencesChannel.CHANNEL_ID, NEW_LUCKY_NUMBER("lucky_number_group", LuckyNumberChannel.CHANNEL_ID),
icon = R.drawable.ic_more_conferences, NEW_MESSAGE("new_message_group", NewMessagesChannel.CHANNEL_ID),
), NEW_NOTE("new_notes_group", NewNotesChannel.CHANNEL_ID),
NEW_EXAM( NEW_ANNOUNCEMENT("new_school_announcements_group", NewSchoolAnnouncementsChannel.CHANNEL_ID),
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
)
} }

View file

@ -3,40 +3,15 @@ package io.github.wulkanowy.services.sync.works
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.AttendanceRepository import io.github.wulkanowy.data.repositories.AttendanceRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.services.sync.notifications.NewAttendanceNotification import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.previousOrSameSchoolDay
import io.github.wulkanowy.utils.waitForResult import io.github.wulkanowy.utils.waitForResult
import kotlinx.coroutines.flow.first
import java.time.LocalDate.now import java.time.LocalDate.now
import javax.inject.Inject import javax.inject.Inject
class AttendanceWork @Inject constructor( class AttendanceWork @Inject constructor(private val attendanceRepository: AttendanceRepository) : Work {
private val attendanceRepository: AttendanceRepository,
private val newAttendanceNotification: NewAttendanceNotification,
private val preferencesRepository: PreferencesRepository
) : Work {
override suspend fun doWork(student: Student, semester: Semester) { override suspend fun doWork(student: Student, semester: Semester) {
attendanceRepository.getAttendance( attendanceRepository.getAttendance(student, semester, now().monday, now().sunday, true).waitForResult()
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
})
}
} }
} }

View file

@ -5,7 +5,8 @@ import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.HomeworkRepository import io.github.wulkanowy.data.repositories.HomeworkRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.services.sync.notifications.NewHomeworkNotification import io.github.wulkanowy.services.sync.notifications.NewHomeworkNotification
import io.github.wulkanowy.utils.nextOrSameSchoolDay import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.waitForResult import io.github.wulkanowy.utils.waitForResult
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import java.time.LocalDate.now import java.time.LocalDate.now
@ -21,13 +22,13 @@ class HomeworkWork @Inject constructor(
homeworkRepository.getHomework( homeworkRepository.getHomework(
student = student, student = student,
semester = semester, semester = semester,
start = now().nextOrSameSchoolDay, start = now().monday,
end = now().nextOrSameSchoolDay, end = now().sunday,
forceRefresh = true, forceRefresh = true,
notify = preferencesRepository.isNotificationsEnable notify = preferencesRepository.isNotificationsEnable
).waitForResult() ).waitForResult()
homeworkRepository.getHomeworkFromDatabase(semester, now(), now().plusDays(7)).first() homeworkRepository.getHomeworkFromDatabase(semester, now().monday, now().sunday).first()
.filter { !it.isNotified }.let { .filter { !it.isNotified }.let {
if (it.isNotEmpty()) newHomeworkNotification.notify(it, student) if (it.isNotEmpty()) newHomeworkNotification.notify(it, student)

View file

@ -2,41 +2,18 @@ package io.github.wulkanowy.services.sync.works
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student 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.data.repositories.TimetableRepository
import io.github.wulkanowy.services.sync.notifications.ChangeTimetableNotification import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.nextOrSameSchoolDay import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.waitForResult import io.github.wulkanowy.utils.waitForResult
import kotlinx.coroutines.flow.first
import java.time.LocalDate.now import java.time.LocalDate.now
import javax.inject.Inject import javax.inject.Inject
class TimetableWork @Inject constructor( class TimetableWork @Inject constructor(
private val timetableRepository: TimetableRepository, private val timetableRepository: TimetableRepository
private val changeTimetableNotification: ChangeTimetableNotification,
private val preferencesRepository: PreferencesRepository
) : Work { ) : Work {
override suspend fun doWork(student: Student, semester: Semester) { override suspend fun doWork(student: Student, semester: Semester) {
timetableRepository.getTimetable( timetableRepository.getTimetable(student, semester, now().monday, now().sunday, true).waitForResult()
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
})
}
} }
} }

View file

@ -1,11 +1,14 @@
package io.github.wulkanowy.ui.base package io.github.wulkanowy.ui.base
import android.app.ActivityManager 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.os.Bundle
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.snackbar.Snackbar.LENGTH_LONG import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
@ -37,6 +40,7 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
themeManager.applyActivityTheme(this) themeManager.applyActivityTheme(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleLogger, true) supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleLogger, true)
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
setTaskDescription( setTaskDescription(
@ -79,8 +83,8 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
} }
override fun openClearLoginView() { override fun openClearLoginView() {
startActivity(LoginActivity.getStartIntent(this)) startActivity(LoginActivity.getStartIntent(this)
finishAffinity() .apply { addFlags(FLAG_ACTIVITY_CLEAR_TASK or FLAG_ACTIVITY_NEW_TASK) })
} }
override fun onDestroy() { override fun onDestroy() {

View file

@ -2,33 +2,32 @@ package io.github.wulkanowy.ui.base
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle import androidx.fragment.app.FragmentPagerAdapter
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
class BaseFragmentPagerAdapter( //TODO Use ViewPager2
private val fragmentManager: FragmentManager, class BaseFragmentPagerAdapter(private val fragmentManager: FragmentManager) :
private val pagesCount: Int, FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
lifecycle: Lifecycle,
) : FragmentStateAdapter(fragmentManager, lifecycle), TabLayoutMediator.TabConfigurationStrategy {
lateinit var itemFactory: (position: Int) -> Fragment private val pages = mutableMapOf<Fragment, String?>()
var titleFactory: (position: Int) -> String? = { "" }
var containerId = 0 var containerId = 0
fun getFragmentInstance(position: Int): Fragment? { fun getFragmentInstance(position: Int): Fragment? {
require(containerId != 0) { "Container id is 0" } require(containerId != 0) { "Container id is 0" }
return fragmentManager.findFragmentByTag("f$position") return fragmentManager.findFragmentByTag("android:switcher:$containerId:$position")
} }
override fun createFragment(position: Int): Fragment = itemFactory(position) fun addFragments(fragments: List<Fragment>) {
fragments.forEach { pages[it] = null }
override fun getItemCount() = pagesCount
override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
tab.text = titleFactory(position)
} }
fun addFragmentsWithTitle(pages: Map<Fragment, String>) {
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)
} }

View file

@ -6,27 +6,29 @@ import io.github.wulkanowy.utils.flowWithResource
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import timber.log.Timber import timber.log.Timber
import kotlin.coroutines.CoroutineContext
open class BasePresenter<T : BaseView>( open class BasePresenter<T : BaseView>(
protected val errorHandler: ErrorHandler, protected val errorHandler: ErrorHandler,
protected val studentRepository: StudentRepository protected val studentRepository: StudentRepository
) { ) : CoroutineScope {
private val job = SupervisorJob()
protected val presenterScope = CoroutineScope(job + Dispatchers.Main) private var job = Job()
private val childrenJobs = mutableMapOf<String, Job>() private val jobs = mutableMapOf<String, Job>()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
var view: T? = null var view: T? = null
open fun onAttachView(view: T) { open fun onAttachView(view: T) {
job = Job()
this.view = view this.view = view
errorHandler.apply { errorHandler.apply {
showErrorMessage = view::showError showErrorMessage = view::showError
@ -62,22 +64,22 @@ open class BasePresenter<T : BaseView>(
} }
fun <T> Flow<T>.launch(individualJobTag: String = "load"): Job { fun <T> Flow<T>.launch(individualJobTag: String = "load"): Job {
childrenJobs[individualJobTag]?.cancel() jobs[individualJobTag]?.cancel()
val job = catch { errorHandler.dispatch(it) }.launchIn(presenterScope) val job = catch { errorHandler.dispatch(it) }.launchIn(this@BasePresenter)
childrenJobs[individualJobTag] = job jobs[individualJobTag] = job
Timber.d("Job $individualJobTag launched in ${this@BasePresenter.javaClass.simpleName}: $job") Timber.d("Job $individualJobTag launched in ${this@BasePresenter.javaClass.simpleName}: $job")
return job return job
} }
fun cancelJobs(vararg names: String) { fun cancelJobs(vararg names: String) {
names.forEach { names.forEach {
childrenJobs[it]?.cancel() jobs[it]?.cancel()
} }
} }
open fun onDetachView() { open fun onDetachView() {
job.cancelChildren()
errorHandler.clear()
view = null view = null
job.cancel()
errorHandler.clear()
} }
} }

View file

@ -11,7 +11,6 @@ import android.widget.Toast
import android.widget.Toast.LENGTH_LONG import android.widget.Toast.LENGTH_LONG
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.view.isGone
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.DialogErrorBinding import io.github.wulkanowy.databinding.DialogErrorBinding
@ -25,6 +24,8 @@ import io.github.wulkanowy.utils.openEmailClient
import io.github.wulkanowy.utils.openInternetBrowser import io.github.wulkanowy.utils.openInternetBrowser
import okhttp3.internal.http2.StreamResetException import okhttp3.internal.http2.StreamResetException
import java.io.InterruptedIOException import java.io.InterruptedIOException
import java.io.PrintWriter
import java.io.StringWriter
import java.net.ConnectException import java.net.ConnectException
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
import java.net.UnknownHostException import java.net.UnknownHostException
@ -63,26 +64,26 @@ class ErrorDialog : BaseDialogFragment<DialogErrorBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val errorStacktrace = error.stackTraceToString() val stringWriter = StringWriter().apply {
error.printStackTrace(PrintWriter(this))
}
with(binding) { with(binding) {
errorDialogContent.text = errorStacktrace.replace(": ${error.localizedMessage}", "") errorDialogContent.text = stringWriter.toString()
with(errorDialogHorizontalScroll) { with(errorDialogHorizontalScroll) {
post { fullScroll(HorizontalScrollView.FOCUS_LEFT) } post { fullScroll(HorizontalScrollView.FOCUS_LEFT) }
} }
errorDialogCopy.setOnClickListener { errorDialogCopy.setOnClickListener {
val clip = ClipData.newPlainText("Error details", errorStacktrace) val clip = ClipData.newPlainText("wulkanowy", stringWriter.toString())
activity?.getSystemService<ClipboardManager>()?.setPrimaryClip(clip) activity?.getSystemService<ClipboardManager>()?.setPrimaryClip(clip)
Toast.makeText(context, R.string.all_copied, LENGTH_LONG).show() Toast.makeText(context, R.string.all_copied, LENGTH_LONG).show()
} }
errorDialogCancel.setOnClickListener { dismiss() } errorDialogCancel.setOnClickListener { dismiss() }
errorDialogReport.setOnClickListener { errorDialogReport.setOnClickListener {
openConfirmDialog { openEmailClient(errorStacktrace) } openConfirmDialog { openEmailClient(stringWriter.toString()) }
} }
errorDialogHumanizedMessage.text = resources.getString(error) errorDialogMessage.text = resources.getString(error)
errorDialogErrorMessage.text = error.localizedMessage
errorDialogErrorMessage.isGone = error.localizedMessage.isNullOrBlank()
errorDialogReport.isEnabled = when (error) { errorDialogReport.isEnabled = when (error) {
is UnknownHostException, is UnknownHostException,
is InterruptedIOException, is InterruptedIOException,

View file

@ -1,7 +1,6 @@
package io.github.wulkanowy.ui.base package io.github.wulkanowy.ui.base
import android.content.Context import android.content.res.Resources
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException
import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException
@ -10,7 +9,7 @@ import io.github.wulkanowy.utils.security.ScramblerException
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
open class ErrorHandler @Inject constructor(@ApplicationContext protected val context: Context) { open class ErrorHandler @Inject constructor(protected val resources: Resources) {
var showErrorMessage: (String, Throwable) -> Unit = { _, _ -> } var showErrorMessage: (String, Throwable) -> Unit = { _, _ -> }
@ -26,7 +25,7 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
} }
protected open fun proceed(error: Throwable) { protected open fun proceed(error: Throwable) {
showErrorMessage(context.resources.getString(error), error) showErrorMessage(resources.getString(error), error)
when (error) { when (error) {
is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl) is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl)
is ScramblerException, is BadCredentialsException -> onSessionExpired() is ScramblerException, is BadCredentialsException -> onSessionExpired()

View file

@ -41,15 +41,14 @@ class ThemeManager @Inject constructor(private val preferencesRepository: Prefer
) )
} }
private fun isThemeApplicable(activity: AppCompatActivity) = private fun isThemeApplicable(activity: AppCompatActivity): Boolean {
activity.packageManager return activity.packageManager
.getPackageInfo(activity.packageName, GET_ACTIVITIES) .getPackageInfo(activity.packageName, GET_ACTIVITIES)
.activities .activities.singleOrNull { it.name == activity::class.java.canonicalName }
.singleOrNull { it.name == activity::class.java.canonicalName } ?.theme.let {
?.theme
.let {
it == R.style.WulkanowyTheme_Black || it == R.style.WulkanowyTheme_NoActionBar 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_Login || it == R.style.WulkanowyTheme_Login_Black
|| it == R.style.WulkanowyTheme_MessageSend || it == R.style.WulkanowyTheme_MessageSend_Black || it == R.style.WulkanowyTheme_MessageSend || it == R.style.WulkanowyTheme_MessageSend_Black
} }
}
} }

View file

@ -1,136 +0,0 @@
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 {
/*
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
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 get() = Type.DASHBOARD
override val fragment get() = DashboardFragment.newInstance()
}
object Grade : Destination {
override val type get() = Type.GRADE
override val fragment get() = GradeFragment.newInstance()
}
object Attendance : Destination {
override val type get() = Type.ATTENDANCE
override val fragment get() = AttendanceFragment.newInstance()
}
object Exam : Destination {
override val type get() = Type.EXAM
override val fragment get() = ExamFragment.newInstance()
}
data class Timetable(val date: LocalDate? = null) : Destination {
override val type get() = Type.TIMETABLE
override val fragment get() = TimetableFragment.newInstance(date)
}
object Homework : Destination {
override val type get() = Type.HOMEWORK
override val fragment get() = HomeworkFragment.newInstance()
}
object Note : Destination {
override val type get() = Type.NOTE
override val fragment get() = NoteFragment.newInstance()
}
object Conference : Destination {
override val type get() = Type.CONFERENCE
override val fragment get() = ConferenceFragment.newInstance()
}
object SchoolAnnouncement : Destination {
override val type get() = Type.SCHOOL_ANNOUNCEMENT
override val fragment get() = SchoolAnnouncementFragment.newInstance()
}
object School : Destination {
override val type get() = Type.SCHOOL
override val fragment get() = SchoolFragment.newInstance()
}
object LuckyNumber : Destination {
override val type get() = Type.LUCKY_NUMBER
override val fragment get() = LuckyNumberFragment.newInstance()
}
object More : Destination {
override val type get() = Type.MORE
override val fragment get() = MoreFragment.newInstance()
}
object Message : Destination {
override val type get() = Type.MESSAGE
override val fragment get() = MessageFragment.newInstance()
}
}

Some files were not shown because too many files have changed in this diff Show more