Compare commits

...

66 Commits
1.2.0 ... 1.3.0

Author SHA1 Message Date
689012131f Merge branch 'release/1.3.0' 2021-09-28 23:26:18 +02:00
6cdcf92782 Version 1.3.0 2021-09-28 23:26:10 +02:00
9c8bcbfdd3 New Crowdin updates (#1544) 2021-09-28 21:11:59 +00:00
0b83a66b85 Remove disappearing teachers workaround from timetable repository (#1545) 2021-09-28 23:10:11 +02:00
9711cc868c New Crowdin updates (#1522) 2021-09-28 22:42:06 +02:00
f8cb7599e6 Add missing auto refresh to recipients, subjects and teachers (#1540) 2021-09-28 22:40:43 +02:00
7636618e23 Update License (#1542) 2021-09-28 21:55:40 +02:00
5bc54c12f1 Add option to make upcoming lesson notification not persistent (#1537) 2021-09-28 11:48:25 +02:00
e10e530dee Remove seconds from timetable timer (#1539) 2021-09-27 23:03:59 +02:00
d69118b085 Add notifications center (#1524) 2021-09-27 20:58:25 +02:00
dc90549b9d Fix hiding last element in messages (#1538) 2021-09-27 17:56:43 +02:00
b552dbc904 Bump constraintlayout from 2.1.0 to 2.1.1 (#1535) 2021-09-27 15:56:11 +00:00
a6a1678b47 Bump core from 1.10.1 to 1.10.2 (#1536) 2021-09-27 15:51:06 +00:00
7a46ef5f19 Add calculated average help dialog (#1379)
Co-authored-by: Rafał Borcz <RafalBO99@outlook.com>
2021-09-25 17:19:21 +02:00
f9e0f7b390 Don't stop loading the timetable when error occurs in upcoming lessons notification scheduling (#1532) 2021-09-25 15:18:40 +02:00
9211baf7ec Add notification piggyback (#1503) 2021-09-25 14:02:38 +02:00
de6131f4f5 Add transparency to lucky number widget (#1530) 2021-09-25 13:46:35 +02:00
2cb11e443c Mark teacher with yellow when new and old are the same (#1529) 2021-09-25 13:46:11 +02:00
a43ffcdef4 Display bad credentials error in the message box above login form (#1525) 2021-09-24 21:02:51 +02:00
6615e68430 Bump kotlin_version from 1.5.30 to 1.5.31 (#1528) 2021-09-22 09:25:54 +02:00
36daa7ccc1 Always include all language resources in app bundle (#1527) 2021-09-22 09:25:16 +02:00
6e5481f345 Upgrade Gradle Play Publisher to 3.6.0 (#1526) 2021-09-20 11:38:13 +02:00
ba1c14ca0e Merge branch 'release/1.2.3' into develop 2021-09-16 12:01:58 +02:00
c69bb2ef71 Merge branch 'release/1.2.3' 2021-09-16 12:01:54 +02:00
9cb4754132 Version 1.2.3 2021-09-16 12:01:49 +02:00
5ba8289c87 Display info in timetable as-is when lesson has change flag (#1521) 2021-09-16 11:59:23 +02:00
258782c648 New Crowdin updates (#1482) 2021-09-16 11:30:05 +02:00
c568bc1515 Fix ghost account after logout not current student (#1518) 2021-09-16 11:29:11 +02:00
da668f93cf Fix bugs in dashboard (#1517) 2021-09-16 11:24:52 +02:00
037dbd792f Add conference dialog (#1519) 2021-09-16 10:51:38 +02:00
7ec7afed87 Bump firebase-bom from 28.4.0 to 28.4.1 (#1520) 2021-09-16 08:22:06 +00:00
bea50e6db5 Merge branch 'release/1.2.2' into develop 2021-09-13 14:53:38 +02:00
6a00e75816 Merge branch 'release/1.2.2' 2021-09-13 14:53:32 +02:00
957adaf6ee Version 1.2.2 2021-09-13 14:53:27 +02:00
827fb33eeb Fix login process after was interrupted (#1505) 2021-09-13 14:36:31 +02:00
19c96ee83f Unlock sunday in navigation datepicker (#1506) 2021-09-13 14:19:46 +02:00
5a7f52c773 Update help email pre-filled content (#1507) 2021-09-13 14:19:24 +02:00
dddeff802f Fix date picker crash after saved state (#1502) 2021-09-12 17:29:46 +02:00
91f6310892 Restore lucky number in more view (#1504) 2021-09-11 19:43:05 +02:00
0389642543 Fix empty list on excuse submit (#1501) 2021-09-11 19:40:09 +02:00
8528e0beff Fix crash in school info when dialer is unavailable (#1500) 2021-09-10 09:49:22 +00:00
e665a8f18b Fix error view in attendance summary (#1492) 2021-09-10 00:48:29 +02:00
6d5acbad2c Fix overlapping error view (#1493) 2021-09-10 00:36:44 +02:00
7217d0f753 Fix NPE in timetable dashboard tile (#1498) 2021-09-10 00:27:48 +02:00
16a5d88dfb Fix overlapping shadow in dashboard (#1494) 2021-09-10 00:25:23 +02:00
646a46727f Update material chips input (#1495) 2021-09-08 09:13:52 +02:00
f5e9197f98 Bump work_manager from 2.5.0 to 2.6.0 (#1478) 2021-09-06 23:38:10 +00:00
b47f26684b Change AppGallery deploy format to aab (#1483) 2021-09-06 03:27:54 +02:00
3a03b5f1c6 Merge branch 'release/1.2.1' into develop 2021-09-05 23:29:30 +02:00
3d0dcead50 Merge branch 'release/1.2.1' 2021-09-05 23:29:23 +02:00
b64b41c11c Version 1.2.1 2021-09-05 23:29:15 +02:00
77c5330f91 Dashboard fixes (#1463) 2021-09-05 23:24:03 +02:00
2b55ec02ff New translations strings.xml (Polish) (#1474) 2021-09-05 23:06:44 +02:00
49ebae6e63 Fix overlaping empty and error view in grade statistics (#1475)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2021-09-05 19:59:03 +00:00
44a9db48a6 Bump hianalytics from 6.2.0.300 to 6.2.0.301 (#1476) 2021-09-05 18:17:18 +00:00
0008a72be1 Bump kotlinx-coroutines-test from 1.5.1 to 1.5.2 (#1477) 2021-09-05 18:10:22 +00:00
a43acaaa07 Bump kotlinx-coroutines-android from 1.5.1 to 1.5.2 (#1479) 2021-09-05 18:10:05 +00:00
e6c9abb4e5 Bump core from 1.10.0 to 1.10.1 (#1480) 2021-09-05 18:09:42 +00:00
3b9451184c Fix preview of second student guardian when first guardian is null (#1473) 2021-09-05 03:15:40 +02:00
45d1727dbe Add missing school announcement dialog (#1470) 2021-09-04 15:54:37 +02:00
8d7b611c44 Fix showing error view in timetable (#1472) 2021-09-04 15:54:05 +02:00
c3adb9b6d6 Bump agp to 7.0.2 (#1469) 2021-09-03 22:54:29 +02:00
d87283eb31 Fix opening twitter link from about on android 11 (#1460) 2021-08-30 00:20:13 +02:00
d139c22782 Bump hianalytics from 6.1.1.300 to 6.2.0.300 (#1457) 2021-08-29 19:37:18 +00:00
e557021ad9 Bump huawei-publish-gradle-plugin from 1.2.4 to 1.3.0 (#1458) 2021-08-29 19:37:01 +00:00
37af5de25c Merge branch 'release/1.2.0' into develop 2021-08-29 21:08:23 +02:00
172 changed files with 5577 additions and 1540 deletions

View File

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

View File

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

View File

@ -21,8 +21,8 @@ android {
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21
targetSdkVersion 30
versionCode 93
versionName "1.2.0"
versionCode 97
versionName "1.3.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
@ -96,10 +96,20 @@ android {
}
}
playConfigs {
play { enabled.set(true) }
}
buildFeatures {
viewBinding true
}
bundle {
language {
enableSplit = false
}
}
testOptions.unitTests {
includeAndroidResources = true
}
@ -130,25 +140,24 @@ kapt {
}
play {
serviceAccountEmail = System.getenv("PLAY_SERVICE_ACCOUNT_EMAIL") ?: "jan@fakelog.cf"
serviceAccountCredentials = file('key.p12')
defaultToAppBundles = false
track = 'beta'
track = 'production'
updatePriority = 3
enabled.set(false)
}
huaweiPublish {
instances {
hmsRelease {
credentialsPath = "$rootDir/app/src/release/agconnect-credentials.json"
buildFormat = "apk"
buildFormat = "aab"
deployType = "draft"
}
}
}
ext {
work_manager = "2.5.0"
work_manager = "2.6.0"
android_hilt = "1.0.0"
room = "2.3.0"
chucker = "3.5.2"
@ -157,11 +166,11 @@ ext {
}
dependencies {
implementation "io.github.wulkanowy:sdk:1.2.0"
implementation "io.github.wulkanowy:sdk:1.3.0"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
implementation "androidx.core:core-ktx:1.6.0"
implementation "androidx.activity:activity-ktx:1.3.1"
@ -174,10 +183,10 @@ dependencies {
implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation "androidx.viewpager:viewpager:1.0.0"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:2.1.0"
implementation "androidx.constraintlayout:constraintlayout:2.1.1"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0"
implementation "com.google.android.material:material:1.4.0"
implementation "com.github.wulkanowy:material-chips-input:2.2.0"
implementation "com.github.wulkanowy:material-chips-input:2.3.1"
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0"
implementation 'com.github.lopspower:CircularImageView:4.2.0'
@ -211,14 +220,14 @@ dependencies {
implementation 'me.xdrop:fuzzywuzzy:1.3.1'
implementation 'com.fredporciuncula:flow-preferences:1.5.0'
playImplementation platform('com.google.firebase:firebase-bom:28.4.0')
playImplementation platform('com.google.firebase:firebase-bom:28.4.1')
playImplementation 'com.google.firebase:firebase-analytics-ktx'
playImplementation 'com.google.firebase:firebase-messaging:'
playImplementation 'com.google.firebase:firebase-crashlytics:'
playImplementation 'com.google.android.play:core:1.10.0'
playImplementation 'com.google.android.play:core:1.10.2'
playImplementation 'com.google.android.play:core-ktx:1.8.1'
hmsImplementation 'com.huawei.hms:hianalytics:6.1.1.300'
hmsImplementation 'com.huawei.hms:hianalytics:6.2.0.301'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.0.300'
releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker"
@ -228,7 +237,7 @@ dependencies {
testImplementation "junit:junit:4.13.2"
testImplementation "io.mockk:mockk:$mockk"
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2'
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
testImplementation 'org.robolectric:robolectric:4.6.1'

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -45,6 +45,7 @@
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
<activity
android:name=".ui.modules.splash.SplashActivity"
android:exported="true"
android:screenOrientation="portrait"
android:theme="@style/WulkanowyTheme.SplashScreen"
tools:ignore="LockedOrientationActivity">
@ -74,6 +75,7 @@
<activity
android:name=".ui.modules.timetablewidget.TimetableWidgetConfigureActivity"
android:excludeFromRecents="true"
android:exported="true"
android:noHistory="true"
android:theme="@style/WulkanowyTheme.WidgetAccountSwitcher">
<intent-filter>
@ -83,6 +85,7 @@
<activity
android:name=".ui.modules.luckynumberwidget.LuckyNumberWidgetConfigureActivity"
android:excludeFromRecents="true"
android:exported="true"
android:noHistory="true"
android:theme="@style/WulkanowyTheme.WidgetAccountSwitcher">
<intent-filter>
@ -93,6 +96,22 @@
<service
android:name=".services.widgets.TimetableWidgetService"
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">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<receiver
android:name=".ui.modules.timetablewidget.TimetableWidgetProvider"
@ -107,6 +126,7 @@
</receiver>
<receiver
android:name=".ui.modules.luckynumberwidget.LuckyNumberWidgetProvider"
android:exported="true"
android:label="@string/lucky_number_title">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@ -119,11 +139,9 @@
<receiver android:name=".services.alarm.TimetableNotificationReceiver" />
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
android:exported="false"
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"

View File

@ -8,8 +8,8 @@ import androidx.preference.PreferenceManager
import com.chuckerteam.chucker.api.ChuckerCollector
import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.chuckerteam.chucker.api.RetentionManager
import com.squareup.moshi.Moshi
import com.fredporciuncula.flow.preferences.FlowSharedPreferences
import com.squareup.moshi.Moshi
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -202,4 +202,8 @@ internal class RepositoryModule {
@Singleton
@Provides
fun provideSchoolAnnouncementDao(database: AppDatabase) = database.schoolAnnouncementDao
@Singleton
@Provides
fun provideNotificationDao(database: AppDatabase) = database.notificationDao
}

View File

@ -10,7 +10,6 @@ import io.github.wulkanowy.data.db.dao.AttendanceDao
import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao
import io.github.wulkanowy.data.db.dao.CompletedLessonsDao
import io.github.wulkanowy.data.db.dao.ConferenceDao
import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao
import io.github.wulkanowy.data.db.dao.ExamDao
import io.github.wulkanowy.data.db.dao.GradeDao
import io.github.wulkanowy.data.db.dao.GradePartialStatisticsDao
@ -23,8 +22,10 @@ import io.github.wulkanowy.data.db.dao.MessageAttachmentDao
import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.dao.MobileDeviceDao
import io.github.wulkanowy.data.db.dao.NoteDao
import io.github.wulkanowy.data.db.dao.NotificationDao
import io.github.wulkanowy.data.db.dao.RecipientDao
import io.github.wulkanowy.data.db.dao.ReportingUnitDao
import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao
import io.github.wulkanowy.data.db.dao.SchoolDao
import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao
@ -38,7 +39,6 @@ import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.db.entities.AttendanceSummary
import io.github.wulkanowy.data.db.entities.CompletedLesson
import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradePartialStatistics
@ -51,9 +51,11 @@ import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageAttachment
import io.github.wulkanowy.data.db.entities.MobileDevice
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Notification
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.github.wulkanowy.data.db.entities.School
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentInfo
@ -95,6 +97,7 @@ import io.github.wulkanowy.data.db.migrations.Migration37
import io.github.wulkanowy.data.db.migrations.Migration38
import io.github.wulkanowy.data.db.migrations.Migration39
import io.github.wulkanowy.data.db.migrations.Migration4
import io.github.wulkanowy.data.db.migrations.Migration40
import io.github.wulkanowy.data.db.migrations.Migration5
import io.github.wulkanowy.data.db.migrations.Migration6
import io.github.wulkanowy.data.db.migrations.Migration7
@ -134,6 +137,7 @@ import javax.inject.Singleton
StudentInfo::class,
TimetableHeader::class,
SchoolAnnouncement::class,
Notification::class
],
version = AppDatabase.VERSION_SCHEMA,
exportSchema = true
@ -142,7 +146,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
const val VERSION_SCHEMA = 39
const val VERSION_SCHEMA = 40
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(),
@ -183,6 +187,7 @@ abstract class AppDatabase : RoomDatabase() {
Migration37(),
Migration38(),
Migration39(),
Migration40()
)
fun newInstance(
@ -252,4 +257,6 @@ abstract class AppDatabase : RoomDatabase() {
abstract val timetableHeaderDao: TimetableHeaderDao
abstract val schoolAnnouncementDao: SchoolAnnouncementDao
abstract val notificationDao: NotificationDao
}

View File

@ -0,0 +1,15 @@
package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Query
import io.github.wulkanowy.data.db.entities.Notification
import kotlinx.coroutines.flow.Flow
import javax.inject.Singleton
@Singleton
@Dao
interface NotificationDao : BaseDao<Notification> {
@Query("SELECT * FROM Notifications WHERE student_id = :studentId OR student_id = -1")
fun loadAll(studentId: Long): Flow<List<Notification>>
}

View File

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

View File

@ -0,0 +1,27 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import io.github.wulkanowy.services.sync.notifications.NotificationType
import java.time.LocalDateTime
@Entity(tableName = "Notifications")
data class Notification(
@ColumnInfo(name = "student_id")
val studentId: Long,
val title: String,
val content: String,
val type: NotificationType,
val date: LocalDateTime,
val data: String? = null
) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}

View File

@ -0,0 +1,23 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration40 : Migration(39, 40) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `Notifications` (
`student_id` INTEGER NOT NULL,
`title` TEXT NOT NULL,
`content` TEXT NOT NULL,
`type` TEXT NOT NULL,
`date` INTEGER NOT NULL,
`data` TEXT,
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
)
"""
)
}
}

View File

@ -6,7 +6,7 @@ import androidx.annotation.StringRes
import io.github.wulkanowy.services.sync.notifications.NotificationType
import io.github.wulkanowy.ui.modules.main.MainView
sealed interface Notification {
sealed interface NotificationData {
val type: NotificationType
val startMenu: MainView.Section
val icon: Int
@ -14,7 +14,7 @@ sealed interface Notification {
val contentStringRes: Int
}
data class MultipleNotifications(
data class MultipleNotificationsData(
override val type: NotificationType,
override val startMenu: MainView.Section,
@DrawableRes override val icon: Int,
@ -23,9 +23,9 @@ data class MultipleNotifications(
@PluralsRes val summaryStringRes: Int,
val lines: List<String>,
) : Notification
) : NotificationData
data class OneNotification(
data class OneNotificationData(
override val type: NotificationType,
override val startMenu: MainView.Section,
@DrawableRes override val icon: Int,
@ -33,4 +33,4 @@ data class OneNotification(
@StringRes override val contentStringRes: Int,
val contentValues: List<String>,
) : Notification
) : NotificationData

View File

@ -32,10 +32,23 @@ class AttendanceRepository @Inject constructor(
private val cacheKey = "attendance"
fun getAttendance(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource(
fun getAttendance(
student: Student,
semester: Semester,
start: LocalDate,
end: LocalDate,
forceRefresh: Boolean,
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) },
query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday) },
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(cacheKey, semester, start, end)
)
it.isEmpty() || forceRefresh || isExpired
},
query = {
attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday)
},
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
.getAttendance(start.monday, end.sunday, semester.semesterId)
@ -50,12 +63,17 @@ class AttendanceRepository @Inject constructor(
filterResult = { it.filter { item -> item.date in start..end } }
)
suspend fun excuseForAbsence(student: Student, semester: Semester, absenceList: List<Attendance>, reason: String? = null) {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).excuseForAbsence(absenceList.map { attendance ->
suspend fun excuseForAbsence(
student: Student, semester: Semester,
absenceList: List<Attendance>, reason: String? = null
) {
val items = absenceList.map { attendance ->
Absent(
date = LocalDateTime.of(attendance.date, LocalTime.of(0, 0)),
timeId = attendance.timeId
)
}, reason)
}
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
.excuseForAbsence(items, reason)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -33,11 +33,16 @@ class GradeRepository @Inject constructor(
private val cacheKey = "grade"
fun getGrades(student: Student, semester: Semester, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource(
fun getGrades(
student: Student,
semester: Semester,
forceRefresh: Boolean,
notify: Boolean = false,
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { (details, summaries) ->
val isShouldBeRefreshed = refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester))
details.isEmpty() || summaries.isEmpty() || forceRefresh || isShouldBeRefreshed
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester))
details.isEmpty() || summaries.isEmpty() || forceRefresh || isExpired
},
query = {
val detailsFlow = gradeDb.loadAll(semester.semesterId, semester.studentId)
@ -59,8 +64,14 @@ class GradeRepository @Inject constructor(
}
)
private suspend fun refreshGradeDetails(student: Student, oldGrades: List<Grade>, newDetails: List<Grade>, notify: Boolean) {
val notifyBreakDate = oldGrades.maxByOrNull { it.date }?.date ?: student.registrationDate.toLocalDate()
private suspend fun refreshGradeDetails(
student: Student,
oldGrades: List<Grade>,
newDetails: List<Grade>,
notify: Boolean
) {
val notifyBreakDate = oldGrades.maxByOrNull {it.date }
?.date ?: student.registrationDate.toLocalDate()
gradeDb.deleteAll(oldGrades uniqueSubtract newDetails)
gradeDb.insertAll((newDetails uniqueSubtract oldGrades).onEach {
if (it.date >= notifyBreakDate) it.apply {
@ -70,10 +81,14 @@ class GradeRepository @Inject constructor(
})
}
private suspend fun refreshGradeSummaries(oldSummaries: List<GradeSummary>, newSummary: List<GradeSummary>, notify: Boolean) {
private suspend fun refreshGradeSummaries(
oldSummaries: List<GradeSummary>,
newSummary: List<GradeSummary>,
notify: Boolean
) {
gradeSummaryDb.deleteAll(oldSummaries uniqueSubtract newSummary)
gradeSummaryDb.insertAll((newSummary uniqueSubtract oldSummaries).onEach { summary ->
val oldSummary = oldSummaries.find { oldSummary -> oldSummary.subject == summary.subject }
val oldSummary = oldSummaries.find { old -> old.subject == summary.subject }
summary.isPredictedGradeNotified = when {
summary.predictedGrade.isEmpty() -> true
notify && oldSummary?.predictedGrade != summary.predictedGrade -> false

View File

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

View File

@ -30,16 +30,19 @@ class HomeworkRepository @Inject constructor(
private val cacheKey = "homework"
fun getHomework(
student: Student, semester: Semester,
start: LocalDate, end: LocalDate,
forceRefresh: Boolean, notify: Boolean = false
student: Student,
semester: Semester,
start: LocalDate,
end: LocalDate,
forceRefresh: Boolean,
notify: Boolean = false,
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = {
val isShouldBeRefreshed = refreshHelper.isShouldBeRefreshed(
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(cacheKey, semester, start, end)
)
it.isEmpty() || forceRefresh || isShouldBeRefreshed
it.isEmpty() || forceRefresh || isExpired
},
query = {
homeworkDb.loadAll(

View File

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

View File

@ -51,14 +51,18 @@ class MessageRepository @Inject constructor(
@Suppress("UNUSED_PARAMETER")
fun getMessages(
student: Student, semester: Semester,
folder: MessageFolder, forceRefresh: Boolean, notify: Boolean = false
student: Student,
semester: Semester,
folder: MessageFolder,
forceRefresh: Boolean,
notify: Boolean = false,
): Flow<Resource<List<Message>>> = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = {
it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(
getRefreshKey(cacheKey, student, folder)
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(cacheKey, student, folder)
)
it.isEmpty() || forceRefresh || isExpired
},
query = { messagesDb.loadAll(student.id.toInt(), folder.id) },
fetch = {
@ -77,7 +81,8 @@ class MessageRepository @Inject constructor(
)
private fun getMessagesWithReadByChange(
old: List<Message>, new: List<Message>,
old: List<Message>,
new: List<Message>,
setNotified: Boolean
): List<Message> {
val oldMeta = old.map { Triple(it, it.readBy, it.unreadBy) }
@ -96,7 +101,9 @@ class MessageRepository @Inject constructor(
}
fun getMessage(
student: Student, message: Message, markAsRead: Boolean = false
student: Student,
message: Message,
markAsRead: Boolean = false,
): Flow<Resource<MessageWithAttachment?>> = networkBoundResource(
shouldFetch = {
checkNotNull(it, { "This message no longer exist!" })
@ -135,8 +142,10 @@ class MessageRepository @Inject constructor(
}
suspend fun sendMessage(
student: Student, subject: String, content: String,
recipients: List<Recipient>
student: Student,
subject: String,
content: String,
recipients: List<Recipient>,
): SentMessage = sdk.init(student).sendMessage(
subject = subject,
content = content,

View File

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

View File

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

View File

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

@ -14,7 +14,6 @@ import io.github.wulkanowy.sdk.toLocalDate
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode
import io.github.wulkanowy.ui.modules.grade.GradeSortingMode
import io.github.wulkanowy.utils.toTimestamp
import io.github.wulkanowy.utils.toLocalDateTime
import io.github.wulkanowy.utils.toTimestamp
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -108,6 +107,22 @@ class PreferencesRepository @Inject constructor(
R.bool.pref_default_notification_upcoming_lessons_enable
)
val isUpcomingLessonsNotificationsPersistentKey =
context.getString(R.string.pref_key_notifications_upcoming_lessons_persistent)
val isUpcomingLessonsNotificationsPersistent: Boolean
get() = getBoolean(
isUpcomingLessonsNotificationsPersistentKey,
R.bool.pref_default_notification_upcoming_lessons_persistent
)
val isNotificationPiggybackEnabledKey =
context.getString(R.string.pref_key_notifications_piggyback)
val isNotificationPiggybackEnabled: Boolean
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 isDebugNotificationEnable: Boolean
get() = getBoolean(isDebugNotificationEnableKey, R.bool.pref_default_notification_debug)
@ -176,10 +191,8 @@ class PreferencesRepository @Inject constructor(
)
var lasSyncDate: LocalDateTime
get() = getLong(
R.string.pref_key_last_sync_date,
R.string.pref_default_last_sync_date
).toLocalDateTime()
get() = getLong(R.string.pref_key_last_sync_date, R.string.pref_default_last_sync_date)
.toLocalDateTime()
set(value) = sharedPref.edit().putLong("last_sync_date", value.toTimestamp()).apply()
var dashboardItemsPosition: Map<DashboardItem.Type, Int>?
@ -230,8 +243,10 @@ class PreferencesRepository @Inject constructor(
set(value) = sharedPref.edit().putInt(PREF_KEY_IN_APP_REVIEW_COUNT, value).apply()
var inAppReviewDate: LocalDate?
get() = sharedPref.getLong(PREF_KEY_IN_APP_REVIEW_DATE, 0).takeIf { it != 0L }?.toLocalDate()
set(value) = sharedPref.edit().putLong(PREF_KEY_IN_APP_REVIEW_DATE, value!!.toTimestamp()).apply()
get() = sharedPref.getLong(PREF_KEY_IN_APP_REVIEW_DATE, 0).takeIf { it != 0L }
?.toLocalDate()
set(value) = sharedPref.edit().putLong(PREF_KEY_IN_APP_REVIEW_DATE, value!!.toTimestamp())
.apply()
var isAppReviewDone: Boolean
get() = sharedPref.getBoolean(PREF_KEY_IN_APP_REVIEW_DONE, false)

View File

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

View File

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

View File

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

View File

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

View File

@ -22,7 +22,11 @@ class SemesterRepository @Inject constructor(
private val dispatchers: DispatchersProvider
) {
suspend fun getSemesters(student: Student, forceRefresh: Boolean = false, refreshOnNoCurrent: Boolean = false) = withContext(dispatchers.backgroundThread) {
suspend fun getSemesters(
student: Student,
forceRefresh: Boolean = false,
refreshOnNoCurrent: Boolean = false
) = withContext(dispatchers.backgroundThread) {
val semesters = semesterDb.loadAll(student.studentId, student.classId)
if (isShouldFetch(student, semesters, forceRefresh, refreshOnNoCurrent)) {
@ -31,14 +35,21 @@ class SemesterRepository @Inject constructor(
} else semesters
}
private fun isShouldFetch(student: Student, semesters: List<Semester>, forceRefresh: Boolean, refreshOnNoCurrent: Boolean): Boolean {
private fun isShouldFetch(
student: Student,
semesters: List<Semester>,
forceRefresh: Boolean,
refreshOnNoCurrent: Boolean
): Boolean {
val isNoSemesters = semesters.isEmpty()
val isRefreshOnModeChangeRequired = if (Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) {
semesters.firstOrNull { it.isCurrent }?.diaryId == 0
} else false
val isRefreshOnModeChangeRequired =
if (Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) {
semesters.firstOrNull { it.isCurrent }?.diaryId == 0
} else false
val isRefreshOnNoCurrentAppropriate = refreshOnNoCurrent && !semesters.any { semester -> semester.isCurrent }
val isRefreshOnNoCurrentAppropriate =
refreshOnNoCurrent && !semesters.any { semester -> semester.isCurrent }
return forceRefresh || isNoSemesters || isRefreshOnModeChangeRequired || isRefreshOnNoCurrentAppropriate
}
@ -52,7 +63,8 @@ class SemesterRepository @Inject constructor(
semesterDb.insertSemesters(new.uniqueSubtract(old))
}
suspend fun getCurrentSemester(student: Student, forceRefresh: Boolean = false) = withContext(dispatchers.backgroundThread) {
getSemesters(student, forceRefresh).getCurrentOrLast()
}
suspend fun getCurrentSemester(student: Student, forceRefresh: Boolean = false) =
withContext(dispatchers.backgroundThread) {
getSemesters(student, forceRefresh).getCurrentOrLast()
}
}

View File

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

View File

@ -1,7 +1,9 @@
package io.github.wulkanowy.data.repositories
import android.content.Context
import androidx.room.withTransaction
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Student
@ -25,7 +27,8 @@ class StudentRepository @Inject constructor(
private val studentDb: StudentDao,
private val semesterDb: SemesterDao,
private val sdk: Sdk,
private val appInfo: AppInfo
private val appInfo: AppInfo,
private val appDatabase: AppDatabase
) {
suspend fun isStudentSaved() = getSavedStudents(false).isNotEmpty()
@ -92,7 +95,7 @@ class StudentRepository @Inject constructor(
return student
}
suspend fun saveStudents(studentsWithSemesters: List<StudentWithSemesters>): List<Long> {
suspend fun saveStudents(studentsWithSemesters: List<StudentWithSemesters>) {
val semesters = studentsWithSemesters.flatMap { it.semesters }
val students = studentsWithSemesters.map { it.student }
.map {
@ -104,16 +107,21 @@ class StudentRepository @Inject constructor(
}
}
}
.mapIndexed { index, student ->
if (index == 0) {
student.copy(isCurrent = true).apply { avatarColor = student.avatarColor }
} else student
}
semesterDb.insertSemesters(semesters)
return studentDb.insertAll(students)
appDatabase.withTransaction {
studentDb.resetCurrent()
semesterDb.insertSemesters(semesters)
studentDb.insertAll(students)
}
}
suspend fun switchStudent(studentWithSemesters: StudentWithSemesters) {
with(studentDb) {
resetCurrent()
updateCurrent(studentWithSemesters.student.id)
}
studentDb.switchCurrent(studentWithSemesters.student.id)
}
suspend fun logoutStudent(student: Student) = studentDb.delete(student)

View File

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

View File

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

View File

@ -41,18 +41,22 @@ class TimetableRepository @Inject constructor(
private val cacheKey = "timetable"
fun getTimetable(
student: Student, semester: Semester, start: LocalDate, end: LocalDate,
forceRefresh: Boolean, refreshAdditional: Boolean = false
student: Student,
semester: Semester,
start: LocalDate,
end: LocalDate,
forceRefresh: Boolean,
refreshAdditional: Boolean = false,
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { (timetable, additional, headers) ->
val refreshKey = getRefreshKey(cacheKey, semester, start, end)
val isShouldRefresh = refreshHelper.isShouldBeRefreshed(refreshKey)
val isExpired = refreshHelper.shouldBeRefreshed(refreshKey)
val isRefreshAdditional = additional.isEmpty() && refreshAdditional
val isNoData = timetable.isEmpty() || isRefreshAdditional || headers.isEmpty()
isNoData || forceRefresh || isShouldRefresh
isNoData || forceRefresh || isExpired
},
query = { getFullTimetableFromDatabase(student, semester, start, end) },
fetch = {
@ -79,8 +83,10 @@ class TimetableRepository @Inject constructor(
)
private fun getFullTimetableFromDatabase(
student: Student, semester: Semester,
start: LocalDate, end: LocalDate
student: Student,
semester: Semester,
start: LocalDate,
end: LocalDate,
): Flow<TimetableFull> {
val timetableFlow = timetableDb.loadAll(
diaryId = semester.diaryId,
@ -113,20 +119,11 @@ class TimetableRepository @Inject constructor(
private suspend fun refreshTimetable(
student: Student,
lessonsOld: List<Timetable>, lessonsNew: List<Timetable>
lessonsOld: List<Timetable>,
lessonsNew: List<Timetable>,
) {
val lessonsToRemove = lessonsOld uniqueSubtract lessonsNew
val lessonsToAdd = (lessonsNew uniqueSubtract lessonsOld).map { new ->
val matchingOld = lessonsOld.singleOrNull { new.start == it.start }
if (matchingOld != null) {
val useOldTeacher = new.teacher.isEmpty() && !new.changes && !matchingOld.changes
new.copy(
room = if (new.room.isEmpty()) matchingOld.room else new.room,
teacher = if (useOldTeacher) matchingOld.teacher
else new.teacher
)
} else new
}
val lessonsToAdd = lessonsNew uniqueSubtract lessonsOld
timetableDb.deleteAll(lessonsToRemove)
timetableDb.insertAll(lessonsToAdd)

View File

@ -11,6 +11,7 @@ import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.services.HiltBroadcastReceiver
import io.github.wulkanowy.services.sync.channels.UpcomingLessonsChannel.Companion.CHANNEL_ID
@ -32,6 +33,9 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() {
@Inject
lateinit var studentRepository: StudentRepository
@Inject
lateinit var preferencesRepository: PreferencesRepository
companion object {
const val NOTIFICATION_TYPE_CURRENT = 1
const val NOTIFICATION_TYPE_UPCOMING = 2
@ -68,6 +72,7 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() {
private fun prepareNotification(context: Context, intent: Intent) {
val type = intent.getIntExtra(LESSON_TYPE, 0)
val notificationId = intent.getIntExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id)
val isPersistent = preferencesRepository.isUpcomingLessonsNotificationsPersistent
if (type == NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION) {
return NotificationManagerCompat.from(context).cancel(notificationId)
@ -87,33 +92,57 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() {
Timber.d("TimetableNotification receive: type: $type, subject: $subject, start: ${start.toLocalDateTime()}, student: $studentId")
showNotification(context, notificationId, studentName,
showNotification(context, notificationId, isPersistent, studentName,
if (type == NOTIFICATION_TYPE_CURRENT) end else start, end - start,
context.getString(if (type == NOTIFICATION_TYPE_CURRENT) R.string.timetable_now else R.string.timetable_next, "($room) $subject".removePrefix("()")),
nextSubject?.let { context.getString(R.string.timetable_later, "($nextRoom) $nextSubject".removePrefix("()")) }
context.getString(
if (type == NOTIFICATION_TYPE_CURRENT) R.string.timetable_now else R.string.timetable_next,
"($room) $subject".removePrefix("()")
),
nextSubject?.let {
context.getString(
R.string.timetable_later,
"($nextRoom) $nextSubject".removePrefix("()")
)
}
)
}
private fun showNotification(context: Context, notificationId: Int, studentName: String?, countDown: Long, timeout: Long, title: String, next: String?) {
NotificationManagerCompat.from(context).notify(notificationId, NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle(title)
.setContentText(next)
.setAutoCancel(false)
.setOngoing(true)
.setWhen(countDown)
.apply {
if (Build.VERSION.SDK_INT >= N) setUsesChronometer(true)
}
.setTimeoutAfter(timeout)
.setSmallIcon(R.drawable.ic_stat_timetable)
.setColor(context.getCompatColor(R.color.colorPrimary))
.setStyle(NotificationCompat.InboxStyle().also {
it.setSummaryText(studentName)
it.addLine(next)
})
.setContentIntent(PendingIntent.getActivity(context, MainView.Section.TIMETABLE.id,
MainActivity.getStartIntent(context, MainView.Section.TIMETABLE, true), FLAG_UPDATE_CURRENT))
.build()
)
private fun showNotification(
context: Context,
notificationId: Int,
isPersistent: Boolean,
studentName: String?,
countDown: Long,
timeout: Long,
title: String,
next: String?
) {
NotificationManagerCompat.from(context)
.notify(notificationId, NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle(title)
.setContentText(next)
.setAutoCancel(false)
.setWhen(countDown)
.setOngoing(isPersistent)
.apply {
if (Build.VERSION.SDK_INT >= N) setUsesChronometer(true)
}
.setTimeoutAfter(timeout)
.setSmallIcon(R.drawable.ic_stat_timetable)
.setColor(context.getCompatColor(R.color.colorPrimary))
.setStyle(NotificationCompat.InboxStyle().also {
it.setSummaryText(studentName)
it.addLine(next)
})
.setContentIntent(
PendingIntent.getActivity(
context,
MainView.Section.TIMETABLE.id,
MainActivity.getStartIntent(context, MainView.Section.TIMETABLE, true),
FLAG_UPDATE_CURRENT
)
)
.build()
)
}
}

View File

@ -31,6 +31,7 @@ import io.github.wulkanowy.utils.nickOrName
import io.github.wulkanowy.utils.toTimestamp
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalDateTime.now
import javax.inject.Inject
@ -57,10 +58,13 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
lessons.sortedBy { it.start }.forEachIndexed { index, lesson ->
val upcomingTime = getUpcomingLessonTime(index, lessons, lesson)
cancelScheduledTo(
upcomingTime..lesson.start,
getRequestCode(upcomingTime, studentId)
range = upcomingTime..lesson.start,
requestCode = getRequestCode(upcomingTime, studentId)
)
cancelScheduledTo(
range = lesson.start..lesson.end,
requestCode = getRequestCode(lesson.start, studentId)
)
cancelScheduledTo(lesson.start..lesson.end, getRequestCode(lesson.start, studentId))
Timber.d("TimetableNotification canceled: type 1 & 2, subject: ${lesson.subject}, start: ${lesson.start}, student: $studentId")
}
@ -82,6 +86,11 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
return cancelScheduled(lessons, student)
}
if (lessons.firstOrNull()?.date?.isAfter(LocalDate.now().plusDays(2)) == true) {
Timber.d("Timetable notification scheduling skipped - lessons are too far")
return
}
withContext(dispatchersProvider.backgroundThread) {
lessons.groupBy { it.date }
.map { it.value.sortedBy { lesson -> lesson.start } }
@ -96,26 +105,26 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
if (lesson.start > now()) {
scheduleBroadcast(
intent,
student.studentId,
NOTIFICATION_TYPE_UPCOMING,
getUpcomingLessonTime(index, active, lesson)
intent = intent,
studentId = student.studentId,
notificationType = NOTIFICATION_TYPE_UPCOMING,
time = getUpcomingLessonTime(index, active, lesson)
)
}
if (lesson.end > now()) {
scheduleBroadcast(
intent,
student.studentId,
NOTIFICATION_TYPE_CURRENT,
lesson.start
intent = intent,
studentId = student.studentId,
notificationType = NOTIFICATION_TYPE_CURRENT,
time = lesson.start
)
if (active.lastIndex == index) {
scheduleBroadcast(
intent,
student.studentId,
NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION,
lesson.end
intent = intent,
studentId = student.studentId,
notificationType = NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION,
time = lesson.end
)
}
}
@ -143,17 +152,21 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
notificationType: Int,
time: LocalDateTime
) {
AlarmManagerCompat.setExactAndAllowWhileIdle(
alarmManager, RTC_WAKEUP, time.toTimestamp(),
PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also {
it.putExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id)
it.putExtra(LESSON_TYPE, notificationType)
}, FLAG_UPDATE_CURRENT)
)
Timber.d(
"TimetableNotification scheduled: type: $notificationType, subject: ${
intent.getStringExtra(LESSON_TITLE)
}, start: $time, student: $studentId"
)
try {
AlarmManagerCompat.setExactAndAllowWhileIdle(
alarmManager, RTC_WAKEUP, time.toTimestamp(),
PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also {
it.putExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id)
it.putExtra(LESSON_TYPE, notificationType)
}, FLAG_UPDATE_CURRENT)
)
Timber.d(
"TimetableNotification scheduled: type: $notificationType, subject: ${
intent.getStringExtra(LESSON_TITLE)
}, start: $time, student: $studentId"
)
} catch (e: IllegalStateException) {
Timber.e(e)
}
}
}

View File

@ -0,0 +1,24 @@
package io.github.wulkanowy.services.piggyback
import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.services.sync.SyncManager
import javax.inject.Inject
@AndroidEntryPoint
class VulcanNotificationListenerService : NotificationListenerService() {
@Inject
lateinit var syncManager: SyncManager
@Inject
lateinit var preferenceRepository: PreferencesRepository
override fun onNotificationPosted(statusBarNotification: StatusBarNotification?) {
if (statusBarNotification?.packageName == "pl.edu.vulcan.hebe" && preferenceRepository.isNotificationPiggybackEnabled) {
syncManager.startOneTimeSyncWorker()
}
}
}

View File

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

View File

@ -0,0 +1,145 @@
package io.github.wulkanowy.services.sync.notifications
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Notification
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotificationsData
import io.github.wulkanowy.data.pojos.NotificationData
import io.github.wulkanowy.data.pojos.OneNotificationData
import io.github.wulkanowy.data.repositories.NotificationRepository
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.getCompatBitmap
import io.github.wulkanowy.utils.getCompatColor
import io.github.wulkanowy.utils.nickOrName
import java.time.LocalDateTime
import javax.inject.Inject
import kotlin.random.Random
class AppNotificationManager @Inject constructor(
private val notificationManager: NotificationManagerCompat,
@ApplicationContext private val context: Context,
private val appInfo: AppInfo,
private val notificationRepository: NotificationRepository
) {
suspend fun sendNotification(notificationData: NotificationData, student: Student) =
when (notificationData) {
is OneNotificationData -> sendOneNotification(notificationData, student)
is MultipleNotificationsData -> sendMultipleNotifications(notificationData, student)
}
private suspend fun sendOneNotification(
notificationData: OneNotificationData,
student: Student
) {
val content = context.getString(
notificationData.contentStringRes,
*notificationData.contentValues.toTypedArray()
)
val title = context.getString(notificationData.titleStringRes)
val notification = getDefaultNotificationBuilder(notificationData)
.setContentTitle(title)
.setContentText(content)
.setStyle(
NotificationCompat.BigTextStyle()
.setSummaryText(student.nickOrName)
.bigText(content)
)
.build()
notificationManager.notify(Random.nextInt(Int.MAX_VALUE), notification)
saveNotification(title, content, notificationData, student)
}
private suspend fun sendMultipleNotifications(
notificationData: MultipleNotificationsData,
student: Student
) {
val groupType = notificationData.type.group ?: return
val group = "${groupType}_${student.id}"
val groupId = student.id * 100 + notificationData.type.ordinal
notificationData.lines.forEach { item ->
val title = context.resources.getQuantityString(notificationData.titleStringRes, 1)
val notification = getDefaultNotificationBuilder(notificationData)
.setContentTitle(title)
.setContentText(item)
.setStyle(
NotificationCompat.BigTextStyle()
.setSummaryText(student.nickOrName)
.bigText(item)
)
.setGroup(group)
.build()
notificationManager.notify(Random.nextInt(Int.MAX_VALUE), notification)
saveNotification(title, item, notificationData, student)
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return
val summaryNotification = getDefaultNotificationBuilder(notificationData)
.setSmallIcon(notificationData.icon)
.setGroup(group)
.setStyle(NotificationCompat.InboxStyle().setSummaryText(student.nickOrName))
.setGroupSummary(true)
.build()
notificationManager.notify(groupId.toInt(), summaryNotification)
}
@SuppressLint("InlinedApi")
private fun getDefaultNotificationBuilder(notificationData: NotificationData): NotificationCompat.Builder {
val pendingIntentsFlags = if (appInfo.systemVersion >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
return NotificationCompat.Builder(context, notificationData.type.channel)
.setLargeIcon(context.getCompatBitmap(notificationData.icon, R.color.colorPrimary))
.setSmallIcon(R.drawable.ic_stat_all)
.setAutoCancel(true)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setColor(context.getCompatColor(R.color.colorPrimary))
.setContentIntent(
PendingIntent.getActivity(
context,
notificationData.startMenu.id,
MainActivity.getStartIntent(context, notificationData.startMenu, true),
pendingIntentsFlags
)
)
}
private suspend fun saveNotification(
title: String,
content: String,
notificationData: NotificationData,
student: Student
) {
val notificationEntity = Notification(
studentId = student.id,
title = title,
content = content,
type = notificationData.type,
date = LocalDateTime.now()
)
notificationRepository.saveNotification(notificationEntity)
}
}

View File

@ -1,102 +0,0 @@
package io.github.wulkanowy.services.sync.notifications
import android.app.PendingIntent
import android.content.Context
import android.os.Build
import androidx.annotation.PluralsRes
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.data.pojos.Notification
import io.github.wulkanowy.data.pojos.OneNotification
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.utils.getCompatBitmap
import io.github.wulkanowy.utils.getCompatColor
import io.github.wulkanowy.utils.nickOrName
import kotlin.random.Random
abstract class BaseNotification(
private val context: Context,
private val notificationManager: NotificationManagerCompat,
) {
protected fun sendNotification(notification: Notification, student: Student) =
when (notification) {
is OneNotification -> sendOneNotification(notification, student)
is MultipleNotifications -> sendMultipleNotifications(notification, student)
}
private fun sendOneNotification(notification: OneNotification, student: Student?) {
notificationManager.notify(
Random.nextInt(Int.MAX_VALUE),
getNotificationBuilder(notification).apply {
val content = context.getString(
notification.contentStringRes,
*notification.contentValues.toTypedArray()
)
setContentTitle(context.getString(notification.titleStringRes))
setContentText(content)
setStyle(
NotificationCompat.BigTextStyle()
.setSummaryText(student?.nickOrName)
.bigText(content)
)
}.build()
)
}
private fun sendMultipleNotifications(notification: MultipleNotifications, student: Student) {
val group = notification.type.group + student.id
val groupId = student.id * 100 + notification.type.ordinal
notification.lines.forEach { item ->
notificationManager.notify(
Random.nextInt(Int.MAX_VALUE),
getNotificationBuilder(notification).apply {
setContentTitle(getQuantityString(notification.titleStringRes, 1))
setContentText(item)
setStyle(
NotificationCompat.BigTextStyle()
.setSummaryText(student.nickOrName)
.bigText(item)
)
setGroup(group)
}.build()
)
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return
notificationManager.notify(
groupId.toInt(),
getNotificationBuilder(notification).apply {
setSmallIcon(notification.icon)
setGroup(group)
setStyle(NotificationCompat.InboxStyle().setSummaryText(student.nickOrName))
setGroupSummary(true)
}.build()
)
}
private fun getNotificationBuilder(notification: Notification) = NotificationCompat
.Builder(context, notification.type.channel)
.setLargeIcon(context.getCompatBitmap(notification.icon, R.color.colorPrimary))
.setSmallIcon(R.drawable.ic_stat_all)
.setAutoCancel(true)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setColor(context.getCompatColor(R.color.colorPrimary))
.setContentIntent(
PendingIntent.getActivity(
context, notification.startMenu.id,
MainActivity.getStartIntent(context, notification.startMenu, true),
PendingIntent.FLAG_UPDATE_CURRENT
)
)
private fun getQuantityString(@PluralsRes id: Int, value: Int): String {
return context.resources.getQuantityString(id, value, value)
}
}

View File

@ -1,29 +1,25 @@
package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.data.pojos.MultipleNotificationsData
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalDateTime
import javax.inject.Inject
class NewConferenceNotification @Inject constructor(
@ApplicationContext private val context: Context,
notificationManager: NotificationManagerCompat,
) : BaseNotification(context, notificationManager) {
private val appNotificationManager: AppNotificationManager
) {
fun notify(items: List<Conference>, student: Student) {
suspend fun notify(items: List<Conference>, student: Student) {
val today = LocalDateTime.now()
val lines = items.filter { !it.date.isBefore(today) }.map {
"${it.date.toFormattedString("dd.MM")} - ${it.title}: ${it.subject}"
}.ifEmpty { return }
val notification = MultipleNotifications(
val notification = MultipleNotificationsData(
type = NotificationType.NEW_CONFERENCE,
icon = R.drawable.ic_more_conferences,
titleStringRes = R.plurals.conference_notify_new_item_title,
@ -33,6 +29,6 @@ class NewConferenceNotification @Inject constructor(
lines = lines
)
sendNotification(notification, student)
appNotificationManager.sendNotification(notification, student)
}
}

View File

@ -1,29 +1,25 @@
package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.data.pojos.MultipleNotificationsData
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalDate
import javax.inject.Inject
class NewExamNotification @Inject constructor(
@ApplicationContext private val context: Context,
notificationManager: NotificationManagerCompat,
) : BaseNotification(context, notificationManager) {
private val appNotificationManager: AppNotificationManager
) {
fun notify(items: List<Exam>, student: Student) {
suspend fun notify(items: List<Exam>, student: Student) {
val today = LocalDate.now()
val lines = items.filter { !it.date.isBefore(today) }.map {
"${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.description}"
}.ifEmpty { return }
val notification = MultipleNotifications(
val notification = MultipleNotificationsData(
type = NotificationType.NEW_EXAM,
icon = R.drawable.ic_main_exam,
titleStringRes = R.plurals.exam_notify_new_item_title,
@ -33,6 +29,6 @@ class NewExamNotification @Inject constructor(
lines = lines
)
sendNotification(notification, student)
appNotificationManager.sendNotification(notification, student)
}
}

View File

@ -1,23 +1,19 @@
package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.data.pojos.MultipleNotificationsData
import io.github.wulkanowy.ui.modules.main.MainView
import javax.inject.Inject
class NewGradeNotification @Inject constructor(
@ApplicationContext private val context: Context,
notificationManager: NotificationManagerCompat,
) : BaseNotification(context, notificationManager) {
private val appNotificationManager: AppNotificationManager
) {
fun notifyDetails(items: List<Grade>, student: Student) {
val notification = MultipleNotifications(
suspend fun notifyDetails(items: List<Grade>, student: Student) {
val notification = MultipleNotificationsData(
type = NotificationType.NEW_GRADE_DETAILS,
icon = R.drawable.ic_stat_grade,
titleStringRes = R.plurals.grade_new_items,
@ -29,11 +25,11 @@ class NewGradeNotification @Inject constructor(
}
)
sendNotification(notification, student)
appNotificationManager.sendNotification(notification, student)
}
fun notifyPredicted(items: List<GradeSummary>, student: Student) {
val notification = MultipleNotifications(
suspend fun notifyPredicted(items: List<GradeSummary>, student: Student) {
val notification = MultipleNotificationsData(
type = NotificationType.NEW_GRADE_PREDICTED,
icon = R.drawable.ic_stat_grade,
titleStringRes = R.plurals.grade_new_items_predicted,
@ -45,11 +41,11 @@ class NewGradeNotification @Inject constructor(
}
)
sendNotification(notification, student)
appNotificationManager.sendNotification(notification, student)
}
fun notifyFinal(items: List<GradeSummary>, student: Student) {
val notification = MultipleNotifications(
suspend fun notifyFinal(items: List<GradeSummary>, student: Student) {
val notification = MultipleNotificationsData(
type = NotificationType.NEW_GRADE_FINAL,
icon = R.drawable.ic_stat_grade,
titleStringRes = R.plurals.grade_new_items_final,
@ -61,6 +57,6 @@ class NewGradeNotification @Inject constructor(
}
)
sendNotification(notification, student)
appNotificationManager.sendNotification(notification, student)
}
}

View File

@ -1,29 +1,25 @@
package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.data.pojos.MultipleNotificationsData
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalDate
import javax.inject.Inject
class NewHomeworkNotification @Inject constructor(
@ApplicationContext private val context: Context,
notificationManager: NotificationManagerCompat,
) : BaseNotification(context, notificationManager) {
private val appNotificationManager: AppNotificationManager
) {
fun notify(items: List<Homework>, student: Student) {
suspend fun notify(items: List<Homework>, student: Student) {
val today = LocalDate.now()
val lines = items.filter { !it.date.isBefore(today) }.map {
"${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.content}"
}.ifEmpty { return }
val notification = MultipleNotifications(
val notification = MultipleNotificationsData(
type = NotificationType.NEW_HOMEWORK,
icon = R.drawable.ic_more_homework,
titleStringRes = R.plurals.homework_notify_new_item_title,
@ -33,6 +29,6 @@ class NewHomeworkNotification @Inject constructor(
lines = lines
)
sendNotification(notification, student)
appNotificationManager.sendNotification(notification, student)
}
}

View File

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

View File

@ -1,22 +1,18 @@
package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.data.pojos.MultipleNotificationsData
import io.github.wulkanowy.ui.modules.main.MainView
import javax.inject.Inject
class NewMessageNotification @Inject constructor(
@ApplicationContext private val context: Context,
notificationManager: NotificationManagerCompat,
) : BaseNotification(context, notificationManager) {
private val appNotificationManager: AppNotificationManager
) {
fun notify(items: List<Message>, student: Student) {
val notification = MultipleNotifications(
suspend fun notify(items: List<Message>, student: Student) {
val notification = MultipleNotificationsData(
type = NotificationType.NEW_MESSAGE,
icon = R.drawable.ic_stat_message,
titleStringRes = R.plurals.message_new_items,
@ -28,6 +24,6 @@ class NewMessageNotification @Inject constructor(
}
)
sendNotification(notification, student)
appNotificationManager.sendNotification(notification, student)
}
}

View File

@ -1,23 +1,19 @@
package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.data.pojos.MultipleNotificationsData
import io.github.wulkanowy.sdk.scrapper.notes.NoteCategory
import io.github.wulkanowy.ui.modules.main.MainView
import javax.inject.Inject
class NewNoteNotification @Inject constructor(
@ApplicationContext private val context: Context,
notificationManager: NotificationManagerCompat,
) : BaseNotification(context, notificationManager) {
private val appNotificationManager: AppNotificationManager
) {
fun notify(items: List<Note>, student: Student) {
val notification = MultipleNotifications(
suspend fun notify(items: List<Note>, student: Student) {
val notification = MultipleNotificationsData(
type = NotificationType.NEW_NOTE,
icon = R.drawable.ic_stat_note,
titleStringRes = when (NoteCategory.getByValue(items.first().categoryType)) {
@ -41,6 +37,6 @@ class NewNoteNotification @Inject constructor(
}
)
sendNotification(notification, student)
appNotificationManager.sendNotification(notification, student)
}
}

View File

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

View File

@ -8,8 +8,9 @@ import io.github.wulkanowy.services.sync.channels.NewHomeworkChannel
import io.github.wulkanowy.services.sync.channels.NewMessagesChannel
import io.github.wulkanowy.services.sync.channels.NewNotesChannel
import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel
import io.github.wulkanowy.services.sync.channels.PushChannel
enum class NotificationType(val group: String, val channel: String) {
enum class NotificationType(val group: String?, val channel: String) {
NEW_CONFERENCE("new_conferences_group", NewConferencesChannel.CHANNEL_ID),
NEW_EXAM("new_exam_group", NewExamChannel.CHANNEL_ID),
NEW_GRADE_DETAILS("new_grade_details_group", NewGradesChannel.CHANNEL_ID),
@ -20,4 +21,5 @@ enum class NotificationType(val group: String, val channel: String) {
NEW_MESSAGE("new_message_group", NewMessagesChannel.CHANNEL_ID),
NEW_NOTE("new_notes_group", NewNotesChannel.CHANNEL_ID),
NEW_ANNOUNCEMENT("new_school_announcements_group", NewSchoolAnnouncementsChannel.CHANNEL_ID),
PUSH(null, PushChannel.CHANNEL_ID)
}

View File

@ -9,9 +9,12 @@ import io.github.wulkanowy.utils.waitForResult
import java.time.LocalDate.now
import javax.inject.Inject
class AttendanceWork @Inject constructor(private val attendanceRepository: AttendanceRepository) : Work {
class AttendanceWork @Inject constructor(
private val attendanceRepository: AttendanceRepository
) : Work {
override suspend fun doWork(student: Student, semester: Semester) {
attendanceRepository.getAttendance(student, semester, now().monday, now().sunday, true).waitForResult()
attendanceRepository.getAttendance(student, semester, now().monday, now().sunday, true)
.waitForResult()
}
}

View File

@ -18,7 +18,7 @@ open class BasePresenter<T : BaseView>(
protected val studentRepository: StudentRepository
) : CoroutineScope {
private var job: Job = Job()
private var job = Job()
private val jobs = mutableMapOf<String, Job>()

View File

@ -8,7 +8,7 @@ import androidx.core.view.get
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.databinding.FragmentAccountBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsFragment
@ -75,9 +75,7 @@ class AccountFragment : BaseFragment<FragmentAccountBinding>(R.layout.fragment_a
}
}
override fun openAccountDetailsView(studentWithSemesters: StudentWithSemesters) {
(activity as? MainActivity)?.pushView(
AccountDetailsFragment.newInstance(studentWithSemesters)
)
override fun openAccountDetailsView(student: Student) {
(activity as? MainActivity)?.pushView(AccountDetailsFragment.newInstance(student))
}
}

View File

@ -28,7 +28,7 @@ class AccountPresenter @Inject constructor(
}
fun onItemSelected(studentWithSemesters: StudentWithSemesters) {
view?.openAccountDetailsView(studentWithSemesters)
view?.openAccountDetailsView(studentWithSemesters.student)
}
private fun loadData() {

View File

@ -1,6 +1,6 @@
package io.github.wulkanowy.ui.modules.account
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.ui.base.BaseView
interface AccountView : BaseView {
@ -11,5 +11,5 @@ interface AccountView : BaseView {
fun openLoginView()
fun openAccountDetailsView(studentWithSemesters: StudentWithSemesters)
fun openAccountDetailsView(student: Student)
}

View File

@ -37,9 +37,9 @@ class AccountDetailsFragment :
private const val ARGUMENT_KEY = "Data"
fun newInstance(studentWithSemesters: StudentWithSemesters) =
fun newInstance(student: Student) =
AccountDetailsFragment().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, studentWithSemesters) }
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, student) }
}
}
@ -51,7 +51,7 @@ class AccountDetailsFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentAccountDetailsBinding.bind(view)
presenter.onAttachView(this, requireArguments()[ARGUMENT_KEY] as StudentWithSemesters)
presenter.onAttachView(this, requireArguments()[ARGUMENT_KEY] as Student)
}
override fun initView() {
@ -121,10 +121,14 @@ class AccountDetailsFragment :
}
}
override fun popView() {
override fun popViewToMain() {
(requireActivity() as MainActivity).popView(2)
}
override fun popViewToAccounts() {
(requireActivity() as MainActivity).popView(1)
}
override fun recreateMainView() {
requireActivity().recreate()
}

View File

@ -2,6 +2,7 @@ package io.github.wulkanowy.ui.modules.account.accountdetails
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.services.sync.SyncManager
@ -27,9 +28,9 @@ class AccountDetailsPresenter @Inject constructor(
private var studentId: Long? = null
fun onAttachView(view: AccountDetailsView, studentWithSemesters: StudentWithSemesters) {
fun onAttachView(view: AccountDetailsView, student: Student) {
super.onAttachView(view)
studentId = studentWithSemesters.student.id
studentId = student.id
view.initView()
errorHandler.showErrorMessage = ::showErrorViewOnError
@ -118,7 +119,7 @@ class AccountDetailsPresenter @Inject constructor(
}
}
}.afterLoading {
view?.popView()
view?.popViewToMain()
}.launch("switch")
}
@ -151,11 +152,14 @@ class AccountDetailsPresenter @Inject constructor(
syncManager.stopSyncWorker()
openClearLoginView()
}
studentWithSemesters!!.student.isCurrent -> {
studentWithSemesters?.student?.isCurrent == true -> {
Timber.i("Logout result: Logout student and switch to another")
recreateMainView()
}
else -> Timber.i("Logout result: Logout student")
else -> {
Timber.i("Logout result: Logout student")
recreateMainView()
}
}
}
Status.ERROR -> {
@ -164,7 +168,11 @@ class AccountDetailsPresenter @Inject constructor(
}
}
}.afterLoading {
view?.popView()
if (studentWithSemesters?.student?.isCurrent == true) {
view?.popViewToMain()
} else {
view?.popViewToAccounts()
}
}.launch("logout")
}

View File

@ -15,7 +15,9 @@ interface AccountDetailsView : BaseView {
fun showLogoutConfirmDialog()
fun popView()
fun popViewToMain()
fun popViewToAccounts()
fun recreateMainView()

View File

@ -245,7 +245,9 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
presenter.onDateSet(date.year, date.monthValue, date.dayOfMonth)
}
datePicker.show(this@AttendanceFragment.parentFragmentManager, null)
if (!parentFragmentManager.isStateSaved) {
datePicker.show(parentFragmentManager, null)
}
}
override fun showExcuseDialog() {

View File

@ -152,6 +152,8 @@ class AttendancePresenter @Inject constructor(
fun onExcuseDialogSubmit(reason: String) {
view?.finishActionMode()
if (attendanceToExcuseList.isEmpty()) return
if (isVulcanExcusedFunctionEnabled) {
excuseAbsence(
reason = reason.takeIf { it.isNotBlank() },
@ -234,6 +236,7 @@ class AttendancePresenter @Inject constructor(
enableSwipe(true)
showRefresh(true)
showProgress(false)
showErrorView(false)
showEmpty(filteredAttendance.isEmpty())
showContent(filteredAttendance.isNotEmpty())
updateData(filteredAttendance.sortedBy { item -> item.number })

View File

@ -82,7 +82,13 @@ class AttendanceSummaryPresenter @Inject constructor(
flowWithResourceIn {
val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student)
attendanceSummaryRepository.getAttendanceSummary(student, semester, subjectId, forceRefresh)
attendanceSummaryRepository.getAttendanceSummary(
student = student,
semester = semester,
subjectId = subjectId,
forceRefresh = forceRefresh
)
}.onEach {
when (it.status) {
Status.LOADING -> {
@ -92,6 +98,7 @@ class AttendanceSummaryPresenter @Inject constructor(
showRefresh(true)
showProgress(false)
showContent(true)
showErrorView(false)
updateDataSet(sortItems(it.data))
}
}
@ -99,6 +106,7 @@ class AttendanceSummaryPresenter @Inject constructor(
Status.SUCCESS -> {
Timber.i("Loading attendance summary result: Success")
view?.apply {
showErrorView(false)
showEmpty(it.data!!.isEmpty())
showContent(it.data.isNotEmpty())
updateDataSet(sortItems(it.data))

View File

@ -14,6 +14,8 @@ class ConferenceAdapter @Inject constructor() :
var items = emptyList<Conference>()
var onItemClickListener: (Conference) -> Unit = {}
override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder(
@ -28,7 +30,10 @@ class ConferenceAdapter @Inject constructor() :
conferenceItemTitle.text = item.title
conferenceItemSubject.text = item.subject
conferenceItemContent.text = item.agenda
conferenceItemContent.visibility = if (item.agenda.isBlank()) View.GONE else View.VISIBLE
conferenceItemContent.visibility =
if (item.agenda.isBlank()) View.GONE else View.VISIBLE
root.setOnClickListener { onItemClickListener(item) }
}
}

View File

@ -0,0 +1,60 @@
package io.github.wulkanowy.ui.modules.conference
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.databinding.DialogConferenceBinding
import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.toFormattedString
class ConferenceDialog : DialogFragment() {
private var binding: DialogConferenceBinding by lifecycleAwareVariable()
private lateinit var conference: Conference
companion object {
private const val ARGUMENT_KEY = "item"
fun newInstance(conference: Conference) = ConferenceDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, conference) }
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
arguments?.let {
conference = it.getSerializable(ARGUMENT_KEY) as Conference
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogConferenceBinding.inflate(inflater).also { binding = it }.root
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding) {
conferenceDialogClose.setOnClickListener { dismiss() }
conferenceDialogSubjectValue.text = conference.subject
conferenceDialogDateValue.text = conference.date.toFormattedString("dd.MM.yyyy HH:mm")
conferenceDialogHeaderValue.text = conference.title
conferenceDialogAgendaValue.text = conference.agenda
conferenceDialogPresentValue.text = conference.presentOnConference
conferenceDialogPresentValue.isVisible = conference.presentOnConference.isNotBlank()
conferenceDialogPresentTitle.isVisible = conference.presentOnConference.isNotBlank()
conferenceDialogAgendaValue.isVisible = conference.agenda.isNotBlank()
conferenceDialogAgendaTitle.isVisible = conference.agenda.isNotBlank()
}
}
}

View File

@ -8,6 +8,7 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.databinding.FragmentConferenceBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.getThemeAttrColor
@ -41,6 +42,8 @@ class ConferenceFragment : BaseFragment<FragmentConferenceBinding>(R.layout.frag
}
override fun initView() {
conferencesAdapter.onItemClickListener = presenter::onItemSelected
with(binding.conferenceRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = conferencesAdapter
@ -50,7 +53,11 @@ class ConferenceFragment : BaseFragment<FragmentConferenceBinding>(R.layout.frag
with(binding) {
conferenceSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
conferenceSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
conferenceSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
conferenceSwipe.setProgressBackgroundColorSchemeColor(
requireContext().getThemeAttrColor(
R.attr.colorSwipeRefresh
)
)
conferenceErrorRetry.setOnClickListener { presenter.onRetry() }
conferenceErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}
@ -98,6 +105,10 @@ class ConferenceFragment : BaseFragment<FragmentConferenceBinding>(R.layout.frag
binding.conferenceRecycler.visibility = if (show) View.VISIBLE else View.GONE
}
override fun openConferenceDialog(conference: Conference) {
(activity as? MainActivity)?.showDialogFragment(ConferenceDialog.newInstance(conference))
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.ui.modules.conference
import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.data.repositories.ConferenceRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
@ -43,6 +44,10 @@ class ConferencePresenter @Inject constructor(
loadData(true)
}
fun onItemSelected(conference: Conference) {
view?.openConferenceDialog(conference)
}
fun onDetailsClick() {
view?.showErrorDetailsDialog(lastError)
}

View File

@ -26,4 +26,6 @@ interface ConferenceView : BaseView {
fun enableSwipe(enable: Boolean)
fun showContent(show: Boolean)
fun openConferenceDialog(conference: Conference)
}

View File

@ -14,6 +14,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.db.entities.TimetableHeader
import io.github.wulkanowy.databinding.ItemDashboardAccountBinding
@ -30,6 +31,7 @@ import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.left
import io.github.wulkanowy.utils.nickOrName
import io.github.wulkanowy.utils.toFormattedString
import timber.log.Timber
import java.time.Duration
import java.time.LocalDate
import java.time.LocalDateTime
@ -41,7 +43,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
private var lessonsTimer: Timer? = null
var onAccountTileClickListener: () -> Unit = {}
var onAccountTileClickListener: (Student) -> Unit = {}
var onLuckyNumberTileClickListener: () -> Unit = {}
@ -51,7 +53,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
var onAttendanceTileClickListener: () -> Unit = {}
var onLessonsTileClickListener: () -> Unit = {}
var onLessonsTileClickListener: (LocalDate) -> Unit = {}
var onHomeworkTileClickListener: () -> Unit = {}
@ -152,7 +154,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
dashboardAccountItemName.text = student?.nickOrName.orEmpty()
dashboardAccountItemSchoolName.text = student?.schoolName.orEmpty()
root.setOnClickListener { onAccountTileClickListener() }
root.setOnClickListener { student?.let(onAccountTileClickListener) }
}
}
@ -169,39 +171,43 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
val isLoading = item.isLoading
val binding = horizontalGroupViewHolder.binding
val context = binding.root.context
val isLoadingVisible =
(isLoading && !item.isDataLoaded) || (isLoading && !item.isFullDataLoaded)
val attendanceColor = when {
attendancePercentage ?: 0.0 <= ATTENDANCE_SECOND_WARNING_THRESHOLD -> {
attendancePercentage == null || attendancePercentage == .0 -> {
context.getThemeAttrColor(R.attr.colorOnSurface)
}
attendancePercentage <= ATTENDANCE_SECOND_WARNING_THRESHOLD -> {
context.getThemeAttrColor(R.attr.colorPrimary)
}
attendancePercentage ?: 0.0 <= ATTENDANCE_FIRST_WARNING_THRESHOLD -> {
attendancePercentage <= ATTENDANCE_FIRST_WARNING_THRESHOLD -> {
context.getThemeAttrColor(R.attr.colorTimetableChange)
}
else -> context.getThemeAttrColor(R.attr.colorOnSurface)
}
val attendanceString = if (attendancePercentage == null || attendancePercentage == .0) {
context.getString(R.string.dashboard_horizontal_group_no_data)
} else {
"%.2f%%".format(attendancePercentage)
}
with(binding.dashboardHorizontalGroupItemAttendanceValue) {
text = "%.2f%%".format(attendancePercentage)
text = attendanceString
setTextColor(attendanceColor)
}
with(binding) {
dashboardHorizontalGroupItemMessageValue.text = unreadMessagesCount.toString()
dashboardHorizontalGroupItemLuckyValue.text = if (luckyNumber == -1) {
context.getString(R.string.dashboard_horizontal_group_no_lukcy_number)
dashboardHorizontalGroupItemLuckyValue.text = if (luckyNumber == 0) {
context.getString(R.string.dashboard_horizontal_group_no_data)
} else luckyNumber?.toString()
if (dashboardHorizontalGroupItemInfoContainer.isVisible != (error != null || isLoading)) {
dashboardHorizontalGroupItemInfoContainer.isVisible = error != null || isLoading
}
if (dashboardHorizontalGroupItemInfoProgress.isVisible != isLoading) {
dashboardHorizontalGroupItemInfoProgress.isVisible = isLoading
}
dashboardHorizontalGroupItemInfoContainer.isVisible = error != null || isLoadingVisible
dashboardHorizontalGroupItemInfoProgress.isVisible = isLoadingVisible
dashboardHorizontalGroupItemInfoErrorText.isVisible = error != null
with(dashboardHorizontalGroupItemLuckyContainer) {
isVisible = error == null && !isLoading && luckyNumber != null
isVisible = luckyNumber != null && luckyNumber != -1 && !isLoadingVisible
setOnClickListener { onLuckyNumberTileClickListener() }
updateLayoutParams<ViewGroup.MarginLayoutParams> {
@ -216,7 +222,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
}
with(dashboardHorizontalGroupItemAttendanceContainer) {
isVisible = error == null && !isLoading && attendancePercentage != null
isVisible =
attendancePercentage != null && attendancePercentage != -1.0 && !isLoadingVisible
updateLayoutParams<ConstraintLayout.LayoutParams> {
matchConstraintPercentWidth = when {
luckyNumber == null && unreadMessagesCount == null -> 1.0f
@ -228,7 +235,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
}
with(dashboardHorizontalGroupItemMessageContainer) {
isVisible = error == null && !isLoading && unreadMessagesCount != null
isVisible =
unreadMessagesCount != null && unreadMessagesCount != -1 && !isLoadingVisible
setOnClickListener { onMessageTileClickListener() }
}
}
@ -267,10 +275,12 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
val item = items[position] as DashboardItem.Lessons
val timetableFull = item.lessons
val binding = lessonsViewHolder.binding
var dateToNavigate = LocalDate.now()
fun updateLessonState() {
val currentDateTime = LocalDateTime.now()
val currentDate = LocalDate.now()
val tomorrowDate = currentDate.plusDays(1)
val currentTimetable = timetableFull?.lessons
.orEmpty()
@ -288,22 +298,27 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
when {
currentTimetable.isNotEmpty() -> {
dateToNavigate = currentDate
updateLessonView(item, currentTimetable, binding)
binding.dashboardLessonsItemTitleTomorrow.isVisible = false
}
currentDayHeader != null && currentDayHeader.content.isNotBlank() -> {
updateLessonView(item, emptyList(), binding, currentDayHeader)
binding.dashboardLessonsItemTitleTomorrow.isVisible = false
}
tomorrowTimetable.isNotEmpty() -> {
dateToNavigate = tomorrowDate
updateLessonView(item, tomorrowTimetable, binding)
binding.dashboardLessonsItemTitleTomorrow.isVisible = true
}
currentDayHeader != null && currentDayHeader.content.isNotBlank() -> {
dateToNavigate = currentDate
updateLessonView(item, emptyList(), binding, currentDayHeader)
binding.dashboardLessonsItemTitleTomorrow.isVisible = false
}
tomorrowDayHeader != null && tomorrowDayHeader.content.isNotBlank() -> {
dateToNavigate = tomorrowDate
updateLessonView(item, emptyList(), binding, tomorrowDayHeader)
binding.dashboardLessonsItemTitleTomorrow.isVisible = true
}
else -> {
dateToNavigate = tomorrowDate
updateLessonView(item, emptyList(), binding)
binding.dashboardLessonsItemTitleTomorrow.isVisible =
!(item.isLoading && item.error == null)
@ -318,7 +333,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
Handler(Looper.getMainLooper()).post { updateLessonState() }
}
binding.root.setOnClickListener { onLessonsTileClickListener() }
binding.root.setOnClickListener { onLessonsTileClickListener(dateToNavigate) }
}
private fun updateLessonView(
@ -348,6 +363,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
}
}
@SuppressLint("SetTextI18n")
private fun updateFirstLessonView(
binding: ItemDashboardLessonsBinding,
firstLesson: Timetable?,
@ -367,7 +383,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
firstLesson ?: return
val minutesToStartLesson =
Duration.between(currentDateTime, firstLesson.start).toMinutes()
Duration.between(currentDateTime, firstLesson.start).toMinutes() + 1
val isFirstTimeVisible: Boolean
val isFirstTimeRangeVisible: Boolean
val firstTimeText: String
@ -376,12 +392,12 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
val firstTitleAndValueTextColor: Int
val firstTitleAndValueTextFont: Typeface
if (currentDateTime.isBefore(firstLesson.start)) {
if (currentDateTime < firstLesson.start) {
if (minutesToStartLesson > 60) {
val formattedStartTime = firstLesson.start.toFormattedString("HH:mm")
val formattedEndTime = firstLesson.end.toFormattedString("HH:mm")
firstTimeRangeText = "${formattedStartTime}-${formattedEndTime}"
firstTimeRangeText = "$formattedStartTime - $formattedEndTime"
firstTimeText = ""
isFirstTimeRangeVisible = true
@ -421,7 +437,10 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
}
}
} else {
val minutesToEndLesson = firstLesson.left!!.toMinutes()
val minutesToEndLesson = firstLesson.left?.toMinutes()?.plus(1) ?: run {
Timber.e(IllegalArgumentException("Lesson left is null. START ${firstLesson.start} ; END ${firstLesson.end} ; CURRENT ${LocalDateTime.now()}"))
0
}
firstTimeText = context.resources.getQuantityString(
R.plurals.dashboard_timetable_first_lesson_time_more_minutes,
@ -454,11 +473,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
with(binding.dashboardLessonsItemFirstValue) {
setTextColor(firstTitleAndValueTextColor)
typeface = firstTitleAndValueTextFont
text = context.getString(
R.string.dashboard_timetable_lesson_value,
firstLesson.subject,
firstLesson.room
)
text =
"${firstLesson.subject} ${if (firstLesson.room.isNotBlank()) "(${firstLesson.room})" else ""}"
}
}
@ -472,13 +488,11 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
val formattedStartTime = secondLesson?.start?.toFormattedString("HH:mm")
val formattedEndTime = secondLesson?.end?.toFormattedString("HH:mm")
val secondTimeText = "${formattedStartTime}-${formattedEndTime}"
val secondTimeText = "$formattedStartTime - $formattedEndTime"
val secondValueText = if (secondLesson != null) {
context.getString(
R.string.dashboard_timetable_lesson_value,
secondLesson.subject,
secondLesson.room
)
val roomString = if (secondLesson.room.isNotBlank()) "(${secondLesson.room})" else ""
"${secondLesson.subject} $roomString"
} else {
context.getString(R.string.dashboard_timetable_second_lesson_value_end)
}

View File

@ -14,7 +14,7 @@ import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.FragmentDashboardBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.account.AccountFragment
import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsFragment
import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment
import io.github.wulkanowy.ui.modules.conference.ConferenceFragment
import io.github.wulkanowy.ui.modules.exam.ExamFragment
@ -24,6 +24,7 @@ import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.notificationscenter.NotificationsCenterFragment
import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
import io.github.wulkanowy.utils.capitalise
@ -70,14 +71,13 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
override fun initView() {
val mainActivity = requireActivity() as MainActivity
val itemTouchHelper = ItemTouchHelper(
DashboardItemMoveCallback(
dashboardAdapter,
presenter::onDragAndDropEnd
)
DashboardItemMoveCallback(dashboardAdapter, presenter::onDragAndDropEnd)
)
dashboardAdapter.apply {
onAccountTileClickListener = { mainActivity.pushView(AccountFragment.newInstance()) }
onAccountTileClickListener = {
mainActivity.pushView(AccountDetailsFragment.newInstance(it))
}
onLuckyNumberTileClickListener = {
mainActivity.pushView(LuckyNumberFragment.newInstance())
}
@ -85,7 +85,9 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
onAttendanceTileClickListener = {
mainActivity.pushView(AttendanceSummaryFragment.newInstance())
}
onLessonsTileClickListener = { mainActivity.pushView(TimetableFragment.newInstance()) }
onLessonsTileClickListener = {
mainActivity.pushView(TimetableFragment.newInstance(it))
}
onGradeTileClickListener = { mainActivity.pushView(GradeFragment.newInstance()) }
onHomeworkTileClickListener = { mainActivity.pushView(HomeworkFragment.newInstance()) }
onAnnouncementsTileClickListener = {
@ -119,6 +121,7 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.dashboard_menu_tiles -> presenter.onDashboardTileSettingsSelected()
R.id.dashboard_menu_notifaction_list -> presenter.onNotificationsCenterSelected()
else -> false
}
}
@ -181,6 +184,10 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
if (::presenter.isInitialized) presenter.onViewReselected()
}
override fun openNotificationsCenterView() {
(requireActivity() as MainActivity).pushView(NotificationsCenterFragment.newInstance())
}
override fun onDestroyView() {
dashboardAdapter.clearTimers()
presenter.onDetachView()

View File

@ -35,6 +35,9 @@ sealed class DashboardItem(val type: Type) {
override val isDataLoaded
get() = unreadMessagesCount != null || attendancePercentage != null || luckyNumber != null
val isFullDataLoaded
get() = luckyNumber != -1 && attendancePercentage != -1.0 && unreadMessagesCount != -1
}
data class Grades(

View File

@ -2,6 +2,8 @@ package io.github.wulkanowy.ui.modules.dashboard
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository
import io.github.wulkanowy.data.repositories.ConferenceRepository
@ -18,11 +20,18 @@ import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.calculatePercentage
import io.github.wulkanowy.utils.flowWithResource
import io.github.wulkanowy.utils.flowWithResourceIn
import io.github.wulkanowy.utils.nextOrSameSchoolDay
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
import java.time.LocalDate
import java.time.LocalDateTime
@ -48,9 +57,11 @@ class DashboardPresenter @Inject constructor(
private val dashboardItemRefreshLoadedList = mutableListOf<DashboardItem>()
private lateinit var dashboardItemsToLoad: Set<DashboardItem.Type>
private var dashboardItemsToLoad = emptySet<DashboardItem.Type>()
private var dashboardTilesToLoad: Set<DashboardItem.Tile> = emptySet()
private var dashboardTileLoadedList = emptySet<DashboardItem.Tile>()
private val firstLoadedItemList = mutableListOf<DashboardItem.Type>()
private lateinit var lastError: Throwable
@ -69,8 +80,10 @@ class DashboardPresenter @Inject constructor(
}
fun onDragAndDropEnd(list: List<DashboardItem>) {
dashboardItemLoadedList.clear()
dashboardItemLoadedList.addAll(list)
with(dashboardItemLoadedList) {
clear()
addAll(list)
}
val positionList =
list.mapIndexed { index, dashboardItem -> Pair(dashboardItem.type, index) }.toMap()
@ -78,87 +91,102 @@ class DashboardPresenter @Inject constructor(
preferencesRepository.dashboardItemsPosition = positionList
}
fun loadData(forceRefresh: Boolean = false, tilesToLoad: Set<DashboardItem.Tile>) {
val oldDashboardDataToLoad = dashboardTilesToLoad
fun loadData(
tilesToLoad: Set<DashboardItem.Tile>,
forceRefresh: Boolean = false,
) {
val oldDashboardTileLoadedList = dashboardTileLoadedList
dashboardItemsToLoad = tilesToLoad.map { it.toDashboardItemType() }.toSet()
dashboardTileLoadedList = tilesToLoad
dashboardTilesToLoad = tilesToLoad
dashboardItemsToLoad = dashboardTilesToLoad.map { it.toDashboardItemType() }.toSet()
val itemsToLoad = generateDashboardTileListToLoad(
dashboardTilesToLoad = tilesToLoad,
dashboardLoadedTiles = oldDashboardTileLoadedList,
forceRefresh = forceRefresh
).map { it.toDashboardItemType() }
removeUnselectedTiles()
val newTileList = generateTileListToLoad(oldDashboardDataToLoad, forceRefresh)
loadTiles(forceRefresh, newTileList)
removeUnselectedTiles(tilesToLoad.toList())
loadTiles(tileList = itemsToLoad, forceRefresh = forceRefresh)
}
private fun removeUnselectedTiles() {
val isLuckyNumberToLoad =
dashboardTilesToLoad.any { it == DashboardItem.Tile.LUCKY_NUMBER }
val isMessagesToLoad =
dashboardTilesToLoad.any { it == DashboardItem.Tile.MESSAGES }
val isAttendanceToLoad =
dashboardTilesToLoad.any { it == DashboardItem.Tile.ATTENDANCE }
private fun generateDashboardTileListToLoad(
dashboardTilesToLoad: Set<DashboardItem.Tile>,
dashboardLoadedTiles: Set<DashboardItem.Tile>,
forceRefresh: Boolean
) = dashboardTilesToLoad.filter { newItemToLoad ->
dashboardLoadedTiles.none { it == newItemToLoad } || forceRefresh
}
private fun removeUnselectedTiles(tilesToLoad: List<DashboardItem.Tile>) {
dashboardItemLoadedList.removeAll { loadedTile -> dashboardItemsToLoad.none { it == loadedTile.type } }
val horizontalGroup =
dashboardItemLoadedList.find { it is DashboardItem.HorizontalGroup } as DashboardItem.HorizontalGroup?
if (horizontalGroup != null) {
val horizontalIndex = dashboardItemLoadedList.indexOf(horizontalGroup)
dashboardItemLoadedList.remove(horizontalGroup)
val isLuckyNumberToLoad = DashboardItem.Tile.LUCKY_NUMBER in tilesToLoad
val isMessagesToLoad = DashboardItem.Tile.MESSAGES in tilesToLoad
val isAttendanceToLoad = DashboardItem.Tile.ATTENDANCE in tilesToLoad
var updatedHorizontalGroup = horizontalGroup
val horizontalGroupIndex = dashboardItemLoadedList.indexOf(horizontalGroup)
if (horizontalGroup.luckyNumber != null && !isLuckyNumberToLoad) {
updatedHorizontalGroup = updatedHorizontalGroup.copy(luckyNumber = null)
val newHorizontalGroup = horizontalGroup.copy(
attendancePercentage = horizontalGroup.attendancePercentage.takeIf { isAttendanceToLoad },
unreadMessagesCount = horizontalGroup.unreadMessagesCount.takeIf { isMessagesToLoad },
luckyNumber = horizontalGroup.luckyNumber.takeIf { isLuckyNumberToLoad }
)
with(dashboardItemLoadedList) {
removeAt(horizontalGroupIndex)
add(horizontalGroupIndex, newHorizontalGroup)
}
if (horizontalGroup.attendancePercentage != null && !isAttendanceToLoad) {
updatedHorizontalGroup = updatedHorizontalGroup.copy(attendancePercentage = null)
}
if (horizontalGroup.unreadMessagesCount != null && !isMessagesToLoad) {
updatedHorizontalGroup = updatedHorizontalGroup.copy(unreadMessagesCount = null)
}
if (horizontalGroup.error != null) {
updatedHorizontalGroup = updatedHorizontalGroup.copy(error = null, isLoading = true)
}
dashboardItemLoadedList.add(horizontalIndex, updatedHorizontalGroup)
}
view?.updateData(dashboardItemLoadedList)
}
private fun loadTiles(forceRefresh: Boolean, tileList: List<DashboardItem.Tile>) {
tileList.forEach {
when (it) {
DashboardItem.Tile.ACCOUNT -> loadCurrentAccount(forceRefresh)
DashboardItem.Tile.LUCKY_NUMBER -> loadLuckyNumber(forceRefresh)
DashboardItem.Tile.MESSAGES -> loadMessages(forceRefresh)
DashboardItem.Tile.ATTENDANCE -> loadAttendance(forceRefresh)
DashboardItem.Tile.LESSONS -> loadLessons(forceRefresh)
DashboardItem.Tile.GRADES -> loadGrades(forceRefresh)
DashboardItem.Tile.HOMEWORK -> loadHomework(forceRefresh)
DashboardItem.Tile.ANNOUNCEMENTS -> loadSchoolAnnouncements(forceRefresh)
DashboardItem.Tile.EXAMS -> loadExams(forceRefresh)
DashboardItem.Tile.CONFERENCES -> loadConferences(forceRefresh)
DashboardItem.Tile.ADS -> TODO()
private fun loadTiles(
tileList: List<DashboardItem.Type>,
forceRefresh: Boolean
) {
launch {
Timber.i("Loading dashboard account data started")
val student = runCatching { studentRepository.getCurrentStudent(true) }
.onFailure {
Timber.i("Loading dashboard account result: An exception occurred")
errorHandler.dispatch(it)
updateData(DashboardItem.Account(error = it), forceRefresh)
}
.onSuccess { Timber.i("Loading dashboard account result: Success") }
.getOrNull() ?: return@launch
tileList.forEach {
when (it) {
DashboardItem.Type.ACCOUNT -> {
updateData(DashboardItem.Account(student), forceRefresh)
}
DashboardItem.Type.HORIZONTAL_GROUP -> {
loadHorizontalGroup(student, forceRefresh)
}
DashboardItem.Type.LESSONS -> loadLessons(student, forceRefresh)
DashboardItem.Type.GRADES -> loadGrades(student, forceRefresh)
DashboardItem.Type.HOMEWORK -> loadHomework(student, forceRefresh)
DashboardItem.Type.ANNOUNCEMENTS -> {
loadSchoolAnnouncements(student, forceRefresh)
}
DashboardItem.Type.EXAMS -> loadExams(student, forceRefresh)
DashboardItem.Type.CONFERENCES -> {
loadConferences(student, forceRefresh)
}
DashboardItem.Type.ADS -> TODO()
}
}
}
}
private fun generateTileListToLoad(
oldDashboardTileToLoad: Set<DashboardItem.Tile>,
forceRefresh: Boolean
) = dashboardTilesToLoad.filter { newTileToLoad ->
oldDashboardTileToLoad.none { it == newTileToLoad } || forceRefresh
}
fun onSwipeRefresh() {
Timber.i("Force refreshing the dashboard")
loadData(true, preferencesRepository.selectedDashboardTiles)
loadData(preferencesRepository.selectedDashboardTiles, forceRefresh = true)
}
fun onRetry() {
@ -166,7 +194,7 @@ class DashboardPresenter @Inject constructor(
showErrorView(false)
showProgress(true)
}
loadData(true, preferencesRepository.selectedDashboardTiles)
loadData(preferencesRepository.selectedDashboardTiles, forceRefresh = true)
}
fun onViewReselected() {
@ -181,6 +209,11 @@ class DashboardPresenter @Inject constructor(
view?.showErrorDetailsDialog(lastError)
}
fun onNotificationsCenterSelected(): Boolean {
view?.openNotificationsCenterView()
return true
}
fun onDashboardTileSettingsSelected(): Boolean {
view?.showDashboardTileSettings(preferencesRepository.selectedDashboardTiles.toList())
return true
@ -192,139 +225,86 @@ class DashboardPresenter @Inject constructor(
}.toSet()
}
private fun loadCurrentAccount(forceRefresh: Boolean) {
flowWithResource { studentRepository.getCurrentStudent(false) }
private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) {
flow {
val semester = semesterRepository.getCurrentSemester(student)
val selectedTiles = preferencesRepository.selectedDashboardTiles
val luckyNumberFlow = luckyNumberRepository.getLuckyNumber(student, forceRefresh)
.map {
if (it.data == null) {
it.copy(data = LuckyNumber(0, LocalDate.now(), 0))
} else it
}
.takeIf { DashboardItem.Tile.LUCKY_NUMBER in selectedTiles } ?: flowOf(null)
val messageFLow = messageRepository.getMessages(
student = student,
semester = semester,
folder = MessageFolder.RECEIVED,
forceRefresh = forceRefresh
).takeIf { DashboardItem.Tile.MESSAGES in selectedTiles } ?: flowOf(null)
val attendanceFlow = attendanceSummaryRepository.getAttendanceSummary(
student = student,
semester = semester,
subjectId = -1,
forceRefresh = forceRefresh
).takeIf { DashboardItem.Tile.ATTENDANCE in selectedTiles } ?: flowOf(null)
emitAll(
combine(
luckyNumberFlow,
messageFLow,
attendanceFlow
) { luckyNumberResource, messageResource, attendanceResource ->
val error =
luckyNumberResource?.error ?: messageResource?.error ?: attendanceResource?.error
error?.let { throw it }
val luckyNumber = luckyNumberResource?.data?.luckyNumber
val messageCount = messageResource?.data?.count { it.unread }
val attendancePercentage = attendanceResource?.data?.calculatePercentage()
val isLoading =
luckyNumberResource?.status == Status.LOADING || messageResource?.status == Status.LOADING || attendanceResource?.status == Status.LOADING
DashboardItem.HorizontalGroup(
isLoading = isLoading,
attendancePercentage = if (attendancePercentage == 0.0 && isLoading) -1.0 else attendancePercentage,
unreadMessagesCount = if (messageCount == 0 && isLoading) -1 else messageCount,
luckyNumber = if (luckyNumber == 0 && isLoading) -1 else luckyNumber
)
})
}
.filterNot { it.isLoading && forceRefresh }
.distinctUntilChanged()
.onEach {
when (it.status) {
Status.LOADING -> {
Timber.i("Loading dashboard account data started")
if (forceRefresh) return@onEach
updateData(DashboardItem.Account(it.data, isLoading = true), forceRefresh)
}
Status.SUCCESS -> {
Timber.i("Loading dashboard account result: Success")
updateData(DashboardItem.Account(it.data), forceRefresh)
}
Status.ERROR -> {
Timber.i("Loading dashboard account result: An exception occurred")
errorHandler.dispatch(it.error!!)
updateData(DashboardItem.Account(error = it.error), forceRefresh)
updateData(it, forceRefresh)
if (it.isLoading) {
Timber.i("Loading horizontal group data started")
if (it.isFullDataLoaded) {
firstLoadedItemList += DashboardItem.Type.HORIZONTAL_GROUP
}
} else {
Timber.i("Loading horizontal group result: Success")
}
}
.launch("dashboard_account")
}
private fun loadLuckyNumber(forceRefresh: Boolean) {
flowWithResourceIn {
val student = studentRepository.getCurrentStudent(true)
luckyNumberRepository.getLuckyNumber(student, forceRefresh)
}.onEach {
when (it.status) {
Status.LOADING -> {
Timber.i("Loading dashboard lucky number data started")
if (forceRefresh) return@onEach
processHorizontalGroupData(
luckyNumber = it.data?.luckyNumber,
isLoading = true,
forceRefresh = forceRefresh
)
}
Status.SUCCESS -> {
Timber.i("Loading dashboard lucky number result: Success")
processHorizontalGroupData(
luckyNumber = it.data?.luckyNumber ?: -1,
forceRefresh = forceRefresh
)
}
Status.ERROR -> {
Timber.i("Loading dashboard lucky number result: An exception occurred")
errorHandler.dispatch(it.error!!)
processHorizontalGroupData(error = it.error, forceRefresh = forceRefresh)
}
.catch {
Timber.i("Loading horizontal group result: An exception occurred")
updateData(
DashboardItem.HorizontalGroup(error = it),
forceRefresh,
)
errorHandler.dispatch(it)
}
}.launch("dashboard_lucky_number")
.launch("horizontal_group")
}
private fun loadMessages(forceRefresh: Boolean) {
private fun loadGrades(student: Student, forceRefresh: Boolean) {
flowWithResourceIn {
val student = studentRepository.getCurrentStudent(true)
val semester = semesterRepository.getCurrentSemester(student)
messageRepository.getMessages(student, semester, MessageFolder.RECEIVED, forceRefresh)
}.onEach {
when (it.status) {
Status.LOADING -> {
Timber.i("Loading dashboard messages data started")
if (forceRefresh) return@onEach
val unreadMessagesCount = it.data?.count { message -> message.unread }
processHorizontalGroupData(
unreadMessagesCount = unreadMessagesCount,
isLoading = true,
forceRefresh = forceRefresh
)
}
Status.SUCCESS -> {
Timber.i("Loading dashboard messages result: Success")
val unreadMessagesCount = it.data?.count { message -> message.unread }
processHorizontalGroupData(
unreadMessagesCount = unreadMessagesCount,
forceRefresh = forceRefresh
)
}
Status.ERROR -> {
Timber.i("Loading dashboard messages result: An exception occurred")
errorHandler.dispatch(it.error!!)
processHorizontalGroupData(error = it.error, forceRefresh = forceRefresh)
}
}
}.launch("dashboard_messages")
}
private fun loadAttendance(forceRefresh: Boolean) {
flowWithResourceIn {
val student = studentRepository.getCurrentStudent(true)
val semester = semesterRepository.getCurrentSemester(student)
attendanceSummaryRepository.getAttendanceSummary(student, semester, -1, forceRefresh)
}.onEach {
when (it.status) {
Status.LOADING -> {
Timber.i("Loading dashboard attendance data started")
if (forceRefresh) return@onEach
val attendancePercentage = it.data?.calculatePercentage()
processHorizontalGroupData(
attendancePercentage = attendancePercentage,
isLoading = true,
forceRefresh = forceRefresh
)
}
Status.SUCCESS -> {
Timber.i("Loading dashboard attendance result: Success")
val attendancePercentage = it.data?.calculatePercentage()
processHorizontalGroupData(
attendancePercentage = attendancePercentage,
forceRefresh = forceRefresh
)
}
Status.ERROR -> {
Timber.i("Loading dashboard attendance result: An exception occurred")
errorHandler.dispatch(it.error!!)
processHorizontalGroupData(error = it.error, forceRefresh = forceRefresh)
}
}
}.launch("dashboard_attendance")
}
private fun loadGrades(forceRefresh: Boolean) {
flowWithResourceIn {
val student = studentRepository.getCurrentStudent(true)
val semester = semesterRepository.getCurrentSemester(student)
gradeRepository.getGrades(student, semester, forceRefresh)
@ -353,6 +333,7 @@ class DashboardPresenter @Inject constructor(
Status.LOADING -> {
Timber.i("Loading dashboard grades data started")
if (forceRefresh) return@onEach
updateData(
DashboardItem.Grades(
subjectWithGrades = it.data,
@ -360,6 +341,10 @@ class DashboardPresenter @Inject constructor(
isLoading = true
), forceRefresh
)
if (!it.data.isNullOrEmpty()) {
firstLoadedItemList += DashboardItem.Type.GRADES
}
}
Status.SUCCESS -> {
Timber.i("Loading dashboard grades result: Success")
@ -367,7 +352,8 @@ class DashboardPresenter @Inject constructor(
DashboardItem.Grades(
subjectWithGrades = it.data,
gradeTheme = preferencesRepository.gradeColorTheme
), forceRefresh
),
forceRefresh
)
}
Status.ERROR -> {
@ -379,9 +365,8 @@ class DashboardPresenter @Inject constructor(
}.launch("dashboard_grades")
}
private fun loadLessons(forceRefresh: Boolean) {
private fun loadLessons(student: Student, forceRefresh: Boolean) {
flowWithResourceIn {
val student = studentRepository.getCurrentStudent(true)
val semester = semesterRepository.getCurrentSemester(student)
val date = LocalDate.now().nextOrSameSchoolDay
@ -398,24 +383,34 @@ class DashboardPresenter @Inject constructor(
Status.LOADING -> {
Timber.i("Loading dashboard lessons data started")
if (forceRefresh) return@onEach
updateData(DashboardItem.Lessons(it.data, isLoading = true), forceRefresh)
updateData(
DashboardItem.Lessons(it.data, isLoading = true),
forceRefresh
)
if (!it.data?.lessons.isNullOrEmpty()) {
firstLoadedItemList += DashboardItem.Type.LESSONS
}
}
Status.SUCCESS -> {
Timber.i("Loading dashboard lessons result: Success")
updateData(DashboardItem.Lessons(it.data), forceRefresh)
updateData(
DashboardItem.Lessons(it.data), forceRefresh
)
}
Status.ERROR -> {
Timber.i("Loading dashboard lessons result: An exception occurred")
errorHandler.dispatch(it.error!!)
updateData(DashboardItem.Lessons(error = it.error), forceRefresh)
updateData(
DashboardItem.Lessons(error = it.error), forceRefresh
)
}
}
}.launch("dashboard_lessons")
}
private fun loadHomework(forceRefresh: Boolean) {
private fun loadHomework(student: Student, forceRefresh: Boolean) {
flowWithResourceIn {
val student = studentRepository.getCurrentStudent(true)
val semester = semesterRepository.getCurrentSemester(student)
val date = LocalDate.now().nextOrSameSchoolDay
@ -443,6 +438,10 @@ class DashboardPresenter @Inject constructor(
DashboardItem.Homework(it.data ?: emptyList(), isLoading = true),
forceRefresh
)
if (!it.data.isNullOrEmpty()) {
firstLoadedItemList += DashboardItem.Type.HOMEWORK
}
}
Status.SUCCESS -> {
Timber.i("Loading dashboard homework result: Success")
@ -457,10 +456,8 @@ class DashboardPresenter @Inject constructor(
}.launch("dashboard_homework")
}
private fun loadSchoolAnnouncements(forceRefresh: Boolean) {
private fun loadSchoolAnnouncements(student: Student, forceRefresh: Boolean) {
flowWithResourceIn {
val student = studentRepository.getCurrentStudent(true)
schoolAnnouncementRepository.getSchoolAnnouncements(student, forceRefresh)
}.onEach {
when (it.status) {
@ -468,11 +465,13 @@ class DashboardPresenter @Inject constructor(
Timber.i("Loading dashboard announcements data started")
if (forceRefresh) return@onEach
updateData(
DashboardItem.Announcements(
it.data ?: emptyList(),
isLoading = true
), forceRefresh
DashboardItem.Announcements(it.data ?: emptyList(), isLoading = true),
forceRefresh
)
if (!it.data.isNullOrEmpty()) {
firstLoadedItemList += DashboardItem.Type.ANNOUNCEMENTS
}
}
Status.SUCCESS -> {
Timber.i("Loading dashboard announcements result: Success")
@ -487,9 +486,8 @@ class DashboardPresenter @Inject constructor(
}.launch("dashboard_announcements")
}
private fun loadExams(forceRefresh: Boolean) {
private fun loadExams(student: Student, forceRefresh: Boolean) {
flowWithResourceIn {
val student = studentRepository.getCurrentStudent(true)
val semester = semesterRepository.getCurrentSemester(student)
examRepository.getExams(
@ -499,32 +497,41 @@ class DashboardPresenter @Inject constructor(
end = LocalDate.now().plusDays(7),
forceRefresh = forceRefresh
)
}.onEach {
when (it.status) {
Status.LOADING -> {
Timber.i("Loading dashboard exams data started")
if (forceRefresh) return@onEach
updateData(
DashboardItem.Exams(it.data.orEmpty(), isLoading = true),
forceRefresh
)
}
Status.SUCCESS -> {
Timber.i("Loading dashboard exams result: Success")
updateData(DashboardItem.Exams(it.data ?: emptyList()), forceRefresh)
}
Status.ERROR -> {
Timber.i("Loading dashboard exams result: An exception occurred")
errorHandler.dispatch(it.error!!)
updateData(DashboardItem.Exams(error = it.error), forceRefresh)
}
}
.map { examResource ->
val sortedExams = examResource.data?.sortedBy { it.date }
examResource.copy(data = sortedExams)
}
}.launch("dashboard_exams")
.onEach {
when (it.status) {
Status.LOADING -> {
Timber.i("Loading dashboard exams data started")
if (forceRefresh) return@onEach
updateData(
DashboardItem.Exams(it.data.orEmpty(), isLoading = true),
forceRefresh
)
if (!it.data.isNullOrEmpty()) {
firstLoadedItemList += DashboardItem.Type.EXAMS
}
}
Status.SUCCESS -> {
Timber.i("Loading dashboard exams result: Success")
updateData(DashboardItem.Exams(it.data ?: emptyList()), forceRefresh)
}
Status.ERROR -> {
Timber.i("Loading dashboard exams result: An exception occurred")
errorHandler.dispatch(it.error!!)
updateData(DashboardItem.Exams(error = it.error), forceRefresh)
}
}
}.launch("dashboard_exams")
}
private fun loadConferences(forceRefresh: Boolean) {
private fun loadConferences(student: Student, forceRefresh: Boolean) {
flowWithResourceIn {
val student = studentRepository.getCurrentStudent(true)
val semester = semesterRepository.getCurrentSemester(student)
conferenceRepository.getConferences(
@ -542,6 +549,10 @@ class DashboardPresenter @Inject constructor(
DashboardItem.Conferences(it.data ?: emptyList(), isLoading = true),
forceRefresh
)
if (!it.data.isNullOrEmpty()) {
firstLoadedItemList += DashboardItem.Type.CONFERENCES
}
}
Status.SUCCESS -> {
Timber.i("Loading dashboard conferences result: Success")
@ -556,145 +567,119 @@ class DashboardPresenter @Inject constructor(
}.launch("dashboard_conferences")
}
private fun processHorizontalGroupData(
luckyNumber: Int? = null,
unreadMessagesCount: Int? = null,
attendancePercentage: Double? = null,
error: Throwable? = null,
isLoading: Boolean = false,
forceRefresh: Boolean
) {
val isLuckyNumberToLoad =
dashboardTilesToLoad.any { it == DashboardItem.Tile.LUCKY_NUMBER }
val isMessagesToLoad =
dashboardTilesToLoad.any { it == DashboardItem.Tile.MESSAGES }
val isAttendanceToLoad =
dashboardTilesToLoad.any { it == DashboardItem.Tile.ATTENDANCE }
val isPushedToList =
dashboardItemLoadedList.any { it.type == DashboardItem.Type.HORIZONTAL_GROUP }
private fun updateData(dashboardItem: DashboardItem, forceRefresh: Boolean) {
val isForceRefreshError = forceRefresh && dashboardItem.error != null
val isFirstRunDataLoadedError =
dashboardItem.type in firstLoadedItemList && dashboardItem.error != null
if (error != null) {
updateData(DashboardItem.HorizontalGroup(error = error), forceRefresh)
return
with(dashboardItemLoadedList) {
removeAll { it.type == dashboardItem.type && !isForceRefreshError && !isFirstRunDataLoadedError }
if (!isForceRefreshError && !isFirstRunDataLoadedError) add(dashboardItem)
}
if (isLoading) {
val horizontalGroup =
dashboardItemLoadedList.find { it is DashboardItem.HorizontalGroup } as DashboardItem.HorizontalGroup?
val updatedHorizontalGroup =
horizontalGroup?.copy(isLoading = true) ?: DashboardItem.HorizontalGroup(isLoading = true)
sortDashboardItems()
updateData(updatedHorizontalGroup, forceRefresh)
}
if (forceRefresh && !isPushedToList) {
updateData(DashboardItem.HorizontalGroup(), forceRefresh)
}
val horizontalGroup =
dashboardItemLoadedList.single { it is DashboardItem.HorizontalGroup } as DashboardItem.HorizontalGroup
when {
luckyNumber != null -> {
updateData(horizontalGroup.copy(luckyNumber = luckyNumber), forceRefresh)
}
unreadMessagesCount != null -> {
updateData(
horizontalGroup.copy(unreadMessagesCount = unreadMessagesCount),
forceRefresh
)
}
attendancePercentage != null -> {
updateData(
horizontalGroup.copy(attendancePercentage = attendancePercentage),
forceRefresh
)
}
}
val isHorizontalGroupLoaded = dashboardItemLoadedList.any {
if (it !is DashboardItem.HorizontalGroup) return@any false
val isLuckyNumberStateCorrect = (it.luckyNumber != null) == isLuckyNumberToLoad
val isMessagesStateCorrect = (it.unreadMessagesCount != null) == isMessagesToLoad
val isAttendanceStateCorrect = (it.attendancePercentage != null) == isAttendanceToLoad
isLuckyNumberStateCorrect && isAttendanceStateCorrect && isMessagesStateCorrect
}
if (isHorizontalGroupLoaded) {
val updatedHorizontalGroup =
dashboardItemLoadedList.single { it is DashboardItem.HorizontalGroup } as DashboardItem.HorizontalGroup
updateData(updatedHorizontalGroup.copy(isLoading = false, error = null), forceRefresh)
if (forceRefresh) {
updateForceRefreshData(dashboardItem)
} else {
updateNormalData()
}
}
private fun updateData(dashboardItem: DashboardItem, forceRefresh: Boolean) {
val isForceRefreshError = forceRefresh && dashboardItem.error != null
val dashboardItemsPosition = preferencesRepository.dashboardItemsPosition
with(dashboardItemLoadedList) {
removeAll { it.type == dashboardItem.type && !isForceRefreshError }
if (!isForceRefreshError) add(dashboardItem)
sortBy { tile -> dashboardItemsToLoad.single { it == tile.type }.ordinal }
private fun updateNormalData() {
val isItemsLoaded =
dashboardItemsToLoad.all { type -> dashboardItemLoadedList.any { it.type == type } }
val isItemsDataLoaded = isItemsLoaded && dashboardItemLoadedList.all {
it.isDataLoaded || it.error != null
}
if (forceRefresh) {
with(dashboardItemRefreshLoadedList) {
removeAll { it.type == dashboardItem.type }
add(dashboardItem)
if (isItemsDataLoaded) {
view?.run {
showProgress(false)
showErrorView(false)
showContent(true)
updateData(dashboardItemLoadedList.toList())
}
}
showErrorIfExists(
isItemsLoaded = isItemsLoaded,
itemsLoadedList = dashboardItemLoadedList,
forceRefresh = false
)
}
private fun updateForceRefreshData(dashboardItem: DashboardItem) {
with(dashboardItemRefreshLoadedList) {
removeAll { it.type == dashboardItem.type }
add(dashboardItem)
}
val isRefreshItemLoaded =
dashboardItemsToLoad.all { type -> dashboardItemRefreshLoadedList.any { it.type == type } }
val isRefreshItemsDataLoaded = isRefreshItemLoaded && dashboardItemRefreshLoadedList.all {
it.isDataLoaded || it.error != null
}
if (isRefreshItemsDataLoaded) {
view?.run {
showRefresh(false)
showErrorView(false)
showContent(true)
updateData(dashboardItemLoadedList.toList())
}
}
showErrorIfExists(
isItemsLoaded = isRefreshItemLoaded,
itemsLoadedList = dashboardItemRefreshLoadedList,
forceRefresh = true
)
if (isRefreshItemsDataLoaded) dashboardItemRefreshLoadedList.clear()
}
private fun showErrorIfExists(
isItemsLoaded: Boolean,
itemsLoadedList: List<DashboardItem>,
forceRefresh: Boolean
) {
val filteredItems = itemsLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT }
val isAccountItemError =
itemsLoadedList.find { it.type == DashboardItem.Type.ACCOUNT }?.error != null
val isGeneralError =
filteredItems.none { it.error == null } && filteredItems.isNotEmpty() || isAccountItemError
val errorMessage = itemsLoadedList.map { it.error?.stackTraceToString() }.toString()
val filteredOriginalLoadedList =
dashboardItemLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT }
val wasAccountItemError =
dashboardItemLoadedList.find { it.type == DashboardItem.Type.ACCOUNT }?.error != null
val wasGeneralError =
filteredOriginalLoadedList.none { it.error == null } && filteredOriginalLoadedList.isNotEmpty() || wasAccountItemError
if (isGeneralError && isItemsLoaded) {
lastError = Exception(errorMessage)
view?.run {
showProgress(false)
showRefresh(false)
if ((forceRefresh && wasGeneralError) || !forceRefresh) {
showContent(false)
showErrorView(true)
}
}
}
}
private fun sortDashboardItems() {
val dashboardItemsPosition = preferencesRepository.dashboardItemsPosition
dashboardItemLoadedList.sortBy { tile ->
dashboardItemsPosition?.getOrDefault(
tile.type,
tile.type.ordinal + 100
) ?: tile.type.ordinal
}
val isItemsLoaded =
dashboardItemsToLoad.all { type -> dashboardItemLoadedList.any { it.type == type } }
val isRefreshItemLoaded =
dashboardItemsToLoad.all { type -> dashboardItemRefreshLoadedList.any { it.type == type } }
val isItemsDataLoaded = isItemsLoaded && dashboardItemLoadedList.all {
it.isDataLoaded || it.error != null
}
val isRefreshItemsDataLoaded = isRefreshItemLoaded && dashboardItemRefreshLoadedList.all {
it.isDataLoaded || it.error != null
}
if (isRefreshItemsDataLoaded) {
view?.showRefresh(false)
dashboardItemRefreshLoadedList.clear()
}
view?.run {
if (!forceRefresh) {
showProgress(!isItemsDataLoaded)
showContent(isItemsDataLoaded)
}
updateData(dashboardItemLoadedList.toList())
}
if (isItemsLoaded) {
val filteredItems =
dashboardItemLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT }
val isAccountItemError =
dashboardItemLoadedList.single { it.type == DashboardItem.Type.ACCOUNT }.error != null
val isGeneralError =
filteredItems.all { it.error != null } && filteredItems.isNotEmpty() || isAccountItemError
val errorMessage = filteredItems.map { it.error?.stackTraceToString() }.toString()
lastError = Exception(errorMessage)
view?.run {
showProgress(false)
showContent(!isGeneralError)
showErrorView(isGeneralError)
}
}
}
}

View File

@ -23,4 +23,6 @@ interface DashboardView : BaseView {
fun resetView()
fun popViewToRoot()
fun openNotificationsCenterView()
}

View File

@ -87,7 +87,7 @@ class NotificationDebugPresenter @Inject constructor(
}
}
private fun withStudent(block: (Student) -> Unit) {
private fun withStudent(block: suspend (Student) -> Unit) {
launch {
block(studentRepository.getCurrentStudent(false))
}

View File

@ -131,7 +131,9 @@ class GradeAverageProvider @Inject constructor(
val updatedFirstSemesterGrades =
firstSemesterSubject?.grades?.updateModifiers(student).orEmpty()
(updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage(isOptionalArithmeticAverage)
(updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage(
isOptionalArithmeticAverage
)
} else {
secondSemesterSubject.average
}
@ -147,7 +149,8 @@ class GradeAverageProvider @Inject constructor(
return if (!isAnyVulcanAverage || isGradeAverageForceCalc) {
val secondSemesterAverage =
secondSemesterSubject.grades.updateModifiers(student).calcAverage(isOptionalArithmeticAverage)
secondSemesterSubject.grades.updateModifiers(student)
.calcAverage(isOptionalArithmeticAverage)
val firstSemesterAverage = firstSemesterSubject?.grades?.updateModifiers(student)
?.calcAverage(isOptionalArithmeticAverage) ?: secondSemesterAverage
@ -213,7 +216,8 @@ class GradeAverageProvider @Inject constructor(
proposedPoints = "",
finalPoints = "",
pointsSum = "",
average = if (calcAverage) details.updateModifiers(student).calcAverage(isOptionalArithmeticAverage) else .0
average = if (calcAverage) details.updateModifiers(student)
.calcAverage(isOptionalArithmeticAverage) else .0
)
}
}

View File

@ -10,7 +10,7 @@ import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.ItemGradeSummaryBinding
import io.github.wulkanowy.databinding.ScrollableHeaderGradeSummaryBinding
import io.github.wulkanowy.utils.calcAverage
import io.github.wulkanowy.utils.calcFinalAverage
import java.util.Locale
import javax.inject.Inject
@ -25,6 +25,10 @@ class GradeSummaryAdapter @Inject constructor(
var items = emptyList<GradeSummary>()
var onCalculatedHelpClickListener: () -> Unit = {}
var onFinalHelpClickListener: () -> Unit = {}
override fun getItemCount() = items.size + if (items.isNotEmpty()) 1 else 0
override fun getItemViewType(position: Int) = when (position) {
@ -60,7 +64,7 @@ class GradeSummaryAdapter @Inject constructor(
val finalItemsCount = items.count { it.finalGrade.matches("[0-6][+-]?".toRegex()) }
val calculatedItemsCount = items.count { value -> value.average != 0.0 }
val allItemsCount = items.count { !it.subject.equals("zachowanie", true) }
val finalAverage = items.calcAverage(
val finalAverage = items.calcFinalAverage(
preferencesRepository.gradePlusModifier,
preferencesRepository.gradeMinusModifier
)
@ -83,6 +87,9 @@ class GradeSummaryAdapter @Inject constructor(
calculatedItemsCount,
allItemsCount
)
gradeSummaryCalculatedAverageHelp.setOnClickListener { onCalculatedHelpClickListener() }
gradeSummaryFinalAverageHelp.setOnClickListener { onFinalHelpClickListener() }
}
}

View File

@ -5,6 +5,7 @@ import android.view.View
import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
@ -48,6 +49,11 @@ class GradeSummaryFragment :
}
override fun initView() {
with(gradeSummaryAdapter) {
onCalculatedHelpClickListener = presenter::onCalculatedAverageHelpClick
onFinalHelpClickListener = presenter::onFinalAverageHelpClick
}
with(binding.gradeSummaryRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = gradeSummaryAdapter
@ -55,7 +61,11 @@ class GradeSummaryFragment :
with(binding) {
gradeSummarySwipe.setOnRefreshListener(presenter::onSwipeRefresh)
gradeSummarySwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
gradeSummarySwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
gradeSummarySwipe.setProgressBackgroundColorSchemeColor(
requireContext().getThemeAttrColor(
R.attr.colorSwipeRefresh
)
)
gradeSummaryErrorRetry.setOnClickListener { presenter.onRetry() }
gradeSummaryErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}
@ -107,6 +117,22 @@ class GradeSummaryFragment :
binding.gradeSummarySwipe.isRefreshing = show
}
override fun showCalculatedAverageHelpDialog() {
AlertDialog.Builder(requireContext())
.setTitle(R.string.grade_summary_calculated_average_help_dialog_title)
.setMessage(R.string.grade_summary_calculated_average_help_dialog_message)
.setPositiveButton(R.string.all_close) { _, _ -> }
.show()
}
override fun showFinalAverageHelpDialog() {
AlertDialog.Builder(requireContext())
.setTitle(R.string.grade_summary_final_average_help_dialog_title)
.setMessage(R.string.grade_summary_final_average_help_dialog_message)
.setPositiveButton(R.string.all_close) { _, _ -> }
.show()
}
override fun onParentLoadData(semesterId: Int, forceRefresh: Boolean) {
presenter.onParentViewLoadData(semesterId, forceRefresh)
}

View File

@ -135,6 +135,14 @@ class GradeSummaryPresenter @Inject constructor(
cancelJobs("load")
}
fun onCalculatedAverageHelpClick() {
view?.showCalculatedAverageHelpDialog()
}
fun onFinalAverageHelpClick() {
view?.showFinalAverageHelpDialog()
}
private fun createGradeSummaryItems(items: List<GradeSubject>): List<GradeSummary> {
return items
.filter { !checkEmpty(it) }

View File

@ -33,6 +33,10 @@ interface GradeSummaryView : BaseView {
fun showEmpty(show: Boolean)
fun showCalculatedAverageHelpDialog()
fun showFinalAverageHelpDialog()
fun notifyParentDataLoaded(semesterId: Int)
fun notifyParentRefresh()

View File

@ -103,9 +103,8 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
}
override fun notifyInitSymbolFragment(loginData: Triple<String, String, String>) {
(loginAdapter.getFragmentInstance(1) as? LoginSymbolFragment)?.onParentInitSymbolFragment(
loginData
)
(loginAdapter.getFragmentInstance(1) as? LoginSymbolFragment)
?.onParentInitSymbolFragment(loginData)
}
override fun notifyInitStudentSelectFragment(studentsWithSemesters: List<StudentWithSemesters>) {

View File

@ -13,7 +13,7 @@ import javax.inject.Inject
class LoginErrorHandler @Inject constructor(resources: Resources) : ErrorHandler(resources) {
var onBadCredentials: () -> Unit = {}
var onBadCredentials: (String?) -> Unit = {}
var onInvalidToken: (String) -> Unit = {}
@ -25,7 +25,7 @@ class LoginErrorHandler @Inject constructor(resources: Resources) : ErrorHandler
override fun proceed(error: Throwable) {
when (error) {
is BadCredentialsException -> onBadCredentials()
is BadCredentialsException -> onBadCredentials(error.message)
is SQLiteConstraintException -> onStudentDuplicate(resources.getString(R.string.login_duplicate_student))
is TokenDeadException -> onInvalidToken(resources.getString(R.string.login_expired_token))
is InvalidTokenException -> onInvalidToken(resources.getString(R.string.login_invalid_token))

View File

@ -51,10 +51,12 @@ class LoginAdvancedFragment :
private lateinit var hostSymbols: Array<String>
override val formHostValue: String
get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty()
get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString()))
.orEmpty()
override val formHostSymbol: String
get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty()
get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString()))
.orEmpty()
override val formPinValue: String
get() = binding.loginFormPin.text.toString().trim()
@ -92,39 +94,62 @@ class LoginAdvancedFragment :
loginFormSignIn.setOnClickListener { presenter.onSignInClick() }
loginTypeSwitch.setOnCheckedChangeListener { _, checkedId ->
presenter.onLoginModeSelected(when (checkedId) {
R.id.loginTypeApi -> Sdk.Mode.API
R.id.loginTypeScrapper -> Sdk.Mode.SCRAPPER
else -> Sdk.Mode.HYBRID
})
presenter.onLoginModeSelected(
when (checkedId) {
R.id.loginTypeApi -> Sdk.Mode.API
R.id.loginTypeScrapper -> Sdk.Mode.SCRAPPER
else -> Sdk.Mode.HYBRID
}
)
}
loginFormPin.setOnEditorDoneSignIn { loginFormSignIn.callOnClick() }
loginFormPass.setOnEditorDoneSignIn { loginFormSignIn.callOnClick() }
loginFormSymbol.setAdapter(ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, resources.getStringArray(R.array.symbols_values)))
loginFormSymbol.setAdapter(
ArrayAdapter(
requireContext(),
android.R.layout.simple_list_item_1,
resources.getStringArray(R.array.symbols_values)
)
)
}
with(binding.loginFormHost) {
setText(hostKeys.getOrNull(0).orEmpty())
setAdapter(LoginSymbolAdapter(context, R.layout.support_simple_spinner_dropdown_item, hostKeys))
setAdapter(
LoginSymbolAdapter(
context,
R.layout.support_simple_spinner_dropdown_item,
hostKeys
)
)
setOnClickListener { if (binding.loginFormContainer.visibility == GONE) dismissDropDown() }
}
}
override fun showMobileApiWarningMessage() {
binding.loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_mobile_api)
binding.loginFormAdvancedWarningInfo.text =
getString(R.string.login_advanced_warning_mobile_api)
}
override fun showScraperWarningMessage() {
binding.loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_scraper)
binding.loginFormAdvancedWarningInfo.text =
getString(R.string.login_advanced_warning_scraper)
}
override fun showHybridWarningMessage() {
binding.loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_hybrid)
binding.loginFormAdvancedWarningInfo.text =
getString(R.string.login_advanced_warning_hybrid)
}
override fun setDefaultCredentials(username: String, pass: String, symbol: String, token: String, pin: String) {
override fun setDefaultCredentials(
username: String,
pass: String,
symbol: String,
token: String,
pin: String
) {
with(binding) {
loginFormUsername.setText(username)
loginFormPass.setText(pass)
@ -177,10 +202,10 @@ class LoginAdvancedFragment :
}
}
override fun setErrorPassIncorrect() {
override fun setErrorPassIncorrect(message: String?) {
with(binding.loginFormPassLayout) {
requestFocus()
error = getString(R.string.login_incorrect_password)
error = message ?: getString(R.string.login_incorrect_password)
}
}
@ -296,11 +321,13 @@ class LoginAdvancedFragment :
}
override fun notifyParentAccountLogged(studentsWithSemesters: List<StudentWithSemesters>) {
(activity as? LoginActivity)?.onFormFragmentAccountLogged(studentsWithSemesters, Triple(
binding.loginFormUsername.text.toString(),
binding.loginFormPass.text.toString(),
resources.getStringArray(R.array.hosts_values)[1]
))
(activity as? LoginActivity)?.onFormFragmentAccountLogged(
studentsWithSemesters, Triple(
binding.loginFormUsername.text.toString(),
binding.loginFormPass.text.toString(),
resources.getStringArray(R.array.hosts_values)[1]
)
)
}
override fun onResume() {

View File

@ -34,9 +34,9 @@ class LoginAdvancedPresenter @Inject constructor(
}
}
private fun onBadCredentials() {
private fun onBadCredentials(message: String?) {
view?.run {
setErrorPassIncorrect()
setErrorPassIncorrect(message)
showSoftKeyboard()
Timber.i("Entered wrong username or password")
}

View File

@ -49,7 +49,7 @@ interface LoginAdvancedView : BaseView {
fun setErrorPassInvalid(focus: Boolean)
fun setErrorPassIncorrect()
fun setErrorPassIncorrect(message: String?)
fun clearUsernameError()

View File

@ -5,6 +5,7 @@ import android.os.Bundle
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
@ -41,10 +42,12 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
get() = binding.loginFormPass.text.toString()
override val formHostValue: String
get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty()
get() = hostValues.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString()))
.orEmpty()
override val formHostSymbol: String
get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString())).orEmpty()
get() = hostSymbols.getOrNull(hostKeys.indexOf(binding.loginFormHost.text.toString()))
.orEmpty()
override val nicknameLabel: String
get() = getString(R.string.login_nickname_hint)
@ -88,7 +91,13 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
with(binding.loginFormHost) {
setText(hostKeys.getOrNull(0).orEmpty())
setAdapter(LoginSymbolAdapter(context, R.layout.support_simple_spinner_dropdown_item, hostKeys))
setAdapter(
LoginSymbolAdapter(
context,
R.layout.support_simple_spinner_dropdown_item,
hostKeys
)
)
setOnClickListener { if (binding.loginFormContainer.visibility == GONE) dismissDropDown() }
}
}
@ -142,24 +151,31 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
}
}
override fun setErrorPassIncorrect() {
with(binding.loginFormPassLayout) {
error = getString(R.string.login_incorrect_password)
override fun setErrorPassIncorrect(message: String?) {
val error = message ?: getString(R.string.login_incorrect_password_default)
with(binding) {
loginFormUsernameLayout.error = " "
loginFormPassLayout.error = " "
loginFormErrorBox.text = getString(R.string.login_incorrect_password, error)
loginFormErrorBox.isVisible = true
}
}
override fun setErrorEmailInvalid(domain: String) {
with(binding.loginFormUsernameLayout) {
error = getString(R.string.login_invalid_custom_email,domain)
error = getString(R.string.login_invalid_custom_email, domain)
}
}
override fun clearUsernameError() {
binding.loginFormUsernameLayout.error = null
binding.loginFormErrorBox.isVisible = false
}
override fun clearPassError() {
binding.loginFormPassLayout.error = null
binding.loginFormErrorBox.isVisible = false
}
override fun showSoftKeyboard() {
@ -183,12 +199,18 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
binding.loginFormVersion.text = "v${appInfo.versionName}"
}
override fun notifyParentAccountLogged(studentsWithSemesters: List<StudentWithSemesters>, loginData: Triple<String, String, String>) {
override fun notifyParentAccountLogged(
studentsWithSemesters: List<StudentWithSemesters>,
loginData: Triple<String, String, String>
) {
(activity as? LoginActivity)?.onFormFragmentAccountLogged(studentsWithSemesters, loginData)
}
override fun openPrivacyPolicyPage() {
context?.openInternetBrowser("https://wulkanowy.github.io/polityka-prywatnosci.html", ::showMessage)
context?.openInternetBrowser(
"https://wulkanowy.github.io/polityka-prywatnosci.html",
::showMessage
)
}
override fun showContact(show: Boolean) {
@ -210,7 +232,10 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
}
override fun openFaqPage() {
context?.openInternetBrowser("https://wulkanowy.github.io/czesto-zadawane-pytania/dlaczego-nie-moge-sie-zalogowac", ::showMessage)
context?.openInternetBrowser(
"https://wulkanowy.github.io/czesto-zadawane-pytania/dlaczego-nie-moge-sie-zalogowac",
::showMessage
)
}
override fun onResume() {
@ -223,7 +248,8 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
chooserTitle = requireContext().getString(R.string.login_email_intent_title),
email = "wulkanowyinc@gmail.com",
subject = requireContext().getString(R.string.login_email_subject),
body = requireContext().getString(R.string.login_email_text,
body = requireContext().getString(
R.string.login_email_text,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
appInfo.versionName,

View File

@ -30,7 +30,7 @@ class LoginFormPresenter @Inject constructor(
showVersion()
loginErrorHandler.onBadCredentials = {
setErrorPassIncorrect()
setErrorPassIncorrect(it)
showSoftKeyboard()
Timber.i("Entered wrong username or password")
}
@ -90,10 +90,10 @@ class LoginFormPresenter @Inject constructor(
flowWithResource {
studentRepository.getStudentsScrapper(
email,
password,
host,
symbol
email = email,
password = password,
scrapperBaseUrl = host,
symbol = symbol
)
}.onEach {
when (it.status) {

View File

@ -37,7 +37,7 @@ interface LoginFormView : BaseView {
fun setErrorPassInvalid(focus: Boolean)
fun setErrorPassIncorrect()
fun setErrorPassIncorrect(message: String?)
fun setErrorEmailInvalid(domain: String)

View File

@ -78,7 +78,9 @@ class LoginStudentSelectPresenter @Inject constructor(
when (it.status) {
Status.LOADING -> Timber.d("Login student select students load started")
Status.SUCCESS -> view?.updateData(studentsWithSemesters.map { studentWithSemesters ->
studentWithSemesters to it.data!!.any { item -> compareStudents(studentWithSemesters.student, item.student) }
studentWithSemesters to it.data!!.any { item ->
compareStudents(studentWithSemesters.student, item.student)
}
})
Status.ERROR -> {
errorHandler.dispatch(it.error!!)
@ -95,35 +97,32 @@ class LoginStudentSelectPresenter @Inject constructor(
}
private fun registerStudents(studentsWithSemesters: List<StudentWithSemesters>) {
flowWithResource {
val savedStudents = studentRepository.saveStudents(studentsWithSemesters)
val firstRegistered = studentsWithSemesters.first().apply { student.id = savedStudents.first() }
studentRepository.switchStudent(firstRegistered)
}.onEach {
when (it.status) {
Status.LOADING -> view?.run {
Timber.i("Registration started")
showProgress(true)
showContent(false)
}
Status.SUCCESS -> {
Timber.i("Registration result: Success")
view?.openMainView()
logRegisterEvent(studentsWithSemesters)
}
Status.ERROR -> {
Timber.i("Registration result: An exception occurred ")
view?.apply {
showProgress(false)
showContent(true)
showContact(true)
flowWithResource { studentRepository.saveStudents(studentsWithSemesters) }
.onEach {
when (it.status) {
Status.LOADING -> view?.run {
Timber.i("Registration started")
showProgress(true)
showContent(false)
}
Status.SUCCESS -> {
Timber.i("Registration result: Success")
view?.openMainView()
logRegisterEvent(studentsWithSemesters)
}
Status.ERROR -> {
Timber.i("Registration result: An exception occurred ")
view?.apply {
showProgress(false)
showContent(true)
showContact(true)
}
lastError = it.error
loginErrorHandler.dispatch(it.error!!)
logRegisterEvent(studentsWithSemesters, it.error)
}
lastError = it.error
loginErrorHandler.dispatch(it.error!!)
logRegisterEvent(studentsWithSemesters, it.error)
}
}
}.launch("register")
}.launch("register")
}
fun onDiscordClick() {
@ -134,7 +133,10 @@ class LoginStudentSelectPresenter @Inject constructor(
view?.openEmail(lastError?.message.ifNullOrBlank { "empty" })
}
private fun logRegisterEvent(studentsWithSemesters: List<StudentWithSemesters>, error: Throwable? = null) {
private fun logRegisterEvent(
studentsWithSemesters: List<StudentWithSemesters>,
error: Throwable? = null
) {
studentsWithSemesters.forEach { student ->
analytics.logEvent(
"registration_student_select",

View File

@ -131,7 +131,9 @@ class LuckyNumberHistoryFragment :
presenter.onDateSet(date.year, date.monthValue, date.dayOfMonth)
}
datePicker.show(this@LuckyNumberHistoryFragment.parentFragmentManager, null)
if (!parentFragmentManager.isStateSaved) {
datePicker.show(parentFragmentManager, null)
}
}
override fun showContent(show: Boolean) {

View File

@ -79,12 +79,7 @@ class MessagePreviewAdapter @Inject constructor() :
val readText = when {
recipientCount > 1 -> {
context.resources.getQuantityString(
R.plurals.message_read_by,
message.readBy,
message.readBy,
recipientCount
)
context.getString(R.string.message_read_by, message.readBy, recipientCount)
}
message.readBy == 1 -> {
context.getString(R.string.message_read, context.getString(R.string.all_yes))

View File

@ -117,6 +117,7 @@ class MessageTabPresenter @Inject constructor(
if (!it.data.isNullOrEmpty()) {
view?.run {
enableSwipe(true)
showErrorView(false)
showRefresh(true)
showProgress(false)
showContent(true)

View File

@ -11,6 +11,7 @@ import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.conference.ConferenceFragment
import io.github.wulkanowy.ui.modules.exam.ExamFragment
import io.github.wulkanowy.ui.modules.homework.HomeworkFragment
import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.message.MessageFragment
@ -66,6 +67,9 @@ class MoreFragment : BaseFragment<FragmentMoreBinding>(R.layout.fragment_more),
override val examRes: Pair<String, Drawable?>?
get() = context?.run { getString(R.string.exam_title) to getCompatDrawable(R.drawable.ic_main_exam) }
override val luckyNumberRes: Pair<String, Drawable?>?
get() = context?.run { getString(R.string.lucky_number_title) to getCompatDrawable(R.drawable.ic_more_lucky_number) }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentMoreBinding.bind(view)
@ -128,6 +132,10 @@ class MoreFragment : BaseFragment<FragmentMoreBinding>(R.layout.fragment_more),
(activity as? MainActivity)?.pushView(ExamFragment.newInstance())
}
override fun openLuckyNumberView() {
(activity as? MainActivity)?.pushView(LuckyNumberFragment.newInstance())
}
override fun popView(depth: Int) {
(activity as? MainActivity)?.popView(depth)
}

View File

@ -31,6 +31,7 @@ class MorePresenter @Inject constructor(
schoolAndTeachersRes?.first -> openSchoolAndTeachersView()
mobileDevicesRes?.first -> openMobileDevicesView()
settingsRes?.first -> openSettingsView()
luckyNumberRes?.first -> openLuckyNumberView()
}
}
}
@ -48,6 +49,7 @@ class MorePresenter @Inject constructor(
examRes,
homeworkRes,
noteRes,
luckyNumberRes,
conferencesRes,
schoolAnnouncementRes,
schoolAndTeachersRes,

View File

@ -23,6 +23,8 @@ interface MoreView : BaseView {
val examRes: Pair<String, Drawable?>?
val luckyNumberRes: Pair<String, Drawable?>?
fun initView()
fun updateData(data: List<Pair<String, Drawable?>>)
@ -46,4 +48,6 @@ interface MoreView : BaseView {
fun openMobileDevicesView()
fun openExamView()
fun openLuckyNumberView()
}

View File

@ -0,0 +1,62 @@
package io.github.wulkanowy.ui.modules.notificationscenter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Notification
import io.github.wulkanowy.databinding.ItemNotificationsCenterBinding
import io.github.wulkanowy.services.sync.notifications.NotificationType
import io.github.wulkanowy.utils.toFormattedString
import javax.inject.Inject
class NotificationsCenterAdapter @Inject constructor() :
ListAdapter<Notification, NotificationsCenterAdapter.ViewHolder>(DiffUtilCallback()) {
var onItemClickListener: (NotificationType) -> Unit = {}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemNotificationsCenterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
with(holder.binding) {
notificationsCenterItemTitle.text = item.title
notificationsCenterItemContent.text = item.content
notificationsCenterItemDate.text = item.date.toFormattedString("HH:mm, d MMM")
notificationsCenterItemIcon.setImageResource(item.type.toDrawableResId())
root.setOnClickListener { onItemClickListener(item.type) }
}
}
private fun NotificationType.toDrawableResId() = when (this) {
NotificationType.NEW_CONFERENCE -> R.drawable.ic_more_conferences
NotificationType.NEW_EXAM -> R.drawable.ic_main_exam
NotificationType.NEW_GRADE_DETAILS -> R.drawable.ic_stat_grade
NotificationType.NEW_GRADE_PREDICTED -> R.drawable.ic_stat_grade
NotificationType.NEW_GRADE_FINAL -> R.drawable.ic_stat_grade
NotificationType.NEW_HOMEWORK -> R.drawable.ic_more_homework
NotificationType.NEW_LUCKY_NUMBER -> R.drawable.ic_stat_luckynumber
NotificationType.NEW_MESSAGE -> R.drawable.ic_stat_message
NotificationType.NEW_NOTE -> R.drawable.ic_stat_note
NotificationType.NEW_ANNOUNCEMENT -> R.drawable.ic_all_about
NotificationType.PUSH -> R.drawable.ic_stat_all
}
class ViewHolder(val binding: ItemNotificationsCenterBinding) :
RecyclerView.ViewHolder(binding.root)
private class DiffUtilCallback : DiffUtil.ItemCallback<Notification>() {
override fun areContentsTheSame(oldItem: Notification, newItem: Notification) =
oldItem == newItem
override fun areItemsTheSame(oldItem: Notification, newItem: Notification) =
oldItem.id == newItem.id
}
}

View File

@ -0,0 +1,108 @@
package io.github.wulkanowy.ui.modules.notificationscenter
import android.os.Bundle
import android.view.View
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Notification
import io.github.wulkanowy.databinding.FragmentNotificationsCenterBinding
import io.github.wulkanowy.services.sync.notifications.NotificationType
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.conference.ConferenceFragment
import io.github.wulkanowy.ui.modules.exam.ExamFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.homework.HomeworkFragment
import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.note.NoteFragment
import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment
import javax.inject.Inject
@AndroidEntryPoint
class NotificationsCenterFragment :
BaseFragment<FragmentNotificationsCenterBinding>(R.layout.fragment_notifications_center),
NotificationsCenterView, MainView.TitledView {
@Inject
lateinit var presenter: NotificationsCenterPresenter
@Inject
lateinit var notificationsCenterAdapter: NotificationsCenterAdapter
companion object {
fun newInstance() = NotificationsCenterFragment()
}
override val titleStringId: Int
get() = R.string.notifications_center_title
override val isViewEmpty: Boolean
get() = notificationsCenterAdapter.itemCount == 0
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentNotificationsCenterBinding.bind(view)
presenter.onAttachView(this)
}
override fun initView() {
notificationsCenterAdapter.onItemClickListener = { notificationType ->
notificationType.toDestinationFragment()
?.let { (requireActivity() as MainActivity).pushView(it) }
}
with(binding.notificationsCenterRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = notificationsCenterAdapter
}
}
override fun updateData(data: List<Notification>) {
notificationsCenterAdapter.submitList(data)
}
override fun showEmpty(show: Boolean) {
binding.notificationsCenterEmpty.isVisible = show
}
override fun showProgress(show: Boolean) {
binding.notificationsCenterProgress.isVisible = show
}
override fun showContent(show: Boolean) {
binding.notificationsCenterRecycler.isVisible = show
}
override fun showErrorView(show: Boolean) {
binding.notificationCenterError.isVisible = show
}
override fun setErrorDetails(message: String) {
binding.notificationCenterErrorMessage.text = message
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
private fun NotificationType.toDestinationFragment(): Fragment? = when (this) {
NotificationType.NEW_CONFERENCE -> ConferenceFragment.newInstance()
NotificationType.NEW_EXAM -> ExamFragment.newInstance()
NotificationType.NEW_GRADE_DETAILS -> GradeFragment.newInstance()
NotificationType.NEW_GRADE_PREDICTED -> GradeFragment.newInstance()
NotificationType.NEW_GRADE_FINAL -> GradeFragment.newInstance()
NotificationType.NEW_HOMEWORK -> HomeworkFragment.newInstance()
NotificationType.NEW_LUCKY_NUMBER -> LuckyNumberFragment.newInstance()
NotificationType.NEW_MESSAGE -> MessageFragment.newInstance()
NotificationType.NEW_NOTE -> NoteFragment.newInstance()
NotificationType.NEW_ANNOUNCEMENT -> SchoolAnnouncementFragment.newInstance()
NotificationType.PUSH -> null
}
}

View File

@ -0,0 +1,83 @@
package io.github.wulkanowy.ui.modules.notificationscenter
import io.github.wulkanowy.data.repositories.NotificationRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import javax.inject.Inject
class NotificationsCenterPresenter @Inject constructor(
private val notificationRepository: NotificationRepository,
errorHandler: ErrorHandler,
studentRepository: StudentRepository
) : BasePresenter<NotificationsCenterView>(errorHandler, studentRepository) {
private lateinit var lastError: Throwable
override fun onAttachView(view: NotificationsCenterView) {
super.onAttachView(view)
view.initView()
Timber.i("Notifications centre view was initialized")
errorHandler.showErrorMessage = ::showErrorViewOnError
loadData()
}
fun onRetry() {
view?.run {
showErrorView(false)
showProgress(true)
}
loadData()
}
fun onDetailsClick() {
view?.showErrorDetailsDialog(lastError)
}
private fun loadData() {
Timber.i("Loading notifications data started")
flow {
val studentId = studentRepository.getCurrentStudent(false).id
emitAll(notificationRepository.getNotifications(studentId))
}
.map { notificationList -> notificationList.sortedByDescending { it.date } }
.catch { Timber.i("Loading notifications result: An exception occurred") }
.onEach {
Timber.i("Loading notifications result: Success")
if (it.isEmpty()) {
view?.run {
showContent(false)
showProgress(false)
showEmpty(true)
}
} else {
view?.run {
showContent(true)
showProgress(false)
showEmpty(false)
updateData(it)
}
}
}
.launch()
}
private fun showErrorViewOnError(message: String, error: Throwable) {
view?.run {
if (isViewEmpty) {
lastError = error
setErrorDetails(message)
showErrorView(true)
showEmpty(false)
} else showError(message, error)
}
}
}

View File

@ -0,0 +1,23 @@
package io.github.wulkanowy.ui.modules.notificationscenter
import io.github.wulkanowy.data.db.entities.Notification
import io.github.wulkanowy.ui.base.BaseView
interface NotificationsCenterView : BaseView {
val isViewEmpty: Boolean
fun initView()
fun updateData(data: List<Notification>)
fun showProgress(show: Boolean)
fun showEmpty(show: Boolean)
fun showContent(show: Boolean)
fun showErrorView(show: Boolean)
fun setErrorDetails(message: String)
}

View File

@ -2,7 +2,7 @@ package io.github.wulkanowy.ui.modules.schoolannouncement
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.text.HtmlCompat
import androidx.core.text.parseAsHtml
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.databinding.ItemSchoolAnnouncementBinding
@ -14,6 +14,8 @@ class SchoolAnnouncementAdapter @Inject constructor() :
var items = emptyList<SchoolAnnouncement>()
var onItemClickListener: (SchoolAnnouncement) -> Unit = {}
override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
@ -26,9 +28,9 @@ class SchoolAnnouncementAdapter @Inject constructor() :
with(holder.binding) {
schoolAnnouncementItemDate.text = item.date.toFormattedString()
schoolAnnouncementItemType.text = item.subject
schoolAnnouncementItemContent.text = HtmlCompat.fromHtml(
item.content, HtmlCompat.FROM_HTML_MODE_COMPACT
)
schoolAnnouncementItemContent.text = item.content.parseAsHtml()
root.setOnClickListener { onItemClickListener(item) }
}
}

View File

@ -0,0 +1,54 @@
package io.github.wulkanowy.ui.modules.schoolannouncement
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.text.parseAsHtml
import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.databinding.DialogSchoolAnnouncementBinding
import io.github.wulkanowy.utils.lifecycleAwareVariable
import io.github.wulkanowy.utils.toFormattedString
class SchoolAnnouncementDialog : DialogFragment() {
private var binding: DialogSchoolAnnouncementBinding by lifecycleAwareVariable()
private lateinit var announcement: SchoolAnnouncement
companion object {
private const val ARGUMENT_KEY = "item"
fun newInstance(exam: SchoolAnnouncement) = SchoolAnnouncementDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) }
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
arguments?.run {
announcement = getSerializable(ARGUMENT_KEY) as SchoolAnnouncement
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogSchoolAnnouncementBinding.inflate(inflater).also { binding = it }.root
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(binding) {
announcementDialogSubjectValue.text = announcement.subject
announcementDialogDateValue.text = announcement.date.toFormattedString()
announcementDialogDescriptionValue.text = announcement.content.parseAsHtml()
announcementDialogClose.setOnClickListener { dismiss() }
}
}
}

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