From e02d93f9797c8ada9bb48372e9363d697e298140 Mon Sep 17 00:00:00 2001 From: Damian Czupryn <60961958+Daxxxis@users.noreply.github.com> Date: Sun, 3 Oct 2021 01:07:45 +0200 Subject: [PATCH 01/69] Add Czech and Slovak listings (#1555) --- .../main/play/listings/cs-CZ/full-description.txt | 14 ++++++++++++++ .../main/play/listings/cs-CZ/short-description.txt | 1 + app/src/main/play/listings/cs-CZ/title.txt | 1 + app/src/main/play/listings/sk/full-description.txt | 14 ++++++++++++++ .../main/play/listings/sk/short-description.txt | 1 + app/src/main/play/listings/sk/title.txt | 1 + 6 files changed, 32 insertions(+) create mode 100644 app/src/main/play/listings/cs-CZ/full-description.txt create mode 100644 app/src/main/play/listings/cs-CZ/short-description.txt create mode 100644 app/src/main/play/listings/cs-CZ/title.txt create mode 100644 app/src/main/play/listings/sk/full-description.txt create mode 100644 app/src/main/play/listings/sk/short-description.txt create mode 100644 app/src/main/play/listings/sk/title.txt diff --git a/app/src/main/play/listings/cs-CZ/full-description.txt b/app/src/main/play/listings/cs-CZ/full-description.txt new file mode 100644 index 000000000..1420f5d67 --- /dev/null +++ b/app/src/main/play/listings/cs-CZ/full-description.txt @@ -0,0 +1,14 @@ +Aplikace je určena pro uživatele deníku VULCAN UONET+. + +Zvýrazněné vlastnosti a funkce: +- výpočet váženého průměru, +- procentuální zobrazení docházky, +- šťastné číslo, +- náhled na další a dokončené lekce, +- tmavý motiv, +- žádné reklamy, +- offline režim, +- upozornění. + +GitHub: https://github.com/wulkanowy/wulkanowy +Discord: https://discord.gg/vccAQBr diff --git a/app/src/main/play/listings/cs-CZ/short-description.txt b/app/src/main/play/listings/cs-CZ/short-description.txt new file mode 100644 index 000000000..0f29ab1b5 --- /dev/null +++ b/app/src/main/play/listings/cs-CZ/short-description.txt @@ -0,0 +1 @@ +Neoficiální aplikace žáka a rodiče pro deníku VULCAN UONET+ diff --git a/app/src/main/play/listings/cs-CZ/title.txt b/app/src/main/play/listings/cs-CZ/title.txt new file mode 100644 index 000000000..b7f42a5b0 --- /dev/null +++ b/app/src/main/play/listings/cs-CZ/title.txt @@ -0,0 +1 @@ +Wulkanowy Deníček diff --git a/app/src/main/play/listings/sk/full-description.txt b/app/src/main/play/listings/sk/full-description.txt new file mode 100644 index 000000000..2a4787d2d --- /dev/null +++ b/app/src/main/play/listings/sk/full-description.txt @@ -0,0 +1,14 @@ +Aplikácia je určená pre užívateľov denníka VULCAN UONET+. + +Zvýraznené vlastnosti a funkcie: +- výpočet váženého priemeru, +- percentuálne zobrazenie dochádzky, +- šťastné číslo, +- náhľad na ďalšie a dokončené lekcie, +- tmavý motív, +- žiadne reklamy, +- offline režim, +- upozornenia. + +GitHub: https://github.com/wulkanowy/wulkanowy +Discord: https://discord.gg/vccAQBr diff --git a/app/src/main/play/listings/sk/short-description.txt b/app/src/main/play/listings/sk/short-description.txt new file mode 100644 index 000000000..645ebbb6a --- /dev/null +++ b/app/src/main/play/listings/sk/short-description.txt @@ -0,0 +1 @@ +Neoficiálna aplikácia žiaka a rodiča pre denníka VULCAN UONET+ diff --git a/app/src/main/play/listings/sk/title.txt b/app/src/main/play/listings/sk/title.txt new file mode 100644 index 000000000..aa50ce77a --- /dev/null +++ b/app/src/main/play/listings/sk/title.txt @@ -0,0 +1 @@ +Wulkanowy Denníček From 8e607d48f7388db76e359d44192e9d01118a5f32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sun, 3 Oct 2021 10:36:17 +0200 Subject: [PATCH 02/69] Add coroutines scope to presenter (#1554) --- .../github/wulkanowy/ui/base/BasePresenter.kt | 26 +++++++++---------- .../modules/dashboard/DashboardPresenter.kt | 2 +- .../NotificationDebugPresenter.kt | 2 +- .../ui/modules/message/MessagePresenter.kt | 2 +- .../message/send/SendMessagePresenter.kt | 4 +-- .../message/tab/MessageTabPresenter.kt | 4 +-- .../mobiledevice/MobileDevicePresenter.kt | 3 +-- .../ui/modules/note/NotePresenter.kt | 19 +++++++------- .../SchoolAndTeachersPresenter.kt | 2 +- 9 files changed, 31 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt index 6f363bfc4..5cd5d0109 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt @@ -6,29 +6,27 @@ import io.github.wulkanowy.utils.flowWithResource import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import timber.log.Timber -import kotlin.coroutines.CoroutineContext open class BasePresenter( protected val errorHandler: ErrorHandler, protected val studentRepository: StudentRepository -) : CoroutineScope { +) { + private val job = SupervisorJob() - private var job = Job() + protected val presenterScope = CoroutineScope(job + Dispatchers.Main) - private val jobs = mutableMapOf() - - override val coroutineContext: CoroutineContext - get() = Dispatchers.Main + job + private val childrenJobs = mutableMapOf() var view: T? = null open fun onAttachView(view: T) { - job = Job() this.view = view errorHandler.apply { showErrorMessage = view::showError @@ -64,22 +62,22 @@ open class BasePresenter( } fun Flow.launch(individualJobTag: String = "load"): Job { - jobs[individualJobTag]?.cancel() - val job = catch { errorHandler.dispatch(it) }.launchIn(this@BasePresenter) - jobs[individualJobTag] = job + childrenJobs[individualJobTag]?.cancel() + val job = catch { errorHandler.dispatch(it) }.launchIn(presenterScope) + childrenJobs[individualJobTag] = job Timber.d("Job $individualJobTag launched in ${this@BasePresenter.javaClass.simpleName}: $job") return job } fun cancelJobs(vararg names: String) { names.forEach { - jobs[it]?.cancel() + childrenJobs[it]?.cancel() } } open fun onDetachView() { - view = null - job.cancel() + job.cancelChildren() errorHandler.clear() + view = null } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt index c513a9f6f..d6d533e28 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt @@ -149,7 +149,7 @@ class DashboardPresenter @Inject constructor( tileList: List, forceRefresh: Boolean ) { - launch { + presenterScope.launch { Timber.i("Loading dashboard account data started") val student = runCatching { studentRepository.getCurrentStudent(true) } .onFailure { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt index 6103317d2..26eca18fd 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt @@ -88,7 +88,7 @@ class NotificationDebugPresenter @Inject constructor( } private fun withStudent(block: suspend (Student) -> Unit) { - launch { + presenterScope.launch { block(studentRepository.getCurrentStudent(false)) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt index 7b8c3d0f5..be6940520 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt @@ -15,7 +15,7 @@ class MessagePresenter @Inject constructor( override fun onAttachView(view: MessageView) { super.onAttachView(view) - launch { + presenterScope.launch { delay(150) view.initView() Timber.i("Message view was initialized") diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt index 60a23e58a..77fa82316 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/SendMessagePresenter.kt @@ -224,14 +224,14 @@ class SendMessagePresenter @Inject constructor( } fun onMessageContentChange() { - launch { + presenterScope.launch { messageUpdateChannel.send(Unit) } } @OptIn(FlowPreview::class) private fun initializeSubjectStream() { - launch { + presenterScope.launch { messageUpdateChannel.consumeAsFlow() .debounce(250) .catch { Timber.e(it) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt index a24f9b79f..4571390b5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt @@ -173,14 +173,14 @@ class MessageTabPresenter @Inject constructor( } fun onSearchQueryTextChange(query: String) { - launch { + presenterScope.launch { searchChannel.send(query) } } @OptIn(FlowPreview::class) private fun initializeSearchStream() { - launch { + presenterScope.launch { searchChannel.consumeAsFlow() .debounce(250) .map { query -> diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDevicePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDevicePresenter.kt index 9591867df..53049891a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDevicePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/mobiledevice/MobileDevicePresenter.kt @@ -11,7 +11,6 @@ import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.flowWithResourceIn -import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -148,6 +147,6 @@ class MobileDevicePresenter @Inject constructor( errorHandler.dispatch(it.error!!) } } - }.launchIn(this) + }.launch("unregister") } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NotePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NotePresenter.kt index c441231e0..10a391820 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/note/NotePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/note/NotePresenter.kt @@ -11,7 +11,6 @@ import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.flowWithResourceIn -import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @@ -123,15 +122,17 @@ class NotePresenter @Inject constructor( } private fun updateNote(note: Note) { - flowWithResource { noteRepository.updateNote(note) }.onEach { - when (it.status) { - Status.LOADING -> Timber.i("Attempt to update note ${note.id}") - Status.SUCCESS -> Timber.i("Update note result: Success") - Status.ERROR -> { - Timber.i("Update note result: An exception occurred") - errorHandler.dispatch(it.error!!) + flowWithResource { noteRepository.updateNote(note) } + .onEach { + when (it.status) { + Status.LOADING -> Timber.i("Attempt to update note ${note.id}") + Status.SUCCESS -> Timber.i("Update note result: Success") + Status.ERROR -> { + Timber.i("Update note result: An exception occurred") + errorHandler.dispatch(it.error!!) + } } } - }.launchIn(this) + .launch("update_note") } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt index 915cc4213..c14272b9d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt @@ -15,7 +15,7 @@ class SchoolAndTeachersPresenter @Inject constructor( override fun onAttachView(view: SchoolAndTeachersView) { super.onAttachView(view) - launch { + presenterScope.launch { delay(150) view.initView() Timber.i("Message view was initialized") From 60501fcd728bae993c110a4ed500b5dd66012ae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sun, 3 Oct 2021 14:13:42 +0200 Subject: [PATCH 03/69] Set buildTimestamp through manifest meta (#1556) --- app/build.gradle | 4 ++-- app/src/main/AndroidManifest.xml | 10 ++++++++-- .../java/io/github/wulkanowy/utils/AppInfo.kt | 16 +++++++++++++--- .../data/db/migrations/AbstractMigrationTest.kt | 5 +++-- .../data/db/migrations/Migration35Test.kt | 6 +++--- .../wulkanowy/data/repositories/StudentTest.kt | 14 +++++++------- 6 files changed, 36 insertions(+), 19 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 1d6e56b11..e8762ddae 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,10 +27,10 @@ android { vectorDrawables.useSupportLibrary = true resValue "string", "app_name", "Wulkanowy" - buildConfigField "long", "BUILD_TIMESTAMP", String.valueOf(System.currentTimeMillis()) manifestPlaceholders = [ - firebase_enabled: project.hasProperty("enableFirebase") + firebase_enabled: project.hasProperty("enableFirebase"), + buildTimestamp: String.valueOf(System.currentTimeMillis()) ] javaCompileOptions { annotationProcessorOptions { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 84c50523c..bebe33c62 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -106,7 +106,8 @@ + android:exported="false" + tools:ignore="MissingClass"> @@ -152,6 +153,10 @@ android:resource="@xml/provider_paths" /> + + @@ -162,7 +167,8 @@ android:name="com.google.firebase.provider.FirebaseInitProvider" android:authorities="${applicationId}.firebaseinitprovider" android:enabled="${firebase_enabled}" - android:exported="false" /> + android:exported="false" + tools:ignore="MissingClass" /> () + @get:Rule val helper: MigrationTestHelper = MigrationTestHelper( InstrumentationRegistry.getInstrumentation(), @@ -24,7 +26,6 @@ abstract class AbstractMigrationTest { ) fun getMigratedRoomDatabase(): AppDatabase { - val context = ApplicationProvider.getApplicationContext() val database = Room.databaseBuilder( ApplicationProvider.getApplicationContext(), AppDatabase::class.java, @@ -32,7 +33,7 @@ abstract class AbstractMigrationTest { ).addMigrations( *AppDatabase.getMigrations( SharedPrefProvider(PreferenceManager.getDefaultSharedPreferences(context)), - AppInfo() + AppInfo(context) ) ).build() // close the database and release any stream resources when the test finishes diff --git a/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt index 883cdb81c..9a38f4286 100644 --- a/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt +++ b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt @@ -29,15 +29,15 @@ class Migration35Test : AbstractMigrationTest() { close() } - helper.runMigrationsAndValidate(dbName, 35, true, Migration35(AppInfo())) + helper.runMigrationsAndValidate(dbName, 35, true, Migration35(AppInfo(context))) val db = getMigratedRoomDatabase() val students = runBlocking { db.studentDao.loadAll() } assertEquals(2, students.size) - assertTrue { students[0].avatarColor in AppInfo().defaultColorsForAvatar } - assertTrue { students[1].avatarColor in AppInfo().defaultColorsForAvatar } + assertTrue { students[0].avatarColor in AppInfo(context).defaultColorsForAvatar } + assertTrue { students[1].avatarColor in AppInfo(context).defaultColorsForAvatar } } private fun createStudent(db: SupportSQLiteDatabase, id: Long) { diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt index b34bbf1b2..39ebb4a19 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt @@ -32,13 +32,13 @@ class StudentTest { fun initApi() { MockKAnnotations.init(this) studentRepository = StudentRepository( - mockk(), - TestDispatchersProvider(), - studentDb, - semesterDb, - mockSdk, - AppInfo(), - mockk() + context = mockk(), + dispatchers = TestDispatchersProvider(), + studentDb = studentDb, + semesterDb = semesterDb, + sdk = mockSdk, + appInfo = AppInfo(mockk()), + appDatabase = mockk() ) } From 031a17ea502159fc33e1e8564abd503ba38ae08a Mon Sep 17 00:00:00 2001 From: Damian Czupryn <60961958+Daxxxis@users.noreply.github.com> Date: Mon, 4 Oct 2021 16:35:37 +0200 Subject: [PATCH 04/69] Change text to bold in notifications center (#1546) --- app/src/main/res/layout/item_notifications_center.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout/item_notifications_center.xml b/app/src/main/res/layout/item_notifications_center.xml index ae3f5586d..16a7ae0c0 100644 --- a/app/src/main/res/layout/item_notifications_center.xml +++ b/app/src/main/res/layout/item_notifications_center.xml @@ -33,6 +33,7 @@ android:layout_marginStart="16dp" android:layout_marginTop="6dp" android:layout_marginEnd="16dp" + android:fontFamily="sans-serif-medium" android:textSize="15sp" app:layout_constraintEnd_toStartOf="@id/notifications_center_item_icon" app:layout_constraintStart_toStartOf="parent" From 04393e60bb860566442712a08893400169eb160a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Oct 2021 14:43:46 +0000 Subject: [PATCH 05/69] Bump agconnect-crash from 1.6.0.300 to 1.6.1.200 (#1563) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index e8762ddae..9cfa670eb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -228,7 +228,7 @@ dependencies { playImplementation 'com.google.android.play:core-ktx:1.8.1' hmsImplementation 'com.huawei.hms:hianalytics:6.2.0.301' - hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.0.300' + hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.1.200' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 8315759c83b967ddbd82cfb5a9e39f1261e03039 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Oct 2021 14:44:15 +0000 Subject: [PATCH 06/69] Bump agcp from 1.6.0.300 to 1.6.1.200 (#1562) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 15d3c792b..370fe296c 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ buildscript { classpath 'com.android.tools.build:gradle:7.0.2' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.10' - classpath 'com.huawei.agconnect:agcp:1.6.0.300' + classpath 'com.huawei.agconnect:agcp:1.6.1.200' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.3.0" From 426379ec17f2bb6c74d583d2d2185f242f1b5b01 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Oct 2021 14:45:09 +0000 Subject: [PATCH 07/69] Bump about_libraries from 8.9.1 to 8.9.3 (#1560) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 370fe296c..fbd127192 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { kotlin_version = '1.5.31' - about_libraries = '8.9.1' + about_libraries = '8.9.3' hilt_version = "2.38.1" } repositories { From 2d84b0775a9586a9119bbed6133a254497e3a20f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Oct 2021 14:51:31 +0000 Subject: [PATCH 08/69] Bump hilt_version from 2.38.1 to 2.39.1 (#1561) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index fbd127192..118efd398 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { kotlin_version = '1.5.31' about_libraries = '8.9.3' - hilt_version = "2.38.1" + hilt_version = "2.39.1" } repositories { mavenCentral() From 1839d7cb8f4b4dd4f0bb99dcfecde86ed6d6d9bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Mon, 4 Oct 2021 17:13:31 +0200 Subject: [PATCH 09/69] Migrate from moshi to kotlinx serialization (#1557) --- app/build.gradle | 12 ++-- .../github/wulkanowy/data/RepositoryModule.kt | 6 +- .../io/github/wulkanowy/data/db/Converters.kt | 29 ++++---- .../data/db/adapters/PairAdapterFactory.kt | 68 ------------------- .../wulkanowy/data/db/entities/Recipient.kt | 3 +- .../wulkanowy/data/pojos/Contributor.kt | 4 +- .../wulkanowy/data/pojos/MessageDraft.kt | 4 +- .../data/repositories/AppCreatorRepository.kt | 15 ++-- .../data/repositories/MessageRepository.kt | 11 +-- .../repositories/PreferencesRepository.kt | 20 +++--- .../modules/message/send/RecipientChipItem.kt | 4 +- .../wulkanowy/data/db/ConvertersTest.kt | 4 +- .../repositories/MessageRepositoryTest.kt | 7 +- build.gradle | 1 + 14 files changed, 56 insertions(+), 132 deletions(-) delete mode 100644 app/src/main/java/io/github/wulkanowy/data/db/adapters/PairAdapterFactory.kt diff --git a/app/build.gradle b/app/build.gradle index 9cfa670eb..91ce5f24a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-kapt' apply plugin: 'dagger.hilt.android.plugin' @@ -162,7 +163,7 @@ ext { room = "2.3.0" chucker = "3.5.2" mockk = "1.12.0" - moshi = "1.12.0" + coroutines = "1.5.2" } dependencies { @@ -170,7 +171,8 @@ dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" implementation "androidx.core:core-ktx:1.6.0" implementation "androidx.activity:activity-ktx:1.3.1" @@ -207,10 +209,6 @@ dependencies { implementation 'com.github.ncapdevi:FragNav:3.3.0' implementation "com.github.YarikSOffice:lingver:1.3.0" - implementation "com.squareup.moshi:moshi:$moshi" - implementation "com.squareup.moshi:moshi-adapters:$moshi" - kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi" - implementation "com.jakewharton.timber:timber:5.0.1" implementation "at.favre.lib:slf4j-timber:1.0.1" implementation 'com.github.bastienpaulfr:Treessence:1.0.4' @@ -237,7 +235,7 @@ dependencies { testImplementation "junit:junit:4.13.2" testImplementation "io.mockk:mockk:$mockk" - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2' + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines" testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" testImplementation 'org.robolectric:robolectric:4.6.1' diff --git a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt b/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt index f1b719e54..863bb5950 100644 --- a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt +++ b/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt @@ -9,7 +9,6 @@ import com.chuckerteam.chucker.api.ChuckerCollector import com.chuckerteam.chucker.api.ChuckerInterceptor import com.chuckerteam.chucker.api.RetentionManager import com.fredporciuncula.flow.preferences.FlowSharedPreferences -import com.squareup.moshi.Moshi import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -21,6 +20,7 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AppInfo import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.serialization.json.Json import timber.log.Timber import javax.inject.Singleton @@ -88,7 +88,9 @@ internal class RepositoryModule { @Singleton @Provides - fun provideMoshi() = Moshi.Builder().build() + fun provideJson() = Json { + ignoreUnknownKeys = true + } @Singleton @Provides diff --git a/app/src/main/java/io/github/wulkanowy/data/db/Converters.kt b/app/src/main/java/io/github/wulkanowy/data/db/Converters.kt index def0b3715..1993c4338 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/Converters.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/Converters.kt @@ -1,9 +1,10 @@ package io.github.wulkanowy.data.db import androidx.room.TypeConverter -import com.squareup.moshi.Moshi -import com.squareup.moshi.Types -import io.github.wulkanowy.data.db.adapters.PairAdapterFactory +import kotlinx.serialization.SerializationException +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime @@ -13,15 +14,7 @@ import java.util.Date class Converters { - private val moshi by lazy { Moshi.Builder().add(PairAdapterFactory).build() } - - private val integerListAdapter by lazy { - moshi.adapter>(Types.newParameterizedType(List::class.java, Integer::class.java)) - } - - private val stringListPairAdapter by lazy { - moshi.adapter>>(Types.newParameterizedType(List::class.java, Pair::class.java, String::class.java, String::class.java)) - } + private val json = Json @TypeConverter fun timestampToDate(value: Long?): LocalDate? = value?.run { @@ -51,21 +44,25 @@ class Converters { @TypeConverter fun intListToJson(list: List): String { - return integerListAdapter.toJson(list) + return json.encodeToString(list) } @TypeConverter fun jsonToIntList(value: String): List { - return integerListAdapter.fromJson(value).orEmpty() + return json.decodeFromString(value) } @TypeConverter fun stringPairListToJson(list: List>): String { - return stringListPairAdapter.toJson(list) + return json.encodeToString(list) } @TypeConverter fun jsonToStringPairList(value: String): List> { - return stringListPairAdapter.fromJson(value).orEmpty() + return try { + json.decodeFromString(value) + } catch (e: SerializationException) { + emptyList() // handle errors from old gson Pair serialized data + } } } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/adapters/PairAdapterFactory.kt b/app/src/main/java/io/github/wulkanowy/data/db/adapters/PairAdapterFactory.kt deleted file mode 100644 index 4a9b168dd..000000000 --- a/app/src/main/java/io/github/wulkanowy/data/db/adapters/PairAdapterFactory.kt +++ /dev/null @@ -1,68 +0,0 @@ -package io.github.wulkanowy.data.db.adapters - -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.JsonReader -import com.squareup.moshi.JsonWriter -import com.squareup.moshi.Moshi -import com.squareup.moshi.Types -import java.lang.reflect.ParameterizedType -import java.lang.reflect.Type - -object PairAdapterFactory : JsonAdapter.Factory { - - override fun create(type: Type, annotations: MutableSet, moshi: Moshi): JsonAdapter<*>? { - if (type !is ParameterizedType || List::class.java != type.rawType) return null - if (type.actualTypeArguments[0] != Pair::class.java) return null - - val listType = Types.newParameterizedType(List::class.java, Map::class.java, String::class.java) - val listAdapter = moshi.adapter>>(listType) - - val mapType = Types.newParameterizedType(MutableMap::class.java, String::class.java, String::class.java) - val mapAdapter = moshi.adapter>(mapType) - - return PairAdapter(listAdapter, mapAdapter) - } - - private class PairAdapter( - private val listAdapter: JsonAdapter>>, - private val mapAdapter: JsonAdapter>, - ) : JsonAdapter>>() { - - override fun toJson(writer: JsonWriter, value: List>?) { - writer.beginArray() - value?.forEach { - writer.beginObject() - writer.name("first").value(it.first) - writer.name("second").value(it.second) - writer.endObject() - } - writer.endArray() - } - - override fun fromJson(reader: JsonReader): List>? { - return if (reader.peek() == JsonReader.Token.BEGIN_OBJECT) deserializeMoshiMap(reader) - else deserializeGsonPair(reader) - } - - // for compatibility with 0.21.0 - private fun deserializeMoshiMap(reader: JsonReader): List>? { - val map = mapAdapter.fromJson(reader) ?: return null - - return map.entries.map { - it.key to it.value - } - } - - private fun deserializeGsonPair(reader: JsonReader): List>? { - val list = listAdapter.fromJson(reader) ?: return null - - return list.map { - require(it.size == 2) { - "pair with more or less than two elements: $list" - } - - it["first"].orEmpty() to it["second"].orEmpty() - } - } - } -} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Recipient.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Recipient.kt index 60e67d32f..223322705 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Recipient.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Recipient.kt @@ -3,10 +3,9 @@ package io.github.wulkanowy.data.db.entities import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey -import com.squareup.moshi.JsonClass import java.io.Serializable -@JsonClass(generateAdapter = true) +@kotlinx.serialization.Serializable @Entity(tableName = "Recipients") data class Recipient( diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/Contributor.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/Contributor.kt index d2338c281..4165b3f14 100644 --- a/app/src/main/java/io/github/wulkanowy/data/pojos/Contributor.kt +++ b/app/src/main/java/io/github/wulkanowy/data/pojos/Contributor.kt @@ -1,8 +1,8 @@ package io.github.wulkanowy.data.pojos -import com.squareup.moshi.JsonClass +import kotlinx.serialization.Serializable -@JsonClass(generateAdapter = true) +@Serializable class Contributor( val displayName: String, val githubUsername: String diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/MessageDraft.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/MessageDraft.kt index a79b70cd4..2e568e376 100644 --- a/app/src/main/java/io/github/wulkanowy/data/pojos/MessageDraft.kt +++ b/app/src/main/java/io/github/wulkanowy/data/pojos/MessageDraft.kt @@ -1,9 +1,9 @@ package io.github.wulkanowy.data.pojos -import com.squareup.moshi.JsonClass import io.github.wulkanowy.ui.modules.message.send.RecipientChipItem +import kotlinx.serialization.Serializable -@JsonClass(generateAdapter = true) +@Serializable data class MessageDraft( val recipients: List, val subject: String, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt index 71b7ea94e..f1fdbfed3 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt @@ -1,25 +1,26 @@ package io.github.wulkanowy.data.repositories import android.content.res.AssetManager -import com.squareup.moshi.Moshi -import com.squareup.moshi.Types import io.github.wulkanowy.data.pojos.Contributor import io.github.wulkanowy.utils.DispatchersProvider import kotlinx.coroutines.withContext +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream import javax.inject.Inject import javax.inject.Singleton @Singleton class AppCreatorRepository @Inject constructor( private val assets: AssetManager, - private val dispatchers: DispatchersProvider + private val dispatchers: DispatchersProvider, + private val json: Json, ) { + @OptIn(ExperimentalSerializationApi::class) @Suppress("BlockingMethodInNonBlockingContext") suspend fun getAppCreators() = withContext(dispatchers.backgroundThread) { - val moshi = Moshi.Builder().build() - val type = Types.newParameterizedType(List::class.java, Contributor::class.java) - val adapter = moshi.adapter>(type) - adapter.fromJson(assets.open("contributors.json").bufferedReader().use { it.readText() }) + val inputStream = assets.open("contributors.json").buffered() + json.decodeFromStream>(inputStream) } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt index ee5164957..224c69bd7 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt @@ -1,7 +1,6 @@ package io.github.wulkanowy.data.repositories import android.content.Context -import com.squareup.moshi.Moshi import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.Resource @@ -18,7 +17,6 @@ import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED import io.github.wulkanowy.data.mappers.mapFromEntities import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.pojos.MessageDraft -import io.github.wulkanowy.data.pojos.MessageDraftJsonAdapter import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.Folder import io.github.wulkanowy.sdk.pojo.SentMessage @@ -29,6 +27,9 @@ import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import timber.log.Timber import java.time.LocalDateTime.now import javax.inject.Inject @@ -42,7 +43,7 @@ class MessageRepository @Inject constructor( @ApplicationContext private val context: Context, private val refreshHelper: AutoRefreshHelper, private val sharedPrefProvider: SharedPrefProvider, - private val moshi: Moshi, + private val json: Json, ) { private val saveFetchResultMutex = Mutex() @@ -168,9 +169,9 @@ class MessageRepository @Inject constructor( var draftMessage: MessageDraft? get() = sharedPrefProvider.getString(context.getString(R.string.pref_key_message_send_draft)) - ?.let { MessageDraftJsonAdapter(moshi).fromJson(it) } + ?.let { json.decodeFromString(it) } set(value) = sharedPrefProvider.putString( context.getString(R.string.pref_key_message_send_draft), - value?.let { MessageDraftJsonAdapter(moshi).toJson(it) } + value?.let { json.encodeToString(it) } ) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index a08045f85..65edd53f0 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -5,9 +5,6 @@ import android.content.SharedPreferences import androidx.core.content.edit import com.fredporciuncula.flow.preferences.FlowSharedPreferences import com.fredporciuncula.flow.preferences.Preference -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.Moshi -import com.squareup.moshi.adapter import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.sdk.toLocalDate @@ -19,6 +16,9 @@ import io.github.wulkanowy.utils.toTimestamp import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import java.time.LocalDate import java.time.LocalDateTime import javax.inject.Inject @@ -27,16 +27,12 @@ import javax.inject.Singleton @OptIn(ExperimentalCoroutinesApi::class) @Singleton class PreferencesRepository @Inject constructor( + @ApplicationContext val context: Context, private val sharedPref: SharedPreferences, private val flowSharedPref: FlowSharedPreferences, - @ApplicationContext val context: Context, - moshi: Moshi + private val json: Json, ) { - @OptIn(ExperimentalStdlibApi::class) - private val dashboardItemsPositionAdapter: JsonAdapter> = - moshi.adapter() - val startMenuIndex: Int get() = getString(R.string.pref_key_start_menu, R.string.pref_default_startup).toInt() @@ -197,14 +193,14 @@ class PreferencesRepository @Inject constructor( var dashboardItemsPosition: Map? get() { - val json = sharedPref.getString(PREF_KEY_DASHBOARD_ITEMS_POSITION, null) ?: return null + val value = sharedPref.getString(PREF_KEY_DASHBOARD_ITEMS_POSITION, null) ?: return null - return dashboardItemsPositionAdapter.fromJson(json) + return json.decodeFromString(value) } set(value) = sharedPref.edit { putString( PREF_KEY_DASHBOARD_ITEMS_POSITION, - dashboardItemsPositionAdapter.toJson(value) + json.encodeToString(value) ) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/RecipientChipItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/RecipientChipItem.kt index 26ab7f488..bd14bc893 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/RecipientChipItem.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/send/RecipientChipItem.kt @@ -1,10 +1,10 @@ package io.github.wulkanowy.ui.modules.message.send -import com.squareup.moshi.JsonClass import io.github.wulkanowy.data.db.entities.Recipient import io.github.wulkanowy.materialchipsinput.ChipItem +import kotlinx.serialization.Serializable -@JsonClass(generateAdapter = true) +@Serializable data class RecipientChipItem( override val title: String, diff --git a/app/src/test/java/io/github/wulkanowy/data/db/ConvertersTest.kt b/app/src/test/java/io/github/wulkanowy/data/db/ConvertersTest.kt index 3b0d7c841..a044a4299 100644 --- a/app/src/test/java/io/github/wulkanowy/data/db/ConvertersTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/db/ConvertersTest.kt @@ -23,8 +23,8 @@ class ConvertersTest { @Test fun jsonToStringPairList_0210() { - assertEquals(Converters().jsonToStringPairList("{\"aaa\":\"bbb\",\"ccc\":\"ddd\"}"), listOf("aaa" to "bbb", "ccc" to "ddd")) - assertEquals(Converters().jsonToStringPairList("{\"aaa\":\"bbb\"}"), listOf("aaa" to "bbb")) + assertEquals(Converters().jsonToStringPairList("{\"aaa\":\"bbb\",\"ccc\":\"ddd\"}"), listOf>()) + assertEquals(Converters().jsonToStringPairList("{\"aaa\":\"bbb\"}"), listOf>()) assertEquals(Converters().jsonToStringPairList("{}"), listOf>()) } } diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt index 25774d74c..052f08f00 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/MessageRepositoryTest.kt @@ -1,7 +1,6 @@ package io.github.wulkanowy.data.repositories import android.content.Context -import com.squareup.moshi.Moshi import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.dao.MessageAttachmentDao @@ -30,6 +29,7 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -63,9 +63,6 @@ class MessageRepositoryTest { private lateinit var repository: MessageRepository - @MockK - private lateinit var moshi: Moshi - @Before fun setUp() { MockKAnnotations.init(this) @@ -78,7 +75,7 @@ class MessageRepositoryTest { context = context, refreshHelper = refreshHelper, sharedPrefProvider = sharedPrefProvider, - moshi = moshi, + json = Json, ) } diff --git a/build.gradle b/build.gradle index 118efd398..153088a6d 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,7 @@ buildscript { } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath 'com.android.tools.build:gradle:7.0.2' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.10' From e8075e30e4f3d6203d66e0dccd8921df97fd71a7 Mon Sep 17 00:00:00 2001 From: Mateusz Idziejczak Date: Fri, 8 Oct 2021 11:19:49 +0200 Subject: [PATCH 10/69] Add "add homework" feature (#1564) --- .../41.json | 2322 +++++++++++++++++ .../github/wulkanowy/data/db/AppDatabase.kt | 6 +- .../wulkanowy/data/db/entities/Homework.kt | 3 + .../data/db/migrations/Migration41.kt | 11 + .../data/repositories/HomeworkRepository.kt | 7 +- .../ui/modules/homework/HomeworkFragment.kt | 9 +- .../ui/modules/homework/HomeworkPresenter.kt | 6 +- .../ui/modules/homework/HomeworkView.kt | 4 +- .../modules/homework/add/HomeworkAddDialog.kt | 124 + .../homework/add/HomeworkAddPresenter.kt | 92 + .../modules/homework/add/HomeworkAddView.kt | 21 + .../details/HomeworkDetailsAdapter.kt | 28 +- .../homework/details/HomeworkDetailsDialog.kt | 8 + .../details/HomeworkDetailsPresenter.kt | 19 + .../homework/details/HomeworkDetailsView.kt | 4 + .../login/advanced/LoginAdvancedFragment.kt | 10 +- .../modules/login/form/LoginFormFragment.kt | 4 +- .../login/recover/LoginRecoverFragment.kt | 2 +- .../login/symbol/LoginSymbolFragment.kt | 2 +- app/src/main/res/layout/dialog_homework.xml | 17 +- .../main/res/layout/dialog_homework_add.xml | 134 + app/src/main/res/layout/fragment_homework.xml | 22 +- .../layout/item_homework_dialog_details.xml | 26 +- app/src/main/res/values/strings.xml | 16 +- 24 files changed, 2850 insertions(+), 47 deletions(-) create mode 100644 app/schemas/io.github.wulkanowy.data.db.AppDatabase/41.json create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddDialog.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddPresenter.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddView.kt create mode 100644 app/src/main/res/layout/dialog_homework_add.xml diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/41.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/41.json new file mode 100644 index 000000000..9d008060a --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/41.json @@ -0,0 +1,2322 @@ +{ + "formatVersion": 1, + "database": { + "version": 41, + "identityHash": "d9ce44a78495a358606612bd91603c0f", + "entities": [ + { + "tableName": "Students", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrapper_base_url` TEXT NOT NULL, `mobile_base_url` TEXT NOT NULL, `login_type` TEXT NOT NULL, `login_mode` TEXT NOT NULL, `certificate_key` TEXT NOT NULL, `private_key` TEXT NOT NULL, `is_parent` INTEGER NOT NULL, `email` TEXT NOT NULL, `password` TEXT NOT NULL, `symbol` TEXT NOT NULL, `student_id` INTEGER NOT NULL, `user_login_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `student_name` TEXT NOT NULL, `school_id` TEXT NOT NULL, `school_short` TEXT NOT NULL, `school_name` TEXT NOT NULL, `class_name` TEXT NOT NULL, `class_id` INTEGER NOT NULL, `is_current` INTEGER NOT NULL, `registration_date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `nick` TEXT NOT NULL, `avatar_color` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "scrapperBaseUrl", + "columnName": "scrapper_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mobileBaseUrl", + "columnName": "mobile_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginType", + "columnName": "login_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginMode", + "columnName": "login_mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificateKey", + "columnName": "certificate_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "private_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isParent", + "columnName": "is_parent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "user_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "student_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolSymbol", + "columnName": "school_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "school_short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolName", + "columnName": "school_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "className", + "columnName": "class_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCurrent", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registrationDate", + "columnName": "registration_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nick", + "columnName": "nick", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarColor", + "columnName": "avatar_color", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Students_email_symbol_student_id_school_id_class_id", + "unique": true, + "columnNames": [ + "email", + "symbol", + "student_id", + "school_id", + "class_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryName", + "columnName": "diary_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolYear", + "columnName": "school_year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterName", + "columnName": "semester_name", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "semester_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `semester_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Exams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Timetable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `number` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `subjectOld` TEXT NOT NULL, `group` TEXT NOT NULL, `room` TEXT NOT NULL, `roomOld` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacherOld` TEXT NOT NULL, `info` TEXT NOT NULL, `student_plan` INTEGER NOT NULL, `changes` INTEGER NOT NULL, `canceled` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectOld", + "columnName": "subjectOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "room", + "columnName": "room", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roomOld", + "columnName": "roomOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherOld", + "columnName": "teacherOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isStudentPlan", + "columnName": "student_plan", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "changes", + "columnName": "changes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canceled", + "columnName": "canceled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Attendance", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `time_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `excusable` INTEGER NOT NULL, `excuse_status` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeId", + "columnName": "time_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excusable", + "columnName": "excusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excuseStatus", + "columnName": "excuse_status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AttendanceSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `subject_id` INTEGER NOT NULL, `month` INTEGER NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `absence_excused` INTEGER NOT NULL, `absence_for_school_reasons` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `lateness_excused` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subject_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "month", + "columnName": "month", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceExcused", + "columnName": "absence_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceForSchoolReasons", + "columnName": "absence_for_school_reasons", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latenessExcused", + "columnName": "lateness_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Grades", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `entry` TEXT NOT NULL, `value` REAL NOT NULL, `modifier` REAL NOT NULL, `comment` TEXT NOT NULL, `color` TEXT NOT NULL, `grade_symbol` TEXT NOT NULL, `description` TEXT NOT NULL, `weight` TEXT NOT NULL, `weightValue` REAL NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entry", + "columnName": "entry", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "modifier", + "columnName": "modifier", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gradeSymbol", + "columnName": "grade_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weightValue", + "columnName": "weightValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `subject` TEXT NOT NULL, `predicted_grade` TEXT NOT NULL, `final_grade` TEXT NOT NULL, `proposed_points` TEXT NOT NULL, `final_points` TEXT NOT NULL, `points_sum` TEXT NOT NULL, `average` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_predicted_grade_notified` INTEGER NOT NULL, `is_final_grade_notified` INTEGER NOT NULL, `predicted_grade_last_change` INTEGER NOT NULL, `final_grade_last_change` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "predictedGrade", + "columnName": "predicted_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalGrade", + "columnName": "final_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "proposedPoints", + "columnName": "proposed_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalPoints", + "columnName": "final_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pointsSum", + "columnName": "points_sum", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "average", + "columnName": "average", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPredictedGradeNotified", + "columnName": "is_predicted_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFinalGradeNotified", + "columnName": "is_final_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "predictedGradeLastChange", + "columnName": "predicted_grade_last_change", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "finalGradeLastChange", + "columnName": "final_grade_last_change", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradePartialStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `class_average` TEXT NOT NULL, `student_average` TEXT NOT NULL, `class_amounts` TEXT NOT NULL, `student_amounts` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAverage", + "columnName": "class_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAverage", + "columnName": "student_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAmounts", + "columnName": "class_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAmounts", + "columnName": "student_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesPointsStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `others` REAL NOT NULL, `student` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "others", + "columnName": "others", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "student", + "columnName": "student", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradeSemesterStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `amounts` TEXT NOT NULL, `student_grade` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amounts", + "columnName": "amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentGrade", + "columnName": "student_grade", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `recipient_name` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `removed` INTEGER NOT NULL, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `unread_by` INTEGER NOT NULL, `read_by` INTEGER NOT NULL, `content` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipient", + "columnName": "recipient_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "removed", + "columnName": "removed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasAttachments", + "columnName": "has_attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `one_drive_id` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`real_id`))", + "fields": [ + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "oneDriveId", + "columnName": "one_drive_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "real_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `category` TEXT NOT NULL, `category_type` INTEGER NOT NULL, `is_points_show` INTEGER NOT NULL, `points` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryType", + "columnName": "category_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPointsShow", + "columnName": "is_points_show", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Homework", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `attachments` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_done` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL, `is_added_by_user` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "is_done", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Subjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "LuckyNumbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `lucky_number` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "luckyNumber", + "columnName": "lucky_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CompletedLesson", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `topic` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `substitution` TEXT NOT NULL, `absence` TEXT NOT NULL, `resources` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "topic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "substitution", + "columnName": "substitution", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "resources", + "columnName": "resources", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ReportingUnits", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `short` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `roles` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "senderName", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roles", + "columnName": "roles", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` TEXT NOT NULL, `name` TEXT NOT NULL, `real_name` TEXT NOT NULL, `login_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `role` INTEGER NOT NULL, `hash` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "realName", + "columnName": "real_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginId", + "columnName": "login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MobileDevices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Teachers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `short_name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "School", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `contact` TEXT NOT NULL, `headmaster` TEXT NOT NULL, `pedagogue` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contact", + "columnName": "contact", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headmaster", + "columnName": "headmaster", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pedagogue", + "columnName": "pedagogue", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Conferences", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `subject` TEXT NOT NULL, `agenda` TEXT NOT NULL, `present_on_conference` TEXT NOT NULL, `conference_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agenda", + "columnName": "agenda", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presentOnConference", + "columnName": "present_on_conference", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conference_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableAdditional", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StudentInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `full_name` TEXT NOT NULL, `first_name` TEXT NOT NULL, `second_name` TEXT NOT NULL, `surname` TEXT NOT NULL, `birth_date` INTEGER NOT NULL, `birth_place` TEXT NOT NULL, `gender` TEXT NOT NULL, `has_polish_citizenship` INTEGER NOT NULL, `family_name` TEXT NOT NULL, `parents_names` TEXT NOT NULL, `address` TEXT NOT NULL, `registered_address` TEXT NOT NULL, `correspondence_address` TEXT NOT NULL, `phone_number` TEXT NOT NULL, `cell_phone_number` TEXT NOT NULL, `email` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `first_guardian_full_name` TEXT, `first_guardian_kinship` TEXT, `first_guardian_address` TEXT, `first_guardian_phones` TEXT, `first_guardian_email` TEXT, `second_guardian_full_name` TEXT, `second_guardian_kinship` TEXT, `second_guardian_address` TEXT, `second_guardian_phones` TEXT, `second_guardian_email` TEXT)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "full_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondName", + "columnName": "second_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "surname", + "columnName": "surname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "birthDate", + "columnName": "birth_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthPlace", + "columnName": "birth_place", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gender", + "columnName": "gender", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPolishCitizenship", + "columnName": "has_polish_citizenship", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "familyName", + "columnName": "family_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentsNames", + "columnName": "parents_names", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "registeredAddress", + "columnName": "registered_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "correspondenceAddress", + "columnName": "correspondence_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cellPhoneNumber", + "columnName": "cell_phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstGuardian.fullName", + "columnName": "first_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.kinship", + "columnName": "first_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.address", + "columnName": "first_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.phones", + "columnName": "first_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.email", + "columnName": "first_guardian_email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.fullName", + "columnName": "second_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.kinship", + "columnName": "second_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.address", + "columnName": "second_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.phones", + "columnName": "second_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.email", + "columnName": "second_guardian_email", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableHeaders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SchoolAnnouncements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `type` TEXT NOT NULL, `date` INTEGER NOT NULL, `data` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd9ce44a78495a358606612bd91603c0f')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt index 09aa972f0..b93d00881 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt @@ -97,6 +97,7 @@ import io.github.wulkanowy.data.db.migrations.Migration37 import io.github.wulkanowy.data.db.migrations.Migration38 import io.github.wulkanowy.data.db.migrations.Migration39 import io.github.wulkanowy.data.db.migrations.Migration4 +import io.github.wulkanowy.data.db.migrations.Migration41 import io.github.wulkanowy.data.db.migrations.Migration40 import io.github.wulkanowy.data.db.migrations.Migration5 import io.github.wulkanowy.data.db.migrations.Migration6 @@ -146,7 +147,7 @@ import javax.inject.Singleton abstract class AppDatabase : RoomDatabase() { companion object { - const val VERSION_SCHEMA = 40 + const val VERSION_SCHEMA = 41 fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( Migration2(), @@ -187,7 +188,8 @@ abstract class AppDatabase : RoomDatabase() { Migration37(), Migration38(), Migration39(), - Migration40() + Migration40(), + Migration41() ) fun newInstance( diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Homework.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Homework.kt index 04ee1e8c6..4538cf31f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Homework.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Homework.kt @@ -40,4 +40,7 @@ data class Homework( @ColumnInfo(name = "is_notified") var isNotified: Boolean = true + + @ColumnInfo(name = "is_added_by_user") + var isAddedByUser: Boolean = false } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt new file mode 100644 index 000000000..17773efdb --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt @@ -0,0 +1,11 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration41 : Migration(40, 41) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE Homework ADD COLUMN is_added_by_user INTEGER NOT NULL DEFAULT 0") + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt index a04085fb7..95a375a45 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/HomeworkRepository.kt @@ -61,8 +61,9 @@ class HomeworkRepository @Inject constructor( val homeWorkToSave = (new uniqueSubtract old).onEach { if (notify) it.isNotified = false } + val filteredOld = old.filterNot { it.isAddedByUser } - homeworkDb.deleteAll(old uniqueSubtract new) + homeworkDb.deleteAll(filteredOld uniqueSubtract new) homeworkDb.insertAll(homeWorkToSave) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end)) @@ -79,4 +80,8 @@ class HomeworkRepository @Inject constructor( homeworkDb.loadAll(semester.semesterId, semester.studentId, start.monday, end.sunday) suspend fun updateHomework(homework: List) = homeworkDb.updateAll(homework) + + suspend fun saveHomework(homework: Homework) = homeworkDb.insertAll(listOf(homework)) + + suspend fun deleteHomework(homework: Homework) = homeworkDb.deleteAll(listOf(homework)) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt index 1d9434dc7..a723b0483 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt @@ -10,6 +10,7 @@ import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Homework import io.github.wulkanowy.databinding.FragmentHomeworkBinding import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.homework.add.HomeworkAddDialog import io.github.wulkanowy.ui.modules.homework.details.HomeworkDetailsDialog import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView @@ -64,6 +65,8 @@ class HomeworkFragment : BaseFragment(R.layout.fragment homeworkPreviousButton.setOnClickListener { presenter.onPreviousDay() } homeworkNextButton.setOnClickListener { presenter.onNextDay() } + openAddHomeworkButton.setOnClickListener { presenter.onHomeworkAddButtonClicked() } + homeworkNavContainer.setElevationCompat(requireContext().dpToPx(8f)) } } @@ -122,10 +125,14 @@ class HomeworkFragment : BaseFragment(R.layout.fragment binding.homeworkNextButton.visibility = if (show) VISIBLE else View.INVISIBLE } - override fun showTimetableDialog(homework: Homework) { + override fun showHomeworkDialog(homework: Homework) { (activity as? MainActivity)?.showDialogFragment(HomeworkDetailsDialog.newInstance(homework)) } + override fun showAddHomeworkDialog() { + (activity as? MainActivity)?.showDialogFragment(HomeworkAddDialog()) + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay()) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkPresenter.kt index 11c54dc24..d7d5d7cb9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkPresenter.kt @@ -78,7 +78,11 @@ class HomeworkPresenter @Inject constructor( fun onHomeworkItemSelected(homework: Homework) { Timber.i("Select homework item ${homework.id}") - view?.showTimetableDialog(homework) + view?.showHomeworkDialog(homework) + } + + fun onHomeworkAddButtonClicked() { + view?.showAddHomeworkDialog() } private fun setBaseDateOnHolidays() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt index a1d6a04a9..7c05ab865 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkView.kt @@ -33,5 +33,7 @@ interface HomeworkView : BaseView { fun showNextButton(show: Boolean) - fun showTimetableDialog(homework: Homework) + fun showHomeworkDialog(homework: Homework) + + fun showAddHomeworkDialog() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddDialog.kt new file mode 100644 index 000000000..12168f142 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddDialog.kt @@ -0,0 +1,124 @@ +package io.github.wulkanowy.ui.modules.homework.add + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.widget.doOnTextChanged +import com.google.android.material.datepicker.CalendarConstraints +import com.google.android.material.datepicker.MaterialDatePicker +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +import io.github.wulkanowy.databinding.DialogHomeworkAddBinding +import io.github.wulkanowy.ui.base.BaseDialogFragment +import io.github.wulkanowy.utils.toFormattedString +import io.github.wulkanowy.utils.toLocalDateTime +import io.github.wulkanowy.utils.toTimestamp +import java.time.LocalDate +import javax.inject.Inject + +@AndroidEntryPoint +class HomeworkAddDialog : BaseDialogFragment(), HomeworkAddView { + + @Inject + lateinit var presenter: HomeworkAddPresenter + + private var date: LocalDate? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NO_TITLE, 0) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = DialogHomeworkAddBinding.inflate(inflater).apply { binding = this }.root + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + presenter.onAttachView(this) + } + + override fun initView() { + with(binding) { + homeworkDialogSubjectEdit.doOnTextChanged { _, _, _, _ -> + homeworkDialogSubject.error = null + homeworkDialogSubject.isErrorEnabled = false + } + homeworkDialogDateEdit.doOnTextChanged { _, _, _, _ -> + homeworkDialogDate.error = null + homeworkDialogDate.isErrorEnabled = false + } + homeworkDialogContentEdit.doOnTextChanged { _, _, _, _ -> + homeworkDialogContent.error = null + homeworkDialogContent.isErrorEnabled = false + } + homeworkDialogClose.setOnClickListener { dismiss() } + homeworkDialogDateEdit.setOnClickListener { presenter.showDatePicker(date) } + homeworkDialogAdd.setOnClickListener { + presenter.onAddHomeworkClicked( + subject = homeworkDialogSubjectEdit.text?.toString(), + teacher = homeworkDialogTeacherEdit.text?.toString(), + date = homeworkDialogDateEdit.text?.toString(), + content = homeworkDialogContentEdit.text?.toString() + ) + } + } + } + + override fun showSuccessMessage() { + showMessage(getString(R.string.homework_add_success)) + } + + override fun setErrorSubjectRequired() { + with(binding.homeworkDialogSubject) { + isErrorEnabled = true + error = getString(R.string.error_field_required) + } + } + + override fun setErrorDateRequired() { + with(binding.homeworkDialogDate) { + isErrorEnabled = true + error = getString(R.string.error_field_required) + } + } + + override fun setErrorContentRequired() { + with(binding.homeworkDialogContent) { + isErrorEnabled = true + error = getString(R.string.error_field_required) + } + } + + override fun closeDialog() { + dismiss() + } + + override fun showDatePickerDialog(currentDate: LocalDate) { + val constraintsBuilder = CalendarConstraints.Builder().apply { + setStart(LocalDate.now().toEpochDay()) + } + val datePicker = + MaterialDatePicker.Builder.datePicker() + .setCalendarConstraints(constraintsBuilder.build()) + .setSelection(currentDate.toTimestamp()) + .build() + + datePicker.addOnPositiveButtonClickListener { + date = it.toLocalDateTime().toLocalDate() + binding.homeworkDialogDate.editText?.setText(date!!.toFormattedString()) + } + + if (!parentFragmentManager.isStateSaved) { + datePicker.show(this.parentFragmentManager, null) + } + } + + override fun onDestroyView() { + presenter.onDetachView() + super.onDestroyView() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddPresenter.kt new file mode 100644 index 000000000..3639c2fef --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddPresenter.kt @@ -0,0 +1,92 @@ +package io.github.wulkanowy.ui.modules.homework.add + +import io.github.wulkanowy.data.Status +import io.github.wulkanowy.data.db.entities.Homework +import io.github.wulkanowy.data.repositories.HomeworkRepository +import io.github.wulkanowy.data.repositories.SemesterRepository +import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.ui.base.ErrorHandler +import io.github.wulkanowy.utils.flowWithResource +import io.github.wulkanowy.utils.toLocalDate +import kotlinx.coroutines.flow.onEach +import timber.log.Timber +import java.time.LocalDate +import javax.inject.Inject + +class HomeworkAddPresenter @Inject constructor( + errorHandler: ErrorHandler, + studentRepository: StudentRepository, + private val homeworkRepository: HomeworkRepository, + private val semesterRepository: SemesterRepository +) : BasePresenter(errorHandler, studentRepository) { + + override fun onAttachView(view: HomeworkAddView) { + super.onAttachView(view) + view.initView() + Timber.i("Homework details view was initialized") + } + + fun showDatePicker(date: LocalDate?) { + view?.showDatePickerDialog(date ?: LocalDate.now()) + } + + fun onAddHomeworkClicked(subject: String?, teacher: String?, date: String?, content: String?) { + var isError = false + + if (subject.isNullOrBlank()) { + view?.setErrorSubjectRequired() + isError = true + } + + if (date.isNullOrBlank()) { + view?.setErrorDateRequired() + isError = true + } + + if (content.isNullOrBlank()) { + view?.setErrorContentRequired() + isError = true + } + + if (!isError) { + saveHomework(subject!!, teacher.orEmpty(), date!!.toLocalDate(), content!!) + } + } + + private fun saveHomework(subject: String, teacher: String, date: LocalDate, content: String) { + flowWithResource { + val student = studentRepository.getCurrentStudent() + val semester = semesterRepository.getCurrentSemester(student) + val entryDate = LocalDate.now() + homeworkRepository.saveHomework( + Homework( + semesterId = semester.semesterId, + studentId = student.studentId, + date = date, + entryDate = entryDate, + subject = subject, + content = content, + teacher = teacher, + teacherSymbol = "", + attachments = emptyList(), + ).apply { isAddedByUser = true } + ) + }.onEach { + when (it.status) { + Status.LOADING -> Timber.i("Homework insert start") + Status.SUCCESS -> { + Timber.i("Homework insert: Success") + view?.run { + showSuccessMessage() + closeDialog() + } + } + Status.ERROR -> { + Timber.i("Homework insert result: An exception occurred") + errorHandler.dispatch(it.error!!) + } + } + }.launch("add_homework") + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddView.kt new file mode 100644 index 000000000..3bb304d9c --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/add/HomeworkAddView.kt @@ -0,0 +1,21 @@ +package io.github.wulkanowy.ui.modules.homework.add + +import io.github.wulkanowy.ui.base.BaseView +import java.time.LocalDate + +interface HomeworkAddView : BaseView { + + fun initView() + + fun showSuccessMessage() + + fun setErrorSubjectRequired() + + fun setErrorDateRequired() + + fun setErrorContentRequired() + + fun closeDialog() + + fun showDatePickerDialog(currentDate: LocalDate) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt index cd9a7e851..e03707a5c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsAdapter.kt @@ -5,10 +5,12 @@ import android.view.View.GONE import android.view.View.VISIBLE import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Homework import io.github.wulkanowy.databinding.ItemHomeworkDialogAttachmentBinding import io.github.wulkanowy.databinding.ItemHomeworkDialogAttachmentsHeaderBinding import io.github.wulkanowy.databinding.ItemHomeworkDialogDetailsBinding +import io.github.wulkanowy.utils.ifNullOrBlank import io.github.wulkanowy.utils.toFormattedString import javax.inject.Inject @@ -37,6 +39,8 @@ class HomeworkDetailsAdapter @Inject constructor() : var onFullScreenExitClickListener = {} + var onDeleteClickListener: (homework: Homework) -> Unit = {} + override fun getItemCount() = 1 + if (attachments.isNotEmpty()) attachments.size + 1 else 0 override fun getItemViewType(position: Int) = when (position) { @@ -49,9 +53,15 @@ class HomeworkDetailsAdapter @Inject constructor() : val inflater = LayoutInflater.from(parent.context) return when (viewType) { - ViewType.ATTACHMENTS_HEADER.id -> AttachmentsHeaderViewHolder(ItemHomeworkDialogAttachmentsHeaderBinding.inflate(inflater, parent, false)) - ViewType.ATTACHMENT.id -> AttachmentViewHolder(ItemHomeworkDialogAttachmentBinding.inflate(inflater, parent, false)) - else -> DetailsViewHolder(ItemHomeworkDialogDetailsBinding.inflate(inflater, parent, false)) + ViewType.ATTACHMENTS_HEADER.id -> AttachmentsHeaderViewHolder( + ItemHomeworkDialogAttachmentsHeaderBinding.inflate(inflater, parent, false) + ) + ViewType.ATTACHMENT.id -> AttachmentViewHolder( + ItemHomeworkDialogAttachmentBinding.inflate(inflater, parent, false) + ) + else -> DetailsViewHolder( + ItemHomeworkDialogDetailsBinding.inflate(inflater, parent, false) + ) } } @@ -63,12 +73,15 @@ class HomeworkDetailsAdapter @Inject constructor() : } private fun bindDetailsViewHolder(holder: DetailsViewHolder) { + val noDataString = holder.binding.root.context.getString(R.string.all_no_data) + with(holder.binding) { homeworkDialogDate.text = homework?.date?.toFormattedString() homeworkDialogEntryDate.text = homework?.entryDate?.toFormattedString() - homeworkDialogSubject.text = homework?.subject - homeworkDialogTeacher.text = homework?.teacher - homeworkDialogContent.text = homework?.content + homeworkDialogSubject.text = homework?.subject.ifNullOrBlank { noDataString } + homeworkDialogTeacher.text = homework?.teacher.ifNullOrBlank { noDataString } + homeworkDialogContent.text = homework?.content.ifNullOrBlank { noDataString } + homeworkDialogDelete.visibility = if (homework?.isAddedByUser == true) VISIBLE else GONE homeworkDialogFullScreen.visibility = if (isHomeworkFullscreen) GONE else VISIBLE homeworkDialogFullScreenExit.visibility = if (isHomeworkFullscreen) VISIBLE else GONE homeworkDialogFullScreen.setOnClickListener { @@ -81,6 +94,9 @@ class HomeworkDetailsAdapter @Inject constructor() : homeworkDialogFullScreenExit.visibility = GONE onFullScreenExitClickListener() } + homeworkDialogDelete.setOnClickListener { + onDeleteClickListener(homework!!) + } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt index 93045a481..f9d463510 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsDialog.kt @@ -25,6 +25,9 @@ class HomeworkDetailsDialog : BaseDialogFragment(), Homew @Inject lateinit var detailsAdapter: HomeworkDetailsAdapter + override val homeworkDeleteSuccess: String + get() = getString(R.string.homework_delete_success) + private lateinit var homework: Homework companion object { @@ -82,12 +85,17 @@ class HomeworkDetailsDialog : BaseDialogFragment(), Homew dialog?.window?.setLayout(WRAP_CONTENT, WRAP_CONTENT) presenter.isHomeworkFullscreen = false } + onDeleteClickListener = { homework -> presenter.deleteHomework(homework) } isHomeworkFullscreen = presenter.isHomeworkFullscreen homework = this@HomeworkDetailsDialog.homework } } } + override fun closeDialog() { + dismiss() + } + override fun updateMarkAsDoneLabel(isDone: Boolean) { binding.homeworkDialogRead.text = view?.context?.getString(if (isDone) R.string.homework_mark_as_undone else R.string.homework_mark_as_done) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsPresenter.kt index ca6fc71ee..ea9b47a05 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsPresenter.kt @@ -33,6 +33,25 @@ class HomeworkDetailsPresenter @Inject constructor( Timber.i("Homework details view was initialized") } + fun deleteHomework(homework: Homework) { + flowWithResource { homeworkRepository.deleteHomework(homework) }.onEach { + when (it.status) { + Status.LOADING -> Timber.i("Homework delete start") + Status.SUCCESS -> { + Timber.i("Homework delete: Success") + view?.run { + showMessage(homeworkDeleteSuccess) + closeDialog() + } + } + Status.ERROR -> { + Timber.i("Homework delete result: An exception occurred") + errorHandler.dispatch(it.error!!) + } + } + }.launch("delete") + } + fun toggleDone(homework: Homework) { flowWithResource { homeworkRepository.toggleDone(homework) }.onEach { when (it.status) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsView.kt index 697f22335..4a47de43b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/details/HomeworkDetailsView.kt @@ -4,7 +4,11 @@ import io.github.wulkanowy.ui.base.BaseView interface HomeworkDetailsView : BaseView { + val homeworkDeleteSuccess: String + fun initView() + fun closeDialog() + fun updateMarkAsDoneLabel(isDone: Boolean) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt index 0672d75fa..4a93d4ca8 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedFragment.kt @@ -170,7 +170,7 @@ class LoginAdvancedFragment : override fun setErrorUsernameRequired() { with(binding.loginFormUsernameLayout) { requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } @@ -191,7 +191,7 @@ class LoginAdvancedFragment : override fun setErrorPassRequired(focus: Boolean) { with(binding.loginFormPassLayout) { if (focus) requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } @@ -212,7 +212,7 @@ class LoginAdvancedFragment : override fun setErrorPinRequired() { with(binding.loginFormPinLayout) { requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } @@ -226,7 +226,7 @@ class LoginAdvancedFragment : override fun setErrorSymbolRequired() { with(binding.loginFormSymbolLayout) { requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } @@ -240,7 +240,7 @@ class LoginAdvancedFragment : override fun setErrorTokenRequired() { with(binding.loginFormTokenLayout) { requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt index 6e0294a4e..78cc6db09 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormFragment.kt @@ -123,7 +123,7 @@ class LoginFormFragment : BaseFragment(R.layout.fragme override fun setErrorUsernameRequired() { with(binding.loginFormUsernameLayout) { - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } @@ -141,7 +141,7 @@ class LoginFormFragment : BaseFragment(R.layout.fragme override fun setErrorPassRequired(focus: Boolean) { with(binding.loginFormPassLayout) { - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverFragment.kt index 2e2f9f5ce..a91dfb618 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverFragment.kt @@ -99,7 +99,7 @@ class LoginRecoverFragment : override fun setErrorNameRequired() { with(bindingLocal.loginRecoverNameLayout) { requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt index e2c37db61..ae0f75a5e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt @@ -77,7 +77,7 @@ class LoginSymbolFragment : override fun setErrorSymbolRequire() { binding.loginSymbolNameLayout.apply { requestFocus() - error = getString(R.string.login_field_required) + error = getString(R.string.error_field_required) } } diff --git a/app/src/main/res/layout/dialog_homework.xml b/app/src/main/res/layout/dialog_homework.xml index 22a03cb21..341cec544 100644 --- a/app/src/main/res/layout/dialog_homework.xml +++ b/app/src/main/res/layout/dialog_homework.xml @@ -4,6 +4,7 @@ android:id="@+id/parent" android:layout_width="match_parent" android:layout_height="match_parent" + android:minWidth="300dp" android:orientation="vertical"> + android:layout_height="1dp" + android:background="@drawable/ic_all_divider" /> @@ -52,6 +53,9 @@ style="@style/Widget.MaterialComponents.Button.TextButton.Dialog" android:layout_width="wrap_content" android:layout_height="36dp" + android:layout_alignParentEnd="true" + android:layout_alignParentBottom="true" + android:layout_gravity="center_vertical" android:layout_marginTop="8dp" android:layout_marginEnd="8dp" android:layout_marginBottom="8dp" @@ -60,9 +64,6 @@ android:insetRight="0dp" android:insetBottom="0dp" android:minWidth="88dp" - android:layout_gravity="center_vertical" - android:layout_alignParentEnd="true" - android:layout_alignParentBottom="true" android:text="@string/all_close" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" /> diff --git a/app/src/main/res/layout/dialog_homework_add.xml b/app/src/main/res/layout/dialog_homework_add.xml new file mode 100644 index 000000000..b9b8d1a2e --- /dev/null +++ b/app/src/main/res/layout/dialog_homework_add.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_homework.xml b/app/src/main/res/layout/fragment_homework.xml index c0b5698d5..e1f0a16be 100644 --- a/app/src/main/res/layout/fragment_homework.xml +++ b/app/src/main/res/layout/fragment_homework.xml @@ -26,6 +26,8 @@ android:id="@+id/homeworkRecycler" android:layout_width="match_parent" android:layout_height="match_parent" + android:clipToPadding="false" + android:paddingBottom="70dp" tools:listitem="@layout/item_homework" /> @@ -105,6 +107,18 @@ android:text="@string/all_retry" /> + + + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/item_homework_dialog_details.xml b/app/src/main/res/layout/item_homework_dialog_details.xml index abc371da8..9d560ba51 100644 --- a/app/src/main/res/layout/item_homework_dialog_details.xml +++ b/app/src/main/res/layout/item_homework_dialog_details.xml @@ -10,26 +10,37 @@ + android:orientation="horizontal"> + + - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 63d4fc423..b662d4ad5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,6 +17,7 @@ Licenses Messages New message + New homework Notes and achievements Homework Accounts manager @@ -57,7 +58,6 @@ Use the assigned login or email in @%1$s Invalid symbol Student not found. Validate the symbol and the chosen variation of the UONET+ register - This field is required Selected student is already logged in The symbol can be found on the register page in Uczeń → Dostęp Mobilny → Zarejestruj urządzenie mobilne.\n\nMake sure that you have set the appropriate register variant in the UONET+ register variant field on the previous screen. Wulkanowy does not detect pre-school students at the moment Select students to log in to the application @@ -316,6 +316,9 @@ No info about homework Mark as done Mark as undone + Add homework + Homework added successfully + Homework deleted successfully Attachments New homework @@ -592,6 +595,10 @@ No Save Title + Add + Copied + Undo + Change @@ -697,12 +704,6 @@ No color - - Copied - Undo - Change - - Download of updates has started… An update has just been downloaded. @@ -721,4 +722,5 @@ An unexpected error occurred Feature disabled by your school Feature not available. Login in a mode other than Mobile API + This field is required From ebf9e741c2cc19d0cb8287878e9d789cf480575e Mon Sep 17 00:00:00 2001 From: Damian Czupryn <60961958+Daxxxis@users.noreply.github.com> Date: Sat, 9 Oct 2021 01:04:04 +0200 Subject: [PATCH 11/69] Fix homework last item padding (#1568) --- app/src/main/res/layout/fragment_homework.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/fragment_homework.xml b/app/src/main/res/layout/fragment_homework.xml index e1f0a16be..ae8270ab1 100644 --- a/app/src/main/res/layout/fragment_homework.xml +++ b/app/src/main/res/layout/fragment_homework.xml @@ -27,7 +27,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:clipToPadding="false" - android:paddingBottom="70dp" + android:paddingBottom="64dp" tools:listitem="@layout/item_homework" /> From 2ab0a57a4168b172f21a5f686c12feeb95756eb2 Mon Sep 17 00:00:00 2001 From: Damian Czupryn <60961958+Daxxxis@users.noreply.github.com> Date: Sat, 9 Oct 2021 01:55:00 +0200 Subject: [PATCH 12/69] Separate calculated average settings (#1558) --- app/src/main/res/values/strings.xml | 1 + .../main/res/xml/scheme_preferences_advanced.xml | 16 ++++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b662d4ad5..d8674705a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -668,6 +668,7 @@ Attendance Timetable Grades + Calculated average Messages Appearance & Behavior diff --git a/app/src/main/res/xml/scheme_preferences_advanced.xml b/app/src/main/res/xml/scheme_preferences_advanced.xml index 461037871..95f6f383a 100644 --- a/app/src/main/res/xml/scheme_preferences_advanced.xml +++ b/app/src/main/res/xml/scheme_preferences_advanced.xml @@ -19,18 +19,16 @@ app:key="@string/pref_key_grade_modifier_minus" app:title="@string/pref_other_grade_modifier_minus" app:useSimpleSummaryProvider="true" /> + + - + Date: Sat, 9 Oct 2021 17:36:22 +0200 Subject: [PATCH 13/69] Change text when there are no lessons today and tomorrow in dashboard (#1571) --- .../ui/modules/dashboard/DashboardAdapter.kt | 9 +++++++-- app/src/main/res/layout/item_dashboard_lessons.xml | 13 +++++++++++++ app/src/main/res/values/strings.xml | 1 + 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt index 11b575c1c..7bd6ed0ca 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt @@ -301,26 +301,31 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter { dateToNavigate = tomorrowDate updateLessonView(item, tomorrowTimetable, binding) binding.dashboardLessonsItemTitleTomorrow.isVisible = true + binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false } currentDayHeader != null && currentDayHeader.content.isNotBlank() -> { dateToNavigate = currentDate updateLessonView(item, emptyList(), binding, currentDayHeader) binding.dashboardLessonsItemTitleTomorrow.isVisible = false + binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false } tomorrowDayHeader != null && tomorrowDayHeader.content.isNotBlank() -> { dateToNavigate = tomorrowDate updateLessonView(item, emptyList(), binding, tomorrowDayHeader) binding.dashboardLessonsItemTitleTomorrow.isVisible = true + binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false } else -> { - dateToNavigate = tomorrowDate + dateToNavigate = currentDate updateLessonView(item, emptyList(), binding) - binding.dashboardLessonsItemTitleTomorrow.isVisible = + binding.dashboardLessonsItemTitleTomorrow.isVisible = false + binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = !(item.isLoading && item.error == null) } } diff --git a/app/src/main/res/layout/item_dashboard_lessons.xml b/app/src/main/res/layout/item_dashboard_lessons.xml index a2a92c54c..39bd317a7 100644 --- a/app/src/main/res/layout/item_dashboard_lessons.xml +++ b/app/src/main/res/layout/item_dashboard_lessons.xml @@ -42,6 +42,19 @@ app:layout_constraintBottom_toBottomOf="@id/dashboard_lessons_item_title" app:layout_constraintStart_toEndOf="@id/dashboard_lessons_item_title" /> + + Lessons (Tomorrow) + (Today and tomorrow) In a moment: Soon: First: From a240fd5d5f1cbe1bdf90f39c6bebdeeb0a05a1c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sat, 9 Oct 2021 18:37:27 +0200 Subject: [PATCH 14/69] Add build timestamp as build config field (#1567) --- app/build.gradle | 11 +++++++---- app/src/main/AndroidManifest.xml | 12 ++++-------- .../java/io/github/wulkanowy/utils/AppInfo.kt | 16 +++------------- .../data/db/migrations/AbstractMigrationTest.kt | 4 ++-- .../data/db/migrations/Migration35Test.kt | 6 +++--- .../wulkanowy/data/repositories/StudentTest.kt | 2 +- 6 files changed, 20 insertions(+), 31 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 91ce5f24a..031385feb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,10 +29,7 @@ android { resValue "string", "app_name", "Wulkanowy" - manifestPlaceholders = [ - firebase_enabled: project.hasProperty("enableFirebase"), - buildTimestamp: String.valueOf(System.currentTimeMillis()) - ] + manifestPlaceholders = [firebase_enabled: project.hasProperty("enableFirebase")] javaCompileOptions { annotationProcessorOptions { arguments += [ @@ -41,6 +38,12 @@ android { ] } } + + if (System.env.SET_BUILD_TIMESTAMP) { + buildConfigField "long", "BUILD_TIMESTAMP", String.valueOf(System.currentTimeMillis()) + } else { + buildConfigField "long", "BUILD_TIMESTAMP", "1486235849000" + } } sourceSets { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bebe33c62..8c7eb8527 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -153,14 +153,6 @@ android:resource="@xml/provider_paths" /> - - - - + + diff --git a/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt b/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt index 4875425f1..a3961aed8 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt @@ -1,12 +1,10 @@ package io.github.wulkanowy.utils -import android.content.Context -import android.content.pm.PackageManager import android.content.res.Resources import android.os.Build.MANUFACTURER import android.os.Build.MODEL import android.os.Build.VERSION.SDK_INT -import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.BuildConfig.BUILD_TIMESTAMP import io.github.wulkanowy.BuildConfig.DEBUG import io.github.wulkanowy.BuildConfig.FLAVOR import io.github.wulkanowy.BuildConfig.VERSION_CODE @@ -15,21 +13,13 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -open class AppInfo @Inject constructor( - @ApplicationContext private val context: Context, -) { +open class AppInfo @Inject constructor() { open val isDebug get() = DEBUG open val versionCode get() = VERSION_CODE - open val buildTimestamp: Long - get() { - val info = context.packageManager.getApplicationInfo( - context.packageName, PackageManager.GET_META_DATA, - ) - return info.metaData?.getFloat("buildTimestamp")?.toLong() ?: 0 - } + open val buildTimestamp get() = BUILD_TIMESTAMP open val buildFlavor get() = FLAVOR diff --git a/app/src/test/java/io/github/wulkanowy/data/db/migrations/AbstractMigrationTest.kt b/app/src/test/java/io/github/wulkanowy/data/db/migrations/AbstractMigrationTest.kt index 5897af738..cc31d8933 100644 --- a/app/src/test/java/io/github/wulkanowy/data/db/migrations/AbstractMigrationTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/db/migrations/AbstractMigrationTest.kt @@ -16,7 +16,7 @@ abstract class AbstractMigrationTest { val dbName = "migration-test" - val context: Context get() = ApplicationProvider.getApplicationContext() + val context: Context get() = ApplicationProvider.getApplicationContext() @get:Rule val helper: MigrationTestHelper = MigrationTestHelper( @@ -33,7 +33,7 @@ abstract class AbstractMigrationTest { ).addMigrations( *AppDatabase.getMigrations( SharedPrefProvider(PreferenceManager.getDefaultSharedPreferences(context)), - AppInfo(context) + AppInfo() ) ).build() // close the database and release any stream resources when the test finishes diff --git a/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt index 9a38f4286..883cdb81c 100644 --- a/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt +++ b/app/src/test/java/io/github/wulkanowy/data/db/migrations/Migration35Test.kt @@ -29,15 +29,15 @@ class Migration35Test : AbstractMigrationTest() { close() } - helper.runMigrationsAndValidate(dbName, 35, true, Migration35(AppInfo(context))) + helper.runMigrationsAndValidate(dbName, 35, true, Migration35(AppInfo())) val db = getMigratedRoomDatabase() val students = runBlocking { db.studentDao.loadAll() } assertEquals(2, students.size) - assertTrue { students[0].avatarColor in AppInfo(context).defaultColorsForAvatar } - assertTrue { students[1].avatarColor in AppInfo(context).defaultColorsForAvatar } + assertTrue { students[0].avatarColor in AppInfo().defaultColorsForAvatar } + assertTrue { students[1].avatarColor in AppInfo().defaultColorsForAvatar } } private fun createStudent(db: SupportSQLiteDatabase, id: Long) { diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt index 39ebb4a19..9d3d7a2ec 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/StudentTest.kt @@ -37,7 +37,7 @@ class StudentTest { studentDb = studentDb, semesterDb = semesterDb, sdk = mockSdk, - appInfo = AppInfo(mockk()), + appInfo = AppInfo(), appDatabase = mockk() ) } From 539be586ce402d74bdfc2ce195a175c02726007d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Mon, 11 Oct 2021 14:39:54 +0200 Subject: [PATCH 15/69] Fix text color of time left indicator on dashboard timetable card (#1572) --- app/src/main/res/layout/item_dashboard_lessons.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/layout/item_dashboard_lessons.xml b/app/src/main/res/layout/item_dashboard_lessons.xml index 39bd317a7..9156c1a2f 100644 --- a/app/src/main/res/layout/item_dashboard_lessons.xml +++ b/app/src/main/res/layout/item_dashboard_lessons.xml @@ -50,10 +50,10 @@ android:text="@string/dashboard_timetable_title_today_and_tomorrow" android:textColor="?colorOnSurface" android:textSize="14sp" - tools:visibility="invisible" app:layout_constraintBaseline_toBaselineOf="@id/dashboard_lessons_item_title" app:layout_constraintBottom_toBottomOf="@id/dashboard_lessons_item_title" - app:layout_constraintStart_toEndOf="@id/dashboard_lessons_item_title" /> + app:layout_constraintStart_toEndOf="@id/dashboard_lessons_item_title" + tools:visibility="invisible" /> Date: Mon, 11 Oct 2021 15:43:29 +0000 Subject: [PATCH 16/69] Bump hianalytics from 6.2.0.301 to 6.3.0.300 (#1576) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 031385feb..fc7d50d51 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -228,7 +228,7 @@ dependencies { playImplementation 'com.google.android.play:core:1.10.2' playImplementation 'com.google.android.play:core-ktx:1.8.1' - hmsImplementation 'com.huawei.hms:hianalytics:6.2.0.301' + hmsImplementation 'com.huawei.hms:hianalytics:6.3.0.300' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.1.200' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 3d0cd11ba45042b612d3c04f146f364852cac373 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 15:44:03 +0000 Subject: [PATCH 17/69] Bump Treessence from 1.0.4 to 1.0.5 (#1575) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index fc7d50d51..ff40c7c85 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -214,7 +214,7 @@ dependencies { implementation "com.jakewharton.timber:timber:5.0.1" implementation "at.favre.lib:slf4j-timber:1.0.1" - implementation 'com.github.bastienpaulfr:Treessence:1.0.4' + implementation 'com.github.bastienpaulfr:Treessence:1.0.5' implementation "com.mikepenz:aboutlibraries-core:$about_libraries" implementation "io.coil-kt:coil:1.3.2" implementation "io.github.wulkanowy:AppKillerManager:3.0.0" From 9d0366d010433994a296b69bb05ea2e3e644396d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 15:44:19 +0000 Subject: [PATCH 18/69] Bump firebase-bom from 28.4.1 to 28.4.2 (#1574) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index ff40c7c85..ce6416069 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -221,7 +221,7 @@ dependencies { implementation 'me.xdrop:fuzzywuzzy:1.3.1' implementation 'com.fredporciuncula:flow-preferences:1.5.0' - playImplementation platform('com.google.firebase:firebase-bom:28.4.1') + playImplementation platform('com.google.firebase:firebase-bom:28.4.2') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' From d6918077bff00d91f51d7cdd5c6a5bd201a1a5e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanis=C5=82aw=20Jelnicki?= Date: Wed, 13 Oct 2021 21:47:03 +0200 Subject: [PATCH 19/69] Set upcoming lesson notification visibility to public (#1581) --- .../wulkanowy/services/alarm/TimetableNotificationReceiver.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt index 5e4bad8cf..9ce96ef3f 100644 --- a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt +++ b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt @@ -124,6 +124,7 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { .setAutoCancel(false) .setWhen(countDown) .setOngoing(isPersistent) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .apply { if (Build.VERSION.SDK_INT >= N) setUsesChronometer(true) } From e3122127c0912fe0854c21a8fc140becec01982b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Wed, 13 Oct 2021 23:58:24 +0200 Subject: [PATCH 20/69] Add admin messages (#1553) --- app/build.gradle | 10 +- .../42.json | 2396 +++++++++++++++++ .../{RepositoryModule.kt => DataModule.kt} | 88 +- .../wulkanowy/data/api/AdminMessageService.kt | 12 + .../github/wulkanowy/data/db/AppDatabase.kt | 15 +- .../wulkanowy/data/db/dao/AdminMessageDao.kt | 25 + .../data/db/entities/AdminMessage.kt | 37 + .../data/db/migrations/Migration42.kt | 24 + .../repositories/AdminMessageRepository.kt | 53 + .../data/repositories/AppCreatorRepository.kt | 7 +- .../repositories/PreferencesRepository.kt | 2 + .../github/wulkanowy/ui/base/ErrorHandler.kt | 7 +- .../ui/modules/dashboard/DashboardAdapter.kt | 40 + .../ui/modules/dashboard/DashboardFragment.kt | 13 + .../ui/modules/dashboard/DashboardItem.kt | 13 + .../dashboard/DashboardItemMoveCallback.kt | 7 +- .../modules/dashboard/DashboardPresenter.kt | 59 +- .../ui/modules/dashboard/DashboardView.kt | 2 + .../ui/modules/login/LoginErrorHandler.kt | 7 +- .../login/recover/RecoverErrorHandler.kt | 9 +- .../completed/CompletedLessonsErrorHandler.kt | 6 +- .../java/io/github/wulkanowy/utils/AppInfo.kt | 28 +- .../res/drawable/ic_dashboard_warning.xml | 10 + .../layout/item_dashboard_admin_message.xml | 60 + app/src/main/res/values-night/styles.xml | 1 + app/src/main/res/values/api_hosts.xml | 2 +- app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/colors.xml | 3 + app/src/main/res/values/styles.xml | 1 + 29 files changed, 2868 insertions(+), 70 deletions(-) create mode 100644 app/schemas/io.github.wulkanowy.data.db.AppDatabase/42.json rename app/src/main/java/io/github/wulkanowy/data/{RepositoryModule.kt => DataModule.kt} (72%) create mode 100644 app/src/main/java/io/github/wulkanowy/data/api/AdminMessageService.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration42.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt create mode 100644 app/src/main/res/drawable/ic_dashboard_warning.xml create mode 100644 app/src/main/res/layout/item_dashboard_admin_message.xml diff --git a/app/build.gradle b/app/build.gradle index ce6416069..227423e4d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -66,12 +66,14 @@ android { shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' signingConfig signingConfigs.release + buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\"" } debug { resValue "string", "app_name", "Wulkanowy DEV " + defaultConfig.versionCode applicationIdSuffix ".dev" versionNameSuffix "-dev" ext.enableCrashlytics = project.hasProperty("enableFirebase") + buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\"" } } @@ -170,7 +172,7 @@ ext { } dependencies { - implementation "io.github.wulkanowy:sdk:1.3.0" + implementation "io.github.wulkanowy:sdk:4efd64264b" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' @@ -212,6 +214,10 @@ dependencies { implementation 'com.github.ncapdevi:FragNav:3.3.0' implementation "com.github.YarikSOffice:lingver:1.3.0" + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0" + implementation "com.squareup.okhttp3:logging-interceptor:4.9.2" + implementation "com.jakewharton.timber:timber:5.0.1" implementation "at.favre.lib:slf4j-timber:1.0.1" implementation 'com.github.bastienpaulfr:Treessence:1.0.5' @@ -234,7 +240,7 @@ dependencies { releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" debugImplementation "com.github.ChuckerTeam.Chucker:library:$chucker" - debugImplementation 'com.github.amitshekhariitbhu.Android-Debug-Database:debug-db:v1.0.6' + debugImplementation 'com.github.amitshekhariitbhu.Android-Debug-Database:debug-db:1.0.6' testImplementation "junit:junit:4.13.2" testImplementation "io.mockk:mockk:$mockk" diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/42.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/42.json new file mode 100644 index 000000000..a5faa57b7 --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/42.json @@ -0,0 +1,2396 @@ +{ + "formatVersion": 1, + "database": { + "version": 42, + "identityHash": "5c8b7f9409294ecdebf9f74a44f8e883", + "entities": [ + { + "tableName": "Students", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrapper_base_url` TEXT NOT NULL, `mobile_base_url` TEXT NOT NULL, `login_type` TEXT NOT NULL, `login_mode` TEXT NOT NULL, `certificate_key` TEXT NOT NULL, `private_key` TEXT NOT NULL, `is_parent` INTEGER NOT NULL, `email` TEXT NOT NULL, `password` TEXT NOT NULL, `symbol` TEXT NOT NULL, `student_id` INTEGER NOT NULL, `user_login_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `student_name` TEXT NOT NULL, `school_id` TEXT NOT NULL, `school_short` TEXT NOT NULL, `school_name` TEXT NOT NULL, `class_name` TEXT NOT NULL, `class_id` INTEGER NOT NULL, `is_current` INTEGER NOT NULL, `registration_date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `nick` TEXT NOT NULL, `avatar_color` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "scrapperBaseUrl", + "columnName": "scrapper_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mobileBaseUrl", + "columnName": "mobile_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginType", + "columnName": "login_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginMode", + "columnName": "login_mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificateKey", + "columnName": "certificate_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "private_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isParent", + "columnName": "is_parent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "user_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "student_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolSymbol", + "columnName": "school_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "school_short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolName", + "columnName": "school_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "className", + "columnName": "class_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCurrent", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registrationDate", + "columnName": "registration_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nick", + "columnName": "nick", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarColor", + "columnName": "avatar_color", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Students_email_symbol_student_id_school_id_class_id", + "unique": true, + "columnNames": [ + "email", + "symbol", + "student_id", + "school_id", + "class_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryName", + "columnName": "diary_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolYear", + "columnName": "school_year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterName", + "columnName": "semester_name", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "semester_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `semester_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Exams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Timetable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `number` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `subjectOld` TEXT NOT NULL, `group` TEXT NOT NULL, `room` TEXT NOT NULL, `roomOld` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacherOld` TEXT NOT NULL, `info` TEXT NOT NULL, `student_plan` INTEGER NOT NULL, `changes` INTEGER NOT NULL, `canceled` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectOld", + "columnName": "subjectOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "room", + "columnName": "room", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roomOld", + "columnName": "roomOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherOld", + "columnName": "teacherOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isStudentPlan", + "columnName": "student_plan", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "changes", + "columnName": "changes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canceled", + "columnName": "canceled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Attendance", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `time_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `excusable` INTEGER NOT NULL, `excuse_status` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeId", + "columnName": "time_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excusable", + "columnName": "excusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excuseStatus", + "columnName": "excuse_status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AttendanceSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `subject_id` INTEGER NOT NULL, `month` INTEGER NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `absence_excused` INTEGER NOT NULL, `absence_for_school_reasons` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `lateness_excused` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subject_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "month", + "columnName": "month", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceExcused", + "columnName": "absence_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceForSchoolReasons", + "columnName": "absence_for_school_reasons", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latenessExcused", + "columnName": "lateness_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Grades", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `entry` TEXT NOT NULL, `value` REAL NOT NULL, `modifier` REAL NOT NULL, `comment` TEXT NOT NULL, `color` TEXT NOT NULL, `grade_symbol` TEXT NOT NULL, `description` TEXT NOT NULL, `weight` TEXT NOT NULL, `weightValue` REAL NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entry", + "columnName": "entry", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "modifier", + "columnName": "modifier", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gradeSymbol", + "columnName": "grade_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weightValue", + "columnName": "weightValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `subject` TEXT NOT NULL, `predicted_grade` TEXT NOT NULL, `final_grade` TEXT NOT NULL, `proposed_points` TEXT NOT NULL, `final_points` TEXT NOT NULL, `points_sum` TEXT NOT NULL, `average` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_predicted_grade_notified` INTEGER NOT NULL, `is_final_grade_notified` INTEGER NOT NULL, `predicted_grade_last_change` INTEGER NOT NULL, `final_grade_last_change` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "predictedGrade", + "columnName": "predicted_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalGrade", + "columnName": "final_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "proposedPoints", + "columnName": "proposed_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalPoints", + "columnName": "final_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pointsSum", + "columnName": "points_sum", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "average", + "columnName": "average", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPredictedGradeNotified", + "columnName": "is_predicted_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFinalGradeNotified", + "columnName": "is_final_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "predictedGradeLastChange", + "columnName": "predicted_grade_last_change", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "finalGradeLastChange", + "columnName": "final_grade_last_change", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradePartialStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `class_average` TEXT NOT NULL, `student_average` TEXT NOT NULL, `class_amounts` TEXT NOT NULL, `student_amounts` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAverage", + "columnName": "class_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAverage", + "columnName": "student_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAmounts", + "columnName": "class_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAmounts", + "columnName": "student_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesPointsStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `others` REAL NOT NULL, `student` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "others", + "columnName": "others", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "student", + "columnName": "student", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradeSemesterStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `amounts` TEXT NOT NULL, `student_grade` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amounts", + "columnName": "amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentGrade", + "columnName": "student_grade", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `recipient_name` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `removed` INTEGER NOT NULL, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `unread_by` INTEGER NOT NULL, `read_by` INTEGER NOT NULL, `content` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipient", + "columnName": "recipient_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "removed", + "columnName": "removed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasAttachments", + "columnName": "has_attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `one_drive_id` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`real_id`))", + "fields": [ + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "oneDriveId", + "columnName": "one_drive_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "real_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `category` TEXT NOT NULL, `category_type` INTEGER NOT NULL, `is_points_show` INTEGER NOT NULL, `points` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryType", + "columnName": "category_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPointsShow", + "columnName": "is_points_show", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Homework", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `attachments` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_done` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL, `is_added_by_user` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "is_done", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Subjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "LuckyNumbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `lucky_number` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "luckyNumber", + "columnName": "lucky_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CompletedLesson", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `topic` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `substitution` TEXT NOT NULL, `absence` TEXT NOT NULL, `resources` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "topic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "substitution", + "columnName": "substitution", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "resources", + "columnName": "resources", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ReportingUnits", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `short` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `roles` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "senderName", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roles", + "columnName": "roles", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` TEXT NOT NULL, `name` TEXT NOT NULL, `real_name` TEXT NOT NULL, `login_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `role` INTEGER NOT NULL, `hash` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "realName", + "columnName": "real_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginId", + "columnName": "login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MobileDevices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Teachers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `short_name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "School", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `contact` TEXT NOT NULL, `headmaster` TEXT NOT NULL, `pedagogue` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contact", + "columnName": "contact", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headmaster", + "columnName": "headmaster", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pedagogue", + "columnName": "pedagogue", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Conferences", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `subject` TEXT NOT NULL, `agenda` TEXT NOT NULL, `present_on_conference` TEXT NOT NULL, `conference_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agenda", + "columnName": "agenda", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presentOnConference", + "columnName": "present_on_conference", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conference_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableAdditional", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StudentInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `full_name` TEXT NOT NULL, `first_name` TEXT NOT NULL, `second_name` TEXT NOT NULL, `surname` TEXT NOT NULL, `birth_date` INTEGER NOT NULL, `birth_place` TEXT NOT NULL, `gender` TEXT NOT NULL, `has_polish_citizenship` INTEGER NOT NULL, `family_name` TEXT NOT NULL, `parents_names` TEXT NOT NULL, `address` TEXT NOT NULL, `registered_address` TEXT NOT NULL, `correspondence_address` TEXT NOT NULL, `phone_number` TEXT NOT NULL, `cell_phone_number` TEXT NOT NULL, `email` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `first_guardian_full_name` TEXT, `first_guardian_kinship` TEXT, `first_guardian_address` TEXT, `first_guardian_phones` TEXT, `first_guardian_email` TEXT, `second_guardian_full_name` TEXT, `second_guardian_kinship` TEXT, `second_guardian_address` TEXT, `second_guardian_phones` TEXT, `second_guardian_email` TEXT)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "full_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondName", + "columnName": "second_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "surname", + "columnName": "surname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "birthDate", + "columnName": "birth_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthPlace", + "columnName": "birth_place", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gender", + "columnName": "gender", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPolishCitizenship", + "columnName": "has_polish_citizenship", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "familyName", + "columnName": "family_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentsNames", + "columnName": "parents_names", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "registeredAddress", + "columnName": "registered_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "correspondenceAddress", + "columnName": "correspondence_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cellPhoneNumber", + "columnName": "cell_phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstGuardian.fullName", + "columnName": "first_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.kinship", + "columnName": "first_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.address", + "columnName": "first_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.phones", + "columnName": "first_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.email", + "columnName": "first_guardian_email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.fullName", + "columnName": "second_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.kinship", + "columnName": "second_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.address", + "columnName": "second_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.phones", + "columnName": "second_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.email", + "columnName": "second_guardian_email", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableHeaders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SchoolAnnouncements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `type` TEXT NOT NULL, `date` INTEGER NOT NULL, `data` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AdminMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `version_name` INTEGER, `version_max` INTEGER, `target_register_host` TEXT, `target_flavor` TEXT, `destination_url` TEXT, `priority` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "versionMin", + "columnName": "version_name", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMax", + "columnName": "version_max", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "targetRegisterHost", + "columnName": "target_register_host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "targetFlavor", + "columnName": "target_flavor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "destinationUrl", + "columnName": "destination_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5c8b7f9409294ecdebf9f74a44f8e883')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt b/app/src/main/java/io/github/wulkanowy/data/DataModule.kt similarity index 72% rename from app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt rename to app/src/main/java/io/github/wulkanowy/data/DataModule.kt index 863bb5950..cac3ffc23 100644 --- a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt +++ b/app/src/main/java/io/github/wulkanowy/data/DataModule.kt @@ -2,62 +2,100 @@ package io.github.wulkanowy.data import android.content.Context import android.content.SharedPreferences -import android.content.res.AssetManager -import android.content.res.Resources import androidx.preference.PreferenceManager import com.chuckerteam.chucker.api.ChuckerCollector import com.chuckerteam.chucker.api.ChuckerInterceptor import com.chuckerteam.chucker.api.RetentionManager import com.fredporciuncula.flow.preferences.FlowSharedPreferences +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import io.github.wulkanowy.data.api.AdminMessageService import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AppInfo import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.create import timber.log.Timber +import java.util.concurrent.TimeUnit import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal class RepositoryModule { +internal class DataModule { @Singleton @Provides - fun provideSdk(chuckerCollector: ChuckerCollector, @ApplicationContext context: Context): Sdk { - return Sdk().apply { + fun provideSdk(chuckerInterceptor: ChuckerInterceptor) = + Sdk().apply { androidVersion = android.os.Build.VERSION.RELEASE buildTag = android.os.Build.MODEL setSimpleHttpLogger { Timber.d(it) } // for debug only - addInterceptor( - ChuckerInterceptor.Builder(context) - .collector(chuckerCollector) - .alwaysReadResponseBody(true) - .build(), network = true - ) + addInterceptor(chuckerInterceptor, network = true) } - } @Singleton @Provides fun provideChuckerCollector( @ApplicationContext context: Context, prefRepository: PreferencesRepository - ): ChuckerCollector { - return ChuckerCollector( - context = context, - showNotification = prefRepository.isDebugNotificationEnable, - retentionPeriod = RetentionManager.Period.ONE_HOUR - ) - } + ) = ChuckerCollector( + context = context, + showNotification = prefRepository.isDebugNotificationEnable, + retentionPeriod = RetentionManager.Period.ONE_HOUR + ) + + @Singleton + @Provides + fun provideChuckerInterceptor( + @ApplicationContext context: Context, + chuckerCollector: ChuckerCollector + ) = ChuckerInterceptor.Builder(context) + .collector(chuckerCollector) + .alwaysReadResponseBody(true) + .build() + + @Singleton + @Provides + fun provideOkHttpClient(chuckerInterceptor: ChuckerInterceptor): OkHttpClient = + OkHttpClient.Builder() + .addNetworkInterceptor(chuckerInterceptor) + .addInterceptor(HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BASIC + }) + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build() + + @OptIn(ExperimentalSerializationApi::class) + @Singleton + @Provides + fun provideRetrofit( + okHttpClient: OkHttpClient, + json: Json, + appInfo: AppInfo + ): Retrofit = Retrofit.Builder() + .baseUrl(appInfo.messagesBaseUrl) + .client(okHttpClient) + .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) + .build() + + @Singleton + @Provides + fun provideAdminMessageService(retrofit: Retrofit): AdminMessageService = retrofit.create() @Singleton @Provides @@ -67,14 +105,6 @@ internal class RepositoryModule { appInfo: AppInfo ) = AppDatabase.newInstance(context, sharedPrefProvider, appInfo) - @Singleton - @Provides - fun provideResources(@ApplicationContext context: Context): Resources = context.resources - - @Singleton - @Provides - fun provideAssets(@ApplicationContext context: Context): AssetManager = context.assets - @Singleton @Provides fun provideSharedPref(@ApplicationContext context: Context): SharedPreferences = @@ -208,4 +238,8 @@ internal class RepositoryModule { @Singleton @Provides fun provideNotificationDao(database: AppDatabase) = database.notificationDao + + @Singleton + @Provides + fun provideAdminMessageDao(database: AppDatabase) = database.adminMessagesDao } diff --git a/app/src/main/java/io/github/wulkanowy/data/api/AdminMessageService.kt b/app/src/main/java/io/github/wulkanowy/data/api/AdminMessageService.kt new file mode 100644 index 000000000..23f5af24a --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/api/AdminMessageService.kt @@ -0,0 +1,12 @@ +package io.github.wulkanowy.data.api + +import io.github.wulkanowy.data.db.entities.AdminMessage +import retrofit2.http.GET +import javax.inject.Singleton + +@Singleton +interface AdminMessageService { + + @GET("/v1.json") + suspend fun getAdminMessages(): List +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt index b93d00881..b8831acd8 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt @@ -6,6 +6,7 @@ import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.RoomDatabase.JournalMode.TRUNCATE import androidx.room.TypeConverters +import io.github.wulkanowy.data.db.dao.AdminMessageDao import io.github.wulkanowy.data.db.dao.AttendanceDao import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao import io.github.wulkanowy.data.db.dao.CompletedLessonsDao @@ -35,6 +36,7 @@ import io.github.wulkanowy.data.db.dao.TeacherDao import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao import io.github.wulkanowy.data.db.dao.TimetableDao import io.github.wulkanowy.data.db.dao.TimetableHeaderDao +import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.AttendanceSummary import io.github.wulkanowy.data.db.entities.CompletedLesson @@ -97,8 +99,9 @@ import io.github.wulkanowy.data.db.migrations.Migration37 import io.github.wulkanowy.data.db.migrations.Migration38 import io.github.wulkanowy.data.db.migrations.Migration39 import io.github.wulkanowy.data.db.migrations.Migration4 -import io.github.wulkanowy.data.db.migrations.Migration41 import io.github.wulkanowy.data.db.migrations.Migration40 +import io.github.wulkanowy.data.db.migrations.Migration41 +import io.github.wulkanowy.data.db.migrations.Migration42 import io.github.wulkanowy.data.db.migrations.Migration5 import io.github.wulkanowy.data.db.migrations.Migration6 import io.github.wulkanowy.data.db.migrations.Migration7 @@ -138,7 +141,8 @@ import javax.inject.Singleton StudentInfo::class, TimetableHeader::class, SchoolAnnouncement::class, - Notification::class + Notification::class, + AdminMessage::class ], version = AppDatabase.VERSION_SCHEMA, exportSchema = true @@ -147,7 +151,7 @@ import javax.inject.Singleton abstract class AppDatabase : RoomDatabase() { companion object { - const val VERSION_SCHEMA = 41 + const val VERSION_SCHEMA = 42 fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( Migration2(), @@ -189,7 +193,8 @@ abstract class AppDatabase : RoomDatabase() { Migration38(), Migration39(), Migration40(), - Migration41() + Migration41(), + Migration42() ) fun newInstance( @@ -261,4 +266,6 @@ abstract class AppDatabase : RoomDatabase() { abstract val schoolAnnouncementDao: SchoolAnnouncementDao abstract val notificationDao: NotificationDao + + abstract val adminMessagesDao: AdminMessageDao } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt new file mode 100644 index 000000000..87f4812da --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/AdminMessageDao.kt @@ -0,0 +1,25 @@ +package io.github.wulkanowy.data.db.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Transaction +import io.github.wulkanowy.data.db.entities.AdminMessage +import kotlinx.coroutines.flow.Flow +import javax.inject.Singleton + +@Singleton +@Dao +abstract class AdminMessageDao : BaseDao { + + @Query("SELECT * FROM AdminMessages") + abstract fun loadAll(): Flow> + + @Transaction + open suspend fun removeOldAndSaveNew( + oldMessages: List, + newMessages: List + ) { + deleteAll(oldMessages) + insertAll(newMessages) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt new file mode 100644 index 000000000..b7b0cf58a --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt @@ -0,0 +1,37 @@ +package io.github.wulkanowy.data.db.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.serialization.Serializable + +@Serializable +@Entity(tableName = "AdminMessages") +data class AdminMessage( + + @PrimaryKey + val id: Int, + + val title: String, + + val content: String, + + @ColumnInfo(name = "version_name") + val versionMin: Int? = null, + + @ColumnInfo(name = "version_max") + val versionMax: Int? = null, + + @ColumnInfo(name = "target_register_host") + val targetRegisterHost: String? = null, + + @ColumnInfo(name = "target_flavor") + val targetFlavor: String? = null, + + @ColumnInfo(name = "destination_url") + val destinationUrl: String? = null, + + val priority: String, + + val type: String +) diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration42.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration42.kt new file mode 100644 index 000000000..3d66f301b --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration42.kt @@ -0,0 +1,24 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration42 : Migration(41, 42) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + """CREATE TABLE IF NOT EXISTS `AdminMessages` ( + `id` INTEGER NOT NULL, + `title` TEXT NOT NULL, + `content` TEXT NOT NULL, + `version_name` INTEGER, + `version_max` INTEGER, + `target_register_host` TEXT, + `target_flavor` TEXT, + `destination_url` TEXT, + `priority` TEXT NOT NULL, + `type` TEXT NOT NULL, + PRIMARY KEY(`id`))""" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt new file mode 100644 index 000000000..a620caade --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt @@ -0,0 +1,53 @@ +package io.github.wulkanowy.data.repositories + +import io.github.wulkanowy.data.api.AdminMessageService +import io.github.wulkanowy.data.db.dao.AdminMessageDao +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.utils.AppInfo +import io.github.wulkanowy.utils.AutoRefreshHelper +import io.github.wulkanowy.utils.getRefreshKey +import io.github.wulkanowy.utils.networkBoundResource +import kotlinx.coroutines.sync.Mutex +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class AdminMessageRepository @Inject constructor( + private val adminMessageService: AdminMessageService, + private val adminMessageDao: AdminMessageDao, + private val appInfo: AppInfo, + private val refreshHelper: AutoRefreshHelper, +) { + private val saveFetchResultMutex = Mutex() + + private val cacheKey = "admin_messages" + + suspend fun getAdminMessages(student: Student, forceRefresh: Boolean) = networkBoundResource( + mutex = saveFetchResultMutex, + query = { adminMessageDao.loadAll() }, + fetch = { adminMessageService.getAdminMessages() }, + shouldFetch = { + refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student)) || forceRefresh + }, + saveFetchResult = { oldItems, newItems -> + adminMessageDao.removeOldAndSaveNew(oldItems, newItems) + refreshHelper.updateLastRefreshTimestamp(cacheKey) + }, + showSavedOnLoading = false, + mapResult = { adminMessages -> + adminMessages.filter { adminMessage -> + val isCorrectRegister = adminMessage.targetRegisterHost?.let { + student.scrapperBaseUrl.contains(it, true) + } ?: true + val isCorrectFlavor = + adminMessage.targetFlavor?.equals(appInfo.buildFlavor, true) ?: true + val isCorrectMaxVersion = + adminMessage.versionMax?.let { it >= appInfo.versionCode } ?: true + val isCorrectMinVersion = + adminMessage.versionMin?.let { it <= appInfo.versionCode } ?: true + + isCorrectRegister && isCorrectFlavor && isCorrectMaxVersion && isCorrectMinVersion + }.maxByOrNull { it.id } + } + ) +} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt index f1fdbfed3..3d83f7181 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.data.repositories -import android.content.res.AssetManager +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.data.pojos.Contributor import io.github.wulkanowy.utils.DispatchersProvider import kotlinx.coroutines.withContext @@ -12,7 +13,7 @@ import javax.inject.Singleton @Singleton class AppCreatorRepository @Inject constructor( - private val assets: AssetManager, + @ApplicationContext private val context: Context, private val dispatchers: DispatchersProvider, private val json: Json, ) { @@ -20,7 +21,7 @@ class AppCreatorRepository @Inject constructor( @OptIn(ExperimentalSerializationApi::class) @Suppress("BlockingMethodInNonBlockingContext") suspend fun getAppCreators() = withContext(dispatchers.backgroundThread) { - val inputStream = assets.open("contributors.json").buffered() + val inputStream = context.assets.open("contributors.json").buffered() json.decodeFromStream>(inputStream) } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index 65edd53f0..b0991bbcb 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -209,6 +209,7 @@ class PreferencesRepository @Inject constructor( .map { set -> set.map { DashboardItem.Tile.valueOf(it) } .plus(DashboardItem.Tile.ACCOUNT) + .plus(DashboardItem.Tile.ADMIN_MESSAGE) .toSet() } @@ -216,6 +217,7 @@ class PreferencesRepository @Inject constructor( get() = selectedDashboardTilesPreference.get() .map { DashboardItem.Tile.valueOf(it) } .plus(DashboardItem.Tile.ACCOUNT) + .plus(DashboardItem.Tile.ADMIN_MESSAGE) .toSet() set(value) { val filteredValue = value.filterNot { it == DashboardItem.Tile.ACCOUNT } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt index 7c32ef184..fbc994e2c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.ui.base -import android.content.res.Resources +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException @@ -9,7 +10,7 @@ import io.github.wulkanowy.utils.security.ScramblerException import timber.log.Timber import javax.inject.Inject -open class ErrorHandler @Inject constructor(protected val resources: Resources) { +open class ErrorHandler @Inject constructor(@ApplicationContext protected val context: Context) { var showErrorMessage: (String, Throwable) -> Unit = { _, _ -> } @@ -25,7 +26,7 @@ open class ErrorHandler @Inject constructor(protected val resources: Resources) } protected open fun proceed(error: Throwable) { - showErrorMessage(resources.getString(error), error) + showErrorMessage(context.resources.getString(error), error) when (error) { is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl) is ScramblerException, is BadCredentialsException -> onSessionExpired() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt index 7bd6ed0ca..405bfbc5a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt @@ -1,6 +1,8 @@ package io.github.wulkanowy.ui.modules.dashboard import android.annotation.SuppressLint +import android.content.res.ColorStateList +import android.graphics.Color import android.graphics.Typeface import android.os.Handler import android.os.Looper @@ -18,6 +20,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.db.entities.TimetableHeader import io.github.wulkanowy.databinding.ItemDashboardAccountBinding +import io.github.wulkanowy.databinding.ItemDashboardAdminMessageBinding import io.github.wulkanowy.databinding.ItemDashboardAnnouncementsBinding import io.github.wulkanowy.databinding.ItemDashboardConferencesBinding import io.github.wulkanowy.databinding.ItemDashboardExamsBinding @@ -63,6 +66,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter Unit = {} + var onAdminMessageClickListener: (String?) -> Unit = {} + val items = mutableListOf() fun submitList(newItems: List) { @@ -109,6 +114,9 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter ConferencesViewHolder( ItemDashboardConferencesBinding.inflate(inflater, parent, false) ) + DashboardItem.Type.ADMIN_MESSAGE.ordinal -> AdminMessageViewHolder( + ItemDashboardAdminMessageBinding.inflate(inflater, parent, false) + ) else -> throw IllegalArgumentException() } } @@ -123,6 +131,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter bindAnnouncementsViewHolder(holder, position) is ExamsViewHolder -> bindExamsViewHolder(holder, position) is ConferencesViewHolder -> bindConferencesViewHolder(holder, position) + is AdminMessageViewHolder -> bindAdminMessage(holder, position) } } @@ -697,6 +706,34 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter { + context.getThemeAttrColor(R.attr.colorPrimary) to + context.getThemeAttrColor(R.attr.colorOnPrimary) + } + "MEDIUM" -> { + context.getThemeAttrColor(R.attr.colorMessageMedium) to Color.BLACK + } + else -> null to context.getThemeAttrColor(R.attr.colorOnSurface) + } + + with(adminMessageViewHolder.binding) { + dashboardAdminMessageItemTitle.text = item.title + dashboardAdminMessageItemTitle.setTextColor(textColor) + dashboardAdminMessageItemDescription.text = item.content + dashboardAdminMessageItemDescription.setTextColor(textColor) + dashboardAdminMessageItemIcon.setColorFilter(textColor) + + root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) }) + item.destinationUrl?.let { url -> + root.setOnClickListener { onAdminMessageClickListener(url) } + } + } + } + class AccountViewHolder(val binding: ItemDashboardAccountBinding) : RecyclerView.ViewHolder(binding.root) @@ -736,6 +773,9 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter, private val oldList: List diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt index 096c1b77a..775b7b557 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt @@ -10,6 +10,7 @@ import androidx.core.view.isVisible import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.databinding.FragmentDashboardBinding @@ -29,6 +30,7 @@ import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragm import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import io.github.wulkanowy.utils.capitalise import io.github.wulkanowy.utils.getThemeAttrColor +import io.github.wulkanowy.utils.openInternetBrowser import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDate import javax.inject.Inject @@ -97,6 +99,13 @@ class DashboardFragment : BaseFragment(R.layout.fragme onConferencesTileClickListener = { mainActivity.pushView(ConferenceFragment.newInstance()) } + onAdminMessageClickListener = presenter::onAdminMessageSelected + + registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + binding.dashboardRecycler.scrollToPosition(0) + } + }) } with(binding) { @@ -188,6 +197,10 @@ class DashboardFragment : BaseFragment(R.layout.fragme (requireActivity() as MainActivity).pushView(NotificationsCenterFragment.newInstance()) } + override fun openInternetBrowser(url: String) { + requireContext().openInternetBrowser(url) + } + override fun onDestroyView() { dashboardAdapter.clearTimers() presenter.onDetachView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt index cf99f0c9a..92665857b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItem.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.ui.modules.dashboard +import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.db.entities.Grade @@ -16,6 +17,15 @@ sealed class DashboardItem(val type: Type) { abstract val isDataLoaded: Boolean + data class AdminMessages( + val adminMessage: AdminMessage? = null, + override val error: Throwable? = null, + override val isLoading: Boolean = false + ) : DashboardItem(Type.ADMIN_MESSAGE) { + + override val isDataLoaded get() = adminMessage != null + } + data class Account( val student: Student? = null, override val error: Throwable? = null, @@ -96,6 +106,7 @@ sealed class DashboardItem(val type: Type) { } enum class Type { + ADMIN_MESSAGE, ACCOUNT, HORIZONTAL_GROUP, LESSONS, @@ -108,6 +119,7 @@ sealed class DashboardItem(val type: Type) { } enum class Tile { + ADMIN_MESSAGE, ACCOUNT, LUCKY_NUMBER, MESSAGES, @@ -123,6 +135,7 @@ sealed class DashboardItem(val type: Type) { } fun DashboardItem.Tile.toDashboardItemType() = when (this) { + DashboardItem.Tile.ADMIN_MESSAGE -> DashboardItem.Type.ADMIN_MESSAGE DashboardItem.Tile.ACCOUNT -> DashboardItem.Type.ACCOUNT DashboardItem.Tile.LUCKY_NUMBER -> DashboardItem.Type.HORIZONTAL_GROUP DashboardItem.Tile.MESSAGES -> DashboardItem.Type.HORIZONTAL_GROUP diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt index cf4097a4a..b9625570f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt @@ -21,7 +21,7 @@ class DashboardItemMoveCallback( recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder ): Int { - val dragFlags = if (viewHolder.bindingAdapterPosition != 0) { + val dragFlags = if (!viewHolder.isAdminMessageOrAccountItem) { ItemTouchHelper.UP or ItemTouchHelper.DOWN } else 0 @@ -32,7 +32,7 @@ class DashboardItemMoveCallback( recyclerView: RecyclerView, current: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder - ) = target.bindingAdapterPosition != 0 + ) = !target.isAdminMessageOrAccountItem override fun onMove( recyclerView: RecyclerView, @@ -52,4 +52,7 @@ class DashboardItemMoveCallback( onUserInteractionEndListener(dashboardAdapter.items.toList()) } + + private val RecyclerView.ViewHolder.isAdminMessageOrAccountItem: Boolean + get() = this is DashboardAdapter.AdminMessageViewHolder || this is DashboardAdapter.AccountViewHolder } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt index d6d533e28..5ff7f2ce8 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt @@ -5,6 +5,7 @@ import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.enums.MessageFolder +import io.github.wulkanowy.data.repositories.AdminMessageRepository import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository import io.github.wulkanowy.data.repositories.ConferenceRepository import io.github.wulkanowy.data.repositories.ExamRepository @@ -50,7 +51,8 @@ class DashboardPresenter @Inject constructor( private val examRepository: ExamRepository, private val conferenceRepository: ConferenceRepository, private val preferencesRepository: PreferencesRepository, - private val schoolAnnouncementRepository: SchoolAnnouncementRepository + private val schoolAnnouncementRepository: SchoolAnnouncementRepository, + private val adminMessageRepository: AdminMessageRepository ) : BasePresenter(errorHandler, studentRepository) { private val dashboardItemLoadedList = mutableListOf() @@ -179,6 +181,7 @@ class DashboardPresenter @Inject constructor( loadConferences(student, forceRefresh) } DashboardItem.Type.ADS -> TODO() + DashboardItem.Type.ADMIN_MESSAGE -> loadAdminMessage(student, forceRefresh) } } } @@ -225,6 +228,10 @@ class DashboardPresenter @Inject constructor( }.toSet() } + fun onAdminMessageSelected(url: String?) { + url?.let { view?.openInternetBrowser(it) } + } + private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) { flow { val semester = semesterRepository.getCurrentSemester(student) @@ -567,6 +574,38 @@ class DashboardPresenter @Inject constructor( }.launch("dashboard_conferences") } + private fun loadAdminMessage(student: Student, forceRefresh: Boolean) { + flowWithResourceIn { adminMessageRepository.getAdminMessages(student, forceRefresh) } + .onEach { + when (it.status) { + Status.LOADING -> { + Timber.i("Loading dashboard admin message data started") + if (forceRefresh) return@onEach + updateData(DashboardItem.AdminMessages(), forceRefresh) + } + Status.SUCCESS -> { + Timber.i("Loading dashboard admin message result: Success") + updateData( + dashboardItem = DashboardItem.AdminMessages(adminMessage = it.data), + forceRefresh = forceRefresh + ) + } + Status.ERROR -> { + Timber.i("Loading dashboard admin message result: An exception occurred") + errorHandler.dispatch(it.error!!) + updateData( + dashboardItem = DashboardItem.AdminMessages( + adminMessage = it.data, + error = it.error + ), + forceRefresh = forceRefresh + ) + } + } + } + .launch("dashboard_admin_messages") + } + private fun updateData(dashboardItem: DashboardItem, forceRefresh: Boolean) { val isForceRefreshError = forceRefresh && dashboardItem.error != null val isFirstRunDataLoadedError = @@ -579,6 +618,11 @@ class DashboardPresenter @Inject constructor( sortDashboardItems() + if (dashboardItem is DashboardItem.AdminMessages && !dashboardItem.isDataLoaded) { + dashboardItemsToLoad = dashboardItemsToLoad - DashboardItem.Type.ADMIN_MESSAGE + dashboardItemLoadedList.removeAll { it.type == DashboardItem.Type.ADMIN_MESSAGE } + } + if (forceRefresh) { updateForceRefreshData(dashboardItem) } else { @@ -644,7 +688,9 @@ class DashboardPresenter @Inject constructor( itemsLoadedList: List, forceRefresh: Boolean ) { - val filteredItems = itemsLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT } + val filteredItems = itemsLoadedList.filterNot { + it.type == DashboardItem.Type.ACCOUNT || it.type == DashboardItem.Type.ADMIN_MESSAGE + } val isAccountItemError = itemsLoadedList.find { it.type == DashboardItem.Type.ACCOUNT }?.error != null val isGeneralError = @@ -676,10 +722,13 @@ class DashboardPresenter @Inject constructor( val dashboardItemsPosition = preferencesRepository.dashboardItemsPosition dashboardItemLoadedList.sortBy { tile -> - dashboardItemsPosition?.getOrDefault( - tile.type, + val defaultPosition = if (tile is DashboardItem.AdminMessages) { + -1 + } else { tile.type.ordinal + 100 - ) ?: tile.type.ordinal + } + + dashboardItemsPosition?.getOrDefault(tile.type, defaultPosition) ?: tile.type.ordinal } } } \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt index 899eb3202..730e19a35 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardView.kt @@ -25,4 +25,6 @@ interface DashboardView : BaseView { fun popViewToRoot() fun openNotificationsCenterView() + + fun openInternetBrowser(url: String) } \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt index 2f76cd51e..ea7215cea 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt @@ -1,7 +1,8 @@ package io.github.wulkanowy.ui.modules.login -import android.content.res.Resources +import android.content.Context import android.database.sqlite.SQLiteConstraintException +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.sdk.mobile.exception.InvalidPinException import io.github.wulkanowy.sdk.mobile.exception.InvalidSymbolException @@ -11,7 +12,8 @@ import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException import io.github.wulkanowy.ui.base.ErrorHandler import javax.inject.Inject -class LoginErrorHandler @Inject constructor(resources: Resources) : ErrorHandler(resources) { +class LoginErrorHandler @Inject constructor(@ApplicationContext context: Context) : + ErrorHandler(context) { var onBadCredentials: (String?) -> Unit = {} @@ -24,6 +26,7 @@ class LoginErrorHandler @Inject constructor(resources: Resources) : ErrorHandler var onStudentDuplicate: (String) -> Unit = {} override fun proceed(error: Throwable) { + val resources = context.resources when (error) { is BadCredentialsException -> onBadCredentials(error.message) is SQLiteConstraintException -> onStudentDuplicate(resources.getString(R.string.login_duplicate_student)) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/RecoverErrorHandler.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/RecoverErrorHandler.kt index 8619369dd..ac4c03130 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/RecoverErrorHandler.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/RecoverErrorHandler.kt @@ -1,13 +1,15 @@ package io.github.wulkanowy.ui.modules.login.recover -import android.content.res.Resources +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.sdk.scrapper.exception.InvalidCaptchaException import io.github.wulkanowy.sdk.scrapper.exception.InvalidEmailException import io.github.wulkanowy.sdk.scrapper.exception.NoAccountFoundException import io.github.wulkanowy.ui.base.ErrorHandler import javax.inject.Inject -class RecoverErrorHandler @Inject constructor(resources: Resources) : ErrorHandler(resources) { +class RecoverErrorHandler @Inject constructor(@ApplicationContext context: Context) : + ErrorHandler(context) { var onInvalidUsername: (String) -> Unit = {} @@ -15,7 +17,8 @@ class RecoverErrorHandler @Inject constructor(resources: Resources) : ErrorHandl override fun proceed(error: Throwable) { when (error) { - is InvalidEmailException, is NoAccountFoundException -> onInvalidUsername(error.localizedMessage.orEmpty()) + is InvalidEmailException, + is NoAccountFoundException -> onInvalidUsername(error.localizedMessage.orEmpty()) is InvalidCaptchaException -> onInvalidCaptcha(error.localizedMessage.orEmpty(), error) else -> super.proceed(error) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsErrorHandler.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsErrorHandler.kt index 00ba0bad8..36e38fb96 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsErrorHandler.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsErrorHandler.kt @@ -1,11 +1,13 @@ package io.github.wulkanowy.ui.modules.timetable.completed -import android.content.res.Resources +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException import io.github.wulkanowy.ui.base.ErrorHandler import javax.inject.Inject -class CompletedLessonsErrorHandler @Inject constructor(resources: Resources) : ErrorHandler(resources) { +class CompletedLessonsErrorHandler @Inject constructor(@ApplicationContext context: Context) : + ErrorHandler(context) { var onFeatureDisabled: () -> Unit = {} diff --git a/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt b/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt index a3961aed8..962e5b208 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/AppInfo.kt @@ -1,35 +1,31 @@ package io.github.wulkanowy.utils import android.content.res.Resources -import android.os.Build.MANUFACTURER -import android.os.Build.MODEL -import android.os.Build.VERSION.SDK_INT -import io.github.wulkanowy.BuildConfig.BUILD_TIMESTAMP -import io.github.wulkanowy.BuildConfig.DEBUG -import io.github.wulkanowy.BuildConfig.FLAVOR -import io.github.wulkanowy.BuildConfig.VERSION_CODE -import io.github.wulkanowy.BuildConfig.VERSION_NAME +import android.os.Build +import io.github.wulkanowy.BuildConfig import javax.inject.Inject import javax.inject.Singleton @Singleton open class AppInfo @Inject constructor() { - open val isDebug get() = DEBUG + open val isDebug get() = BuildConfig.DEBUG - open val versionCode get() = VERSION_CODE + open val versionCode get() = BuildConfig.VERSION_CODE - open val buildTimestamp get() = BUILD_TIMESTAMP + open val buildTimestamp get() = BuildConfig.BUILD_TIMESTAMP - open val buildFlavor get() = FLAVOR + open val buildFlavor get() = BuildConfig.FLAVOR - open val versionName get() = VERSION_NAME + open val versionName get() = BuildConfig.VERSION_NAME - open val systemVersion get() = SDK_INT + open val systemVersion get() = Build.VERSION.SDK_INT - open val systemManufacturer: String get() = MANUFACTURER + open val systemManufacturer: String get() = Build.MANUFACTURER - open val systemModel: String get() = MODEL + open val systemModel: String get() = Build.MODEL + + open val messagesBaseUrl = BuildConfig.MESSAGES_BASE_URL @Suppress("DEPRECATION") open val systemLanguage: String diff --git a/app/src/main/res/drawable/ic_dashboard_warning.xml b/app/src/main/res/drawable/ic_dashboard_warning.xml new file mode 100644 index 000000000..e7a5dc5a4 --- /dev/null +++ b/app/src/main/res/drawable/ic_dashboard_warning.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/item_dashboard_admin_message.xml b/app/src/main/res/layout/item_dashboard_admin_message.xml new file mode 100644 index 000000000..265cc14e0 --- /dev/null +++ b/app/src/main/res/layout/item_dashboard_admin_message.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml index 5eca4680e..881d5bd4f 100644 --- a/app/src/main/res/values-night/styles.xml +++ b/app/src/main/res/values-night/styles.xml @@ -8,6 +8,7 @@ @color/colorErrorLight @color/colorDividerInverse @color/colorSwipeRefreshDark + @color/dashboard_message_medium_light ?colorSurface ?android:textColorPrimary @color/colorNavigationBarLight diff --git a/app/src/main/res/values/api_hosts.xml b/app/src/main/res/values/api_hosts.xml index dac94c3ff..158490471 100644 --- a/app/src/main/res/values/api_hosts.xml +++ b/app/src/main/res/values/api_hosts.xml @@ -40,7 +40,7 @@ https://vulcan.net.pl/?login https://vulcan.net.pl/?login https://vulcan.net.pl/?login - http://fakelog.tk/?email + http://fakelog.cf/?email Default diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 49ef39ab3..d4ed6e971 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -3,4 +3,5 @@ + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index a68e2710f..f3112b101 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -12,6 +12,9 @@ #1C1C1C #0D0D0D + #FFD980 + #ffd54f + #ffd54f #ff8f00 diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 628aa2974..45382e6d2 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -11,6 +11,7 @@ @color/colorError @color/colorDivider @color/colorSwipeRefresh + @color/dashboard_message_medium_dark @android:color/darker_gray ?android:textColorPrimary @style/PreferenceThemeOverlay From ac86737050fbeb95d593b8747279cc593eb025ad Mon Sep 17 00:00:00 2001 From: Michael <5672750+mibac138@users.noreply.github.com> Date: Thu, 14 Oct 2021 01:44:34 +0200 Subject: [PATCH 21/69] Fix NPE in SyncPresenter (#1582) --- .../java/io/github/wulkanowy/services/sync/SyncManager.kt | 2 +- .../wulkanowy/ui/modules/settings/sync/SyncPresenter.kt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt b/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt index 02d8b964f..32ca20afc 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/SyncManager.kt @@ -74,7 +74,7 @@ class SyncManager @Inject constructor( } } - fun startOneTimeSyncWorker(): Flow { + fun startOneTimeSyncWorker(): Flow { val work = OneTimeWorkRequestBuilder() .setInputData( Data.Builder() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncPresenter.kt index 63e86a475..0d404a138 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncPresenter.kt @@ -46,7 +46,7 @@ class SyncPresenter @Inject constructor( fun onSyncNowClicked() { view?.run { syncManager.startOneTimeSyncWorker().onEach { workInfo -> - when (workInfo.state) { + when (workInfo?.state) { WorkInfo.State.ENQUEUED -> { setSyncInProgress(true) Timber.i("Setting sync now started") @@ -63,9 +63,9 @@ class SyncPresenter @Inject constructor( ) analytics.logEvent("sync_now", "status" to "failed") } - else -> Timber.d("Sync now state: ${workInfo.state}") + else -> Timber.d("Sync now state: ${workInfo?.state}") } - if (workInfo.state.isFinished) { + if (workInfo?.state?.isFinished == true) { setSyncInProgress(false) setSyncDateInView() } From e7550f7a43a74b02df366501481eb679be7a9979 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Oct 2021 00:10:22 +0000 Subject: [PATCH 22/69] Bump gradle from 7.0.2 to 7.0.3 (#1586) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 153088a6d..ad27aba07 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ buildscript { dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" - classpath 'com.android.tools.build:gradle:7.0.2' + classpath 'com.android.tools.build:gradle:7.0.3' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.10' classpath 'com.huawei.agconnect:agcp:1.6.1.200' From 4c8d9c8f7fa2fb347d467ccc08a0f864ed5b07c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 14 Oct 2021 16:25:09 +0200 Subject: [PATCH 23/69] Fix infinite refresh when admin messages list is empty (#1587) --- .../wulkanowy/ui/modules/dashboard/DashboardPresenter.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt index 5ff7f2ce8..f791fa0d0 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt @@ -620,6 +620,8 @@ class DashboardPresenter @Inject constructor( if (dashboardItem is DashboardItem.AdminMessages && !dashboardItem.isDataLoaded) { dashboardItemsToLoad = dashboardItemsToLoad - DashboardItem.Type.ADMIN_MESSAGE + dashboardTileLoadedList = dashboardTileLoadedList - DashboardItem.Tile.ADMIN_MESSAGE + dashboardItemLoadedList.removeAll { it.type == DashboardItem.Type.ADMIN_MESSAGE } } @@ -654,9 +656,12 @@ class DashboardPresenter @Inject constructor( } private fun updateForceRefreshData(dashboardItem: DashboardItem) { + val isNotLoadedAdminMessage = + dashboardItem is DashboardItem.AdminMessages && !dashboardItem.isDataLoaded + with(dashboardItemRefreshLoadedList) { removeAll { it.type == dashboardItem.type } - add(dashboardItem) + if (!isNotLoadedAdminMessage) add(dashboardItem) } val isRefreshItemLoaded = From 54e9ea6478226f40363c279dddc993406aae7220 Mon Sep 17 00:00:00 2001 From: Michael <5672750+mibac138@users.noreply.github.com> Date: Sat, 16 Oct 2021 11:17:00 +0200 Subject: [PATCH 24/69] Use FloatingActionButton.{show,hide} instead of using setVisibility (#1591) --- .../wulkanowy/ui/modules/attendance/AttendanceFragment.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt index 3fbdaec5a..092c9d0d8 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt @@ -218,7 +218,13 @@ class AttendanceFragment : BaseFragment(R.layout.frag } override fun showExcuseButton(show: Boolean) { - binding.attendanceExcuseButton.visibility = if (show) VISIBLE else GONE + with(binding.attendanceExcuseButton) { + if (show) { + show() + } else { + hide() + } + } } override fun showAttendanceDialog(lesson: Attendance) { From 84e4167dbdaeb7a419693f1ba0978601b247a0bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Mon, 18 Oct 2021 00:11:46 +0200 Subject: [PATCH 25/69] Mi Band notification improvements (#1579) --- .../notifications/AppNotificationManager.kt | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt index 69d0092c1..ddad9bf27 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Context import android.os.Build +import androidx.annotation.PluralsRes import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import dagger.hilt.android.qualifiers.ApplicationContext @@ -68,7 +69,8 @@ class AppNotificationManager @Inject constructor( ) { val groupType = notificationData.type.group ?: return val group = "${groupType}_${student.id}" - val groupId = student.id * 100 + notificationData.type.ordinal + + notificationData.sendSummaryNotification(group, student) notificationData.lines.forEach { item -> val title = context.resources.getQuantityString(notificationData.titleStringRes, 1) @@ -88,16 +90,26 @@ class AppNotificationManager @Inject constructor( saveNotification(title, item, notificationData, student) } + } + private fun MultipleNotificationsData.sendSummaryNotification(group: String, student: Student) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return - val summaryNotification = getDefaultNotificationBuilder(notificationData) - .setSmallIcon(notificationData.icon) + val summaryNotification = getDefaultNotificationBuilder(this) + .setSmallIcon(icon) + .setContentTitle(getQuantityString(titleStringRes, lines.size)) + .setContentText(getQuantityString(contentStringRes, lines.size)) + .setStyle( + NotificationCompat.InboxStyle() + .setSummaryText(student.nickOrName) + .also { builder -> lines.forEach { builder.addLine(it) } } + ) + .setLocalOnly(true) .setGroup(group) - .setStyle(NotificationCompat.InboxStyle().setSummaryText(student.nickOrName)) .setGroupSummary(true) .build() + val groupId = student.id * 100 + type.ordinal notificationManager.notify(groupId.toInt(), summaryNotification) } @@ -116,6 +128,7 @@ class AppNotificationManager @Inject constructor( .setDefaults(NotificationCompat.DEFAULT_ALL) .setPriority(NotificationCompat.PRIORITY_HIGH) .setColor(context.getCompatColor(R.color.colorPrimary)) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) .setContentIntent( PendingIntent.getActivity( context, @@ -142,4 +155,8 @@ class AppNotificationManager @Inject constructor( notificationRepository.saveNotification(notificationEntity) } -} \ No newline at end of file + + private fun getQuantityString(@PluralsRes res: Int, arg: Int): String { + return context.resources.getQuantityString(res, arg, arg) + } +} From 58ea2c530e55991843e47ecbb9824e46678921a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Oct 2021 06:36:46 +0000 Subject: [PATCH 26/69] Bump hianalytics from 6.3.0.300 to 6.3.0.301 (#1593) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 227423e4d..90b4cdb94 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -234,7 +234,7 @@ dependencies { playImplementation 'com.google.android.play:core:1.10.2' playImplementation 'com.google.android.play:core-ktx:1.8.1' - hmsImplementation 'com.huawei.hms:hianalytics:6.3.0.300' + hmsImplementation 'com.huawei.hms:hianalytics:6.3.0.301' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.1.200' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 09a134d442c26d3e2a3e242ef5765b348b8a2c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 21 Oct 2021 10:47:37 +0200 Subject: [PATCH 27/69] Fix last sync date to save only successful sync (#1595) --- .../data/repositories/AppCreatorRepository.kt | 2 +- .../data/repositories/LoggerRepository.kt | 23 +++++++++--------- .../data/repositories/SemesterRepository.kt | 4 ++-- .../data/repositories/StudentRepository.kt | 8 +++---- .../TimetableNotificationSchedulerHelper.kt | 4 ++-- .../wulkanowy/services/sync/SyncWorker.kt | 24 +++++++++++-------- .../modules/about/license/LicensePresenter.kt | 2 +- .../wulkanowy/utils/DispatchersProvider.kt | 4 +--- .../wulkanowy/TestDispatchersProvider.kt | 4 +--- 9 files changed, 37 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt index 3d83f7181..cbaa12bd3 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AppCreatorRepository.kt @@ -20,7 +20,7 @@ class AppCreatorRepository @Inject constructor( @OptIn(ExperimentalSerializationApi::class) @Suppress("BlockingMethodInNonBlockingContext") - suspend fun getAppCreators() = withContext(dispatchers.backgroundThread) { + suspend fun getAppCreators() = withContext(dispatchers.io) { val inputStream = context.assets.open("contributors.json").buffered() json.decodeFromStream>(inputStream) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/LoggerRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/LoggerRepository.kt index 6d509b026..1a8cd6ea3 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/LoggerRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/LoggerRepository.kt @@ -15,24 +15,23 @@ class LoggerRepository @Inject constructor( suspend fun getLastLogLines() = getLastModified().readText().split("\n") - suspend fun getLogFiles() = withContext(dispatchers.backgroundThread) { - File(context.filesDir.absolutePath).listFiles(File::isFile)?.filter { - it.name.endsWith(".log") - }!! + suspend fun getLogFiles() = withContext(dispatchers.io) { + File(context.filesDir.absolutePath).listFiles(File::isFile) + ?.filter { it.name.endsWith(".log") }!! } - private suspend fun getLastModified(): File { - return withContext(dispatchers.backgroundThread) { - var lastModifiedTime = Long.MIN_VALUE - var chosenFile: File? = null - File(context.filesDir.absolutePath).listFiles(File::isFile)?.forEach { file -> + private suspend fun getLastModified() = withContext(dispatchers.io) { + var lastModifiedTime = Long.MIN_VALUE + var chosenFile: File? = null + + File(context.filesDir.absolutePath).listFiles(File::isFile) + ?.forEach { file -> if (file.lastModified() > lastModifiedTime) { lastModifiedTime = file.lastModified() chosenFile = file } } - if (chosenFile == null) throw FileNotFoundException("Log file not found") - chosenFile!! - } + + chosenFile ?: throw FileNotFoundException("Log file not found") } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt index 4336877a5..cc954558f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SemesterRepository.kt @@ -26,7 +26,7 @@ class SemesterRepository @Inject constructor( student: Student, forceRefresh: Boolean = false, refreshOnNoCurrent: Boolean = false - ) = withContext(dispatchers.backgroundThread) { + ) = withContext(dispatchers.io) { val semesters = semesterDb.loadAll(student.studentId, student.classId) if (isShouldFetch(student, semesters, forceRefresh, refreshOnNoCurrent)) { @@ -64,7 +64,7 @@ class SemesterRepository @Inject constructor( } suspend fun getCurrentSemester(student: Student, forceRefresh: Boolean = false) = - withContext(dispatchers.backgroundThread) { + withContext(dispatchers.io) { getSemesters(student, forceRefresh).getCurrentOrLast() } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt index 2ac892d01..9e4a1aabc 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt @@ -66,7 +66,7 @@ class StudentRepository @Inject constructor( .map { it.apply { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { - student.password = withContext(dispatchers.backgroundThread) { + student.password = withContext(dispatchers.io) { decrypt(student.password) } } @@ -77,7 +77,7 @@ class StudentRepository @Inject constructor( val student = studentDb.loadById(id) ?: throw NoCurrentStudentException() if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { - student.password = withContext(dispatchers.backgroundThread) { + student.password = withContext(dispatchers.io) { decrypt(student.password) } } @@ -88,7 +88,7 @@ class StudentRepository @Inject constructor( val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException() if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { - student.password = withContext(dispatchers.backgroundThread) { + student.password = withContext(dispatchers.io) { decrypt(student.password) } } @@ -101,7 +101,7 @@ class StudentRepository @Inject constructor( .map { it.apply { if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.API) { - password = withContext(dispatchers.backgroundThread) { + password = withContext(dispatchers.io) { encrypt(password, context) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt index a42a0ab4b..2ec4e527b 100644 --- a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt +++ b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt @@ -54,7 +54,7 @@ class TimetableNotificationSchedulerHelper @Inject constructor( suspend fun cancelScheduled(lessons: List, student: Student) { val studentId = student.studentId - withContext(dispatchersProvider.backgroundThread) { + withContext(dispatchersProvider.io) { lessons.sortedBy { it.start }.forEachIndexed { index, lesson -> val upcomingTime = getUpcomingLessonTime(index, lessons, lesson) cancelScheduledTo( @@ -91,7 +91,7 @@ class TimetableNotificationSchedulerHelper @Inject constructor( return } - withContext(dispatchersProvider.backgroundThread) { + withContext(dispatchersProvider.io) { lessons.groupBy { it.date } .map { it.value.sortedBy { lesson -> lesson.start } } .map { it.filter { lesson -> lesson.isStudentPlan } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/SyncWorker.kt b/app/src/main/java/io/github/wulkanowy/services/sync/SyncWorker.kt index ea1f79cbf..52979e635 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/SyncWorker.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/SyncWorker.kt @@ -19,11 +19,11 @@ import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException import io.github.wulkanowy.services.sync.channels.DebugChannel import io.github.wulkanowy.services.sync.works.Work +import io.github.wulkanowy.utils.DispatchersProvider import io.github.wulkanowy.utils.getCompatColor -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext import timber.log.Timber import java.time.LocalDateTime -import java.time.ZoneId import kotlin.random.Random @HiltWorker @@ -34,13 +34,14 @@ class SyncWorker @AssistedInject constructor( private val semesterRepository: SemesterRepository, private val works: Set<@JvmSuppressWildcards Work>, private val preferencesRepository: PreferencesRepository, - private val notificationManager: NotificationManagerCompat + private val notificationManager: NotificationManagerCompat, + private val dispatchersProvider: DispatchersProvider ) : CoroutineWorker(appContext, workerParameters) { - override suspend fun doWork() = coroutineScope { + override suspend fun doWork() = withContext(dispatchersProvider.io) { Timber.i("SyncWorker is starting") - if (!studentRepository.isCurrentStudentSet()) return@coroutineScope Result.failure() + if (!studentRepository.isCurrentStudentSet()) return@withContext Result.failure() val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student, true) @@ -50,12 +51,12 @@ class SyncWorker @AssistedInject constructor( Timber.i("${work::class.java.simpleName} is starting") work.doWork(student, semester) Timber.i("${work::class.java.simpleName} result: Success") - preferencesRepository.lasSyncDate = LocalDateTime.now(ZoneId.systemDefault()) null } catch (e: Throwable) { Timber.w("${work::class.java.simpleName} result: An exception ${e.message} occurred") - if (e is FeatureDisabledException || e is FeatureNotAvailableException) null - else { + if (e is FeatureDisabledException || e is FeatureNotAvailableException) { + null + } else { Timber.e(e) e } @@ -70,13 +71,16 @@ class SyncWorker @AssistedInject constructor( ) } exceptions.isNotEmpty() -> Result.retry() - else -> Result.success() + else -> { + preferencesRepository.lasSyncDate = LocalDateTime.now() + Result.success() + } } if (preferencesRepository.isDebugNotificationEnable) notify(result) Timber.i("SyncWorker result: $result") - result + return@withContext result } private fun notify(result: Result) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicensePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicensePresenter.kt index cc430fc2c..5368cc19d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicensePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/license/LicensePresenter.kt @@ -31,7 +31,7 @@ class LicensePresenter @Inject constructor( private fun loadData() { flowWithResource { - withContext(dispatchers.backgroundThread) { + withContext(dispatchers.io) { view?.appLibraries.orEmpty() } }.onEach { diff --git a/app/src/main/java/io/github/wulkanowy/utils/DispatchersProvider.kt b/app/src/main/java/io/github/wulkanowy/utils/DispatchersProvider.kt index ecc8e05eb..8aaa57f4f 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/DispatchersProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/DispatchersProvider.kt @@ -1,10 +1,8 @@ package io.github.wulkanowy.utils -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers open class DispatchersProvider { - open val backgroundThread: CoroutineDispatcher - get() = Dispatchers.IO + open val io get() = Dispatchers.IO } diff --git a/app/src/test/java/io/github/wulkanowy/TestDispatchersProvider.kt b/app/src/test/java/io/github/wulkanowy/TestDispatchersProvider.kt index e60b1d7a2..d4cba7b5e 100644 --- a/app/src/test/java/io/github/wulkanowy/TestDispatchersProvider.kt +++ b/app/src/test/java/io/github/wulkanowy/TestDispatchersProvider.kt @@ -1,11 +1,9 @@ package io.github.wulkanowy import io.github.wulkanowy.utils.DispatchersProvider -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers class TestDispatchersProvider : DispatchersProvider() { - override val backgroundThread: CoroutineDispatcher - get() = Dispatchers.Unconfined + override val io get() = Dispatchers.Unconfined } From 94fd303f8ea6edcdd0f8e528cf623615c8aa3619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 21 Oct 2021 10:51:00 +0200 Subject: [PATCH 28/69] Add single support advert (#1484) --- app/build.gradle | 20 ++-- app/src/main/AndroidManifest.xml | 13 ++- .../ui/modules/about/AboutPresenter.kt | 26 ++--- .../ui/modules/dashboard/DashboardAdapter.kt | 3 +- .../settings/advanced/AdvancedFragment.kt | 4 - .../settings/appearance/AppearanceFragment.kt | 4 - .../notifications/NotificationsFragment.kt | 4 - .../ui/modules/settings/sync/SyncFragment.kt | 4 - app/src/main/res/drawable/ic_settings_ads.xml | 22 +++++ app/src/main/res/values/preferences_keys.xml | 1 + app/src/main/res/values/strings.xml | 10 ++ app/src/main/res/xml/scheme_preferences.xml | 30 +++--- .../ui/modules/settings/ads/AdsFragment.kt | 94 +++++++++++++++++++ .../ui/modules/settings/ads/AdsPresenter.kt | 41 ++++++++ .../ui/modules/settings/ads/AdsView.kt | 17 ++++ .../io/github/wulkanowy/utils/AdsHelper.kt | 39 ++++++++ app/src/play/res/xml/scheme_preferences.xml | 39 ++++++++ .../play/res/xml/scheme_preferences_ads.xml | 12 +++ 18 files changed, 324 insertions(+), 59 deletions(-) create mode 100644 app/src/main/res/drawable/ic_settings_ads.xml create mode 100644 app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt create mode 100644 app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsPresenter.kt create mode 100644 app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsView.kt create mode 100644 app/src/play/java/io/github/wulkanowy/utils/AdsHelper.kt create mode 100644 app/src/play/res/xml/scheme_preferences.xml create mode 100644 app/src/play/res/xml/scheme_preferences_ads.xml diff --git a/app/build.gradle b/app/build.gradle index 90b4cdb94..421774062 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,7 +29,10 @@ android { resValue "string", "app_name", "Wulkanowy" - manifestPlaceholders = [firebase_enabled: project.hasProperty("enableFirebase")] + manifestPlaceholders = [ + firebase_enabled: project.hasProperty("enableFirebase"), + admob_project_id: "" + ] javaCompileOptions { annotationProcessorOptions { arguments += [ @@ -39,6 +42,8 @@ android { } } + buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null" + if (System.env.SET_BUILD_TIMESTAMP) { buildConfigField "long", "BUILD_TIMESTAMP", String.valueOf(System.currentTimeMillis()) } else { @@ -82,23 +87,21 @@ android { productFlavors { hms { dimension "platform" - manifestPlaceholders = [ - install_channel: "AppGallery" - ] + manifestPlaceholders = [install_channel: "AppGallery"] } play { dimension "platform" manifestPlaceholders = [ - install_channel: "Google Play" + install_channel : "Google Play", + admob_project_id: System.getenv("ADMOB_PROJECT_ID") ?: "ca-app-pub-3940256099942544~3347511713" ] + buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "\"${System.getenv("SINGLE_SUPPORT_AD_ID") ?: "ca-app-pub-3940256099942544/5354046379"}\"" } fdroid { dimension "platform" - manifestPlaceholders = [ - install_channel: "F-Droid" - ] + manifestPlaceholders = [install_channel: "F-Droid"] } } @@ -233,6 +236,7 @@ dependencies { playImplementation 'com.google.firebase:firebase-crashlytics:' playImplementation 'com.google.android.play:core:1.10.2' playImplementation 'com.google.android.play:core-ktx:1.8.1' + playImplementation 'com.google.android.gms:play-services-ads:20.4.0' hmsImplementation 'com.huawei.hms:hianalytics:6.3.0.301' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.1.200' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8c7eb8527..810d469f4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -165,33 +165,32 @@ - - - - - - - + + diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutPresenter.kt index 6bcf5f77b..552749349 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutPresenter.kt @@ -82,18 +82,20 @@ class AboutPresenter @Inject constructor( private fun loadData() { view?.run { - updateData(listOfNotNull( - versionRes, - creatorsRes, - feedbackRes, - faqRes, - discordRes, - facebookRes, - twitterRes, - homepageRes, - licensesRes, - privacyRes - )) + updateData( + listOfNotNull( + versionRes, + creatorsRes, + feedbackRes, + faqRes, + discordRes, + facebookRes, + twitterRes, + homepageRes, + licensesRes, + privacyRes + ) + ) } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt index 405bfbc5a..440bbd5d4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt @@ -299,7 +299,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter + + + + + + diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index c512a5f21..1aba7d856 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -34,4 +34,5 @@ message_send_recipients last_sync_date notifications_piggyback + single_ad_support diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 48c7712dc..13030366a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -658,10 +658,19 @@ Reply with message history Show arithmetic average when no weights provided + Support + Watch single ad to support project + Consent to data processing + To view an advertisement you must agree to the data processing terms of our Privacy Policy + Agree + Privacy policy + Ad is loading + Advanced Appearance & Behavior Notifications Synchronization + Advertisements Grades Dashboard @@ -681,6 +690,7 @@ Plus and minus values, average calculation Advanced App version, contributors, social portals, licenses + Displaying advertisements, project support diff --git a/app/src/main/res/xml/scheme_preferences.xml b/app/src/main/res/xml/scheme_preferences.xml index 086214923..5bf7ad8aa 100644 --- a/app/src/main/res/xml/scheme_preferences.xml +++ b/app/src/main/res/xml/scheme_preferences.xml @@ -1,33 +1,33 @@ + app:title="@string/pref_appearance_category" /> + app:title="@string/pref_notifications_category" /> + app:title="@string/pref_sync_category" /> + app:title="@string/pref_advanced_category" /> + app:title="@string/about_title" /> diff --git a/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt new file mode 100644 index 000000000..960a54b8c --- /dev/null +++ b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt @@ -0,0 +1,94 @@ +package io.github.wulkanowy.ui.modules.settings.ads + +import android.os.Bundle +import android.view.View +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAd +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +import io.github.wulkanowy.ui.base.BaseActivity +import io.github.wulkanowy.ui.base.ErrorDialog +import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.utils.openInternetBrowser +import javax.inject.Inject + +@AndroidEntryPoint +class AdsFragment : PreferenceFragmentCompat(), MainView.TitledView, AdsView { + + @Inject + lateinit var presenter: AdsPresenter + + override val titleStringId = R.string.pref_settings_ads_title + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.scheme_preferences_ads, rootKey) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + presenter.onAttachView(this) + } + + override fun initView() { + findPreference(getString(R.string.pref_key_ads_single_support))?.setOnPreferenceClickListener { + presenter.onWatchSingleAdSelected() + true + } + } + + override fun showAd(ad: RewardedInterstitialAd) { + if (isVisible) { + ad.show(requireActivity()) {} + } + } + + override fun showPrivacyPolicyDialog() { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.pref_ads_privacy_title)) + .setMessage(getString(R.string.pref_ads_privacy_description)) + .setPositiveButton(getString(R.string.pref_ads_privacy_agree)) { _, _ -> presenter.onAgreedPrivacy() } + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .setNeutralButton(getString(R.string.pref_ads_privacy_link)) { _, _ -> presenter.onPrivacySelected() } + .show() + } + + override fun openPrivacyPolicy() { + requireContext().openInternetBrowser( + "https://wulkanowy.github.io/polityka-prywatnosci.html", + ::showMessage + ) + } + + override fun showLoadingSupportAd(show: Boolean) { + findPreference(getString(R.string.pref_key_ads_single_support))?.run { + isEnabled = !show + summary = if (show) getString(R.string.pref_ads_loading) else null + } + } + + override fun showError(text: String, error: Throwable) { + (activity as? BaseActivity<*, *>)?.showError(text, error) + } + + override fun showMessage(text: String) { + (activity as? BaseActivity<*, *>)?.showMessage(text) + } + + override fun showExpiredDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredDialog() + } + + override fun showChangePasswordSnackbar(redirectUrl: String) { + (activity as? BaseActivity<*, *>)?.showChangePasswordSnackbar(redirectUrl) + } + + override fun openClearLoginView() { + (activity as? BaseActivity<*, *>)?.openClearLoginView() + } + + override fun showErrorDetailsDialog(error: Throwable) { + ErrorDialog.newInstance(error).show(childFragmentManager, error.toString()) + } +} \ No newline at end of file diff --git a/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsPresenter.kt b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsPresenter.kt new file mode 100644 index 000000000..fd5cc9b6f --- /dev/null +++ b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsPresenter.kt @@ -0,0 +1,41 @@ +package io.github.wulkanowy.ui.modules.settings.ads + +import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.ui.base.ErrorHandler +import io.github.wulkanowy.utils.AdsHelper +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +class AdsPresenter @Inject constructor( + errorHandler: ErrorHandler, + studentRepository: StudentRepository, + private val adsHelper: AdsHelper +) : BasePresenter(errorHandler, studentRepository) { + + override fun onAttachView(view: AdsView) { + super.onAttachView(view) + view.initView() + Timber.i("Settings ads view was initialized") + } + + fun onWatchSingleAdSelected() { + view?.showPrivacyPolicyDialog() + } + + fun onPrivacySelected() { + view?.openPrivacyPolicy() + } + + fun onAgreedPrivacy() { + view?.showLoadingSupportAd(true) + presenterScope.launch { + runCatching { adsHelper.getSupportAd() } + .onFailure(errorHandler::dispatch) + .onSuccess { it?.let { view?.showAd(it) } } + + view?.showLoadingSupportAd(false) + } + } +} \ No newline at end of file diff --git a/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsView.kt b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsView.kt new file mode 100644 index 000000000..25eeaaeca --- /dev/null +++ b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsView.kt @@ -0,0 +1,17 @@ +package io.github.wulkanowy.ui.modules.settings.ads + +import com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAd +import io.github.wulkanowy.ui.base.BaseView + +interface AdsView : BaseView { + + fun initView() + + fun showAd(ad: RewardedInterstitialAd) + + fun showPrivacyPolicyDialog() + + fun openPrivacyPolicy() + + fun showLoadingSupportAd(show: Boolean) +} \ No newline at end of file diff --git a/app/src/play/java/io/github/wulkanowy/utils/AdsHelper.kt b/app/src/play/java/io/github/wulkanowy/utils/AdsHelper.kt new file mode 100644 index 000000000..f363c13f9 --- /dev/null +++ b/app/src/play/java/io/github/wulkanowy/utils/AdsHelper.kt @@ -0,0 +1,39 @@ +package io.github.wulkanowy.utils + +import android.content.Context +import com.google.android.gms.ads.AdRequest +import com.google.android.gms.ads.LoadAdError +import com.google.android.gms.ads.MobileAds +import com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAd +import com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAdLoadCallback +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.BuildConfig +import javax.inject.Inject +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +class AdsHelper @Inject constructor(@ApplicationContext private val context: Context) { + + suspend fun getSupportAd(): RewardedInterstitialAd? { + MobileAds.initialize(context) + + val adRequest = AdRequest.Builder().build() + + return suspendCoroutine { + RewardedInterstitialAd.load( + context, + BuildConfig.SINGLE_SUPPORT_AD_ID, + adRequest, + object : RewardedInterstitialAdLoadCallback() { + override fun onAdLoaded(rewardedInterstitialAd: RewardedInterstitialAd) { + it.resume(rewardedInterstitialAd) + } + + override fun onAdFailedToLoad(loadAdError: LoadAdError) { + it.resumeWithException(IllegalArgumentException(loadAdError.message)) + } + }) + } + } +} \ No newline at end of file diff --git a/app/src/play/res/xml/scheme_preferences.xml b/app/src/play/res/xml/scheme_preferences.xml new file mode 100644 index 000000000..05b0bf644 --- /dev/null +++ b/app/src/play/res/xml/scheme_preferences.xml @@ -0,0 +1,39 @@ + + + + + + + + + diff --git a/app/src/play/res/xml/scheme_preferences_ads.xml b/app/src/play/res/xml/scheme_preferences_ads.xml new file mode 100644 index 000000000..6b3625cae --- /dev/null +++ b/app/src/play/res/xml/scheme_preferences_ads.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file From 0f800b61f63f8b7ae303316354ae371eafc9799d Mon Sep 17 00:00:00 2001 From: Michael <5672750+mibac138@users.noreply.github.com> Date: Sun, 24 Oct 2021 01:23:36 +0200 Subject: [PATCH 29/69] Allow expanding multiple subject' grades at once (#1584) --- .../github/wulkanowy/data/db/AppDatabase.kt | 2 +- .../data/db/migrations/Migration41.kt | 12 +- .../repositories/PreferencesRepository.kt | 15 +- .../ui/modules/grade/GradeExpandMode.kt | 9 ++ .../grade/details/GradeDetailsAdapter.kt | 146 +++++++++++++----- .../grade/details/GradeDetailsFragment.kt | 5 +- .../grade/details/GradeDetailsPresenter.kt | 33 ++-- .../modules/grade/details/GradeDetailsView.kt | 3 +- .../main/res/values/preferences_defaults.xml | 2 +- app/src/main/res/values/preferences_keys.xml | 3 +- .../main/res/values/preferences_values.xml | 11 ++ .../res/xml/scheme_preferences_appearance.xml | 11 +- 12 files changed, 187 insertions(+), 65 deletions(-) create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeExpandMode.kt diff --git a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt index b8831acd8..d96856068 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt @@ -193,7 +193,7 @@ abstract class AppDatabase : RoomDatabase() { Migration38(), Migration39(), Migration40(), - Migration41(), + Migration41(sharedPrefProvider), Migration42() ) diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt index 17773efdb..0080e057c 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration41.kt @@ -2,10 +2,20 @@ package io.github.wulkanowy.data.db.migrations import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import io.github.wulkanowy.data.db.SharedPrefProvider +import io.github.wulkanowy.ui.modules.grade.GradeExpandMode -class Migration41 : Migration(40, 41) { +class Migration41(private val sharedPrefProvider: SharedPrefProvider) : Migration(40, 41) { override fun migrate(database: SupportSQLiteDatabase) { + migrateSharedPreferences() database.execSQL("ALTER TABLE Homework ADD COLUMN is_added_by_user INTEGER NOT NULL DEFAULT 0") } + + private fun migrateSharedPreferences() { + if (sharedPrefProvider.getBoolean("pref_key_expand_grade", false)) { + sharedPrefProvider.putString("pref_key_expand_grade_mode", GradeExpandMode.ALWAYS_EXPANDED.value) + } + sharedPrefProvider.delete("pref_key_expand_grade") + } } \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index b0991bbcb..696ffd63e 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -10,6 +10,7 @@ import io.github.wulkanowy.R import io.github.wulkanowy.sdk.toLocalDate import io.github.wulkanowy.ui.modules.dashboard.DashboardItem import io.github.wulkanowy.ui.modules.grade.GradeAverageMode +import io.github.wulkanowy.ui.modules.grade.GradeExpandMode import io.github.wulkanowy.ui.modules.grade.GradeSortingMode import io.github.wulkanowy.utils.toLocalDateTime import io.github.wulkanowy.utils.toTimestamp @@ -19,6 +20,8 @@ import kotlinx.coroutines.flow.map import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import java.lang.ClassCastException +import java.lang.IllegalStateException import java.time.LocalDate import java.time.LocalDateTime import javax.inject.Inject @@ -56,8 +59,13 @@ class PreferencesRepository @Inject constructor( R.bool.pref_default_grade_average_force_calc ) - val isGradeExpandable: Boolean - get() = !getBoolean(R.string.pref_key_expand_grade, R.bool.pref_default_expand_grade) + val gradeExpandMode: GradeExpandMode + get() = GradeExpandMode.getByValue( + getString( + R.string.pref_key_expand_grade_mode, + R.string.pref_default_expand_grade_mode + ) + ) val showAllSubjectsOnStatisticsList: Boolean get() = getBoolean( @@ -265,6 +273,9 @@ class PreferencesRepository @Inject constructor( private fun getBoolean(id: String, default: Int) = sharedPref.getBoolean(id, context.resources.getBoolean(default)) + private fun getBoolean(id: Int, default: Boolean) = + sharedPref.getBoolean(context.getString(id), default) + private companion object { private const val PREF_KEY_DASHBOARD_ITEMS_POSITION = "dashboard_items_position" diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeExpandMode.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeExpandMode.kt new file mode 100644 index 000000000..722e986ee --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeExpandMode.kt @@ -0,0 +1,9 @@ +package io.github.wulkanowy.ui.modules.grade + +enum class GradeExpandMode(val value: String) { + ONE("one"), UNLIMITED("any"), ALWAYS_EXPANDED("always"); + + companion object { + fun getByValue(value: String) = values().firstOrNull { it.value == value } ?: ONE + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt index 01631140c..d96ac0928 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsAdapter.kt @@ -5,6 +5,7 @@ import android.content.res.Resources import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.NO_POSITION @@ -13,9 +14,11 @@ import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.databinding.HeaderGradeDetailsBinding import io.github.wulkanowy.databinding.ItemGradeDetailsBinding import io.github.wulkanowy.ui.base.BaseExpandableAdapter +import io.github.wulkanowy.ui.modules.grade.GradeExpandMode import io.github.wulkanowy.utils.getBackgroundColor import io.github.wulkanowy.utils.toFormattedString import timber.log.Timber +import java.util.BitSet import javax.inject.Inject class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter() { @@ -24,19 +27,20 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter() - private var expandedPosition = NO_POSITION + private val expandedPositions = BitSet(items.size) - private var isExpandable = false + private var expandMode = GradeExpandMode.ONE var onClickListener: (Grade, position: Int) -> Unit = { _, _ -> } var colorTheme = "" - fun setDataItems(data: List, isExpanded: Boolean = isExpandable) { + fun setDataItems(data: List, expandMode: GradeExpandMode = this.expandMode) { headers = data.filter { it.viewType == ViewType.HEADER }.toMutableList() - items = if (isExpanded) headers else data.toMutableList() - isExpandable = isExpanded - expandedPosition = NO_POSITION + items = + (if (expandMode != GradeExpandMode.ALWAYS_EXPANDED) headers else data).toMutableList() + this.expandMode = expandMode + expandedPositions.clear() } fun updateDetailsItem(position: Int, grade: Grade) { @@ -48,7 +52,7 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter 1) { - Timber.e("Header with subject $subject found ${candidates.size} times! Expanded: $expandedPosition. Items: $candidates") + Timber.e("Header with subject $subject found ${candidates.size} times! Expanded: $expandedPositions. Items: $candidates") } return candidates.first() @@ -64,9 +68,9 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter HeaderViewHolder(HeaderGradeDetailsBinding.inflate(inflater, parent, false)) - ViewType.ITEM.id -> ItemViewHolder(ItemGradeDetailsBinding.inflate(inflater, parent, false)) + ViewType.HEADER.id -> HeaderViewHolder( + HeaderGradeDetailsBinding.inflate(inflater, parent, false) + ) + ViewType.ITEM.id -> ItemViewHolder( + ItemGradeDetailsBinding.inflate(inflater, parent, false) + ) else -> throw IllegalStateException() } } @@ -106,46 +114,91 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter 0) View.VISIBLE else View.GONE - if (header.newGrades > 0) gradeHeaderNote.text = header.newGrades.toString(10) + gradeHeaderPointsSum.text = + context.getString(R.string.grade_points_sum, header.pointsSum) + gradeHeaderPointsSum.isVisible = !header.pointsSum.isNullOrEmpty() + gradeHeaderNumber.text = context.resources.getQuantityString( + R.plurals.grade_number_item, + header.grades.size, + header.grades.size + ) + gradeHeaderNote.isVisible = header.newGrades > 0 - gradeHeaderContainer.isEnabled = isExpandable + if (header.newGrades > 0) { + gradeHeaderNote.text = header.newGrades.toString() + } + + gradeHeaderContainer.isEnabled = expandMode != GradeExpandMode.ALWAYS_EXPANDED gradeHeaderContainer.setOnClickListener { - expandedPosition = if (expandedPosition == adapterPosition) -1 else adapterPosition - - if (expandedPosition != NO_POSITION) { - refreshList(headers.toMutableList().apply { - addAll(headerPosition + 1, header.grades) - }) - scrollToHeaderWithSubItems(headerPosition, header.grades.size) - } else { - refreshList(headers) - } + expandGradeHeader(headerPosition, header, holder) } } } - private fun formatAverage(average: Double?, resources: Resources): String { - return if (average == null || average == .0) resources.getString(R.string.grade_no_average) - else resources.getString(R.string.grade_average, average) + private fun expandGradeHeader( + headerPosition: Int, + header: GradeDetailsHeader, + holder: HeaderViewHolder + ) { + if (expandMode == GradeExpandMode.ONE) { + val isHeaderExpanded = expandedPositions[headerPosition] + + expandedPositions.clear() + + if (!isHeaderExpanded) { + val updatedItemList = headers.toMutableList() + .apply { addAll(headerPosition + 1, header.grades) } + + expandedPositions.set(headerPosition) + refreshList(updatedItemList) + scrollToHeaderWithSubItems(headerPosition, header.grades.size) + } else { + refreshList(headers.toMutableList()) + } + } else if (expandMode == GradeExpandMode.UNLIMITED) { + val headerAdapterPosition = holder.bindingAdapterPosition + val isHeaderExpanded = expandedPositions[headerPosition] + + expandedPositions.flip(headerPosition) + + if (!isHeaderExpanded) { + val updatedList = items.toMutableList() + .apply { addAll(headerAdapterPosition + 1, header.grades) } + + refreshList(updatedList) + scrollToHeaderWithSubItems(headerAdapterPosition, header.grades.size) + } else { + val startPosition = headerAdapterPosition + 1 + val updatedList = items.toMutableList() + .apply { + subList(startPosition, startPosition + header.grades.size).clear() + } + + refreshList(updatedList) + } + } } @SuppressLint("SetTextI18n") private fun bindItemViewHolder(holder: ItemViewHolder, grade: Grade) { + val context = holder.binding.root.context + with(holder.binding) { gradeItemValue.run { text = grade.entry @@ -154,26 +207,37 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter grade.description grade.gradeSymbol.isNotBlank() -> grade.gradeSymbol - else -> root.context.getString(R.string.all_no_description) + else -> context.getString(R.string.all_no_description) } gradeItemDate.text = grade.date.toFormattedString() - gradeItemWeight.text = "${root.context.getString(R.string.grade_weight)}: ${grade.weight}" + gradeItemWeight.text = "${context.getString(R.string.grade_weight)}: ${grade.weight}" gradeItemNote.visibility = if (!grade.isRead) View.VISIBLE else View.GONE root.setOnClickListener { - holder.bindingAdapterPosition.let { if (it != NO_POSITION) onClickListener(grade, it) } + holder.bindingAdapterPosition.let { + if (it != NO_POSITION) onClickListener(grade, it) + } } } } + private fun formatAverage(average: Double?, resources: Resources) = + if (average == null || average == .0) { + resources.getString(R.string.grade_no_average) + } else { + resources.getString(R.string.grade_average, average) + } + private class HeaderViewHolder(val binding: HeaderGradeDetailsBinding) : RecyclerView.ViewHolder(binding.root) private class ItemViewHolder(val binding: ItemGradeDetailsBinding) : RecyclerView.ViewHolder(binding.root) - class GradeDetailsDiffUtil(private val old: List, private val new: List) : - DiffUtil.Callback() { + private class GradeDetailsDiffUtil( + private val old: List, + private val new: List + ) : DiffUtil.Callback() { override fun getOldListSize() = old.size diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsFragment.kt index 9d4da767d..c93600d49 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsFragment.kt @@ -12,6 +12,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.ui.modules.grade.GradeExpandMode import io.github.wulkanowy.databinding.FragmentGradeDetailsBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment @@ -79,10 +80,10 @@ class GradeDetailsFragment : else false } - override fun updateData(data: List, isGradeExpandable: Boolean, gradeColorTheme: String) { + override fun updateData(data: List, expandMode: GradeExpandMode, gradeColorTheme: String) { with(gradeDetailsAdapter) { colorTheme = gradeColorTheme - setDataItems(data, isGradeExpandable) + setDataItems(data, expandMode) notifyDataSetChanged() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt index 7544d2aa1..9b9651e1c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt @@ -9,6 +9,7 @@ import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider +import io.github.wulkanowy.ui.modules.grade.GradeExpandMode import io.github.wulkanowy.ui.modules.grade.GradeSortingMode.ALPHABETIC import io.github.wulkanowy.ui.modules.grade.GradeSortingMode.DATE import io.github.wulkanowy.ui.modules.grade.GradeSubject @@ -113,7 +114,7 @@ class GradeDetailsPresenter @Inject constructor( fun onParentViewReselected() { view?.run { if (!isViewEmpty) { - if (preferencesRepository.isGradeExpandable) collapseAllItems() + if (preferencesRepository.gradeExpandMode != GradeExpandMode.ALWAYS_EXPANDED) collapseAllItems() scrollToStart() } } @@ -157,7 +158,7 @@ class GradeDetailsPresenter @Inject constructor( showContent(true) updateData( data = items, - isGradeExpandable = preferencesRepository.isGradeExpandable, + expandMode = preferencesRepository.gradeExpandMode, gradeColorTheme = preferencesRepository.gradeColorTheme ) notifyParentDataLoaded(semesterId) @@ -175,7 +176,7 @@ class GradeDetailsPresenter @Inject constructor( showContent(items.isNotEmpty()) updateData( data = items, - isGradeExpandable = preferencesRepository.isGradeExpandable, + expandMode = preferencesRepository.gradeExpandMode, gradeColorTheme = preferencesRepository.gradeColorTheme ) } @@ -235,14 +236,24 @@ class GradeDetailsPresenter @Inject constructor( .sortedByDescending { it.date } .map { GradeDetailsItem(it, ViewType.ITEM) } - listOf(GradeDetailsItem(GradeDetailsHeader( - subject = subject, - average = average, - pointsSum = points, - grades = subItems - ).apply { - newGrades = grades.filter { grade -> !grade.isRead }.size - }, ViewType.HEADER)) + if (preferencesRepository.isGradeExpandable) emptyList() else subItems + val gradeDetailsItems = listOf( + GradeDetailsItem( + GradeDetailsHeader( + subject = subject, + average = average, + pointsSum = points, + grades = subItems + ).apply { + newGrades = grades.filter { grade -> !grade.isRead }.size + }, ViewType.HEADER + ) + ) + + if (preferencesRepository.gradeExpandMode == GradeExpandMode.ALWAYS_EXPANDED) { + gradeDetailsItems + subItems + } else { + gradeDetailsItems + } }.flatten() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsView.kt index e71fcc3c8..556332290 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsView.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.ui.modules.grade.details import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.ui.modules.grade.GradeExpandMode import io.github.wulkanowy.ui.base.BaseView interface GradeDetailsView : BaseView { @@ -9,7 +10,7 @@ interface GradeDetailsView : BaseView { fun initView() - fun updateData(data: List, isGradeExpandable: Boolean, gradeColorTheme: String) + fun updateData(data: List, expandMode: GradeExpandMode, gradeColorTheme: String) fun updateItem(item: Grade, position: Int) diff --git a/app/src/main/res/values/preferences_defaults.xml b/app/src/main/res/values/preferences_defaults.xml index df84d37d6..7fb3d5c05 100644 --- a/app/src/main/res/values/preferences_defaults.xml +++ b/app/src/main/res/values/preferences_defaults.xml @@ -4,7 +4,7 @@ true only_one_semester false - false + one false light vulcan diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index 1aba7d856..fef062dd1 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -5,7 +5,8 @@ app_theme dashboard_tiles grade_color_scheme - expand_grade + expand_grade + expand_grade_mode grade_average_mode grade_average_always_calc grade_statistics_list diff --git a/app/src/main/res/values/preferences_values.xml b/app/src/main/res/values/preferences_values.xml index bd3e8b47d..1d777bdb6 100644 --- a/app/src/main/res/values/preferences_values.xml +++ b/app/src/main/res/values/preferences_values.xml @@ -99,6 +99,17 @@ grade_color + + Up to 1 at once + Always expanded + Unlimited expansions + + + one + always + any + + Average of grades only from selected semester Average of averages from both semesters diff --git a/app/src/main/res/xml/scheme_preferences_appearance.xml b/app/src/main/res/xml/scheme_preferences_appearance.xml index b34fd417d..b2da0287f 100644 --- a/app/src/main/res/xml/scheme_preferences_appearance.xml +++ b/app/src/main/res/xml/scheme_preferences_appearance.xml @@ -50,11 +50,14 @@ app:iconSpaceReserved="false" app:key="@string/pref_key_grade_color_scheme" app:title="@string/pref_view_grade_color_scheme" /> - + app:key="@string/pref_key_expand_grade_mode" + app:title="@string/pref_view_expand_grade" + app:useSimpleSummaryProvider="true" /> Date: Tue, 26 Oct 2021 09:51:36 +0000 Subject: [PATCH 30/69] Bump about_libraries from 8.9.3 to 8.9.4 (#1604) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ad27aba07..6a563d2e6 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { kotlin_version = '1.5.31' - about_libraries = '8.9.3' + about_libraries = '8.9.4' hilt_version = "2.39.1" } repositories { From 621db49fbf860bad00910f906226e68fe6f02f32 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Oct 2021 09:52:55 +0000 Subject: [PATCH 31/69] Bump agcp from 1.6.1.200 to 1.6.1.300 (#1606) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6a563d2e6..c345e4c7d 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ buildscript { classpath 'com.android.tools.build:gradle:7.0.3' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.10' - classpath 'com.huawei.agconnect:agcp:1.6.1.200' + classpath 'com.huawei.agconnect:agcp:1.6.1.300' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.3.0" From de11644e9b6b599c45d204dd30a777ab40bc47ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Oct 2021 09:53:16 +0000 Subject: [PATCH 32/69] Bump agconnect-crash from 1.6.1.200 to 1.6.1.300 (#1605) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 421774062..94dfc1666 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -239,7 +239,7 @@ dependencies { playImplementation 'com.google.android.gms:play-services-ads:20.4.0' hmsImplementation 'com.huawei.hms:hianalytics:6.3.0.301' - hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.1.200' + hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.1.300' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 1d910f8d661ba218ab674447528d764071fa7ae1 Mon Sep 17 00:00:00 2001 From: Michael <5672750+mibac138@users.noreply.github.com> Date: Wed, 27 Oct 2021 10:07:04 +0200 Subject: [PATCH 33/69] Fix excuse button showing up despite no lessons available to excuse (#1607) This was happening when there was an unexcused lesson that you excused until the teacher sent a response (accepted or denied it) --- .../wulkanowy/ui/modules/attendance/AttendanceAdapter.kt | 2 +- .../wulkanowy/ui/modules/attendance/AttendancePresenter.kt | 5 ++--- .../java/io/github/wulkanowy/utils/AttendanceExtension.kt | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt index 6cee23963..bb4f70224 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt @@ -46,7 +46,7 @@ class AttendanceAdapter @Inject constructor() : onExcuseCheckboxSelect(item, checked) } - when (if (item.excuseStatus != null) SentExcuseStatus.valueOf(item.excuseStatus) else null) { + when (item.excuseStatus?.let { SentExcuseStatus.valueOf(it)}) { SentExcuseStatus.WAITING -> { attendanceItemExcuseInfo.setImageResource(R.drawable.ic_excuse_waiting) attendanceItemExcuseInfo.visibility = View.VISIBLE diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt index 03545b25b..5ed14f67e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt @@ -259,9 +259,8 @@ class AttendancePresenter @Inject constructor( showEmpty(filteredAttendance.isEmpty()) showErrorView(false) showContent(filteredAttendance.isNotEmpty()) - showExcuseButton(filteredAttendance.any { item -> - (!isParent && isVulcanExcusedFunctionEnabled) || (isParent && item.isExcusableOrNotExcused) - }) + val anyExcusables = filteredAttendance.any { it.isExcusableOrNotExcused } + showExcuseButton(anyExcusables && (isParent || isVulcanExcusedFunctionEnabled)) } analytics.logEvent( "load_data", diff --git a/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt index 479cc5188..b89ad57d3 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt @@ -17,7 +17,7 @@ private inline val AttendanceSummary.allAbsences: Double get() = absence.toDouble() + absenceExcused inline val Attendance.isExcusableOrNotExcused: Boolean - get() = excusable || ((absence || lateness) && !excused) + get() = (excusable || ((absence || lateness) && !excused)) && excuseStatus == null fun AttendanceSummary.calculatePercentage() = calculatePercentage(allPresences, allAbsences) From 26c749c219b8d35e302fd12cd82641015d1dba9a Mon Sep 17 00:00:00 2001 From: Michael <5672750+mibac138@users.noreply.github.com> Date: Fri, 29 Oct 2021 20:30:27 +0200 Subject: [PATCH 34/69] Fix about header layout to support long app names (for DEV builds) (#1602) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mikołaj Pich --- app/build.gradle | 2 +- app/src/main/res/layout/scrollable_header_about.xml | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 94dfc1666..1d5411955 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -74,7 +74,7 @@ android { buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\"" } debug { - resValue "string", "app_name", "Wulkanowy DEV " + defaultConfig.versionCode + resValue "string", "app_name", "Wulkanowy DEV" applicationIdSuffix ".dev" versionNameSuffix "-dev" ext.enableCrashlytics = project.hasProperty("enableFirebase") diff --git a/app/src/main/res/layout/scrollable_header_about.xml b/app/src/main/res/layout/scrollable_header_about.xml index e203d98d4..5a7669fdf 100644 --- a/app/src/main/res/layout/scrollable_header_about.xml +++ b/app/src/main/res/layout/scrollable_header_about.xml @@ -6,6 +6,7 @@ android:layout_height="wrap_content" android:minHeight="104dp" android:orientation="vertical" + android:paddingHorizontal="20dp" tools:context=".ui.modules.about.AboutAdapter"> + app:layout_constraintTop_toTopOf="@id/aboutScrollableHeaderIcon" + app:layout_constraintWidth_max="wrap" /> From 8ed8b5a33cdf10431ecb582f319e59c9f7d4ab66 Mon Sep 17 00:00:00 2001 From: Damian Czupryn <60961958+Daxxxis@users.noreply.github.com> Date: Sun, 31 Oct 2021 20:28:01 +0100 Subject: [PATCH 35/69] Error messages content wrap in error dialog (#1577) --- .../io/github/wulkanowy/ui/base/ErrorDialog.kt | 17 ++++++++--------- app/src/main/res/layout/dialog_error.xml | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorDialog.kt index 4ce977709..4c279d816 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/ErrorDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/ErrorDialog.kt @@ -11,6 +11,7 @@ import android.widget.Toast import android.widget.Toast.LENGTH_LONG import androidx.appcompat.app.AlertDialog import androidx.core.content.getSystemService +import androidx.core.view.isGone import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.databinding.DialogErrorBinding @@ -24,8 +25,6 @@ import io.github.wulkanowy.utils.openEmailClient import io.github.wulkanowy.utils.openInternetBrowser import okhttp3.internal.http2.StreamResetException import java.io.InterruptedIOException -import java.io.PrintWriter -import java.io.StringWriter import java.net.ConnectException import java.net.SocketTimeoutException import java.net.UnknownHostException @@ -64,26 +63,26 @@ class ErrorDialog : BaseDialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val stringWriter = StringWriter().apply { - error.printStackTrace(PrintWriter(this)) - } + val errorStacktrace = error.stackTraceToString() with(binding) { - errorDialogContent.text = stringWriter.toString() + errorDialogContent.text = errorStacktrace.replace(": ${error.localizedMessage}", "") with(errorDialogHorizontalScroll) { post { fullScroll(HorizontalScrollView.FOCUS_LEFT) } } errorDialogCopy.setOnClickListener { - val clip = ClipData.newPlainText("wulkanowy", stringWriter.toString()) + val clip = ClipData.newPlainText("Error details", errorStacktrace) activity?.getSystemService()?.setPrimaryClip(clip) Toast.makeText(context, R.string.all_copied, LENGTH_LONG).show() } errorDialogCancel.setOnClickListener { dismiss() } errorDialogReport.setOnClickListener { - openConfirmDialog { openEmailClient(stringWriter.toString()) } + openConfirmDialog { openEmailClient(errorStacktrace) } } - errorDialogMessage.text = resources.getString(error) + errorDialogHumanizedMessage.text = resources.getString(error) + errorDialogErrorMessage.text = error.localizedMessage + errorDialogErrorMessage.isGone = error.localizedMessage.isNullOrBlank() errorDialogReport.isEnabled = when (error) { is UnknownHostException, is InterruptedIOException, diff --git a/app/src/main/res/layout/dialog_error.xml b/app/src/main/res/layout/dialog_error.xml index a78790bce..75abcf0fd 100644 --- a/app/src/main/res/layout/dialog_error.xml +++ b/app/src/main/res/layout/dialog_error.xml @@ -4,7 +4,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:minWidth="300dp" - android:orientation="vertical"> + android:orientation="vertical" + tools:context=".ui.base.ErrorDialog"> + + From 36a570eeb0120a8211e62a6ab9a39a04bfbc6e49 Mon Sep 17 00:00:00 2001 From: Damian Czupryn <60961958+Daxxxis@users.noreply.github.com> Date: Mon, 1 Nov 2021 01:46:23 +0100 Subject: [PATCH 36/69] Allow selecting text in error dialog (#1615) --- app/src/main/res/layout/dialog_error.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout/dialog_error.xml b/app/src/main/res/layout/dialog_error.xml index 75abcf0fd..b52c99aca 100644 --- a/app/src/main/res/layout/dialog_error.xml +++ b/app/src/main/res/layout/dialog_error.xml @@ -32,6 +32,7 @@ android:layout_marginBottom="5dp" android:paddingHorizontal="20dp" android:paddingTop="10dp" + android:textIsSelectable="true" android:textColor="?android:textColorSecondary" tools:text="@tools:sample/lorem" /> From a62ed54d07728c4949464e7619c3eddf83b0e398 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 16:54:10 +0000 Subject: [PATCH 37/69] Bump hianalytics from 6.3.0.301 to 6.3.0.302 (#1621) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 1d5411955..b5eb455c1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -238,7 +238,7 @@ dependencies { playImplementation 'com.google.android.play:core-ktx:1.8.1' playImplementation 'com.google.android.gms:play-services-ads:20.4.0' - hmsImplementation 'com.huawei.hms:hianalytics:6.3.0.301' + hmsImplementation 'com.huawei.hms:hianalytics:6.3.0.302' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.1.300' releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" From 1a3d58011650afefd16e73633457120a4a293287 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 16:54:43 +0000 Subject: [PATCH 38/69] Bump firebase-bom from 28.4.2 to 29.0.0 (#1619) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index b5eb455c1..53814dae3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -230,7 +230,7 @@ dependencies { implementation 'me.xdrop:fuzzywuzzy:1.3.1' implementation 'com.fredporciuncula:flow-preferences:1.5.0' - playImplementation platform('com.google.firebase:firebase-bom:28.4.2') + playImplementation platform('com.google.firebase:firebase-bom:29.0.0') playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-crashlytics:' From 8be605629a4ace0411b5598eb4f098f650771d72 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 16:56:18 +0000 Subject: [PATCH 39/69] Bump firebase-crashlytics-gradle from 2.7.1 to 2.8.0 (#1623) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c345e4c7d..575293aa2 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.3.10' classpath 'com.huawei.agconnect:agcp:1.6.1.300' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.0' classpath "com.github.triplet.gradle:play-publisher:3.6.0" classpath "ru.cian:huawei-publish-gradle-plugin:1.3.0" classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3" From b7134221cb5d1d7b862c57512025685b93ea9b67 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 17:05:57 +0000 Subject: [PATCH 40/69] Bump hilt_version from 2.39.1 to 2.40 (#1617) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 575293aa2..99c4820e8 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { kotlin_version = '1.5.31' about_libraries = '8.9.4' - hilt_version = "2.39.1" + hilt_version = "2.40" } repositories { mavenCentral() From eea20ced57e2d06b79cc04c4a24711866b084c4a Mon Sep 17 00:00:00 2001 From: Damian Czupryn <60961958+Daxxxis@users.noreply.github.com> Date: Thu, 4 Nov 2021 03:06:54 +0100 Subject: [PATCH 41/69] Notifications settings reorganize and strings update (#1616) --- app/src/main/res/values/strings.xml | 7 ++++--- .../res/xml/scheme_preferences_notifications.xml | 16 ++++++++++------ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 13030366a..1e88ee1ad 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -611,13 +611,13 @@ - App appearance & behavior + App Default view Calculated average options Force average calculation by app Show presence Theme - Expand grades + Grades expanding Mark current lesson Show groups next to subjects Show chart list in class grades @@ -627,6 +627,7 @@ Language Notifications + Other Show notifications Show upcoming lesson notifications Make upcoming lesson notification persistent @@ -689,7 +690,7 @@ Automatic update, synchronization interval Plus and minus values, average calculation Advanced - App version, contributors, social portals, licenses + App version, contributors, social portals Displaying advertisements, project support diff --git a/app/src/main/res/xml/scheme_preferences_notifications.xml b/app/src/main/res/xml/scheme_preferences_notifications.xml index 78e91cf06..442581bfd 100644 --- a/app/src/main/res/xml/scheme_preferences_notifications.xml +++ b/app/src/main/res/xml/scheme_preferences_notifications.xml @@ -22,18 +22,22 @@ app:singleLineTitle="false" app:summary="@string/pref_notify_upcoming_lessons_persistent_summary" app:title="@string/pref_notify_upcoming_lessons_persistent_switch" /> - + + + Date: Thu, 4 Nov 2021 03:09:47 +0100 Subject: [PATCH 42/69] Add support for user ca in debug flavor(#1624) --- app/src/main/AndroidManifest.xml | 1 + app/src/main/res/xml/network_security_config.xml | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 app/src/main/res/xml/network_security_config.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 810d469f4..e43a0ec08 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -38,6 +38,7 @@ android:allowBackup="false" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:networkSecurityConfig="@xml/network_security_config" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="false" android:theme="@style/WulkanowyTheme" diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 000000000..84ff05a04 --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,9 @@ + + + + + + + + + From 4401df6203bb408e1aa8cb63cf0976fe4a6f2d01 Mon Sep 17 00:00:00 2001 From: Michael <5672750+mibac138@users.noreply.github.com> Date: Sat, 6 Nov 2021 19:07:26 +0100 Subject: [PATCH 43/69] Migrate from ViewPager to ViewPager2 (#1601) --- .../ui/base/BaseFragmentPagerAdapter.kt | 37 ++++++++-------- .../ui/modules/grade/GradeFragment.kt | 42 ++++++++++++------ .../ui/modules/grade/GradePresenter.kt | 1 - .../ui/modules/login/LoginActivity.kt | 42 +++++++++++------- .../ui/modules/message/MessageFragment.kt | 40 ++++++++++++----- .../ui/modules/message/MessagePresenter.kt | 1 - .../SchoolAndTeachersFragment.kt | 44 ++++++++++++++----- .../SchoolAndTeachersPresenter.kt | 1 - .../ui/widgets/SwipeDisabledViewPager.kt | 19 -------- .../wulkanowy/utils/ViewPagerExtension.kt | 8 ++-- app/src/main/res/layout/activity_login.xml | 2 +- app/src/main/res/layout/fragment_grade.xml | 2 +- app/src/main/res/layout/fragment_message.xml | 2 +- .../res/layout/fragment_schoolandteachers.xml | 2 +- 14 files changed, 142 insertions(+), 101 deletions(-) delete mode 100644 app/src/main/java/io/github/wulkanowy/ui/widgets/SwipeDisabledViewPager.kt diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragmentPagerAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragmentPagerAdapter.kt index bd735535d..6bca87f15 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragmentPagerAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragmentPagerAdapter.kt @@ -2,32 +2,33 @@ package io.github.wulkanowy.ui.base import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentPagerAdapter +import androidx.lifecycle.Lifecycle +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.google.android.material.tabs.TabLayout +import com.google.android.material.tabs.TabLayoutMediator -//TODO Use ViewPager2 -class BaseFragmentPagerAdapter(private val fragmentManager: FragmentManager) : - FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { +class BaseFragmentPagerAdapter( + private val fragmentManager: FragmentManager, + private val pagesCount: Int, + lifecycle: Lifecycle, +) : FragmentStateAdapter(fragmentManager, lifecycle), TabLayoutMediator.TabConfigurationStrategy { - private val pages = mutableMapOf() + lateinit var itemFactory: (position: Int) -> Fragment + + var titleFactory: (position: Int) -> String? = { "" } var containerId = 0 fun getFragmentInstance(position: Int): Fragment? { require(containerId != 0) { "Container id is 0" } - return fragmentManager.findFragmentByTag("android:switcher:$containerId:$position") + return fragmentManager.findFragmentByTag("f$position") } - fun addFragments(fragments: List) { - fragments.forEach { pages[it] = null } + override fun createFragment(position: Int): Fragment = itemFactory(position) + + override fun getItemCount() = pagesCount + + override fun onConfigureTab(tab: TabLayout.Tab, position: Int) { + tab.text = titleFactory(position) } - - fun addFragmentsWithTitle(pages: Map) { - this.pages.putAll(pages) - } - - override fun getItem(position: Int) = pages.keys.elementAt(position) - - override fun getCount() = pages.size - - override fun getPageTitle(position: Int) = pages.values.elementAt(position) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt index b3ef3037a..f9ecb681b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt @@ -8,6 +8,7 @@ import android.view.View import android.view.View.INVISIBLE import android.view.View.VISIBLE import androidx.appcompat.app.AlertDialog +import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Semester @@ -29,7 +30,13 @@ class GradeFragment : BaseFragment(R.layout.fragment_grade @Inject lateinit var presenter: GradePresenter - private val pagerAdapter by lazy { BaseFragmentPagerAdapter(childFragmentManager) } + private val pagerAdapter by lazy { + BaseFragmentPagerAdapter( + fragmentManager = childFragmentManager, + pagesCount = 3, + lifecycle = lifecycle, + ) + } private var semesterSwitchMenu: MenuItem? = null @@ -62,25 +69,34 @@ class GradeFragment : BaseFragment(R.layout.fragment_grade } override fun initView() { - with(pagerAdapter) { - containerId = binding.gradeViewPager.id - addFragmentsWithTitle( - mapOf( - GradeDetailsFragment.newInstance() to getString(R.string.all_details), - GradeSummaryFragment.newInstance() to getString(R.string.grade_menu_summary), - GradeStatisticsFragment.newInstance() to getString(R.string.grade_menu_statistics) - ) - ) - } - with(binding.gradeViewPager) { adapter = pagerAdapter offscreenPageLimit = 3 setOnSelectPageListener(presenter::onPageSelected) } + with(pagerAdapter) { + containerId = binding.gradeViewPager.id + titleFactory = { + when (it) { + 0 -> getString(R.string.all_details) + 1 -> getString(R.string.grade_menu_summary) + 2 -> getString(R.string.grade_menu_statistics) + else -> throw IllegalStateException() + } + } + itemFactory = { + when (it) { + 0 -> GradeDetailsFragment.newInstance() + 1 -> GradeSummaryFragment.newInstance() + 2 -> GradeStatisticsFragment.newInstance() + else -> throw IllegalStateException() + } + } + TabLayoutMediator(binding.gradeTabLayout, binding.gradeViewPager, this).attach() + } + with(binding.gradeTabLayout) { - setupWithViewPager(binding.gradeViewPager) setElevationCompat(context.dpToPx(4f)) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradePresenter.kt index 504c730de..76e88bcdb 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradePresenter.kt @@ -101,7 +101,6 @@ class GradePresenter @Inject constructor( private fun loadData() { flowWithResource { val student = studentRepository.getCurrentStudent() - delay(200) semesterRepository.getSemesters(student, refreshOnNoCurrent = true) }.onEach { when (it.status) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt index 10f6c0737..f7bab526c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt @@ -24,7 +24,13 @@ class LoginActivity : BaseActivity(), Logi @Inject override lateinit var presenter: LoginPresenter - private val loginAdapter = BaseFragmentPagerAdapter(supportFragmentManager) + private val pagerAdapter by lazy { + BaseFragmentPagerAdapter( + fragmentManager = supportFragmentManager, + pagesCount = 5, + lifecycle = lifecycle, + ) + } @Inject lateinit var updateHelper: UpdateHelper @@ -65,24 +71,26 @@ class LoginActivity : BaseActivity(), Logi setDisplayShowTitleEnabled(false) } - with(loginAdapter) { - containerId = binding.loginViewpager.id - addFragments( - listOf( - LoginFormFragment.newInstance(), - LoginSymbolFragment.newInstance(), - LoginStudentSelectFragment.newInstance(), - LoginAdvancedFragment.newInstance(), - LoginRecoverFragment.newInstance() - ) - ) - } - with(binding.loginViewpager) { offscreenPageLimit = 2 - adapter = loginAdapter + adapter = pagerAdapter + isUserInputEnabled = false setOnSelectPageListener(presenter::onViewSelected) } + + with(pagerAdapter) { + containerId = binding.loginViewpager.id + itemFactory = { + when (it) { + 0 -> LoginFormFragment.newInstance() + 1 -> LoginSymbolFragment.newInstance() + 2 -> LoginStudentSelectFragment.newInstance() + 3 -> LoginAdvancedFragment.newInstance() + 4 -> LoginRecoverFragment.newInstance() + else -> throw IllegalStateException() + } + } + } } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -103,12 +111,12 @@ class LoginActivity : BaseActivity(), Logi } override fun notifyInitSymbolFragment(loginData: Triple) { - (loginAdapter.getFragmentInstance(1) as? LoginSymbolFragment) + (pagerAdapter.getFragmentInstance(1) as? LoginSymbolFragment) ?.onParentInitSymbolFragment(loginData) } override fun notifyInitStudentSelectFragment(studentsWithSemesters: List) { - (loginAdapter.getFragmentInstance(2) as? LoginStudentSelectFragment) + (pagerAdapter.getFragmentInstance(2) as? LoginStudentSelectFragment) ?.onParentInitStudentSelectFragment(studentsWithSemesters) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt index 72fc627f8..51076c649 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.View import android.view.View.INVISIBLE import android.view.View.VISIBLE +import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED @@ -26,7 +27,13 @@ class MessageFragment : BaseFragment(R.layout.fragment_m @Inject lateinit var presenter: MessagePresenter - private val pagerAdapter by lazy { BaseFragmentPagerAdapter(childFragmentManager) } + private val pagerAdapter by lazy { + BaseFragmentPagerAdapter( + fragmentManager = childFragmentManager, + pagesCount = 3, + lifecycle = lifecycle, + ) + } companion object { fun newInstance() = MessageFragment() @@ -43,23 +50,34 @@ class MessageFragment : BaseFragment(R.layout.fragment_m } override fun initView() { - with(pagerAdapter) { - containerId = binding.messageViewPager.id - addFragmentsWithTitle(mapOf( - MessageTabFragment.newInstance(RECEIVED) to getString(R.string.message_inbox), - MessageTabFragment.newInstance(SENT) to getString(R.string.message_sent), - MessageTabFragment.newInstance(TRASHED) to getString(R.string.message_trash) - )) - } - with(binding.messageViewPager) { adapter = pagerAdapter offscreenPageLimit = 2 setOnSelectPageListener(presenter::onPageSelected) } + with(pagerAdapter) { + containerId = binding.messageViewPager.id + titleFactory = { + when (it) { + 0 -> getString(R.string.message_inbox) + 1 -> getString(R.string.message_sent) + 2 -> getString(R.string.message_trash) + else -> throw IllegalStateException() + } + } + itemFactory = { + when (it) { + 0 -> MessageTabFragment.newInstance(RECEIVED) + 1 -> MessageTabFragment.newInstance(SENT) + 2 -> MessageTabFragment.newInstance(TRASHED) + else -> throw IllegalStateException() + } + } + TabLayoutMediator(binding.messageTabLayout, binding.messageViewPager, this).attach() + } + with(binding.messageTabLayout) { - setupWithViewPager(binding.messageViewPager) setElevationCompat(context.dpToPx(4f)) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt index be6940520..9e19517bd 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt @@ -16,7 +16,6 @@ class MessagePresenter @Inject constructor( override fun onAttachView(view: MessageView) { super.onAttachView(view) presenterScope.launch { - delay(150) view.initView() Timber.i("Message view was initialized") loadData() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt index c1c569611..9892b77ad 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.View import android.view.View.INVISIBLE import android.view.View.VISIBLE +import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.databinding.FragmentSchoolandteachersBinding @@ -24,7 +25,13 @@ class SchoolAndTeachersFragment : @Inject lateinit var presenter: SchoolAndTeachersPresenter - private val pagerAdapter by lazy { BaseFragmentPagerAdapter(childFragmentManager) } + private val pagerAdapter by lazy { + BaseFragmentPagerAdapter( + fragmentManager = childFragmentManager, + pagesCount = 2, + lifecycle = lifecycle, + ) + } companion object { fun newInstance() = SchoolAndTeachersFragment() @@ -41,22 +48,36 @@ class SchoolAndTeachersFragment : } override fun initView() { - with(pagerAdapter) { - containerId = binding.schoolandteachersViewPager.id - addFragmentsWithTitle(mapOf( - SchoolFragment.newInstance() to getString(R.string.school_title), - TeacherFragment.newInstance() to getString(R.string.teachers_title) - )) - } - with(binding.schoolandteachersViewPager) { adapter = pagerAdapter offscreenPageLimit = 2 setOnSelectPageListener(presenter::onPageSelected) } + with(pagerAdapter) { + containerId = binding.schoolandteachersViewPager.id + titleFactory = { + when (it) { + 0 -> getString(R.string.school_title) + 1 -> getString(R.string.teachers_title) + else -> throw IllegalStateException() + } + } + itemFactory = { + when (it) { + 0 -> SchoolFragment.newInstance() + 1 -> TeacherFragment.newInstance() + else -> throw IllegalStateException() + } + } + TabLayoutMediator( + binding.schoolandteachersTabLayout, + binding.schoolandteachersViewPager, + this + ).attach() + } + with(binding.schoolandteachersTabLayout) { - setupWithViewPager(binding.schoolandteachersViewPager) setElevationCompat(context.dpToPx(4f)) } } @@ -77,7 +98,8 @@ class SchoolAndTeachersFragment : } override fun notifyChildLoadData(index: Int, forceRefresh: Boolean) { - (pagerAdapter.getFragmentInstance(index) as? SchoolAndTeachersChildView)?.onParentLoadData(forceRefresh) + (pagerAdapter.getFragmentInstance(index) as? SchoolAndTeachersChildView) + ?.onParentLoadData(forceRefresh) } override fun onDestroyView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt index c14272b9d..43823d6b4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersPresenter.kt @@ -16,7 +16,6 @@ class SchoolAndTeachersPresenter @Inject constructor( override fun onAttachView(view: SchoolAndTeachersView) { super.onAttachView(view) presenterScope.launch { - delay(150) view.initView() Timber.i("Message view was initialized") loadData() diff --git a/app/src/main/java/io/github/wulkanowy/ui/widgets/SwipeDisabledViewPager.kt b/app/src/main/java/io/github/wulkanowy/ui/widgets/SwipeDisabledViewPager.kt deleted file mode 100644 index eb5cae4f3..000000000 --- a/app/src/main/java/io/github/wulkanowy/ui/widgets/SwipeDisabledViewPager.kt +++ /dev/null @@ -1,19 +0,0 @@ -package io.github.wulkanowy.ui.widgets - -import android.annotation.SuppressLint -import android.content.Context -import android.util.AttributeSet -import android.view.MotionEvent -import androidx.viewpager.widget.ViewPager - -class SwipeDisabledViewPager : ViewPager { - - constructor(context: Context) : super(context) - - constructor(context: Context, attr: AttributeSet) : super(context, attr) - - @SuppressLint("ClickableViewAccessibility") - override fun onTouchEvent(ev: MotionEvent) = false - - override fun onInterceptTouchEvent(ev: MotionEvent) = false -} diff --git a/app/src/main/java/io/github/wulkanowy/utils/ViewPagerExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/ViewPagerExtension.kt index 6a5ad880a..700ac2f1d 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/ViewPagerExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/ViewPagerExtension.kt @@ -1,13 +1,11 @@ package io.github.wulkanowy.utils -import androidx.viewpager.widget.ViewPager +import androidx.viewpager2.widget.ViewPager2 -inline fun ViewPager.setOnSelectPageListener(crossinline selectListener: (position: Int) -> Unit) { - addOnPageChangeListener(object : ViewPager.OnPageChangeListener { +inline fun ViewPager2.setOnSelectPageListener(crossinline selectListener: (position: Int) -> Unit) { + registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { selectListener(position) } - override fun onPageScrollStateChanged(state: Int) {} - override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {} }) } diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index e55ea8b95..1d5b5280d 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -10,7 +10,7 @@ android:layout_height="wrap_content" android:background="@android:color/transparent" /> - diff --git a/app/src/main/res/layout/fragment_grade.xml b/app/src/main/res/layout/fragment_grade.xml index ed0447fb5..989929d4c 100644 --- a/app/src/main/res/layout/fragment_grade.xml +++ b/app/src/main/res/layout/fragment_grade.xml @@ -17,7 +17,7 @@ tools:ignore="UnusedAttribute" tools:visibility="visible" /> - - - Date: Sat, 6 Nov 2021 22:21:34 +0100 Subject: [PATCH 44/69] Add timetable changes, attendance notifications and refactor notification deeplinks (#1547) --- .../43.json | 2408 +++++++++++++++++ .../github/wulkanowy/data/db/AppDatabase.kt | 6 +- .../wulkanowy/data/db/dao/AttendanceDao.kt | 9 +- .../wulkanowy/data/db/entities/Attendance.kt | 3 + .../wulkanowy/data/db/entities/Timetable.kt | 3 + .../data/db/migrations/Migration43.kt | 12 + .../wulkanowy/data/pojos/NotificationData.kt | 43 +- .../data/repositories/AttendanceRepository.kt | 19 +- .../data/repositories/TimetableRepository.kt | 20 +- .../wulkanowy/services/ServicesModule.kt | 10 + .../alarm/TimetableNotificationReceiver.kt | 17 +- .../TimetableNotificationSchedulerHelper.kt | 4 +- .../services/shortcuts/ShortcutsHelper.kt | 90 + .../sync/channels/NewAttendanceChannel.kt | 36 + .../sync/channels/TimetableChangeChannel.kt | 36 + .../notifications/AppNotificationManager.kt | 220 +- .../ChangeTimetableNotification.kt | 125 + .../NewAttendanceNotification.kt | 55 + .../NewConferenceNotification.kt | 48 +- .../sync/notifications/NewExamNotification.kt | 48 +- .../notifications/NewGradeNotification.kt | 98 +- .../notifications/NewHomeworkNotification.kt | 46 +- .../NewLuckyNumberNotification.kt | 34 +- .../notifications/NewMessageNotification.kt | 38 +- .../sync/notifications/NewNoteNotification.kt | 48 +- .../NewSchoolAnnouncementNotification.kt | 57 +- .../sync/notifications/NotificationType.kt | 85 +- .../services/sync/works/AttendanceWork.kt | 30 +- .../services/sync/works/TimetableWork.kt | 31 +- .../wulkanowy/ui/modules/Destination.kt | 132 + .../modules/attendance/AttendanceAdapter.kt | 4 +- .../ui/modules/attendance/AttendanceDialog.kt | 4 +- .../NotificationDebugPresenter.kt | 12 + .../debug/notification/mock/attendance.kt | 35 + .../debug/notification/mock/timetable.kt | 39 + .../LoginStudentSelectFragment.kt | 9 +- .../LuckyNumberWidgetProvider.kt | 59 +- .../wulkanowy/ui/modules/main/MainActivity.kt | 191 +- .../ui/modules/main/MainPresenter.kt | 61 +- .../wulkanowy/ui/modules/main/MainView.kt | 32 +- .../NotificationsCenterAdapter.kt | 19 +- .../NotificationsCenterFragment.kt | 6 +- .../ui/modules/settings/SettingsFragment.kt | 14 +- .../ui/modules/settings/SettingsView.kt | 5 + .../TimetableWidgetProvider.kt | 8 +- .../wulkanowy/utils/AttendanceExtension.kt | 2 +- .../wulkanowy/utils/ContextExtension.kt | 4 + .../utils/FragNavControlerExtension.kt | 13 +- .../wulkanowy/utils/FragmentExtension.kt | 44 - app/src/main/res/values/strings.xml | 34 + .../ui/modules/main/MainPresenterTest.kt | 14 +- 51 files changed, 3819 insertions(+), 601 deletions(-) create mode 100644 app/schemas/io.github.wulkanowy.data.db.AppDatabase/43.json create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration43.kt create mode 100644 app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt create mode 100644 app/src/main/java/io/github/wulkanowy/services/sync/channels/NewAttendanceChannel.kt create mode 100644 app/src/main/java/io/github/wulkanowy/services/sync/channels/TimetableChangeChannel.kt create mode 100644 app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt create mode 100644 app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewAttendanceNotification.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/attendance.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/timetable.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsView.kt delete mode 100644 app/src/main/java/io/github/wulkanowy/utils/FragmentExtension.kt diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/43.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/43.json new file mode 100644 index 000000000..22c0d8125 --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/43.json @@ -0,0 +1,2408 @@ +{ + "formatVersion": 1, + "database": { + "version": 43, + "identityHash": "66946510bb620ae82686a5a1a31aba18", + "entities": [ + { + "tableName": "Students", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrapper_base_url` TEXT NOT NULL, `mobile_base_url` TEXT NOT NULL, `login_type` TEXT NOT NULL, `login_mode` TEXT NOT NULL, `certificate_key` TEXT NOT NULL, `private_key` TEXT NOT NULL, `is_parent` INTEGER NOT NULL, `email` TEXT NOT NULL, `password` TEXT NOT NULL, `symbol` TEXT NOT NULL, `student_id` INTEGER NOT NULL, `user_login_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `student_name` TEXT NOT NULL, `school_id` TEXT NOT NULL, `school_short` TEXT NOT NULL, `school_name` TEXT NOT NULL, `class_name` TEXT NOT NULL, `class_id` INTEGER NOT NULL, `is_current` INTEGER NOT NULL, `registration_date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `nick` TEXT NOT NULL, `avatar_color` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "scrapperBaseUrl", + "columnName": "scrapper_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mobileBaseUrl", + "columnName": "mobile_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginType", + "columnName": "login_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginMode", + "columnName": "login_mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificateKey", + "columnName": "certificate_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "private_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isParent", + "columnName": "is_parent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "user_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "student_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolSymbol", + "columnName": "school_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "school_short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolName", + "columnName": "school_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "className", + "columnName": "class_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCurrent", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registrationDate", + "columnName": "registration_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nick", + "columnName": "nick", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarColor", + "columnName": "avatar_color", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Students_email_symbol_student_id_school_id_class_id", + "unique": true, + "columnNames": [ + "email", + "symbol", + "student_id", + "school_id", + "class_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryName", + "columnName": "diary_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolYear", + "columnName": "school_year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterName", + "columnName": "semester_name", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "semester_id" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `semester_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Exams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Timetable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `number` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `subjectOld` TEXT NOT NULL, `group` TEXT NOT NULL, `room` TEXT NOT NULL, `roomOld` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacherOld` TEXT NOT NULL, `info` TEXT NOT NULL, `student_plan` INTEGER NOT NULL, `changes` INTEGER NOT NULL, `canceled` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectOld", + "columnName": "subjectOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "room", + "columnName": "room", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roomOld", + "columnName": "roomOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherOld", + "columnName": "teacherOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isStudentPlan", + "columnName": "student_plan", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "changes", + "columnName": "changes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canceled", + "columnName": "canceled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Attendance", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `time_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `excusable` INTEGER NOT NULL, `excuse_status` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeId", + "columnName": "time_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excusable", + "columnName": "excusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excuseStatus", + "columnName": "excuse_status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AttendanceSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `subject_id` INTEGER NOT NULL, `month` INTEGER NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `absence_excused` INTEGER NOT NULL, `absence_for_school_reasons` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `lateness_excused` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subject_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "month", + "columnName": "month", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceExcused", + "columnName": "absence_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceForSchoolReasons", + "columnName": "absence_for_school_reasons", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latenessExcused", + "columnName": "lateness_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Grades", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `entry` TEXT NOT NULL, `value` REAL NOT NULL, `modifier` REAL NOT NULL, `comment` TEXT NOT NULL, `color` TEXT NOT NULL, `grade_symbol` TEXT NOT NULL, `description` TEXT NOT NULL, `weight` TEXT NOT NULL, `weightValue` REAL NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entry", + "columnName": "entry", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "modifier", + "columnName": "modifier", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gradeSymbol", + "columnName": "grade_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weightValue", + "columnName": "weightValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `subject` TEXT NOT NULL, `predicted_grade` TEXT NOT NULL, `final_grade` TEXT NOT NULL, `proposed_points` TEXT NOT NULL, `final_points` TEXT NOT NULL, `points_sum` TEXT NOT NULL, `average` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_predicted_grade_notified` INTEGER NOT NULL, `is_final_grade_notified` INTEGER NOT NULL, `predicted_grade_last_change` INTEGER NOT NULL, `final_grade_last_change` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "predictedGrade", + "columnName": "predicted_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalGrade", + "columnName": "final_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "proposedPoints", + "columnName": "proposed_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalPoints", + "columnName": "final_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pointsSum", + "columnName": "points_sum", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "average", + "columnName": "average", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPredictedGradeNotified", + "columnName": "is_predicted_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFinalGradeNotified", + "columnName": "is_final_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "predictedGradeLastChange", + "columnName": "predicted_grade_last_change", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "finalGradeLastChange", + "columnName": "final_grade_last_change", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradePartialStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `class_average` TEXT NOT NULL, `student_average` TEXT NOT NULL, `class_amounts` TEXT NOT NULL, `student_amounts` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAverage", + "columnName": "class_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAverage", + "columnName": "student_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAmounts", + "columnName": "class_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAmounts", + "columnName": "student_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesPointsStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `others` REAL NOT NULL, `student` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "others", + "columnName": "others", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "student", + "columnName": "student", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradeSemesterStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `amounts` TEXT NOT NULL, `student_grade` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amounts", + "columnName": "amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentGrade", + "columnName": "student_grade", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `recipient_name` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `removed` INTEGER NOT NULL, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `unread_by` INTEGER NOT NULL, `read_by` INTEGER NOT NULL, `content` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipient", + "columnName": "recipient_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "removed", + "columnName": "removed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasAttachments", + "columnName": "has_attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`real_id` INTEGER NOT NULL, `message_id` INTEGER NOT NULL, `one_drive_id` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`real_id`))", + "fields": [ + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "oneDriveId", + "columnName": "one_drive_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "real_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `category` TEXT NOT NULL, `category_type` INTEGER NOT NULL, `is_points_show` INTEGER NOT NULL, `points` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryType", + "columnName": "category_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPointsShow", + "columnName": "is_points_show", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Homework", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `attachments` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_done` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL, `is_added_by_user` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "is_done", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Subjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "LuckyNumbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `lucky_number` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "luckyNumber", + "columnName": "lucky_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CompletedLesson", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `topic` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `substitution` TEXT NOT NULL, `absence` TEXT NOT NULL, `resources` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "topic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "substitution", + "columnName": "substitution", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "resources", + "columnName": "resources", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ReportingUnits", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `short` TEXT NOT NULL, `sender_id` INTEGER NOT NULL, `sender_name` TEXT NOT NULL, `roles` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderId", + "columnName": "sender_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "senderName", + "columnName": "sender_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roles", + "columnName": "roles", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `real_id` TEXT NOT NULL, `name` TEXT NOT NULL, `real_name` TEXT NOT NULL, `login_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `role` INTEGER NOT NULL, `hash` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "realName", + "columnName": "real_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginId", + "columnName": "login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "role", + "columnName": "role", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MobileDevices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Teachers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `short_name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "School", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `contact` TEXT NOT NULL, `headmaster` TEXT NOT NULL, `pedagogue` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contact", + "columnName": "contact", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headmaster", + "columnName": "headmaster", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pedagogue", + "columnName": "pedagogue", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Conferences", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `subject` TEXT NOT NULL, `agenda` TEXT NOT NULL, `present_on_conference` TEXT NOT NULL, `conference_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agenda", + "columnName": "agenda", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presentOnConference", + "columnName": "present_on_conference", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conference_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableAdditional", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StudentInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `full_name` TEXT NOT NULL, `first_name` TEXT NOT NULL, `second_name` TEXT NOT NULL, `surname` TEXT NOT NULL, `birth_date` INTEGER NOT NULL, `birth_place` TEXT NOT NULL, `gender` TEXT NOT NULL, `has_polish_citizenship` INTEGER NOT NULL, `family_name` TEXT NOT NULL, `parents_names` TEXT NOT NULL, `address` TEXT NOT NULL, `registered_address` TEXT NOT NULL, `correspondence_address` TEXT NOT NULL, `phone_number` TEXT NOT NULL, `cell_phone_number` TEXT NOT NULL, `email` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `first_guardian_full_name` TEXT, `first_guardian_kinship` TEXT, `first_guardian_address` TEXT, `first_guardian_phones` TEXT, `first_guardian_email` TEXT, `second_guardian_full_name` TEXT, `second_guardian_kinship` TEXT, `second_guardian_address` TEXT, `second_guardian_phones` TEXT, `second_guardian_email` TEXT)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "full_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondName", + "columnName": "second_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "surname", + "columnName": "surname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "birthDate", + "columnName": "birth_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthPlace", + "columnName": "birth_place", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gender", + "columnName": "gender", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPolishCitizenship", + "columnName": "has_polish_citizenship", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "familyName", + "columnName": "family_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentsNames", + "columnName": "parents_names", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "registeredAddress", + "columnName": "registered_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "correspondenceAddress", + "columnName": "correspondence_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cellPhoneNumber", + "columnName": "cell_phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstGuardian.fullName", + "columnName": "first_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.kinship", + "columnName": "first_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.address", + "columnName": "first_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.phones", + "columnName": "first_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.email", + "columnName": "first_guardian_email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.fullName", + "columnName": "second_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.kinship", + "columnName": "second_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.address", + "columnName": "second_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.phones", + "columnName": "second_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.email", + "columnName": "second_guardian_email", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableHeaders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SchoolAnnouncements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `type` TEXT NOT NULL, `date` INTEGER NOT NULL, `data` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AdminMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `version_name` INTEGER, `version_max` INTEGER, `target_register_host` TEXT, `target_flavor` TEXT, `destination_url` TEXT, `priority` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "versionMin", + "columnName": "version_name", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMax", + "columnName": "version_max", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "targetRegisterHost", + "columnName": "target_register_host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "targetFlavor", + "columnName": "target_flavor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "destinationUrl", + "columnName": "destination_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '66946510bb620ae82686a5a1a31aba18')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt index d96856068..7e6d609f7 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt @@ -102,6 +102,7 @@ import io.github.wulkanowy.data.db.migrations.Migration4 import io.github.wulkanowy.data.db.migrations.Migration40 import io.github.wulkanowy.data.db.migrations.Migration41 import io.github.wulkanowy.data.db.migrations.Migration42 +import io.github.wulkanowy.data.db.migrations.Migration43 import io.github.wulkanowy.data.db.migrations.Migration5 import io.github.wulkanowy.data.db.migrations.Migration6 import io.github.wulkanowy.data.db.migrations.Migration7 @@ -151,7 +152,7 @@ import javax.inject.Singleton abstract class AppDatabase : RoomDatabase() { companion object { - const val VERSION_SCHEMA = 42 + const val VERSION_SCHEMA = 43 fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( Migration2(), @@ -194,7 +195,8 @@ abstract class AppDatabase : RoomDatabase() { Migration39(), Migration40(), Migration41(sharedPrefProvider), - Migration42() + Migration42(), + Migration43() ) fun newInstance( diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/AttendanceDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/AttendanceDao.kt index 8ef3fd446..c6c255a1f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/AttendanceDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/AttendanceDao.kt @@ -11,6 +11,11 @@ import javax.inject.Singleton @Dao interface AttendanceDao : BaseDao { - @Query("SELECT * FROM Attendance WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :from AND date <= :end") - fun loadAll(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): Flow> + @Query("SELECT * FROM Attendance WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :start AND date <= :end") + fun loadAll( + diaryId: Int, + studentId: Int, + start: LocalDate, + end: LocalDate + ): Flow> } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Attendance.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Attendance.kt index f141d5d52..b40dd52e5 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Attendance.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Attendance.kt @@ -47,4 +47,7 @@ data class Attendance( @PrimaryKey(autoGenerate = true) var id: Long = 0 + + @ColumnInfo(name = "is_notified") + var isNotified: Boolean = true } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Timetable.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Timetable.kt index 1bf159efd..29b3737bc 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Timetable.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Timetable.kt @@ -50,4 +50,7 @@ data class Timetable( @PrimaryKey(autoGenerate = true) var id: Long = 0 + + @ColumnInfo(name = "is_notified") + var isNotified: Boolean = true } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration43.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration43.kt new file mode 100644 index 000000000..68c2834d6 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration43.kt @@ -0,0 +1,12 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration43 : Migration(42, 43) { + + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE Timetable ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1") + database.execSQL("ALTER TABLE Attendance ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1") + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/NotificationData.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/NotificationData.kt index 0b4603ef7..0748ba647 100644 --- a/app/src/main/java/io/github/wulkanowy/data/pojos/NotificationData.kt +++ b/app/src/main/java/io/github/wulkanowy/data/pojos/NotificationData.kt @@ -1,36 +1,19 @@ package io.github.wulkanowy.data.pojos -import androidx.annotation.DrawableRes -import androidx.annotation.PluralsRes -import androidx.annotation.StringRes +import android.content.Intent import io.github.wulkanowy.services.sync.notifications.NotificationType -import io.github.wulkanowy.ui.modules.main.MainView -sealed interface NotificationData { +data class NotificationData( + val intentToStart: Intent, + val title: String, + val content: String +) + +data class GroupNotificationData( + val notificationDataList: List, + val title: String, + val content: String, + val intentToStart: Intent, val type: NotificationType - val startMenu: MainView.Section - val icon: Int - val titleStringRes: Int - val contentStringRes: Int -} +) -data class MultipleNotificationsData( - override val type: NotificationType, - override val startMenu: MainView.Section, - @DrawableRes override val icon: Int, - @PluralsRes override val titleStringRes: Int, - @PluralsRes override val contentStringRes: Int, - - @PluralsRes val summaryStringRes: Int, - val lines: List, -) : NotificationData - -data class OneNotificationData( - override val type: NotificationType, - override val startMenu: MainView.Section, - @DrawableRes override val icon: Int, - @StringRes override val titleStringRes: Int, - @StringRes override val contentStringRes: Int, - - val contentValues: List, -) : NotificationData diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt index d21ffb5fb..ec9198175 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceRepository.kt @@ -14,6 +14,7 @@ import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.networkBoundResource import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.uniqueSubtract +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex import java.time.LocalDate import java.time.LocalDateTime @@ -38,6 +39,7 @@ class AttendanceRepository @Inject constructor( start: LocalDate, end: LocalDate, forceRefresh: Boolean, + notify: Boolean = false, ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { @@ -56,13 +58,28 @@ class AttendanceRepository @Inject constructor( }, saveFetchResult = { old, new -> attendanceDb.deleteAll(old uniqueSubtract new) - attendanceDb.insertAll(new uniqueSubtract old) + val attendanceToAdd = (new uniqueSubtract old).map { newAttendance -> + newAttendance.apply { if (notify) isNotified = false } + } + attendanceDb.insertAll(attendanceToAdd) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end)) }, filterResult = { it.filter { item -> item.date in start..end } } ) + fun getAttendanceFromDatabase( + semester: Semester, + start: LocalDate, + end: LocalDate + ): Flow> { + return attendanceDb.loadAll(semester.diaryId, semester.studentId, start, end) + } + + suspend fun updateTimetable(timetable: List) { + return attendanceDb.updateAll(timetable) + } + suspend fun excuseForAbsence( student: Student, semester: Semester, absenceList: List, reason: String? = null diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt index 769fa0f0d..8be621122 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt @@ -47,6 +47,7 @@ class TimetableRepository @Inject constructor( end: LocalDate, forceRefresh: Boolean, refreshAdditional: Boolean = false, + notify: Boolean = false ) = networkBoundResource( mutex = saveFetchResultMutex, shouldFetch = { (timetable, additional, headers) -> @@ -67,7 +68,7 @@ class TimetableRepository @Inject constructor( timetableFull.mapToEntities(semester) }, saveFetchResult = { timetableOld, timetableNew -> - refreshTimetable(student, timetableOld.lessons, timetableNew.lessons) + refreshTimetable(student, timetableOld.lessons, timetableNew.lessons, notify) refreshAdditional(timetableOld.additional, timetableNew.additional) refreshDayHeaders(timetableOld.headers, timetableNew.headers) @@ -117,13 +118,28 @@ class TimetableRepository @Inject constructor( } } + fun getTimetableFromDatabase( + semester: Semester, + from: LocalDate, + end: LocalDate + ): Flow> { + return timetableDb.loadAll(semester.diaryId, semester.studentId, from, end) + } + + suspend fun updateTimetable(timetable: List) { + return timetableDb.updateAll(timetable) + } + private suspend fun refreshTimetable( student: Student, lessonsOld: List, lessonsNew: List, + notify: Boolean ) { val lessonsToRemove = lessonsOld uniqueSubtract lessonsNew - val lessonsToAdd = lessonsNew uniqueSubtract lessonsOld + val lessonsToAdd = (lessonsNew uniqueSubtract lessonsOld).map { new -> + new.apply { if (notify) isNotified = false } + } timetableDb.deleteAll(lessonsToRemove) timetableDb.insertAll(lessonsToAdd) diff --git a/app/src/main/java/io/github/wulkanowy/services/ServicesModule.kt b/app/src/main/java/io/github/wulkanowy/services/ServicesModule.kt index cdf0c26a9..1729f1006 100644 --- a/app/src/main/java/io/github/wulkanowy/services/ServicesModule.kt +++ b/app/src/main/java/io/github/wulkanowy/services/ServicesModule.kt @@ -15,6 +15,7 @@ import dagger.multibindings.IntoSet import io.github.wulkanowy.services.sync.channels.Channel import io.github.wulkanowy.services.sync.channels.DebugChannel import io.github.wulkanowy.services.sync.channels.LuckyNumberChannel +import io.github.wulkanowy.services.sync.channels.NewAttendanceChannel import io.github.wulkanowy.services.sync.channels.NewConferencesChannel import io.github.wulkanowy.services.sync.channels.NewExamChannel import io.github.wulkanowy.services.sync.channels.NewGradesChannel @@ -23,6 +24,7 @@ import io.github.wulkanowy.services.sync.channels.NewMessagesChannel import io.github.wulkanowy.services.sync.channels.NewNotesChannel import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel import io.github.wulkanowy.services.sync.channels.PushChannel +import io.github.wulkanowy.services.sync.channels.TimetableChangeChannel import io.github.wulkanowy.services.sync.channels.UpcomingLessonsChannel import io.github.wulkanowy.services.sync.works.AttendanceSummaryWork import io.github.wulkanowy.services.sync.works.AttendanceWork @@ -167,4 +169,12 @@ abstract class ServicesModule { @Binds @IntoSet abstract fun provideUpcomingLessonsChannel(channel: UpcomingLessonsChannel): Channel + + @Binds + @IntoSet + abstract fun provideChangeTimetableChannel(channel: TimetableChangeChannel): Channel + + @Binds + @IntoSet + abstract fun provideNewAttendanceChannel(channel: NewAttendanceChannel): Channel } diff --git a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt index 9ce96ef3f..38ae7884c 100644 --- a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt +++ b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationReceiver.kt @@ -15,8 +15,8 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.services.HiltBroadcastReceiver import io.github.wulkanowy.services.sync.channels.UpcomingLessonsChannel.Companion.CHANNEL_ID +import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.main.MainActivity -import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.getCompatColor import io.github.wulkanowy.utils.toLocalDateTime @@ -41,7 +41,7 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { const val NOTIFICATION_TYPE_UPCOMING = 2 const val NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION = 3 - const val NOTIFICATION_ID = "id" + const val NOTIFICATION_ID = 2137 const val STUDENT_NAME = "student_name" const val STUDENT_ID = "student_id" @@ -71,11 +71,10 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { private fun prepareNotification(context: Context, intent: Intent) { val type = intent.getIntExtra(LESSON_TYPE, 0) - val notificationId = intent.getIntExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id) val isPersistent = preferencesRepository.isUpcomingLessonsNotificationsPersistent if (type == NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION) { - return NotificationManagerCompat.from(context).cancel(notificationId) + return NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID) } val studentId = intent.getIntExtra(STUDENT_ID, 0) @@ -92,7 +91,8 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { Timber.d("TimetableNotification receive: type: $type, subject: $subject, start: ${start.toLocalDateTime()}, student: $studentId") - showNotification(context, notificationId, isPersistent, studentName, + showNotification( + context, isPersistent, studentName, if (type == NOTIFICATION_TYPE_CURRENT) end else start, end - start, context.getString( if (type == NOTIFICATION_TYPE_CURRENT) R.string.timetable_now else R.string.timetable_next, @@ -109,7 +109,6 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { private fun showNotification( context: Context, - notificationId: Int, isPersistent: Boolean, studentName: String?, countDown: Long, @@ -118,7 +117,7 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { next: String? ) { NotificationManagerCompat.from(context) - .notify(notificationId, NotificationCompat.Builder(context, CHANNEL_ID) + .notify(NOTIFICATION_ID, NotificationCompat.Builder(context, CHANNEL_ID) .setContentTitle(title) .setContentText(next) .setAutoCancel(false) @@ -138,8 +137,8 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() { .setContentIntent( PendingIntent.getActivity( context, - MainView.Section.TIMETABLE.id, - MainActivity.getStartIntent(context, MainView.Section.TIMETABLE, true), + NOTIFICATION_ID, + MainActivity.getStartIntent(context, Destination.Timetable(), true), FLAG_UPDATE_CURRENT ) ) diff --git a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt index 2ec4e527b..28a9b3737 100644 --- a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt +++ b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt @@ -25,7 +25,6 @@ import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companio import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.NOTIFICATION_TYPE_UPCOMING import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.STUDENT_ID import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.STUDENT_NAME -import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.DispatchersProvider import io.github.wulkanowy.utils.nickOrName import io.github.wulkanowy.utils.toTimestamp @@ -79,7 +78,7 @@ class TimetableNotificationSchedulerHelper @Inject constructor( } fun cancelNotification() = - NotificationManagerCompat.from(context).cancel(MainView.Section.TIMETABLE.id) + NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID) suspend fun scheduleNotifications(lessons: List, student: Student) { if (!preferencesRepository.isUpcomingLessonsNotificationsEnable) { @@ -156,7 +155,6 @@ class TimetableNotificationSchedulerHelper @Inject constructor( AlarmManagerCompat.setExactAndAllowWhileIdle( alarmManager, RTC_WAKEUP, time.toTimestamp(), PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also { - it.putExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id) it.putExtra(LESSON_TYPE, notificationType) }, FLAG_UPDATE_CURRENT) ) diff --git a/app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt b/app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt new file mode 100644 index 000000000..d1c215f26 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt @@ -0,0 +1,90 @@ +package io.github.wulkanowy.services.shortcuts + +import android.content.Context +import android.content.Intent +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.R +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ShortcutsHelper @Inject constructor(@ApplicationContext private val context: Context) { + + private val destinations = mapOf( + "grade" to Destination.Grade, + "attendance" to Destination.Attendance, + "exam" to Destination.Exam, + "timetable" to Destination.Timetable() + ) + + init { + initializeShortcuts() + } + + fun getDestination(intent: Intent) = + destinations[intent.getStringExtra(EXTRA_SHORTCUT_DESTINATION_ID)] + + private fun initializeShortcuts() { + val shortcutsInfo = listOf( + ShortcutInfoCompat.Builder(context, "grade_shortcut") + .setShortLabel(context.getString(R.string.grade_title)) + .setLongLabel(context.getString(R.string.grade_title)) + .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_grade)) + .setIntent(MainActivity.getStartIntent(context, startNewTask = true) + .apply { + action = Intent.ACTION_VIEW + putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "grade") + } + ) + .build(), + + ShortcutInfoCompat.Builder(context, "attendance_shortcut") + .setShortLabel(context.getString(R.string.attendance_title)) + .setLongLabel(context.getString(R.string.attendance_title)) + .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_attendance)) + .setIntent(MainActivity.getStartIntent(context, startNewTask = true) + .apply { + action = Intent.ACTION_VIEW + putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "attendance") + } + ) + .build(), + + ShortcutInfoCompat.Builder(context, "exam_shortcut") + .setShortLabel(context.getString(R.string.exam_title)) + .setLongLabel(context.getString(R.string.exam_title)) + .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_exam)) + .setIntent(MainActivity.getStartIntent(context, startNewTask = true) + .apply { + action = Intent.ACTION_VIEW + putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "exam") + } + ) + .build(), + + ShortcutInfoCompat.Builder(context, "timetable_shortcut") + .setShortLabel(context.getString(R.string.timetable_title)) + .setLongLabel(context.getString(R.string.timetable_title)) + .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_timetable)) + .setIntent(MainActivity.getStartIntent(context, startNewTask = true) + .apply { + action = Intent.ACTION_VIEW + putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "timetable") + } + ) + .build() + ) + + shortcutsInfo.forEach { ShortcutManagerCompat.pushDynamicShortcut(context, it) } + } + + private companion object { + + private const val EXTRA_SHORTCUT_DESTINATION_ID = "shortcut_destination_id" + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/channels/NewAttendanceChannel.kt b/app/src/main/java/io/github/wulkanowy/services/sync/channels/NewAttendanceChannel.kt new file mode 100644 index 000000000..3110099e5 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/sync/channels/NewAttendanceChannel.kt @@ -0,0 +1,36 @@ +package io.github.wulkanowy.services.sync.channels + +import android.annotation.TargetApi +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import androidx.core.app.NotificationManagerCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.R +import javax.inject.Inject + +@TargetApi(26) +class NewAttendanceChannel @Inject constructor( + private val notificationManager: NotificationManagerCompat, + @ApplicationContext private val context: Context +) : Channel { + + companion object { + const val CHANNEL_ID = "new_attendance_channel" + } + + override fun create() { + notificationManager.createNotificationChannel( + NotificationChannel( + CHANNEL_ID, + context.getString(R.string.channel_new_attendance), + NotificationManager.IMPORTANCE_HIGH + ) + .apply { + enableLights(true) + enableVibration(true) + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + }) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/channels/TimetableChangeChannel.kt b/app/src/main/java/io/github/wulkanowy/services/sync/channels/TimetableChangeChannel.kt new file mode 100644 index 000000000..10dd3e004 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/sync/channels/TimetableChangeChannel.kt @@ -0,0 +1,36 @@ +package io.github.wulkanowy.services.sync.channels + +import android.annotation.TargetApi +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import androidx.core.app.NotificationManagerCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.R +import javax.inject.Inject + +@TargetApi(26) +class TimetableChangeChannel @Inject constructor( + private val notificationManager: NotificationManagerCompat, + @ApplicationContext private val context: Context +) : Channel { + + companion object { + const val CHANNEL_ID = "change_timetable_channel" + } + + override fun create() { + notificationManager.createNotificationChannel( + NotificationChannel( + CHANNEL_ID, + context.getString(R.string.channel_change_timetable), + NotificationManager.IMPORTANCE_HIGH + ) + .apply { + enableLights(true) + enableVibration(true) + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + }) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt index ddad9bf27..542b9346b 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt @@ -4,19 +4,15 @@ import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Context import android.os.Build -import androidx.annotation.PluralsRes import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Notification import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotificationsData +import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData -import io.github.wulkanowy.data.pojos.OneNotificationData import io.github.wulkanowy.data.repositories.NotificationRepository -import io.github.wulkanowy.ui.modules.main.MainActivity -import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.getCompatBitmap import io.github.wulkanowy.utils.getCompatColor import io.github.wulkanowy.utils.nickOrName @@ -27,102 +23,17 @@ import kotlin.random.Random class AppNotificationManager @Inject constructor( private val notificationManager: NotificationManagerCompat, @ApplicationContext private val context: Context, - private val appInfo: AppInfo, private val notificationRepository: NotificationRepository ) { - suspend fun sendNotification(notificationData: NotificationData, student: Student) = - when (notificationData) { - is OneNotificationData -> sendOneNotification(notificationData, student) - is MultipleNotificationsData -> sendMultipleNotifications(notificationData, student) - } - - private suspend fun sendOneNotification( - notificationData: OneNotificationData, - student: Student - ) { - val content = context.getString( - notificationData.contentStringRes, - *notificationData.contentValues.toTypedArray() - ) - - val title = context.getString(notificationData.titleStringRes) - - val notification = getDefaultNotificationBuilder(notificationData) - .setContentTitle(title) - .setContentText(content) - .setStyle( - NotificationCompat.BigTextStyle() - .setSummaryText(student.nickOrName) - .bigText(content) - ) - .build() - - notificationManager.notify(Random.nextInt(Int.MAX_VALUE), notification) - - saveNotification(title, content, notificationData, student) - } - - private suspend fun sendMultipleNotifications( - notificationData: MultipleNotificationsData, - student: Student - ) { - val groupType = notificationData.type.group ?: return - val group = "${groupType}_${student.id}" - - notificationData.sendSummaryNotification(group, student) - - notificationData.lines.forEach { item -> - val title = context.resources.getQuantityString(notificationData.titleStringRes, 1) - - val notification = getDefaultNotificationBuilder(notificationData) - .setContentTitle(title) - .setContentText(item) - .setStyle( - NotificationCompat.BigTextStyle() - .setSummaryText(student.nickOrName) - .bigText(item) - ) - .setGroup(group) - .build() - - notificationManager.notify(Random.nextInt(Int.MAX_VALUE), notification) - - saveNotification(title, item, notificationData, student) - } - } - - private fun MultipleNotificationsData.sendSummaryNotification(group: String, student: Student) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return - - val summaryNotification = getDefaultNotificationBuilder(this) - .setSmallIcon(icon) - .setContentTitle(getQuantityString(titleStringRes, lines.size)) - .setContentText(getQuantityString(contentStringRes, lines.size)) - .setStyle( - NotificationCompat.InboxStyle() - .setSummaryText(student.nickOrName) - .also { builder -> lines.forEach { builder.addLine(it) } } - ) - .setLocalOnly(true) - .setGroup(group) - .setGroupSummary(true) - .build() - - val groupId = student.id * 100 + type.ordinal - notificationManager.notify(groupId.toInt(), summaryNotification) - } - @SuppressLint("InlinedApi") - private fun getDefaultNotificationBuilder(notificationData: NotificationData): NotificationCompat.Builder { - val pendingIntentsFlags = if (appInfo.systemVersion >= Build.VERSION_CODES.M) { - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - } else { - PendingIntent.FLAG_UPDATE_CURRENT - } - - return NotificationCompat.Builder(context, notificationData.type.channel) - .setLargeIcon(context.getCompatBitmap(notificationData.icon, R.color.colorPrimary)) + suspend fun sendSingleNotification( + notificationData: NotificationData, + notificationType: NotificationType, + student: Student + ) { + val notification = NotificationCompat.Builder(context, notificationType.channel) + .setLargeIcon(context.getCompatBitmap(notificationType.icon, R.color.colorPrimary)) .setSmallIcon(R.drawable.ic_stat_all) .setAutoCancel(true) .setDefaults(NotificationCompat.DEFAULT_ALL) @@ -132,31 +43,122 @@ class AppNotificationManager @Inject constructor( .setContentIntent( PendingIntent.getActivity( context, - notificationData.startMenu.id, - MainActivity.getStartIntent(context, notificationData.startMenu, true), - pendingIntentsFlags + Random.nextInt(), + notificationData.intentToStart, + PendingIntent.FLAG_UPDATE_CURRENT ) ) + .setContentTitle(notificationData.title) + .setContentText(notificationData.content) + .setStyle( + NotificationCompat.BigTextStyle() + .setSummaryText(student.nickOrName) + .bigText(notificationData.content) + ) + .build() + + notificationManager.notify(Random.nextInt(), notification) + saveNotification(notificationData, notificationType, student) + } + + @SuppressLint("InlinedApi") + suspend fun sendMultipleNotifications( + groupNotificationData: GroupNotificationData, + student: Student + ) { + val notificationType = groupNotificationData.type + val groupType = notificationType.group ?: return + val group = "${groupType}_${student.id}" + + sendSummaryNotification(groupNotificationData, group, student) + + groupNotificationData.notificationDataList.forEach { notificationData -> + val notification = NotificationCompat.Builder(context, notificationType.channel) + .setLargeIcon(context.getCompatBitmap(notificationType.icon, R.color.colorPrimary)) + .setSmallIcon(R.drawable.ic_stat_all) + .setAutoCancel(true) + .setDefaults(NotificationCompat.DEFAULT_ALL) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setColor(context.getCompatColor(R.color.colorPrimary)) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) + .setContentIntent( + PendingIntent.getActivity( + context, + Random.nextInt(), + notificationData.intentToStart, + PendingIntent.FLAG_UPDATE_CURRENT + ) + ) + .setContentTitle(notificationData.title) + .setContentText(notificationData.content) + .setStyle( + NotificationCompat.BigTextStyle() + .setSummaryText(student.nickOrName) + .bigText(notificationData.content) + ) + .setGroup(group) + .build() + + notificationManager.notify(Random.nextInt(), notification) + saveNotification(notificationData, groupNotificationData.type, student) + } + } + + private fun sendSummaryNotification( + groupNotificationData: GroupNotificationData, + group: String, + student: Student + ) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return + + val summaryNotification = + NotificationCompat.Builder(context, groupNotificationData.type.channel) + .setContentTitle(groupNotificationData.title) + .setContentText(groupNotificationData.content) + .setSmallIcon(groupNotificationData.type.icon) + .setAutoCancel(true) + .setDefaults(NotificationCompat.DEFAULT_ALL) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setColor(context.getCompatColor(R.color.colorPrimary)) + .setStyle( + NotificationCompat.InboxStyle() + .setSummaryText(student.nickOrName) + .also { builder -> + groupNotificationData.notificationDataList.forEach { + builder.addLine(it.content) + } + } + ) + .setContentIntent( + PendingIntent.getActivity( + context, + Random.nextInt(), + groupNotificationData.intentToStart, + PendingIntent.FLAG_UPDATE_CURRENT + ) + ) + .setLocalOnly(true) + .setGroup(group) + .setGroupSummary(true) + .build() + + val groupId = student.id * 100 + groupNotificationData.type.ordinal + notificationManager.notify(groupId.toInt(), summaryNotification) } private suspend fun saveNotification( - title: String, - content: String, notificationData: NotificationData, + notificationType: NotificationType, student: Student ) { val notificationEntity = Notification( studentId = student.id, - title = title, - content = content, - type = notificationData.type, + title = notificationData.title, + content = notificationData.content, + type = notificationType, date = LocalDateTime.now() ) notificationRepository.saveNotification(notificationEntity) } - - private fun getQuantityString(@PluralsRes res: Int, arg: Int): String { - return context.resources.getQuantityString(res, arg, arg) - } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt new file mode 100644 index 000000000..6d2d3a59f --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt @@ -0,0 +1,125 @@ +package io.github.wulkanowy.services.sync.notifications + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.Timetable +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.getPlural +import io.github.wulkanowy.utils.toFormattedString +import java.time.LocalDate +import java.time.LocalDateTime +import javax.inject.Inject + +class ChangeTimetableNotification @Inject constructor( + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context, +) { + + suspend fun notify(items: List, student: Student) { + val currentTime = LocalDateTime.now() + val changedLessons = items.filter { (it.canceled || it.changes) && it.start > currentTime } + val notificationDataList = changedLessons.groupBy { it.date } + .map { (date, lessons) -> + getNotificationContents(date, lessons).map { + NotificationData( + title = context.getPlural( + R.plurals.timetable_notify_new_items_title, + 1 + ), + content = it, + intentToStart = MainActivity.getStartIntent( + context = context, + destination = Destination.Timetable(date), + startNewTask = true + ) + ) + } + } + .flatten() + .ifEmpty { return } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural( + R.plurals.timetable_notify_new_items_title, + changedLessons.size + ), + content = context.getPlural( + R.plurals.timetable_notify_new_items_group, + changedLessons.size, + changedLessons.size + ), + intentToStart = MainActivity.getStartIntent(context, Destination.Timetable(), true), + type = NotificationType.CHANGE_TIMETABLE + ) + + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) + } + + private fun getNotificationContents(date: LocalDate, lessons: List): List { + val formattedDate = date.toFormattedString("EEE dd.MM") + + return if (lessons.size > 2) { + listOf( + context.getPlural( + R.plurals.timetable_notify_new_items, + lessons.size, + formattedDate, + lessons.size, + ) + ) + } else { + lessons.map { + buildString { + append( + context.getString( + R.string.timetable_notify_lesson, + formattedDate, + it.number, + it.subject + ) + ) + if (it.roomOld.isNotBlank()) { + appendLine() + append( + context.getString( + R.string.timetable_notify_change_room, + it.roomOld, + it.room + ) + ) + } + if (it.teacherOld.isNotBlank() && it.teacher != it.teacherOld) { + appendLine() + append( + context.getString( + R.string.timetable_notify_change_teacher, + it.teacherOld, + it.teacher + ) + ) + } + if (it.subjectOld.isNotBlank()) { + appendLine() + append( + context.getString( + R.string.timetable_notify_change_subject, + it.subjectOld, + it.subject + ) + ) + } + if (it.info.isNotBlank()) { + appendLine() + append(it.info) + } + } + } + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewAttendanceNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewAttendanceNotification.kt new file mode 100644 index 000000000..695552721 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewAttendanceNotification.kt @@ -0,0 +1,55 @@ +package io.github.wulkanowy.services.sync.notifications + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Attendance +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.descriptionRes +import io.github.wulkanowy.utils.getPlural +import io.github.wulkanowy.utils.toFormattedString +import javax.inject.Inject + +class NewAttendanceNotification @Inject constructor( + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context +) { + + suspend fun notify(items: List, student: Student) { + val lines = items.filterNot { it.presence || it.name == "UNKNOWN" } + .map { + val description = context.getString(it.descriptionRes) + "${it.date.toFormattedString("dd.MM")} - ${it.subject}: $description" + } + .ifEmpty { return } + + val notificationDataList = lines.map { + NotificationData( + title = context.getPlural(R.plurals.attendance_notify_new_items_title, 1), + content = it, + intentToStart = MainActivity.getStartIntent(context, Destination.Attendance, true) + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural( + R.plurals.attendance_notify_new_items_title, + notificationDataList.size + ), + content = context.getPlural( + R.plurals.attendance_notify_new_items, + notificationDataList.size, + notificationDataList.size + ), + intentToStart = MainActivity.getStartIntent(context, Destination.Attendance, true), + type = NotificationType.NEW_ATTENDANCE + ) + + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt index 994cb8d4b..97b1332d8 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt @@ -1,34 +1,52 @@ package io.github.wulkanowy.services.sync.notifications +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotificationsData -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDateTime import javax.inject.Inject class NewConferenceNotification @Inject constructor( - private val appNotificationManager: AppNotificationManager + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context ) { suspend fun notify(items: List, student: Student) { val today = LocalDateTime.now() - val lines = items.filter { !it.date.isBefore(today) }.map { - "${it.date.toFormattedString("dd.MM")} - ${it.title}: ${it.subject}" - }.ifEmpty { return } + val lines = items.filter { !it.date.isBefore(today) } + .map { + "${it.date.toFormattedString("dd.MM")} - ${it.title}: ${it.subject}" + } + .ifEmpty { return } - val notification = MultipleNotificationsData( - type = NotificationType.NEW_CONFERENCE, - icon = R.drawable.ic_more_conferences, - titleStringRes = R.plurals.conference_notify_new_item_title, - contentStringRes = R.plurals.conference_notify_new_items, - summaryStringRes = R.plurals.conference_number_item, - startMenu = MainView.Section.CONFERENCE, - lines = lines + val notificationDataList = lines.map { + NotificationData( + title = context.getPlural(R.plurals.conference_notify_new_item_title, 1), + content = it, + intentToStart = MainActivity.getStartIntent(context, Destination.Conference, true) + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural(R.plurals.conference_notify_new_item_title, lines.size), + content = context.getPlural( + R.plurals.conference_notify_new_items, + lines.size, + lines.size + ), + intentToStart = MainActivity.getStartIntent(context, Destination.Conference, true), + type = NotificationType.NEW_CONFERENCE ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt index f148fa34f..6f8ed8961 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt @@ -1,34 +1,52 @@ package io.github.wulkanowy.services.sync.notifications +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotificationsData -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDate import javax.inject.Inject class NewExamNotification @Inject constructor( - private val appNotificationManager: AppNotificationManager + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context ) { suspend fun notify(items: List, student: Student) { val today = LocalDate.now() - val lines = items.filter { !it.date.isBefore(today) }.map { - "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.description}" - }.ifEmpty { return } + val lines = items.filter { !it.date.isBefore(today) } + .map { + "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.description}" + } + .ifEmpty { return } - val notification = MultipleNotificationsData( - type = NotificationType.NEW_EXAM, - icon = R.drawable.ic_main_exam, - titleStringRes = R.plurals.exam_notify_new_item_title, - contentStringRes = R.plurals.exam_notify_new_item_content, - summaryStringRes = R.plurals.exam_number_item, - startMenu = MainView.Section.EXAM, - lines = lines + val notificationDataList = lines.map { + NotificationData( + title = context.getPlural(R.plurals.exam_notify_new_item_title, 1), + content = it, + intentToStart = MainActivity.getStartIntent(context, Destination.Exam, true), + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural(R.plurals.exam_notify_new_item_title, lines.size), + content = context.getPlural( + R.plurals.exam_notify_new_item_content, + lines.size, + lines.size + ), + intentToStart = MainActivity.getStartIntent(context, Destination.Exam, true), + type = NotificationType.NEW_EXAM ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt index 52bdff588..09692fb7c 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt @@ -1,62 +1,88 @@ package io.github.wulkanowy.services.sync.notifications +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotificationsData -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.getPlural import javax.inject.Inject class NewGradeNotification @Inject constructor( - private val appNotificationManager: AppNotificationManager + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context ) { suspend fun notifyDetails(items: List, student: Student) { - val notification = MultipleNotificationsData( - type = NotificationType.NEW_GRADE_DETAILS, - icon = R.drawable.ic_stat_grade, - titleStringRes = R.plurals.grade_new_items, - contentStringRes = R.plurals.grade_notify_new_items, - summaryStringRes = R.plurals.grade_number_item, - startMenu = MainView.Section.GRADE, - lines = items.map { - "${it.subject}: ${it.entry}" - } + val notificationDataList = items.map { + NotificationData( + title = context.getPlural(R.plurals.grade_new_items, 1), + content = "${it.subject}: ${it.entry}", + intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural(R.plurals.grade_new_items, items.size), + content = context.getPlural(R.plurals.grade_notify_new_items, items.size, items.size), + intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + type = NotificationType.NEW_GRADE_DETAILS ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } suspend fun notifyPredicted(items: List, student: Student) { - val notification = MultipleNotificationsData( - type = NotificationType.NEW_GRADE_PREDICTED, - icon = R.drawable.ic_stat_grade, - titleStringRes = R.plurals.grade_new_items_predicted, - contentStringRes = R.plurals.grade_notify_new_items_predicted, - summaryStringRes = R.plurals.grade_number_item, - startMenu = MainView.Section.GRADE, - lines = items.map { - "${it.subject}: ${it.predictedGrade}" - } + val notificationDataList = items.map { + NotificationData( + title = context.getPlural(R.plurals.grade_new_items_predicted, 1), + content = "${it.subject}: ${it.predictedGrade}", + intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural(R.plurals.grade_new_items_predicted, items.size), + content = context.getPlural( + R.plurals.grade_notify_new_items_predicted, + items.size, + items.size + ), + intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + type = NotificationType.NEW_GRADE_PREDICTED ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } suspend fun notifyFinal(items: List, student: Student) { - val notification = MultipleNotificationsData( - type = NotificationType.NEW_GRADE_FINAL, - icon = R.drawable.ic_stat_grade, - titleStringRes = R.plurals.grade_new_items_final, - contentStringRes = R.plurals.grade_notify_new_items_final, - summaryStringRes = R.plurals.grade_number_item, - startMenu = MainView.Section.GRADE, - lines = items.map { - "${it.subject}: ${it.finalGrade}" - } + val notificationDataList = items.map { + NotificationData( + title = context.getPlural(R.plurals.grade_new_items_final, 1), + content = "${it.subject}: ${it.finalGrade}", + intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural(R.plurals.grade_new_items_final, items.size), + content = context.getPlural( + R.plurals.grade_notify_new_items_final, + items.size, + items.size + ), + intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + type = NotificationType.NEW_GRADE_FINAL ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt index 4c34cb8ff..cdada844e 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt @@ -1,34 +1,52 @@ package io.github.wulkanowy.services.sync.notifications +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Homework import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotificationsData -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDate import javax.inject.Inject class NewHomeworkNotification @Inject constructor( - private val appNotificationManager: AppNotificationManager + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context ) { suspend fun notify(items: List, student: Student) { val today = LocalDate.now() - val lines = items.filter { !it.date.isBefore(today) }.map { - "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.content}" - }.ifEmpty { return } + val lines = items.filter { !it.date.isBefore(today) } + .map { + "${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.content}" + } + .ifEmpty { return } - val notification = MultipleNotificationsData( + val notificationDataList = lines.map { + NotificationData( + title = context.getPlural(R.plurals.homework_notify_new_item_title, 1), + content = it, + intentToStart = MainActivity.getStartIntent(context, Destination.Homework, true), + ) + } + + val groupNotificationData = GroupNotificationData( + title = context.getPlural(R.plurals.homework_notify_new_item_title, lines.size), + content = context.getPlural( + R.plurals.homework_notify_new_item_content, + lines.size, + lines.size + ), + intentToStart = MainActivity.getStartIntent(context, Destination.Homework, true), type = NotificationType.NEW_HOMEWORK, - icon = R.drawable.ic_more_homework, - titleStringRes = R.plurals.homework_notify_new_item_title, - contentStringRes = R.plurals.homework_notify_new_item_content, - summaryStringRes = R.plurals.homework_number_item, - startMenu = MainView.Section.HOMEWORK, - lines = lines + notificationDataList = notificationDataList ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt index 08c985106..d9f138b5c 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt @@ -1,26 +1,34 @@ package io.github.wulkanowy.services.sync.notifications +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.OneNotificationData -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity import javax.inject.Inject class NewLuckyNumberNotification @Inject constructor( - private val appNotificationManager: AppNotificationManager + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context ) { - suspend fun notify(item: LuckyNumber, student: Student) { - val notification = OneNotificationData( - type = NotificationType.NEW_LUCKY_NUMBER, - icon = R.drawable.ic_stat_luckynumber, - titleStringRes = R.string.lucky_number_notify_new_item_title, - contentStringRes = R.string.lucky_number_notify_new_item, - startMenu = MainView.Section.LUCKY_NUMBER, - contentValues = listOf(item.luckyNumber.toString()) - ) + suspend fun notify(item: LuckyNumber, student: Student) { + val notificationData = NotificationData( + title = context.getString(R.string.lucky_number_notify_new_item_title), + content = context.getString( + R.string.lucky_number_notify_new_item, + item.luckyNumber.toString() + ), + intentToStart = MainActivity.getStartIntent(context, Destination.LuckyNumber, true) + ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendSingleNotification( + notificationData = notificationData, + notificationType = NotificationType.NEW_LUCKY_NUMBER, + student = student + ) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt index a6d503aa0..cdb5ab9c8 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt @@ -1,29 +1,39 @@ package io.github.wulkanowy.services.sync.notifications +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotificationsData -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.getPlural import javax.inject.Inject class NewMessageNotification @Inject constructor( - private val appNotificationManager: AppNotificationManager + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context ) { suspend fun notify(items: List, student: Student) { - val notification = MultipleNotificationsData( - type = NotificationType.NEW_MESSAGE, - icon = R.drawable.ic_stat_message, - titleStringRes = R.plurals.message_new_items, - contentStringRes = R.plurals.message_notify_new_items, - summaryStringRes = R.plurals.message_number_item, - startMenu = MainView.Section.MESSAGE, - lines = items.map { - "${it.sender}: ${it.subject}" - } + val notificationDataList = items.map { + NotificationData( + title = context.getPlural(R.plurals.message_new_items, 1), + content = "${it.sender}: ${it.subject}", + intentToStart = MainActivity.getStartIntent(context, Destination.Message, true), + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural(R.plurals.message_new_items, items.size), + content = context.getPlural(R.plurals.message_notify_new_items, items.size, items.size), + intentToStart = MainActivity.getStartIntent(context, Destination.Message, true), + type = NotificationType.NEW_MESSAGE ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt index ffa3cc9cb..16be1ca50 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt @@ -1,42 +1,46 @@ package io.github.wulkanowy.services.sync.notifications +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Note import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotificationsData +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.sdk.scrapper.notes.NoteCategory -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.getPlural import javax.inject.Inject class NewNoteNotification @Inject constructor( - private val appNotificationManager: AppNotificationManager + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context ) { suspend fun notify(items: List, student: Student) { - val notification = MultipleNotificationsData( - type = NotificationType.NEW_NOTE, - icon = R.drawable.ic_stat_note, - titleStringRes = when (NoteCategory.getByValue(items.first().categoryType)) { + val notificationDataList = items.map { + val titleRes = when (NoteCategory.getByValue(it.categoryType)) { NoteCategory.POSITIVE -> R.plurals.praise_new_items NoteCategory.NEUTRAL -> R.plurals.neutral_note_new_items else -> R.plurals.note_new_items - }, - contentStringRes = when (NoteCategory.getByValue(items.first().categoryType)) { - NoteCategory.POSITIVE -> R.plurals.praise_notify_new_items - NoteCategory.NEUTRAL -> R.plurals.neutral_note_notify_new_items - else -> R.plurals.note_notify_new_items - }, - summaryStringRes = when (NoteCategory.getByValue(items.first().categoryType)) { - NoteCategory.POSITIVE -> R.plurals.praise_number_item - NoteCategory.NEUTRAL -> R.plurals.neutral_note_number_item - else -> R.plurals.note_number_item - }, - startMenu = MainView.Section.NOTE, - lines = items.map { - "${it.teacher}: ${it.category}" } + + NotificationData( + title = context.getPlural(titleRes, 1), + content = "${it.teacher}: ${it.category}", + intentToStart = MainActivity.getStartIntent(context, Destination.Note, true), + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + intentToStart = MainActivity.getStartIntent(context, Destination.Note, true), + title = context.getPlural(R.plurals.note_new_items, items.size), + content = context.getPlural(R.plurals.note_notify_new_items, items.size, items.size), + type = NotificationType.NEW_NOTE ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt index 990a950b1..1f603624b 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt @@ -1,29 +1,56 @@ package io.github.wulkanowy.services.sync.notifications +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.SchoolAnnouncement import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.pojos.MultipleNotificationsData -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.data.pojos.GroupNotificationData +import io.github.wulkanowy.data.pojos.NotificationData +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.utils.getPlural import javax.inject.Inject class NewSchoolAnnouncementNotification @Inject constructor( - private val appNotificationManager: AppNotificationManager + private val appNotificationManager: AppNotificationManager, + @ApplicationContext private val context: Context ) { - suspend fun notify(items: List, student: Student) { - val notification = MultipleNotificationsData( - type = NotificationType.NEW_ANNOUNCEMENT, - icon = R.drawable.ic_all_about, - titleStringRes = R.plurals.school_announcement_notify_new_item_title, - contentStringRes = R.plurals.school_announcement_notify_new_items, - summaryStringRes = R.plurals.school_announcement_number_item, - startMenu = MainView.Section.SCHOOL_ANNOUNCEMENT, - lines = items.map { - "${it.subject}: ${it.content}" - } + suspend fun notify(items: List, student: Student) { + val notificationDataList = items.map { + NotificationData( + intentToStart = MainActivity.getStartIntent( + context = context, + destination = Destination.SchoolAnnouncement, + startNewTask = true + ), + title = context.getPlural( + R.plurals.school_announcement_notify_new_item_title, + 1 + ), + content = "${it.subject}: ${it.content}" + ) + } + val groupNotificationData = GroupNotificationData( + type = NotificationType.NEW_ANNOUNCEMENT, + intentToStart = MainActivity.getStartIntent( + context = context, + destination = Destination.SchoolAnnouncement, + startNewTask = true + ), + title = context.getPlural( + R.plurals.school_announcement_notify_new_item_title, + items.size + ), + content = context.getPlural( + R.plurals.school_announcement_notify_new_items, + items.size, + items.size + ), + notificationDataList = notificationDataList ) - appNotificationManager.sendNotification(notification, student) + appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt index 49cbcfe9e..af79fcd2a 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NotificationType.kt @@ -1,6 +1,8 @@ package io.github.wulkanowy.services.sync.notifications +import io.github.wulkanowy.R import io.github.wulkanowy.services.sync.channels.LuckyNumberChannel +import io.github.wulkanowy.services.sync.channels.NewAttendanceChannel import io.github.wulkanowy.services.sync.channels.NewConferencesChannel import io.github.wulkanowy.services.sync.channels.NewExamChannel import io.github.wulkanowy.services.sync.channels.NewGradesChannel @@ -9,17 +11,76 @@ import io.github.wulkanowy.services.sync.channels.NewMessagesChannel import io.github.wulkanowy.services.sync.channels.NewNotesChannel import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel import io.github.wulkanowy.services.sync.channels.PushChannel +import io.github.wulkanowy.services.sync.channels.TimetableChangeChannel -enum class NotificationType(val group: String?, val channel: String) { - NEW_CONFERENCE("new_conferences_group", NewConferencesChannel.CHANNEL_ID), - NEW_EXAM("new_exam_group", NewExamChannel.CHANNEL_ID), - NEW_GRADE_DETAILS("new_grade_details_group", NewGradesChannel.CHANNEL_ID), - NEW_GRADE_PREDICTED("new_grade_predicted_group", NewGradesChannel.CHANNEL_ID), - NEW_GRADE_FINAL("new_grade_final_group", NewGradesChannel.CHANNEL_ID), - NEW_HOMEWORK("new_homework_group", NewHomeworkChannel.CHANNEL_ID), - NEW_LUCKY_NUMBER("lucky_number_group", LuckyNumberChannel.CHANNEL_ID), - NEW_MESSAGE("new_message_group", NewMessagesChannel.CHANNEL_ID), - NEW_NOTE("new_notes_group", NewNotesChannel.CHANNEL_ID), - NEW_ANNOUNCEMENT("new_school_announcements_group", NewSchoolAnnouncementsChannel.CHANNEL_ID), - PUSH(null, PushChannel.CHANNEL_ID) +enum class NotificationType( + val group: String?, + val channel: String, + val icon: Int +) { + NEW_CONFERENCE( + group = "new_conferences_group", + channel = NewConferencesChannel.CHANNEL_ID, + icon = R.drawable.ic_more_conferences, + ), + NEW_EXAM( + group = "new_exam_group", + channel = NewExamChannel.CHANNEL_ID, + icon = R.drawable.ic_main_exam + ), + NEW_GRADE_DETAILS( + group = "new_grade_details_group", + channel = NewGradesChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_grade, + ), + NEW_GRADE_PREDICTED( + group = "new_grade_predicted_group", + channel = NewGradesChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_grade, + ), + NEW_GRADE_FINAL( + group = "new_grade_final_group", + channel = NewGradesChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_grade, + ), + NEW_HOMEWORK( + group = "new_homework_group", + channel = NewHomeworkChannel.CHANNEL_ID, + icon = R.drawable.ic_more_homework, + ), + NEW_LUCKY_NUMBER( + group = null, + channel = LuckyNumberChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_luckynumber, + ), + NEW_MESSAGE( + group = "new_message_group", + channel = NewMessagesChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_message, + ), + NEW_NOTE( + group = "new_notes_group", + channel = NewNotesChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_note + ), + NEW_ANNOUNCEMENT( + group = "new_school_announcements_group", + channel = NewSchoolAnnouncementsChannel.CHANNEL_ID, + icon = R.drawable.ic_all_about + ), + CHANGE_TIMETABLE( + group = "change_timetable_group", + channel = TimetableChangeChannel.CHANNEL_ID, + icon = R.drawable.ic_main_timetable + ), + NEW_ATTENDANCE( + group = "new_attendance_group", + channel = NewAttendanceChannel.CHANNEL_ID, + icon = R.drawable.ic_main_attendance + ), + PUSH( + group = null, + channel = PushChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_all + ) } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt index 4823b2b5b..f7b680e31 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/AttendanceWork.kt @@ -3,18 +3,40 @@ package io.github.wulkanowy.services.sync.works import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.AttendanceRepository -import io.github.wulkanowy.utils.monday -import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.services.sync.notifications.NewAttendanceNotification +import io.github.wulkanowy.utils.previousOrSameSchoolDay import io.github.wulkanowy.utils.waitForResult +import kotlinx.coroutines.flow.first import java.time.LocalDate.now import javax.inject.Inject class AttendanceWork @Inject constructor( - private val attendanceRepository: AttendanceRepository + private val attendanceRepository: AttendanceRepository, + private val newAttendanceNotification: NewAttendanceNotification, + private val preferencesRepository: PreferencesRepository ) : Work { override suspend fun doWork(student: Student, semester: Semester) { - attendanceRepository.getAttendance(student, semester, now().monday, now().sunday, true) + attendanceRepository.getAttendance( + student = student, + semester = semester, + start = now().previousOrSameSchoolDay, + end = now().previousOrSameSchoolDay, + forceRefresh = true, + notify = preferencesRepository.isNotificationsEnable + ) .waitForResult() + + attendanceRepository.getAttendanceFromDatabase(semester, now().minusDays(7), now()) + .first() + .filterNot { it.isNotified } + .let { + if (it.isNotEmpty()) newAttendanceNotification.notify(it, student) + + attendanceRepository.updateTimetable(it.onEach { attendance -> + attendance.isNotified = true + }) + } } } diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/TimetableWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/TimetableWork.kt index 2df2c9dcb..fcc330638 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/TimetableWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/TimetableWork.kt @@ -2,18 +2,41 @@ package io.github.wulkanowy.services.sync.works import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.TimetableRepository -import io.github.wulkanowy.utils.monday -import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.services.sync.notifications.ChangeTimetableNotification +import io.github.wulkanowy.utils.nextOrSameSchoolDay import io.github.wulkanowy.utils.waitForResult +import kotlinx.coroutines.flow.first import java.time.LocalDate.now import javax.inject.Inject class TimetableWork @Inject constructor( - private val timetableRepository: TimetableRepository + private val timetableRepository: TimetableRepository, + private val changeTimetableNotification: ChangeTimetableNotification, + private val preferencesRepository: PreferencesRepository ) : Work { override suspend fun doWork(student: Student, semester: Semester) { - timetableRepository.getTimetable(student, semester, now().monday, now().sunday, true).waitForResult() + timetableRepository.getTimetable( + student = student, + semester = semester, + start = now().nextOrSameSchoolDay, + end = now().nextOrSameSchoolDay, + forceRefresh = true, + notify = preferencesRepository.isNotificationsEnable + ) + .waitForResult() + + timetableRepository.getTimetableFromDatabase(semester, now(), now().plusDays(7)) + .first() + .filterNot { it.isNotified } + .let { + if (it.isNotEmpty()) changeTimetableNotification.notify(it, student) + + timetableRepository.updateTimetable(it.onEach { timetable -> + timetable.isNotified = true + }) + } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt new file mode 100644 index 000000000..0406afa41 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt @@ -0,0 +1,132 @@ +package io.github.wulkanowy.ui.modules + +import androidx.fragment.app.Fragment +import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment +import io.github.wulkanowy.ui.modules.conference.ConferenceFragment +import io.github.wulkanowy.ui.modules.dashboard.DashboardFragment +import io.github.wulkanowy.ui.modules.exam.ExamFragment +import io.github.wulkanowy.ui.modules.grade.GradeFragment +import io.github.wulkanowy.ui.modules.homework.HomeworkFragment +import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment +import io.github.wulkanowy.ui.modules.message.MessageFragment +import io.github.wulkanowy.ui.modules.more.MoreFragment +import io.github.wulkanowy.ui.modules.note.NoteFragment +import io.github.wulkanowy.ui.modules.schoolandteachers.school.SchoolFragment +import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment +import io.github.wulkanowy.ui.modules.timetable.TimetableFragment +import java.io.Serializable +import java.time.LocalDate + +sealed interface Destination : Serializable { + + val type: Type + + val fragment: Fragment + + enum class Type(val defaultDestination: Destination) { + DASHBOARD(Dashboard), + GRADE(Grade), + ATTENDANCE(Attendance), + EXAM(Exam), + TIMETABLE(Timetable()), + HOMEWORK(Homework), + NOTE(Note), + CONFERENCE(Conference), + SCHOOL_ANNOUNCEMENT(SchoolAnnouncement), + SCHOOL(School), + LUCKY_NUMBER(More), + MORE(More), + MESSAGE(Message); + } + + object Dashboard : Destination { + + override val type = Type.DASHBOARD + + override val fragment get() = DashboardFragment.newInstance() + } + + object Grade : Destination { + + override val type = Type.GRADE + + override val fragment get() = GradeFragment.newInstance() + } + + object Attendance : Destination { + + override val type = Type.ATTENDANCE + + override val fragment get() = AttendanceFragment.newInstance() + } + + object Exam : Destination { + + override val type = Type.EXAM + + override val fragment get() = ExamFragment.newInstance() + } + + data class Timetable(val date: LocalDate? = null) : Destination { + + override val type = Type.TIMETABLE + + override val fragment get() = TimetableFragment.newInstance(date) + } + + object Homework : Destination { + + override val type = Type.HOMEWORK + + override val fragment get() = HomeworkFragment.newInstance() + } + + object Note : Destination { + + override val type = Type.NOTE + + override val fragment get() = NoteFragment.newInstance() + } + + object Conference : Destination { + + override val type = Type.CONFERENCE + + override val fragment get() = ConferenceFragment.newInstance() + } + + object SchoolAnnouncement : Destination { + + override val type = Type.SCHOOL_ANNOUNCEMENT + + override val fragment get() = SchoolAnnouncementFragment.newInstance() + } + + object School : Destination { + + override val type = Type.SCHOOL + + override val fragment get() = SchoolFragment.newInstance() + } + + object LuckyNumber : Destination { + + override val type = Type.LUCKY_NUMBER + + override val fragment get() = LuckyNumberFragment.newInstance() + } + + object More : Destination { + + override val type = Type.MORE + + override val fragment get() = MoreFragment.newInstance() + } + + object Message : Destination { + + override val type = Type.MESSAGE + + override val fragment get() = MessageFragment.newInstance() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt index bb4f70224..5d5ed504c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceAdapter.kt @@ -9,7 +9,7 @@ import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.enums.SentExcuseStatus import io.github.wulkanowy.databinding.ItemAttendanceBinding -import io.github.wulkanowy.utils.description +import io.github.wulkanowy.utils.descriptionRes import io.github.wulkanowy.utils.isExcusableOrNotExcused import javax.inject.Inject @@ -36,7 +36,7 @@ class AttendanceAdapter @Inject constructor() : with(holder.binding) { attendanceItemNumber.text = item.number.toString() attendanceItemSubject.text = item.subject - attendanceItemDescription.setText(item.description) + attendanceItemDescription.setText(item.descriptionRes) attendanceItemAlert.visibility = item.run { if (absence && !excused) View.VISIBLE else View.INVISIBLE } attendanceItemNumber.visibility = View.GONE attendanceItemExcuseInfo.visibility = View.GONE diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt index d816d8f00..9b5c63e4c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceDialog.kt @@ -7,7 +7,7 @@ import android.view.ViewGroup import androidx.fragment.app.DialogFragment import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.databinding.DialogAttendanceBinding -import io.github.wulkanowy.utils.description +import io.github.wulkanowy.utils.descriptionRes import io.github.wulkanowy.utils.lifecycleAwareVariable import io.github.wulkanowy.utils.toFormattedString @@ -45,7 +45,7 @@ class AttendanceDialog : DialogFragment() { with(binding) { attendanceDialogSubjectValue.text = attendance.subject - attendanceDialogDescriptionValue.setText(attendance.description) + attendanceDialogDescriptionValue.setText(attendance.descriptionRes) attendanceDialogDateValue.text = attendance.date.toFormattedString() attendanceDialogNumberValue.text = attendance.number.toString() attendanceDialogClose.setOnClickListener { dismiss() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt index 26eca18fd..d0dfcd696 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/NotificationDebugPresenter.kt @@ -3,6 +3,8 @@ package io.github.wulkanowy.ui.modules.debug.notification import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.services.sync.notifications.ChangeTimetableNotification +import io.github.wulkanowy.services.sync.notifications.NewAttendanceNotification import io.github.wulkanowy.services.sync.notifications.NewConferenceNotification import io.github.wulkanowy.services.sync.notifications.NewExamNotification import io.github.wulkanowy.services.sync.notifications.NewGradeNotification @@ -13,6 +15,7 @@ import io.github.wulkanowy.services.sync.notifications.NewNoteNotification import io.github.wulkanowy.services.sync.notifications.NewSchoolAnnouncementNotification import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler +import io.github.wulkanowy.ui.modules.debug.notification.mock.debugAttendanceItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugConferenceItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugExamItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugGradeDetailsItems @@ -22,6 +25,7 @@ import io.github.wulkanowy.ui.modules.debug.notification.mock.debugLuckyNumber import io.github.wulkanowy.ui.modules.debug.notification.mock.debugMessageItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugNoteItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugSchoolAnnouncementItems +import io.github.wulkanowy.ui.modules.debug.notification.mock.debugTimetableItems import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -37,6 +41,8 @@ class NotificationDebugPresenter @Inject constructor( private val newNoteNotification: NewNoteNotification, private val newSchoolAnnouncementNotification: NewSchoolAnnouncementNotification, private val newLuckyNumberNotification: NewLuckyNumberNotification, + private val changeTimetableNotification: ChangeTimetableNotification, + private val newAttendanceNotification: NewAttendanceNotification, ) : BasePresenter(errorHandler, studentRepository) { private val items = listOf( @@ -64,6 +70,12 @@ class NotificationDebugPresenter @Inject constructor( NotificationDebugItem(R.string.note_title) { n -> withStudent { newNoteNotification.notify(debugNoteItems.take(n), it) } }, + NotificationDebugItem(R.string.attendance_title) { n -> + withStudent { newAttendanceNotification.notify(debugAttendanceItems.take(n), it) } + }, + NotificationDebugItem(R.string.timetable_title) { n -> + withStudent { changeTimetableNotification.notify(debugTimetableItems.take(n), it) } + }, NotificationDebugItem(R.string.school_announcement_title) { n -> withStudent { newSchoolAnnouncementNotification.notify(debugSchoolAnnouncementItems.take(n), it) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/attendance.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/attendance.kt new file mode 100644 index 000000000..042cf07e7 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/attendance.kt @@ -0,0 +1,35 @@ +package io.github.wulkanowy.ui.modules.debug.notification.mock + +import io.github.wulkanowy.data.db.entities.Attendance +import java.time.LocalDate + +val debugAttendanceItems = listOf( + generateAttendance("Matematyka", "PRESENCE"), + generateAttendance("Język angielski", "UNEXCUSED_LATENESS"), + generateAttendance("Geografia", "ABSENCE_UNEXCUSED"), + generateAttendance("Sieci komputerowe", "ABSENCE_EXCUSED"), + generateAttendance("Systemy operacyjne", "EXCUSED_LATENESS"), + generateAttendance("Język niemiecki", "ABSENCE_UNEXCUSED"), + generateAttendance("Biologia", "ABSENCE_UNEXCUSED"), + generateAttendance("Chemia", "ABSENCE_EXCUSED"), + generateAttendance("Fizyka", "ABSENCE_UNEXCUSED"), + generateAttendance("Matematyka", "ABSENCE_EXCUSED"), +) + +private fun generateAttendance(subject: String, name: String) = Attendance( + subject = subject, + studentId = 0, + diaryId = 0, + date = LocalDate.now(), + timeId = 0, + number = 1, + name = name, + presence = false, + absence = false, + exemption = false, + lateness = false, + excused = false, + deleted = false, + excusable = false, + excuseStatus = "" +) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/timetable.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/timetable.kt new file mode 100644 index 000000000..428c001d6 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/timetable.kt @@ -0,0 +1,39 @@ +package io.github.wulkanowy.ui.modules.debug.notification.mock + +import io.github.wulkanowy.data.db.entities.Timetable +import java.time.LocalDate +import java.time.LocalDateTime +import kotlin.random.Random + +val debugTimetableItems = listOf( + generateTimetable("Matematyka", "12", "01"), + generateTimetable("Język angielski", "23", "12"), + generateTimetable("Geografia", "34", "23"), + generateTimetable("Sieci komputerowe", "45", "34"), + generateTimetable("Systemy operacyjne", "56", "45"), + generateTimetable("Język niemiecki", "67", "56"), + generateTimetable("Biologia", "78", "67"), + generateTimetable("Chemia", "89", "78"), + generateTimetable("Fizyka", "90", "89"), + generateTimetable("Matematyka", "01", "90"), +) + +private fun generateTimetable(subject: String, room: String, roomOld: String) = Timetable( + subject = subject, + studentId = 0, + diaryId = 0, + date = LocalDate.now().minusDays(Random.nextLong(0, 8)), + number = 1, + start = LocalDateTime.now().plusHours(1), + end = LocalDateTime.now(), + subjectOld = "", + group = "", + room = room, + roomOld = roomOld, + teacher = "Wtorkowska Renata", + teacherOld = "", + info = "", + isStudentPlan = true, + changes = true, + canceled = true +) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt index e71fc0f6f..04e35d318 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt @@ -66,7 +66,14 @@ class LoginStudentSelectFragment : } override fun openMainView() { - activity?.let { startActivity(MainActivity.getStartIntent(context = it, clear = true)) } + activity?.let { + startActivity( + MainActivity.getStartIntent( + context = it, + startNewTask = true + ) + ) + } } override fun showProgress(show: Boolean) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt index 49a199431..51585ac58 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt @@ -18,8 +18,8 @@ import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.data.repositories.LuckyNumberRepository import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.main.MainActivity -import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.toFirstResult import kotlinx.coroutines.runBlocking import timber.log.Timber @@ -39,6 +39,8 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { companion object { + const val LUCKY_NUMBER_PENDING_INTENT_ID = 200 + fun getStudentWidgetKey(appWidgetId: Int) = "lucky_number_widget_student_$appWidgetId" fun getThemeWidgetKey(appWidgetId: Int) = "lucky_number_widget_theme_$appWidgetId" @@ -48,18 +50,31 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { fun getWidthWidgetKey(appWidgetId: Int) = "lucky_number_widget_width_$appWidgetId" } - override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray?) { + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray? + ) { super.onUpdate(context, appWidgetManager, appWidgetIds) appWidgetIds?.forEach { appWidgetId -> + val luckyNumber = + getLuckyNumber(sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0), appWidgetId) + val appIntent = PendingIntent.getActivity( + context, + LUCKY_NUMBER_PENDING_INTENT_ID, + MainActivity.getStartIntent(context, Destination.LuckyNumber, true), + FLAG_UPDATE_CURRENT + ) - val luckyNumber = getLuckyNumber(sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0), appWidgetId) - val appIntent = PendingIntent.getActivity(context, MainView.Section.LUCKY_NUMBER.id, - MainActivity.getStartIntent(context, MainView.Section.LUCKY_NUMBER, true), FLAG_UPDATE_CURRENT) - - val remoteView = RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context)).apply { - setTextViewText(R.id.luckyNumberWidgetNumber, luckyNumber?.luckyNumber?.toString() ?: "#") - setOnClickPendingIntent(R.id.luckyNumberWidgetContainer, appIntent) - } + val remoteView = + RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context)) + .apply { + setTextViewText( + R.id.luckyNumberWidgetNumber, + luckyNumber?.luckyNumber?.toString() ?: "#" + ) + setOnClickPendingIntent(R.id.luckyNumberWidgetContainer, appIntent) + } setStyles(remoteView, appWidgetId) appWidgetManager.updateAppWidget(appWidgetId, remoteView) @@ -78,7 +93,12 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { } } - override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, newOptions: Bundle?) { + override fun onAppWidgetOptionsChanged( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetId: Int, + newOptions: Bundle? + ) { super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions) val remoteView = RemoteViews(context.packageName, getCorrectLayoutId(appWidgetId, context)) @@ -88,8 +108,12 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { } private fun setStyles(views: RemoteViews, appWidgetId: Int, options: Bundle? = null) { - val width = options?.getInt(OPTION_APPWIDGET_MIN_WIDTH) ?: sharedPref.getLong(getWidthWidgetKey(appWidgetId), 74).toInt() - val height = options?.getInt(OPTION_APPWIDGET_MAX_HEIGHT) ?: sharedPref.getLong(getHeightWidgetKey(appWidgetId), 74).toInt() + val width = options?.getInt(OPTION_APPWIDGET_MIN_WIDTH) ?: sharedPref.getLong( + getWidthWidgetKey(appWidgetId), 74 + ).toInt() + val height = options?.getInt(OPTION_APPWIDGET_MAX_HEIGHT) ?: sharedPref.getLong( + getHeightWidgetKey(appWidgetId), 74 + ).toInt() with(sharedPref) { putLong(getWidthWidgetKey(appWidgetId), width.toLong()) @@ -112,7 +136,11 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { } } - private fun RemoteViews.setVisibility(imageTop: Boolean, imageLeft: Boolean, title: Boolean = false) { + private fun RemoteViews.setVisibility( + imageTop: Boolean, + imageLeft: Boolean, + title: Boolean = false + ) { setViewVisibility(R.id.luckyNumberWidgetImageTop, if (imageTop) VISIBLE else GONE) setViewVisibility(R.id.luckyNumberWidgetImageLeft, if (imageLeft) VISIBLE else GONE) setViewVisibility(R.id.luckyNumberWidgetTitle, if (title) VISIBLE else GONE) @@ -152,7 +180,8 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { private fun getCorrectLayoutId(appWidgetId: Int, context: Context): Int { val savedTheme = sharedPref.getLong(getThemeWidgetKey(appWidgetId), 0) - val isSystemDarkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + val isSystemDarkMode = + context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES return if (savedTheme == 1L || (savedTheme == 2L && isSystemDarkMode)) { R.layout.widget_luckynumber_dark diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt index d758ac0da..92bc36bc7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt @@ -5,16 +5,10 @@ import android.content.Context import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK import android.content.Intent.FLAG_ACTIVITY_NEW_TASK -import android.content.pm.ShortcutInfo -import android.content.pm.ShortcutManager -import android.graphics.drawable.Icon -import android.os.Build import android.os.Build.VERSION_CODES.P import android.os.Bundle import android.view.Menu import android.view.MenuItem -import androidx.annotation.RequiresApi -import androidx.core.content.getSystemService import androidx.core.view.ViewCompat import androidx.core.view.isVisible import androidx.fragment.app.DialogFragment @@ -29,20 +23,10 @@ import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.databinding.ActivityMainBinding +import io.github.wulkanowy.services.shortcuts.ShortcutsHelper import io.github.wulkanowy.ui.base.BaseActivity +import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog -import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment -import io.github.wulkanowy.ui.modules.conference.ConferenceFragment -import io.github.wulkanowy.ui.modules.dashboard.DashboardFragment -import io.github.wulkanowy.ui.modules.exam.ExamFragment -import io.github.wulkanowy.ui.modules.grade.GradeFragment -import io.github.wulkanowy.ui.modules.homework.HomeworkFragment -import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment -import io.github.wulkanowy.ui.modules.message.MessageFragment -import io.github.wulkanowy.ui.modules.more.MoreFragment -import io.github.wulkanowy.ui.modules.note.NoteFragment -import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment -import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.InAppReviewHelper @@ -75,6 +59,9 @@ class MainActivity : BaseActivity(), MainVie @Inject lateinit var appInfo: AppInfo + @Inject + lateinit var shortcutsHelper: ShortcutsHelper + private var accountMenu: MenuItem? = null private val overlayProvider by lazy { ElevationOverlayProvider(this) } @@ -83,15 +70,19 @@ class MainActivity : BaseActivity(), MainVie FragNavController(supportFragmentManager, R.id.main_fragment_container) companion object { - const val EXTRA_START_MENU = "extraStartMenu" + + private const val EXTRA_START_DESTINATION = "start_destination" fun getStartIntent( context: Context, - startMenu: MainView.Section? = null, - clear: Boolean = false + destination: Destination? = null, + startNewTask: Boolean = false ) = Intent(context, MainActivity::class.java).apply { - if (clear) flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK - startMenu?.let { putExtra(EXTRA_START_MENU, it.id) } + putExtra(EXTRA_START_DESTINATION, destination) + + if (startNewTask) { + flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK + } } } @@ -106,42 +97,21 @@ class MainActivity : BaseActivity(), MainVie override val currentViewSubtitle get() = (navController.currentFrag as? MainView.TitledView)?.subtitleString - override var startMenuIndex = 0 - - override var startMenuMoreIndex = -1 - - private val moreMenuFragments = mapOf( - MainView.Section.MESSAGE.id to MessageFragment.newInstance(), - MainView.Section.EXAM.id to ExamFragment.newInstance(), - MainView.Section.HOMEWORK.id to HomeworkFragment.newInstance(), - MainView.Section.NOTE.id to NoteFragment.newInstance(), - MainView.Section.CONFERENCE.id to ConferenceFragment.newInstance(), - MainView.Section.SCHOOL_ANNOUNCEMENT.id to SchoolAnnouncementFragment.newInstance(), - MainView.Section.LUCKY_NUMBER.id to LuckyNumberFragment.newInstance(), - ) + private var savedInstanceState: Bundle? = null @SuppressLint("NewApi") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(ActivityMainBinding.inflate(layoutInflater).apply { binding = this }.root) setSupportActionBar(binding.mainToolbar) + this.savedInstanceState = savedInstanceState messageContainer = binding.mainMessageContainer updateHelper.messageContainer = binding.mainFragmentContainer - val section = MainView.Section.values() - .singleOrNull { it.id == intent.getIntExtra(EXTRA_START_MENU, -1) } - - presenter.onAttachView(this, section) - - with(navController) { - initialize(startMenuIndex, savedInstanceState) - pushFragment(moreMenuFragments[startMenuMoreIndex]) - } - - if (appInfo.systemVersion >= Build.VERSION_CODES.N_MR1) { - initShortcuts() - } + val destination = intent.getSerializableExtra(EXTRA_START_DESTINATION) as Destination? + ?: shortcutsHelper.getDestination(intent) + presenter.onAttachView(this, destination) updateHelper.checkAndInstallUpdates(this) } @@ -157,54 +127,6 @@ class MainActivity : BaseActivity(), MainVie updateHelper.onActivityResult(requestCode, resultCode) } - @RequiresApi(Build.VERSION_CODES.N_MR1) - fun initShortcuts() { - val shortcutsList = mutableListOf() - - listOf( - Triple( - getString(R.string.grade_title), - R.drawable.ic_shortcut_grade, - MainView.Section.GRADE - ), - Triple( - getString(R.string.attendance_title), - R.drawable.ic_shortcut_attendance, - MainView.Section.ATTENDANCE - ), - Triple( - getString(R.string.exam_title), - R.drawable.ic_shortcut_exam, - MainView.Section.EXAM - ), - Triple( - getString(R.string.timetable_title), - R.drawable.ic_shortcut_timetable, - MainView.Section.TIMETABLE - ) - ).forEach { (title, icon, enum) -> - shortcutsList.add( - ShortcutInfo.Builder(applicationContext, title) - .setShortLabel(title) - .setLongLabel(title) - .setIcon(Icon.createWithResource(applicationContext, icon)) - .setIntents( - arrayOf( - Intent(applicationContext, MainActivity::class.java) - .setAction(Intent.ACTION_VIEW), - Intent(applicationContext, MainActivity::class.java) - .putExtra(EXTRA_START_MENU, enum.id) - .setAction(Intent.ACTION_VIEW) - .addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK) - ) - ) - .build() - ) - } - - getSystemService()?.dynamicShortcuts = shortcutsList - } - override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.action_menu_main, menu) accountMenu = menu?.findItem(R.id.mainMenuAccount) @@ -213,15 +135,38 @@ class MainActivity : BaseActivity(), MainVie return true } - @SuppressLint("NewApi") - override fun initView() { + override fun initView(startMenuIndex: Int, rootDestinations: List) { + initializeToolbar() + initializeBottomNavigation(startMenuIndex) + initializeNavController(startMenuIndex, rootDestinations) + } + + private fun initializeNavController(startMenuIndex: Int, rootDestinations: List) { + with(navController) { + setOnViewChangeListener { destinationView -> + presenter.onViewChange(destinationView) + analytics.setCurrentScreen( + this@MainActivity, + destinationView::class.java.simpleName + ) + } + fragmentHideStrategy = HIDE + rootFragments = rootDestinations.map { it.fragment } + + initialize(startMenuIndex, savedInstanceState) + } + } + + private fun initializeToolbar() { with(binding.mainToolbar) { stateListAnimator = null setBackgroundColor( overlayProvider.compositeOverlayWithThemeSurfaceColorIfNeeded(dpToPx(4f)) ) } + } + private fun initializeBottomNavigation(startMenuIndex: Int) { with(binding.mainBottomNav) { with(menu) { add(Menu.NONE, 0, Menu.NONE, R.string.dashboard_title) @@ -239,36 +184,6 @@ class MainActivity : BaseActivity(), MainVie setOnItemSelectedListener { presenter.onTabSelected(it.itemId, false) } setOnItemReselectedListener { presenter.onTabSelected(it.itemId, true) } } - - with(navController) { - setOnViewChangeListener { section, name -> - if (section == MainView.Section.ACCOUNT || section == MainView.Section.STUDENT_INFO) { - binding.mainBottomNav.isVisible = false - - if (appInfo.systemVersion >= P) { - window.navigationBarColor = getThemeAttrColor(R.attr.colorSurface) - } - } else { - binding.mainBottomNav.isVisible = true - - if (appInfo.systemVersion >= P) { - window.navigationBarColor = - getThemeAttrColor(android.R.attr.navigationBarColor) - } - } - - analytics.setCurrentScreen(this@MainActivity, name) - presenter.onViewChange(section) - } - fragmentHideStrategy = HIDE - rootFragments = listOf( - DashboardFragment.newInstance(), - GradeFragment.newInstance(), - AttendanceFragment.newInstance(), - TimetableFragment.newInstance(), - MoreFragment.newInstance() - ) - } } override fun onPreferenceStartFragment( @@ -317,6 +232,22 @@ class MainActivity : BaseActivity(), MainVie ViewCompat.setElevation(binding.mainToolbar, if (show) dpToPx(4f) else 0f) } + override fun showBottomNavigation(show: Boolean) { + binding.mainBottomNav.isVisible = show + + if (appInfo.systemVersion >= P) { + window.navigationBarColor = if (show) { + getThemeAttrColor(android.R.attr.navigationBarColor) + } else { + getThemeAttrColor(R.attr.colorSurface) + } + } + } + + override fun openMoreDestination(destination: Destination) { + pushView(destination.fragment) + } + override fun notifyMenuViewReselected() { (navController.currentStack?.getOrNull(0) as? MainView.MainChildView)?.onFragmentReselected() } @@ -373,6 +304,6 @@ class MainActivity : BaseActivity(), MainVie override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) navController.onSaveInstanceState(outState) - intent.removeExtra(EXTRA_START_MENU) + intent.removeExtra(EXTRA_START_DESTINATION) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt index 4805b5a1c..0aca51afe 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt @@ -6,10 +6,15 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.ErrorHandler -import io.github.wulkanowy.ui.modules.main.MainView.Section.GRADE -import io.github.wulkanowy.ui.modules.main.MainView.Section.MESSAGE -import io.github.wulkanowy.ui.modules.main.MainView.Section.SCHOOL +import io.github.wulkanowy.ui.modules.Destination +import io.github.wulkanowy.ui.modules.account.AccountView +import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsView +import io.github.wulkanowy.ui.modules.grade.GradeView +import io.github.wulkanowy.ui.modules.message.MessageView +import io.github.wulkanowy.ui.modules.schoolandteachers.school.SchoolView +import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.flowWithResource import kotlinx.coroutines.flow.onEach @@ -27,19 +32,40 @@ class MainPresenter @Inject constructor( private var studentsWitSemesters: List? = null - fun onAttachView(view: MainView, initMenu: MainView.Section?) { - super.onAttachView(view) - view.apply { - getProperViewIndexes(initMenu).let { (main, more) -> - startMenuIndex = main - startMenuMoreIndex = more + private val rootDestinationTypeList = listOf( + Destination.Type.DASHBOARD, + Destination.Type.GRADE, + Destination.Type.ATTENDANCE, + Destination.Type.TIMETABLE, + Destination.Type.MORE + ) + + private val Destination?.startMenuIndex + get() = when { + this == null -> prefRepository.startMenuIndex + type in rootDestinationTypeList -> { + rootDestinationTypeList.indexOf(type) } - initView() - Timber.i("Main view was initialized with $startMenuIndex menu index and $startMenuMoreIndex more index") + else -> 4 + } + + fun onAttachView(view: MainView, initDestination: Destination?) { + super.onAttachView(view) + + val startMenuIndex = initDestination.startMenuIndex + val destinations = rootDestinationTypeList.map { + if (it == initDestination?.type) initDestination else it.defaultDestination + } + + view.initView(startMenuIndex, destinations) + if (initDestination != null && startMenuIndex == 4) { + view.openMoreDestination(initDestination) } syncManager.startPeriodicSyncWorker() - analytics.logEvent("app_open", "destination" to initMenu?.name) + + analytics.logEvent("app_open", "destination" to initDestination.toString()) + Timber.i("Main view was initialized with $initDestination") } fun onActionMenuCreated() { @@ -64,9 +90,10 @@ class MainPresenter @Inject constructor( }.launch("avatar") } - fun onViewChange(section: MainView.Section?) { + fun onViewChange(destinationView: BaseView) { view?.apply { - showActionBarElevation(section != GRADE && section != MESSAGE && section != SCHOOL) + showBottomNavigation(destinationView !is AccountView && destinationView !is StudentInfoView && destinationView !is AccountDetailsView) + showActionBarElevation(destinationView !is GradeView && destinationView !is MessageView && destinationView !is SchoolView) currentViewTitle?.let { setViewTitle(it) } currentViewSubtitle?.let { setViewSubTitle(it.ifBlank { null }) } currentStackSize?.let { @@ -134,10 +161,4 @@ class MainPresenter @Inject constructor( view?.showStudentAvatar(currentStudent) } - - private fun getProperViewIndexes(initMenu: MainView.Section?) = when (initMenu?.id) { - in 0..3 -> initMenu!!.id to -1 - in 4..100 -> 4 to initMenu!!.id - else -> prefRepository.startMenuIndex to -1 - } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt index 8851f5878..3a57fcc6b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt @@ -3,13 +3,10 @@ package io.github.wulkanowy.ui.modules.main import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.ui.base.BaseView +import io.github.wulkanowy.ui.modules.Destination interface MainView : BaseView { - var startMenuIndex: Int - - var startMenuMoreIndex: Int - val isRootView: Boolean val currentViewTitle: String? @@ -18,7 +15,7 @@ interface MainView : BaseView { val currentStackSize: Int? - fun initView() + fun initView(startMenuIndex: Int, rootDestinations: List) fun switchMenuView(position: Int) @@ -28,6 +25,8 @@ interface MainView : BaseView { fun showActionBarElevation(show: Boolean) + fun showBottomNavigation(show: Boolean) + fun notifyMenuViewReselected() fun notifyMenuViewChanged() @@ -42,6 +41,8 @@ interface MainView : BaseView { fun showInAppReview() + fun openMoreDestination(destination: Destination) + interface MainChildView { fun onFragmentReselected() @@ -57,25 +58,4 @@ interface MainView : BaseView { get() = "" set(_) {} } - - enum class Section { - DASHBOARD, - GRADE, - ATTENDANCE, - TIMETABLE, - MORE, - MESSAGE, - EXAM, - HOMEWORK, - NOTE, - CONFERENCE, - SCHOOL_ANNOUNCEMENT, - SCHOOL, - LUCKY_NUMBER, - ACCOUNT, - STUDENT_INFO, - SETTINGS; - - val id get() = ordinal - } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterAdapter.kt index 7ee326f8f..27b3637ad 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterAdapter.kt @@ -5,7 +5,6 @@ import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView -import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Notification import io.github.wulkanowy.databinding.ItemNotificationsCenterBinding import io.github.wulkanowy.services.sync.notifications.NotificationType @@ -28,26 +27,12 @@ class NotificationsCenterAdapter @Inject constructor() : notificationsCenterItemTitle.text = item.title notificationsCenterItemContent.text = item.content notificationsCenterItemDate.text = item.date.toFormattedString("HH:mm, d MMM") - notificationsCenterItemIcon.setImageResource(item.type.toDrawableResId()) + notificationsCenterItemIcon.setImageResource(item.type.icon) root.setOnClickListener { onItemClickListener(item.type) } } } - private fun NotificationType.toDrawableResId() = when (this) { - NotificationType.NEW_CONFERENCE -> R.drawable.ic_more_conferences - NotificationType.NEW_EXAM -> R.drawable.ic_main_exam - NotificationType.NEW_GRADE_DETAILS -> R.drawable.ic_stat_grade - NotificationType.NEW_GRADE_PREDICTED -> R.drawable.ic_stat_grade - NotificationType.NEW_GRADE_FINAL -> R.drawable.ic_stat_grade - NotificationType.NEW_HOMEWORK -> R.drawable.ic_more_homework - NotificationType.NEW_LUCKY_NUMBER -> R.drawable.ic_stat_luckynumber - NotificationType.NEW_MESSAGE -> R.drawable.ic_stat_message - NotificationType.NEW_NOTE -> R.drawable.ic_stat_note - NotificationType.NEW_ANNOUNCEMENT -> R.drawable.ic_all_about - NotificationType.PUSH -> R.drawable.ic_stat_all - } - class ViewHolder(val binding: ItemNotificationsCenterBinding) : RecyclerView.ViewHolder(binding.root) @@ -59,4 +44,4 @@ class NotificationsCenterAdapter @Inject constructor() : override fun areItemsTheSame(oldItem: Notification, newItem: Notification) = oldItem.id == newItem.id } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterFragment.kt index b9bfb447e..f3bbc42de 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/notificationscenter/NotificationsCenterFragment.kt @@ -11,6 +11,7 @@ import io.github.wulkanowy.data.db.entities.Notification import io.github.wulkanowy.databinding.FragmentNotificationsCenterBinding import io.github.wulkanowy.services.sync.notifications.NotificationType import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment import io.github.wulkanowy.ui.modules.conference.ConferenceFragment import io.github.wulkanowy.ui.modules.exam.ExamFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment @@ -21,6 +22,7 @@ import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.message.MessageFragment import io.github.wulkanowy.ui.modules.note.NoteFragment import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment +import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import javax.inject.Inject @AndroidEntryPoint @@ -104,5 +106,7 @@ class NotificationsCenterFragment : NotificationType.NEW_NOTE -> NoteFragment.newInstance() NotificationType.NEW_ANNOUNCEMENT -> SchoolAnnouncementFragment.newInstance() NotificationType.PUSH -> null + NotificationType.CHANGE_TIMETABLE -> TimetableFragment.newInstance() + NotificationType.NEW_ATTENDANCE -> AttendanceFragment.newInstance() } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt index 2612fab3a..d56cdfa7c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt @@ -6,7 +6,7 @@ import io.github.wulkanowy.R import io.github.wulkanowy.ui.modules.main.MainView import timber.log.Timber -class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView { +class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView, SettingsView { companion object { @@ -19,4 +19,16 @@ class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView { setPreferencesFromResource(R.xml.scheme_preferences, rootKey) Timber.i("Settings view was initialized") } + + override fun showError(text: String, error: Throwable) {} + + override fun showMessage(text: String) {} + + override fun showExpiredDialog() {} + + override fun openClearLoginView() {} + + override fun showErrorDetailsDialog(error: Throwable) {} + + override fun showChangePasswordSnackbar(redirectUrl: String) {} } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsView.kt new file mode 100644 index 000000000..79f91bc5d --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsView.kt @@ -0,0 +1,5 @@ +package io.github.wulkanowy.ui.modules.settings + +import io.github.wulkanowy.ui.base.BaseView + +interface SettingsView : BaseView \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt index f9079b5f9..63fc84962 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt @@ -24,8 +24,8 @@ import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.services.HiltBroadcastReceiver import io.github.wulkanowy.services.widgets.TimetableWidgetService +import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.main.MainActivity -import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.capitalise import io.github.wulkanowy.utils.createNameInitialsDrawable @@ -60,6 +60,8 @@ class TimetableWidgetProvider : HiltBroadcastReceiver() { companion object { + private const val TIMETABLE_PENDING_INTENT_ID = 201 + private const val EXTRA_TOGGLED_WIDGET_ID = "extraToggledWidget" private const val EXTRA_BUTTON_TYPE = "extraButtonType" @@ -174,8 +176,8 @@ class TimetableWidgetProvider : HiltBroadcastReceiver() { ) val appIntent = PendingIntent.getActivity( context, - MainView.Section.TIMETABLE.id, - MainActivity.getStartIntent(context, MainView.Section.TIMETABLE, true), + TIMETABLE_PENDING_INTENT_ID, + MainActivity.getStartIntent(context, Destination.Timetable(), true), FLAG_UPDATE_CURRENT ) diff --git a/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt index b89ad57d3..397c95953 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt @@ -29,7 +29,7 @@ private fun calculatePercentage(presence: Double, absence: Double): Double { return if ((presence + absence) == 0.0) 0.0 else (presence / (presence + absence)) * 100 } -inline val Attendance.description +inline val Attendance.descriptionRes get() = when (AttendanceCategory.getCategoryByName(name)) { AttendanceCategory.PRESENCE -> R.string.attendance_present AttendanceCategory.ABSENCE_UNEXCUSED -> R.string.attendance_absence_unexcused diff --git a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt index 2cd4459eb..ecd982a18 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/ContextExtension.kt @@ -18,6 +18,7 @@ import androidx.annotation.AttrRes import androidx.annotation.ColorInt import androidx.annotation.ColorRes import androidx.annotation.DrawableRes +import androidx.annotation.PluralsRes import androidx.core.content.ContextCompat import androidx.core.graphics.ColorUtils import androidx.core.graphics.applyCanvas @@ -57,6 +58,9 @@ fun Context.getCompatDrawable(@DrawableRes drawableRes: Int, @ColorRes colorRes: fun Context.getCompatBitmap(@DrawableRes drawableRes: Int, @ColorRes colorRes: Int) = getCompatDrawable(drawableRes, colorRes)?.toBitmap() +fun Context.getPlural(@PluralsRes pluralRes: Int, quantity: Int, vararg arguments: Any) = + resources.getQuantityString(pluralRes, quantity, *arguments) + fun Context.openInternetBrowser(uri: String, onActivityNotFound: (uri: String) -> Unit = {}) { Intent.parseUri(uri, 0).let { try { diff --git a/app/src/main/java/io/github/wulkanowy/utils/FragNavControlerExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/FragNavControlerExtension.kt index 9dc1e18a0..01c876dd2 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/FragNavControlerExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/FragNavControlerExtension.kt @@ -2,16 +2,19 @@ package io.github.wulkanowy.utils import androidx.fragment.app.Fragment import com.ncapdevi.fragnav.FragNavController -import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.ui.base.BaseView -inline fun FragNavController.setOnViewChangeListener(crossinline listener: (section: MainView.Section?, name: String?) -> Unit) { +inline fun FragNavController.setOnViewChangeListener(crossinline listener: (view: BaseView) -> Unit) { transactionListener = object : FragNavController.TransactionListener { - override fun onFragmentTransaction(fragment: Fragment?, transactionType: FragNavController.TransactionType) { - listener(fragment?.toSection(), fragment?.let { it::class.java.simpleName }) + override fun onFragmentTransaction( + fragment: Fragment?, + transactionType: FragNavController.TransactionType + ) { + fragment?.let { listener(it as BaseView) } } override fun onTabTransaction(fragment: Fragment?, index: Int) { - listener(fragment?.toSection(), fragment?.let { it::class.java.simpleName }) + fragment?.let { listener(it as BaseView) } } } } diff --git a/app/src/main/java/io/github/wulkanowy/utils/FragmentExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/FragmentExtension.kt deleted file mode 100644 index 210a62090..000000000 --- a/app/src/main/java/io/github/wulkanowy/utils/FragmentExtension.kt +++ /dev/null @@ -1,44 +0,0 @@ -package io.github.wulkanowy.utils - -import androidx.fragment.app.Fragment -import io.github.wulkanowy.ui.modules.account.AccountFragment -import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsFragment -import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment -import io.github.wulkanowy.ui.modules.conference.ConferenceFragment -import io.github.wulkanowy.ui.modules.dashboard.DashboardFragment -import io.github.wulkanowy.ui.modules.exam.ExamFragment -import io.github.wulkanowy.ui.modules.grade.GradeFragment -import io.github.wulkanowy.ui.modules.homework.HomeworkFragment -import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment -import io.github.wulkanowy.ui.modules.main.MainView -import io.github.wulkanowy.ui.modules.message.MessageFragment -import io.github.wulkanowy.ui.modules.more.MoreFragment -import io.github.wulkanowy.ui.modules.note.NoteFragment -import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersFragment -import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment -import io.github.wulkanowy.ui.modules.settings.SettingsFragment -import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoFragment -import io.github.wulkanowy.ui.modules.timetable.TimetableFragment - -fun Fragment.toSection(): MainView.Section? { - return when (this) { - is GradeFragment -> MainView.Section.GRADE - is AttendanceFragment -> MainView.Section.ATTENDANCE - is ExamFragment -> MainView.Section.EXAM - is TimetableFragment -> MainView.Section.TIMETABLE - is MoreFragment -> MainView.Section.MORE - is MessageFragment -> MainView.Section.MESSAGE - is HomeworkFragment -> MainView.Section.HOMEWORK - is NoteFragment -> MainView.Section.NOTE - is LuckyNumberFragment -> MainView.Section.LUCKY_NUMBER - is SettingsFragment -> MainView.Section.SETTINGS - is SchoolAndTeachersFragment -> MainView.Section.SCHOOL - is AccountFragment -> MainView.Section.ACCOUNT - is AccountDetailsFragment -> MainView.Section.ACCOUNT - is StudentInfoFragment -> MainView.Section.STUDENT_INFO - is ConferenceFragment -> MainView.Section.CONFERENCE - is SchoolAnnouncementFragment -> MainView.Section.SCHOOL_ANNOUNCEMENT - is DashboardFragment -> MainView.Section.DASHBOARD - else -> null - } -} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e88ee1ad..008069562 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -163,6 +163,26 @@ Now: %s Next: %s Later: %s + %1$s lesson %2$d - %3$s + Change of room from %1$s to %2$s + Change of teacher from %1$s to %2$s + Change of subject from %1$s to %2$s + + Timetable change + Timetable changes + + + %1$s - %2$d change in timetable + %1$s - %2$d changes in timetable + + + %1$d change in timetable + %1$d changes in timetable + + + %d change + %d changes + @@ -200,6 +220,18 @@ Excuse z powodu Dzień dobry,\nProszę o usprawiedliwienie mojego dziecka w dniu %s z lekcji %s%s%s.\n\nPozdrawiam. + + New attendance + New attendance + + + %1$d new attendance + %1$d attendance + + + %d attendance + %d attendance + @@ -706,6 +738,8 @@ Push notifications Upcoming lessons Debug + Timetable change + New attendance diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt index 2c44e9fad..7557d745a 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt @@ -42,20 +42,12 @@ class MainPresenterTest { MockKAnnotations.init(this) clearMocks(mainView) - every { mainView.startMenuIndex = any() } just Runs - every { mainView.startMenuMoreIndex = any() } just Runs - every { mainView.startMenuIndex } returns 1 - every { mainView.startMenuMoreIndex } returns 1 - every { mainView.initView() } just Runs - presenter = MainPresenter(errorHandler, studentRepository, prefRepository, syncManager, analytics) + every { mainView.initView(any(), any()) } just Runs + presenter = + MainPresenter(errorHandler, studentRepository, prefRepository, syncManager, analytics) presenter.onAttachView(mainView, null) } - @Test - fun initMenuTest() { - verify { mainView.initView() } - } - @Test fun onTabSelectedTest() { every { mainView.notifyMenuViewChanged() } just Runs From 007d62e61d79c89ab5d6f22563280f354086e32b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 11 Nov 2021 15:23:20 +0100 Subject: [PATCH 45/69] Update project to Android SDK 31 (#1570) --- app/build.gradle | 11 ++--- app/src/main/AndroidManifest.xml | 1 - .../wulkanowy/data/db/SharedPrefProvider.kt | 9 ++-- .../alarm/TimetableNotificationReceiver.kt | 8 ++-- .../TimetableNotificationSchedulerHelper.kt | 12 +++-- .../services/shortcuts/ShortcutsHelper.kt | 10 ++-- .../notifications/AppNotificationManager.kt | 7 +-- .../ChangeTimetableNotification.kt | 6 +-- .../NewAttendanceNotification.kt | 6 +-- .../NewConferenceNotification.kt | 6 +-- .../sync/notifications/NewExamNotification.kt | 6 +-- .../notifications/NewGradeNotification.kt | 14 +++--- .../notifications/NewHomeworkNotification.kt | 6 +-- .../NewLuckyNumberNotification.kt | 4 +- .../notifications/NewMessageNotification.kt | 6 +-- .../sync/notifications/NewNoteNotification.kt | 6 +-- .../NewSchoolAnnouncementNotification.kt | 6 +-- .../github/wulkanowy/ui/base/BaseActivity.kt | 8 +--- .../github/wulkanowy/ui/base/BasePresenter.kt | 2 +- .../github/wulkanowy/ui/base/ThemeManager.kt | 11 +++-- .../wulkanowy/ui/modules/Destination.kt | 30 ++++++------ .../accountedit/AccountEditColorAdapter.kt | 32 +++---------- .../modules/attendance/AttendanceFragment.kt | 6 +-- .../summary/AttendanceSummaryFragment.kt | 2 +- .../wulkanowy/ui/modules/exam/ExamFragment.kt | 2 +- .../ui/modules/grade/GradeFragment.kt | 4 +- .../statistics/GradeStatisticsFragment.kt | 2 +- .../ui/modules/homework/HomeworkFragment.kt | 2 +- .../ui/modules/login/LoginActivity.kt | 4 +- .../LoginStudentSelectFragment.kt | 13 ++---- .../history/LuckyNumberHistoryFragment.kt | 2 +- .../LuckyNumberWidgetProvider.kt | 8 ++-- .../wulkanowy/ui/modules/main/MainActivity.kt | 12 ----- .../ui/modules/message/MessageFragment.kt | 5 +- .../message/preview/MessagePreviewFragment.kt | 10 +--- .../preview/MessagePreviewPresenter.kt | 24 +++++----- .../message/preview/MessagePreviewView.kt | 7 +-- .../message/tab/MessageTabPresenter.kt | 9 ++-- .../SchoolAndTeachersFragment.kt | 4 +- .../notifications/NotificationsFragment.kt | 2 - .../ui/modules/splash/SplashActivity.kt | 43 +++++++++++++++--- .../ui/modules/splash/SplashPresenter.kt | 27 ++++++----- .../wulkanowy/ui/modules/splash/SplashView.kt | 3 +- .../ui/modules/timetable/TimetableFragment.kt | 2 +- .../additional/AdditionalLessonsFragment.kt | 2 +- .../completed/CompletedLessonsFragment.kt | 2 +- .../TimetableWidgetProvider.kt | 34 +++++++------- .../ui/widgets/FittedScrollableTabLayout.kt | 12 ++--- .../ui/widgets/MaterialLinearLayout.kt | 25 +++------- .../wulkanowy/ui/widgets/MaterialTabLayout.kt | 25 ---------- .../wulkanowy/utils/PendingIntentCompat.kt | 11 +++++ .../github/wulkanowy/utils/StringExtension.kt | 4 +- .../wulkanowy/utils/security/Scrambler.kt | 3 -- ...ompat_splash_screen_no_icon_background.xml | 24 ++++++++++ .../main/res/drawable-v23/img_splash_logo.png | Bin 19474 -> 0 bytes .../drawable-v23/layer_splash_background.xml | 12 ----- app/src/main/res/drawable/ic_splash_logo.xml | 12 +++++ app/src/main/res/drawable/img_splash_logo.png | Bin 20340 -> 0 bytes .../res/drawable/layer_splash_background.xml | 12 ----- .../main/res/layout/fragment_attendance.xml | 8 ++-- app/src/main/res/layout/fragment_exam.xml | 8 ++-- .../layout/fragment_lucky_number_history.xml | 8 ++-- app/src/main/res/layout/fragment_message.xml | 2 +- app/src/main/res/layout/fragment_school.xml | 8 ++-- .../res/layout/fragment_schoolandteachers.xml | 2 +- .../main/res/layout/fragment_timetable.xml | 8 ++-- .../layout/fragment_timetable_additional.xml | 8 ++-- .../layout/fragment_timetable_completed.xml | 8 ++-- .../layout/subitem_dashboard_conferences.xml | 7 +-- app/src/main/res/values-v23/styles.xml | 8 +--- app/src/main/res/values-v26/styles.xml | 7 --- app/src/main/res/values-v28/styles.xml | 8 ---- app/src/main/res/values-v29/styles.xml | 8 ---- app/src/main/res/values/styles.xml | 12 ++--- .../wulkanowy/utils/CrashlyticsUtils.kt | 6 --- .../ui/modules/splash/SplashPresenterTest.kt | 6 +-- 76 files changed, 316 insertions(+), 384 deletions(-) delete mode 100644 app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialTabLayout.kt create mode 100644 app/src/main/java/io/github/wulkanowy/utils/PendingIntentCompat.kt create mode 100644 app/src/main/res/drawable-v23/compat_splash_screen_no_icon_background.xml delete mode 100644 app/src/main/res/drawable-v23/img_splash_logo.png delete mode 100644 app/src/main/res/drawable-v23/layer_splash_background.xml create mode 100644 app/src/main/res/drawable/ic_splash_logo.xml delete mode 100644 app/src/main/res/drawable/img_splash_logo.png delete mode 100644 app/src/main/res/drawable/layer_splash_background.xml diff --git a/app/build.gradle b/app/build.gradle index 53814dae3..bef984aa2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,17 +15,16 @@ apply from: 'sonarqube.gradle' apply from: 'hooks.gradle' android { - compileSdkVersion 30 + compileSdkVersion 31 defaultConfig { applicationId "io.github.wulkanowy" testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 - targetSdkVersion 30 + targetSdkVersion 31 versionCode 97 versionName "1.3.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - vectorDrawables.useSupportLibrary = true resValue "string", "app_name", "Wulkanowy" @@ -166,7 +165,7 @@ huaweiPublish { } ext { - work_manager = "2.6.0" + work_manager = "2.7.0" android_hilt = "1.0.0" room = "2.3.0" chucker = "3.5.2" @@ -183,9 +182,9 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" implementation "androidx.core:core-ktx:1.6.0" + implementation 'androidx.core:core-splashscreen:1.0.0-alpha02' implementation "androidx.activity:activity-ktx:1.3.1" - implementation "androidx.appcompat:appcompat:1.3.1" - implementation "androidx.appcompat:appcompat-resources:1.3.1" + implementation "androidx.appcompat:appcompat:1.4.0-beta01" implementation "androidx.fragment:fragment-ktx:1.3.6" implementation "androidx.annotation:annotation:1.2.0" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e43a0ec08..5928c23a0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -42,7 +42,6 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="false" android:theme="@style/WulkanowyTheme" - android:usesCleartextTraffic="true" tools:ignore="GoogleAppIndexingWarning,UnusedAttribute"> , requestCode: Int) { if (now() in range) cancelNotification() + alarmManager.cancel( - PendingIntent.getBroadcast(context, requestCode, Intent(), FLAG_UPDATE_CURRENT) + PendingIntent.getBroadcast( + context, + requestCode, + Intent(), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) ) } @@ -156,7 +162,7 @@ class TimetableNotificationSchedulerHelper @Inject constructor( alarmManager, RTC_WAKEUP, time.toTimestamp(), PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also { it.putExtra(LESSON_TYPE, notificationType) - }, FLAG_UPDATE_CURRENT) + }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE) ) Timber.d( "TimetableNotification scheduled: type: $notificationType, subject: ${ diff --git a/app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt b/app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt index d1c215f26..bb09434b5 100644 --- a/app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt +++ b/app/src/main/java/io/github/wulkanowy/services/shortcuts/ShortcutsHelper.kt @@ -8,7 +8,7 @@ import androidx.core.graphics.drawable.IconCompat import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import javax.inject.Inject import javax.inject.Singleton @@ -35,7 +35,7 @@ class ShortcutsHelper @Inject constructor(@ApplicationContext private val contex .setShortLabel(context.getString(R.string.grade_title)) .setLongLabel(context.getString(R.string.grade_title)) .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_grade)) - .setIntent(MainActivity.getStartIntent(context, startNewTask = true) + .setIntent(SplashActivity.getStartIntent(context, startNewTask = true) .apply { action = Intent.ACTION_VIEW putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "grade") @@ -47,7 +47,7 @@ class ShortcutsHelper @Inject constructor(@ApplicationContext private val contex .setShortLabel(context.getString(R.string.attendance_title)) .setLongLabel(context.getString(R.string.attendance_title)) .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_attendance)) - .setIntent(MainActivity.getStartIntent(context, startNewTask = true) + .setIntent(SplashActivity.getStartIntent(context, startNewTask = true) .apply { action = Intent.ACTION_VIEW putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "attendance") @@ -59,7 +59,7 @@ class ShortcutsHelper @Inject constructor(@ApplicationContext private val contex .setShortLabel(context.getString(R.string.exam_title)) .setLongLabel(context.getString(R.string.exam_title)) .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_exam)) - .setIntent(MainActivity.getStartIntent(context, startNewTask = true) + .setIntent(SplashActivity.getStartIntent(context, startNewTask = true) .apply { action = Intent.ACTION_VIEW putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "exam") @@ -71,7 +71,7 @@ class ShortcutsHelper @Inject constructor(@ApplicationContext private val contex .setShortLabel(context.getString(R.string.timetable_title)) .setLongLabel(context.getString(R.string.timetable_title)) .setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_timetable)) - .setIntent(MainActivity.getStartIntent(context, startNewTask = true) + .setIntent(SplashActivity.getStartIntent(context, startNewTask = true) .apply { action = Intent.ACTION_VIEW putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "timetable") diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt index 542b9346b..2848fe303 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/AppNotificationManager.kt @@ -13,6 +13,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.data.repositories.NotificationRepository +import io.github.wulkanowy.utils.PendingIntentCompat import io.github.wulkanowy.utils.getCompatBitmap import io.github.wulkanowy.utils.getCompatColor import io.github.wulkanowy.utils.nickOrName @@ -45,7 +46,7 @@ class AppNotificationManager @Inject constructor( context, Random.nextInt(), notificationData.intentToStart, - PendingIntent.FLAG_UPDATE_CURRENT + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) ) .setContentTitle(notificationData.title) @@ -86,7 +87,7 @@ class AppNotificationManager @Inject constructor( context, Random.nextInt(), notificationData.intentToStart, - PendingIntent.FLAG_UPDATE_CURRENT + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) ) .setContentTitle(notificationData.title) @@ -134,7 +135,7 @@ class AppNotificationManager @Inject constructor( context, Random.nextInt(), groupNotificationData.intentToStart, - PendingIntent.FLAG_UPDATE_CURRENT + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) ) .setLocalOnly(true) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt index 6d2d3a59f..f8860b7f1 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/ChangeTimetableNotification.kt @@ -8,7 +8,7 @@ import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDate @@ -32,7 +32,7 @@ class ChangeTimetableNotification @Inject constructor( 1 ), content = it, - intentToStart = MainActivity.getStartIntent( + intentToStart = SplashActivity.getStartIntent( context = context, destination = Destination.Timetable(date), startNewTask = true @@ -54,7 +54,7 @@ class ChangeTimetableNotification @Inject constructor( changedLessons.size, changedLessons.size ), - intentToStart = MainActivity.getStartIntent(context, Destination.Timetable(), true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Timetable(), true), type = NotificationType.CHANGE_TIMETABLE ) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewAttendanceNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewAttendanceNotification.kt index 695552721..3792725ca 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewAttendanceNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewAttendanceNotification.kt @@ -8,7 +8,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.descriptionRes import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString @@ -31,7 +31,7 @@ class NewAttendanceNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.attendance_notify_new_items_title, 1), content = it, - intentToStart = MainActivity.getStartIntent(context, Destination.Attendance, true) + intentToStart = SplashActivity.getStartIntent(context, Destination.Attendance, true) ) } @@ -46,7 +46,7 @@ class NewAttendanceNotification @Inject constructor( notificationDataList.size, notificationDataList.size ), - intentToStart = MainActivity.getStartIntent(context, Destination.Attendance, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Attendance, true), type = NotificationType.NEW_ATTENDANCE ) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt index 97b1332d8..4ec359b08 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewConferenceNotification.kt @@ -8,7 +8,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDateTime @@ -31,7 +31,7 @@ class NewConferenceNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.conference_notify_new_item_title, 1), content = it, - intentToStart = MainActivity.getStartIntent(context, Destination.Conference, true) + intentToStart = SplashActivity.getStartIntent(context, Destination.Conference, true) ) } @@ -43,7 +43,7 @@ class NewConferenceNotification @Inject constructor( lines.size, lines.size ), - intentToStart = MainActivity.getStartIntent(context, Destination.Conference, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Conference, true), type = NotificationType.NEW_CONFERENCE ) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt index 6f8ed8961..feb7c320a 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewExamNotification.kt @@ -8,7 +8,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDate @@ -31,7 +31,7 @@ class NewExamNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.exam_notify_new_item_title, 1), content = it, - intentToStart = MainActivity.getStartIntent(context, Destination.Exam, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Exam, true), ) } @@ -43,7 +43,7 @@ class NewExamNotification @Inject constructor( lines.size, lines.size ), - intentToStart = MainActivity.getStartIntent(context, Destination.Exam, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Exam, true), type = NotificationType.NEW_EXAM ) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt index 09692fb7c..425a57fc6 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewGradeNotification.kt @@ -9,7 +9,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.getPlural import javax.inject.Inject @@ -23,7 +23,7 @@ class NewGradeNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.grade_new_items, 1), content = "${it.subject}: ${it.entry}", - intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Grade, true), ) } @@ -31,7 +31,7 @@ class NewGradeNotification @Inject constructor( notificationDataList = notificationDataList, title = context.getPlural(R.plurals.grade_new_items, items.size), content = context.getPlural(R.plurals.grade_notify_new_items, items.size, items.size), - intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Grade, true), type = NotificationType.NEW_GRADE_DETAILS ) @@ -43,7 +43,7 @@ class NewGradeNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.grade_new_items_predicted, 1), content = "${it.subject}: ${it.predictedGrade}", - intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Grade, true), ) } @@ -55,7 +55,7 @@ class NewGradeNotification @Inject constructor( items.size, items.size ), - intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Grade, true), type = NotificationType.NEW_GRADE_PREDICTED ) @@ -67,7 +67,7 @@ class NewGradeNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.grade_new_items_final, 1), content = "${it.subject}: ${it.finalGrade}", - intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Grade, true), ) } @@ -79,7 +79,7 @@ class NewGradeNotification @Inject constructor( items.size, items.size ), - intentToStart = MainActivity.getStartIntent(context, Destination.Grade, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Grade, true), type = NotificationType.NEW_GRADE_FINAL ) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt index cdada844e..fcaed10f0 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewHomeworkNotification.kt @@ -8,7 +8,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import java.time.LocalDate @@ -31,7 +31,7 @@ class NewHomeworkNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.homework_notify_new_item_title, 1), content = it, - intentToStart = MainActivity.getStartIntent(context, Destination.Homework, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Homework, true), ) } @@ -42,7 +42,7 @@ class NewHomeworkNotification @Inject constructor( lines.size, lines.size ), - intentToStart = MainActivity.getStartIntent(context, Destination.Homework, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Homework, true), type = NotificationType.NEW_HOMEWORK, notificationDataList = notificationDataList ) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt index d9f138b5c..ab2c1ca76 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewLuckyNumberNotification.kt @@ -7,7 +7,7 @@ import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import javax.inject.Inject class NewLuckyNumberNotification @Inject constructor( @@ -22,7 +22,7 @@ class NewLuckyNumberNotification @Inject constructor( R.string.lucky_number_notify_new_item, item.luckyNumber.toString() ), - intentToStart = MainActivity.getStartIntent(context, Destination.LuckyNumber, true) + intentToStart = SplashActivity.getStartIntent(context, Destination.LuckyNumber, true) ) appNotificationManager.sendSingleNotification( diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt index cdb5ab9c8..1a06cf6fa 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewMessageNotification.kt @@ -8,7 +8,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.getPlural import javax.inject.Inject @@ -22,7 +22,7 @@ class NewMessageNotification @Inject constructor( NotificationData( title = context.getPlural(R.plurals.message_new_items, 1), content = "${it.sender}: ${it.subject}", - intentToStart = MainActivity.getStartIntent(context, Destination.Message, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Message, true), ) } @@ -30,7 +30,7 @@ class NewMessageNotification @Inject constructor( notificationDataList = notificationDataList, title = context.getPlural(R.plurals.message_new_items, items.size), content = context.getPlural(R.plurals.message_notify_new_items, items.size, items.size), - intentToStart = MainActivity.getStartIntent(context, Destination.Message, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Message, true), type = NotificationType.NEW_MESSAGE ) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt index 16be1ca50..533d7f387 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewNoteNotification.kt @@ -9,7 +9,7 @@ import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.sdk.scrapper.notes.NoteCategory import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.getPlural import javax.inject.Inject @@ -29,13 +29,13 @@ class NewNoteNotification @Inject constructor( NotificationData( title = context.getPlural(titleRes, 1), content = "${it.teacher}: ${it.category}", - intentToStart = MainActivity.getStartIntent(context, Destination.Note, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Note, true), ) } val groupNotificationData = GroupNotificationData( notificationDataList = notificationDataList, - intentToStart = MainActivity.getStartIntent(context, Destination.Note, true), + intentToStart = SplashActivity.getStartIntent(context, Destination.Note, true), title = context.getPlural(R.plurals.note_new_items, items.size), content = context.getPlural(R.plurals.note_notify_new_items, items.size, items.size), type = NotificationType.NEW_NOTE diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt index 1f603624b..1efe13ae9 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/notifications/NewSchoolAnnouncementNotification.kt @@ -8,7 +8,7 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.getPlural import javax.inject.Inject @@ -20,7 +20,7 @@ class NewSchoolAnnouncementNotification @Inject constructor( suspend fun notify(items: List, student: Student) { val notificationDataList = items.map { NotificationData( - intentToStart = MainActivity.getStartIntent( + intentToStart = SplashActivity.getStartIntent( context = context, destination = Destination.SchoolAnnouncement, startNewTask = true @@ -34,7 +34,7 @@ class NewSchoolAnnouncementNotification @Inject constructor( } val groupNotificationData = GroupNotificationData( type = NotificationType.NEW_ANNOUNCEMENT, - intentToStart = MainActivity.getStartIntent( + intentToStart = SplashActivity.getStartIntent( context = context, destination = Destination.SchoolAnnouncement, startNewTask = true diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt index 0521b4a08..075557a5c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseActivity.kt @@ -1,14 +1,11 @@ package io.github.wulkanowy.ui.base import android.app.ActivityManager -import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK -import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.os.Bundle import android.view.View import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.app.AppCompatDelegate import androidx.viewbinding.ViewBinding import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar.LENGTH_LONG @@ -40,7 +37,6 @@ abstract class BaseActivity, VB : ViewBinding> : themeManager.applyActivityTheme(this) super.onCreate(savedInstanceState) supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleLogger, true) - AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) @Suppress("DEPRECATION") setTaskDescription( @@ -83,8 +79,8 @@ abstract class BaseActivity, VB : ViewBinding> : } override fun openClearLoginView() { - startActivity(LoginActivity.getStartIntent(this) - .apply { addFlags(FLAG_ACTIVITY_CLEAR_TASK or FLAG_ACTIVITY_NEW_TASK) }) + startActivity(LoginActivity.getStartIntent(this)) + finishAffinity() } override fun onDestroy() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt index 5cd5d0109..40d548607 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt @@ -20,7 +20,7 @@ open class BasePresenter( ) { private val job = SupervisorJob() - protected val presenterScope = CoroutineScope(job + Dispatchers.Main) + protected val presenterScope = CoroutineScope(job + Dispatchers.Main.immediate) private val childrenJobs = mutableMapOf() diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt b/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt index b560ed2e7..e934a1822 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/ThemeManager.kt @@ -41,14 +41,15 @@ class ThemeManager @Inject constructor(private val preferencesRepository: Prefer ) } - private fun isThemeApplicable(activity: AppCompatActivity): Boolean { - return activity.packageManager + private fun isThemeApplicable(activity: AppCompatActivity) = + activity.packageManager .getPackageInfo(activity.packageName, GET_ACTIVITIES) - .activities.singleOrNull { it.name == activity::class.java.canonicalName } - ?.theme.let { + .activities + .singleOrNull { it.name == activity::class.java.canonicalName } + ?.theme + .let { it == R.style.WulkanowyTheme_Black || it == R.style.WulkanowyTheme_NoActionBar || it == R.style.WulkanowyTheme_Login || it == R.style.WulkanowyTheme_Login_Black || it == R.style.WulkanowyTheme_MessageSend || it == R.style.WulkanowyTheme_MessageSend_Black } - } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt index 0406afa41..43d4b5f9f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt @@ -19,6 +19,10 @@ import java.time.LocalDate sealed interface Destination : Serializable { + /* + Type in children classes have to be as getter to avoid null in enums + https://stackoverflow.com/questions/68866453/kotlin-enum-val-is-returning-null-despite-being-set-at-compile-time + */ val type: Type val fragment: Fragment @@ -41,91 +45,91 @@ sealed interface Destination : Serializable { object Dashboard : Destination { - override val type = Type.DASHBOARD + override val type get() = Type.DASHBOARD override val fragment get() = DashboardFragment.newInstance() } object Grade : Destination { - override val type = Type.GRADE + override val type get() = Type.GRADE override val fragment get() = GradeFragment.newInstance() } object Attendance : Destination { - override val type = Type.ATTENDANCE + override val type get() = Type.ATTENDANCE override val fragment get() = AttendanceFragment.newInstance() } object Exam : Destination { - override val type = Type.EXAM + override val type get() = Type.EXAM override val fragment get() = ExamFragment.newInstance() } data class Timetable(val date: LocalDate? = null) : Destination { - override val type = Type.TIMETABLE + override val type get() = Type.TIMETABLE override val fragment get() = TimetableFragment.newInstance(date) } object Homework : Destination { - override val type = Type.HOMEWORK + override val type get() = Type.HOMEWORK override val fragment get() = HomeworkFragment.newInstance() } object Note : Destination { - override val type = Type.NOTE + override val type get() = Type.NOTE override val fragment get() = NoteFragment.newInstance() } object Conference : Destination { - override val type = Type.CONFERENCE + override val type get() = Type.CONFERENCE override val fragment get() = ConferenceFragment.newInstance() } object SchoolAnnouncement : Destination { - override val type = Type.SCHOOL_ANNOUNCEMENT + override val type get() = Type.SCHOOL_ANNOUNCEMENT override val fragment get() = SchoolAnnouncementFragment.newInstance() } object School : Destination { - override val type = Type.SCHOOL + override val type get() = Type.SCHOOL override val fragment get() = SchoolFragment.newInstance() } object LuckyNumber : Destination { - override val type = Type.LUCKY_NUMBER + override val type get() = Type.LUCKY_NUMBER override val fragment get() = LuckyNumberFragment.newInstance() } object More : Destination { - override val type = Type.MORE + override val type get() = Type.MORE override val fragment get() = MoreFragment.newInstance() } object Message : Destination { - override val type = Type.MESSAGE + override val type get() = Type.MESSAGE override val fragment get() = MessageFragment.newInstance() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditColorAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditColorAdapter.kt index ab6eec417..66e39fc72 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditColorAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/account/accountedit/AccountEditColorAdapter.kt @@ -3,12 +3,9 @@ package io.github.wulkanowy.ui.modules.account.accountedit import android.annotation.SuppressLint import android.content.res.ColorStateList import android.graphics.Color -import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable import android.graphics.drawable.RippleDrawable -import android.graphics.drawable.StateListDrawable -import android.os.Build import android.view.LayoutInflater import android.view.ViewGroup import androidx.core.view.isVisible @@ -52,30 +49,13 @@ class AccountEditColorAdapter @Inject constructor() : } } - private fun Int.createForegroundDrawable(): Drawable = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - val mask = GradientDrawable().apply { - shape = GradientDrawable.OVAL - setColor(Color.BLACK) - } - RippleDrawable(ColorStateList.valueOf(this.rippleColor), null, mask) - } else { - val foreground = StateListDrawable().apply { - alpha = 80 - setEnterFadeDuration(250) - setExitFadeDuration(250) - } - - val mask = GradientDrawable().apply { - shape = GradientDrawable.OVAL - setColor(this@createForegroundDrawable.rippleColor) - } - - foreground.apply { - addState(intArrayOf(android.R.attr.state_pressed), mask) - addState(intArrayOf(), ColorDrawable(Color.TRANSPARENT)) - } + private fun Int.createForegroundDrawable(): Drawable { + val mask = GradientDrawable().apply { + shape = GradientDrawable.OVAL + setColor(Color.BLACK) } + return RippleDrawable(ColorStateList.valueOf(this.rippleColor), null, mask) + } private inline val Int.rippleColor: Int get() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt index 092c9d0d8..c6ee60ee4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt @@ -121,9 +121,7 @@ class AttendanceFragment : BaseFragment(R.layout.frag attendanceSwipe.setOnRefreshListener(presenter::onSwipeRefresh) attendanceSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) attendanceSwipe.setProgressBackgroundColorSchemeColor( - requireContext().getThemeAttrColor( - R.attr.colorSwipeRefresh - ) + requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh) ) attendanceErrorRetry.setOnClickListener { presenter.onRetry() } attendanceErrorDetails.setOnClickListener { presenter.onDetailsClick() } @@ -134,7 +132,7 @@ class AttendanceFragment : BaseFragment(R.layout.frag attendanceExcuseButton.setOnClickListener { presenter.onExcuseButtonClick() } - attendanceNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + attendanceNavContainer.elevation = requireContext().dpToPx(8f) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryFragment.kt index 118971e62..dd1644a98 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/summary/AttendanceSummaryFragment.kt @@ -71,7 +71,7 @@ class AttendanceSummaryFragment : setOnItemSelectedListener { presenter.onSubjectSelected(it?.text?.toString()) } } - binding.attendanceSummarySubjectsContainer.setElevationCompat(requireContext().dpToPx(1f)) + binding.attendanceSummarySubjectsContainer.elevation = requireContext().dpToPx(1f) } override fun updateSubjects(data: ArrayList) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt index fb7939bc5..ddd0e4a19 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/exam/ExamFragment.kt @@ -64,7 +64,7 @@ class ExamFragment : BaseFragment(R.layout.fragment_exam), examPreviousButton.setOnClickListener { presenter.onPreviousWeek() } examNextButton.setOnClickListener { presenter.onNextWeek() } - examNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + examNavContainer.elevation = requireContext().dpToPx(8f) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt index f9ecb681b..0a8561eec 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeFragment.kt @@ -96,9 +96,7 @@ class GradeFragment : BaseFragment(R.layout.fragment_grade TabLayoutMediator(binding.gradeTabLayout, binding.gradeViewPager, this).attach() } - with(binding.gradeTabLayout) { - setElevationCompat(context.dpToPx(4f)) - } + binding.gradeTabLayout.elevation = requireContext().dpToPx(4f) with(binding) { gradeErrorRetry.setOnClickListener { presenter.onRetry() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsFragment.kt index 0adac300a..dbc4c10c6 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/statistics/GradeStatisticsFragment.kt @@ -68,7 +68,7 @@ class GradeStatisticsFragment : } with(binding) { - gradeStatisticsSubjectsContainer.setElevationCompat(requireContext().dpToPx(1f)) + gradeStatisticsSubjectsContainer.elevation = requireContext().dpToPx(1f) gradeStatisticsSwipe.setOnRefreshListener(presenter::onSwipeRefresh) gradeStatisticsSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt index a723b0483..d4eaade2c 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/homework/HomeworkFragment.kt @@ -67,7 +67,7 @@ class HomeworkFragment : BaseFragment(R.layout.fragment openAddHomeworkButton.setOnClickListener { presenter.onHomeworkAddButtonClicked() } - homeworkNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + homeworkNavContainer.elevation = requireContext().dpToPx(8f) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt index f7bab526c..70b54c493 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginActivity.kt @@ -35,13 +35,13 @@ class LoginActivity : BaseActivity(), Logi @Inject lateinit var updateHelper: UpdateHelper + override val currentViewIndex get() = binding.loginViewpager.currentItem + companion object { fun getStartIntent(context: Context) = Intent(context, LoginActivity::class.java) } - override val currentViewIndex get() = binding.loginViewpager.currentItem - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(ActivityLoginBinding.inflate(layoutInflater).apply { binding = this }.root) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt index 04e35d318..87cb505c4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt @@ -66,14 +66,8 @@ class LoginStudentSelectFragment : } override fun openMainView() { - activity?.let { - startActivity( - MainActivity.getStartIntent( - context = it, - startNewTask = true - ) - ) - } + startActivity(MainActivity.getStartIntent(requireContext())) + requireActivity().finish() } override fun showProgress(show: Boolean) { @@ -115,7 +109,8 @@ class LoginStudentSelectFragment : chooserTitle = requireContext().getString(R.string.login_email_intent_title), email = "wulkanowyinc@gmail.com", subject = requireContext().getString(R.string.login_email_subject), - body = requireContext().getString(R.string.login_email_text, appInfo.systemModel, + body = requireContext().getString( + R.string.login_email_text, appInfo.systemModel, appInfo.systemVersion.toString(), appInfo.versionName, "Select users to log in", diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt index 3a84b2dd5..49d094b78 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryFragment.kt @@ -65,7 +65,7 @@ class LuckyNumberHistoryFragment : luckyNumberHistoryPreviousButton.setOnClickListener { presenter.onPreviousWeek() } luckyNumberHistoryNextButton.setOnClickListener { presenter.onNextWeek() } - luckyNumberHistoryNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + luckyNumberHistoryNavContainer.elevation = requireContext().dpToPx(8f) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt index 51585ac58..9ef410683 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt @@ -1,7 +1,6 @@ package io.github.wulkanowy.ui.modules.luckynumberwidget import android.app.PendingIntent -import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH @@ -19,7 +18,8 @@ import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.data.repositories.LuckyNumberRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity +import io.github.wulkanowy.utils.PendingIntentCompat import io.github.wulkanowy.utils.toFirstResult import kotlinx.coroutines.runBlocking import timber.log.Timber @@ -62,8 +62,8 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { val appIntent = PendingIntent.getActivity( context, LUCKY_NUMBER_PENDING_INTENT_ID, - MainActivity.getStartIntent(context, Destination.LuckyNumber, true), - FLAG_UPDATE_CURRENT + SplashActivity.getStartIntent(context, Destination.LuckyNumber, true), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) val remoteView = diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt index 92bc36bc7..3969e1d3e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt @@ -3,8 +3,6 @@ package io.github.wulkanowy.ui.modules.main import android.annotation.SuppressLint import android.content.Context import android.content.Intent -import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK -import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.os.Build.VERSION_CODES.P import android.os.Bundle import android.view.Menu @@ -23,7 +21,6 @@ import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.databinding.ActivityMainBinding -import io.github.wulkanowy.services.shortcuts.ShortcutsHelper import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog @@ -59,9 +56,6 @@ class MainActivity : BaseActivity(), MainVie @Inject lateinit var appInfo: AppInfo - @Inject - lateinit var shortcutsHelper: ShortcutsHelper - private var accountMenu: MenuItem? = null private val overlayProvider by lazy { ElevationOverlayProvider(this) } @@ -76,13 +70,8 @@ class MainActivity : BaseActivity(), MainVie fun getStartIntent( context: Context, destination: Destination? = null, - startNewTask: Boolean = false ) = Intent(context, MainActivity::class.java).apply { putExtra(EXTRA_START_DESTINATION, destination) - - if (startNewTask) { - flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK - } } } @@ -109,7 +98,6 @@ class MainActivity : BaseActivity(), MainVie updateHelper.messageContainer = binding.mainFragmentContainer val destination = intent.getSerializableExtra(EXTRA_START_DESTINATION) as Destination? - ?: shortcutsHelper.getDestination(intent) presenter.onAttachView(this, destination) updateHelper.checkAndInstallUpdates(this) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt index 51076c649..a45ab6234 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt @@ -77,9 +77,8 @@ class MessageFragment : BaseFragment(R.layout.fragment_m TabLayoutMediator(binding.messageTabLayout, binding.messageViewPager, this).attach() } - with(binding.messageTabLayout) { - setElevationCompat(context.dpToPx(4f)) - } + binding.messageTabLayout.elevation = requireContext().dpToPx(4f) + binding.openSendMessageButton.setOnClickListener { presenter.onSendMessageButtonClicked() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt index 74f8f57ec..e1cc2e374 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.message.preview -import android.os.Build import android.os.Bundle import android.print.PrintAttributes import android.print.PrintManager @@ -13,7 +12,6 @@ import android.view.View.VISIBLE import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient -import androidx.annotation.RequiresApi import androidx.core.content.getSystemService import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint @@ -25,7 +23,6 @@ import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.message.send.SendMessageActivity -import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.shareText import javax.inject.Inject @@ -40,9 +37,6 @@ class MessagePreviewFragment : @Inject lateinit var previewAdapter: MessagePreviewAdapter - @Inject - lateinit var appInfo: AppInfo - private var menuReplyButton: MenuItem? = null private var menuForwardButton: MenuItem? = null @@ -140,7 +134,7 @@ class MessagePreviewFragment : menuForwardButton?.isVisible = show menuDeleteButton?.isVisible = show menuShareButton?.isVisible = show - menuPrintButton?.isVisible = show && appInfo.systemVersion >= Build.VERSION_CODES.LOLLIPOP + menuPrintButton?.isVisible = show } override fun setDeletedOptionsLabels() { @@ -175,7 +169,6 @@ class MessagePreviewFragment : context?.shareText(text, subject) } - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) override fun printDocument(html: String, jobName: String) { val webView = WebView(requireContext()) webView.webViewClient = object : WebViewClient() { @@ -190,7 +183,6 @@ class MessagePreviewFragment : webView.loadDataWithBaseURL("file:///android_asset/", html, "text/HTML", "UTF-8", null) } - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) private fun createWebPrintJob(webView: WebView, jobName: String) { activity?.getSystemService()?.let { printManager -> val printAdapter = webView.createPrintDocumentAdapter(jobName) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt index 702e54676..eb33ee6ea 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt @@ -1,7 +1,6 @@ package io.github.wulkanowy.ui.modules.message.preview import android.annotation.SuppressLint -import android.os.Build import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.MessageAttachment @@ -11,7 +10,6 @@ import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper -import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.flowWithResourceIn @@ -24,8 +22,7 @@ class MessagePreviewPresenter @Inject constructor( errorHandler: ErrorHandler, studentRepository: StudentRepository, private val messageRepository: MessageRepository, - private val analytics: AnalyticsHelper, - private var appInfo: AppInfo + private val analytics: AnalyticsHelper ) : BasePresenter(errorHandler, studentRepository) { var message: Message? = null @@ -112,10 +109,11 @@ class MessagePreviewPresenter @Inject constructor( fun onShare(): Boolean { message?.let { - var text = "Temat: ${it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }}\n" + when (it.sender.isNotEmpty()) { - true -> "Od: ${it.sender}\n" - false -> "Do: ${it.recipient}\n" - } + "Data: ${it.date.toFormattedString("yyyy-MM-dd HH:mm:ss")}\n\n${it.content}" + var text = + "Temat: ${it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }}\n" + when (it.sender.isNotEmpty()) { + true -> "Od: ${it.sender}\n" + false -> "Do: ${it.recipient}\n" + } + "Data: ${it.date.toFormattedString("yyyy-MM-dd HH:mm:ss")}\n\n${it.content}" attachments?.let { attachments -> if (attachments.isNotEmpty()) { @@ -127,7 +125,10 @@ class MessagePreviewPresenter @Inject constructor( } } - view?.shareText(text, "FW: ${it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }}") + view?.shareText( + text, + "FW: ${it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }}" + ) return true } return false @@ -135,7 +136,6 @@ class MessagePreviewPresenter @Inject constructor( @SuppressLint("NewApi") fun onPrint(): Boolean { - if (appInfo.systemVersion < Build.VERSION_CODES.LOLLIPOP) return false message?.let { val dateString = it.date.toFormattedString("yyyy-MM-dd HH:mm:ss") val infoContent = "

Data wysłania

$dateString
" + when { @@ -154,7 +154,9 @@ class MessagePreviewPresenter @Inject constructor( view?.apply { val html = printHTML - .replace("%SUBJECT%", it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }) + .replace( + "%SUBJECT%", + it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }) .replace("%CONTENT%", messageContent) .replace("%INFO%", infoContent) printDocument(html, jobName) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt index 583ba6878..88fe77d94 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt @@ -1,7 +1,5 @@ package io.github.wulkanowy.ui.modules.message.preview -import android.os.Build -import androidx.annotation.RequiresApi import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.MessageWithAttachment import io.github.wulkanowy.ui.base.BaseView @@ -42,8 +40,7 @@ interface MessagePreviewView : BaseView { fun shareText(text: String, subject: String) - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - fun printDocument(html: String, jobName: String) - fun popView() + + fun printDocument(html: String, jobName: String) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt index 4571390b5..9b76a7f08 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt @@ -127,10 +127,13 @@ class MessageTabPresenter @Inject constructor( onlyUnread, onlyWithAttachments ) - val newItems = listOf(MessageTabDataItem.Header) + filteredData.map { - MessageTabDataItem.MessageItem(it) + val messageItems = filteredData.map { message -> + MessageTabDataItem.MessageItem(message) } - updateData(newItems, folder.id == MessageFolder.SENT.id) + val messageItemsWithHeader = + listOf(MessageTabDataItem.Header) + messageItems + + updateData(messageItemsWithHeader, folder.id == MessageFolder.SENT.id) notifyParentDataLoaded() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt index 9892b77ad..f4fa8e01d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/schoolandteachers/SchoolAndTeachersFragment.kt @@ -77,9 +77,7 @@ class SchoolAndTeachersFragment : ).attach() } - with(binding.schoolandteachersTabLayout) { - setElevationCompat(context.dpToPx(4f)) - } + binding.schoolandteachersTabLayout.elevation = requireContext().dpToPx(4f) } override fun showContent(show: Boolean) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt index 43920588c..dd6cf0ecd 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.settings.notifications -import android.annotation.SuppressLint import android.content.Intent import android.content.SharedPreferences import android.net.Uri @@ -152,7 +151,6 @@ class NotificationsFragment : PreferenceFragmentCompat(), .show() } - @SuppressLint("InlinedApi") override fun openSystemSettings() { val intent = if (appInfo.systemVersion >= Build.VERSION_CODES.O) { Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashActivity.kt index 376ef3743..b27f06ef4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashActivity.kt @@ -1,29 +1,60 @@ package io.github.wulkanowy.ui.modules.splash +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent import android.os.Bundle import android.widget.Toast import android.widget.Toast.LENGTH_LONG +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.viewbinding.ViewBinding import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.services.shortcuts.ShortcutsHelper import io.github.wulkanowy.ui.base.BaseActivity +import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.main.MainActivity -import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.openInternetBrowser import javax.inject.Inject +@SuppressLint("CustomSplashScreen") @AndroidEntryPoint class SplashActivity : BaseActivity(), SplashView { @Inject - lateinit var appInfo: AppInfo + override lateinit var presenter: SplashPresenter @Inject - override lateinit var presenter: SplashPresenter + lateinit var shortcutsHelper: ShortcutsHelper + + companion object { + + private const val EXTRA_START_DESTINATION = "start_destination" + + private const val EXTRA_EXTERNAL_URL = "external_url" + + fun getStartIntent( + context: Context, + destination: Destination? = null, + startNewTask: Boolean = false + ) = Intent(context, SplashActivity::class.java).apply { + putExtra(EXTRA_START_DESTINATION, destination) + + if (startNewTask) { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + } + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - presenter.onAttachView(this, intent?.getStringExtra("external_url")) + installSplashScreen().setKeepVisibleCondition { true } + + val externalLink = intent?.getStringExtra(EXTRA_EXTERNAL_URL) + val startDestination = intent?.getSerializableExtra(EXTRA_START_DESTINATION) as Destination? + ?: shortcutsHelper.getDestination(intent) + + presenter.onAttachView(this, externalLink, startDestination) } override fun openLoginView() { @@ -31,8 +62,8 @@ class SplashActivity : BaseActivity(), SplashView finish() } - override fun openMainView() { - startActivity(MainActivity.getStartIntent(this)) + override fun openMainView(destination: Destination?) { + startActivity(MainActivity.getStartIntent(this, destination)) finish() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashPresenter.kt index 03e43efa8..0b7409020 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashPresenter.kt @@ -1,12 +1,10 @@ package io.github.wulkanowy.ui.modules.splash -import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler -import io.github.wulkanowy.utils.flowWithResource -import kotlinx.coroutines.flow.onEach -import timber.log.Timber +import io.github.wulkanowy.ui.modules.Destination +import kotlinx.coroutines.launch import javax.inject.Inject class SplashPresenter @Inject constructor( @@ -14,7 +12,7 @@ class SplashPresenter @Inject constructor( studentRepository: StudentRepository, ) : BasePresenter(errorHandler, studentRepository) { - fun onAttachView(view: SplashView, externalUrl: String?) { + fun onAttachView(view: SplashView, externalUrl: String?, startDestination: Destination?) { super.onAttachView(view) if (!externalUrl.isNullOrBlank()) { @@ -22,15 +20,16 @@ class SplashPresenter @Inject constructor( return } - flowWithResource { studentRepository.isCurrentStudentSet() }.onEach { - when (it.status) { - Status.LOADING -> Timber.d("Is current user set check started") - Status.SUCCESS -> { - if (it.data!!) view.openMainView() - else view.openLoginView() + presenterScope.launch { + runCatching { studentRepository.isCurrentStudentSet() } + .onFailure(errorHandler::dispatch) + .onSuccess { + if (it) { + view.openMainView(startDestination) + } else { + view.openLoginView() + } } - Status.ERROR -> errorHandler.dispatch(it.error!!) - } - }.launch() + } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashView.kt index a5aa14091..1c5d8bfd4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashView.kt @@ -1,12 +1,13 @@ package io.github.wulkanowy.ui.modules.splash import io.github.wulkanowy.ui.base.BaseView +import io.github.wulkanowy.ui.modules.Destination interface SplashView : BaseView { fun openLoginView() - fun openMainView() + fun openMainView(destination: Destination?) fun openExternalUrlAndFinish(url: String) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt index 4478a2a66..07a9f6c76 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt @@ -97,7 +97,7 @@ class TimetableFragment : BaseFragment(R.layout.fragme timetableNavDate.setOnClickListener { presenter.onPickDate() } timetableNextButton.setOnClickListener { presenter.onNextDay() } - timetableNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + timetableNavContainer.elevation = requireContext().dpToPx(8f) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt index 47bee1e39..e8fb9e440 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt @@ -72,7 +72,7 @@ class AdditionalLessonsFragment : additionalLessonsNavDate.setOnClickListener { presenter.onPickDate() } additionalLessonsNextButton.setOnClickListener { presenter.onNextDay() } - additionalLessonsNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + additionalLessonsNavContainer.elevation = requireContext().dpToPx(8f) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsFragment.kt index b8da1c0fd..a6b126447 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/completed/CompletedLessonsFragment.kt @@ -79,7 +79,7 @@ class CompletedLessonsFragment : completedLessonsNavDate.setOnClickListener { presenter.onPickDate() } completedLessonsNextButton.setOnClickListener { presenter.onNextDay() } - completedLessonsNavContainer.setElevationCompat(requireContext().dpToPx(8f)) + completedLessonsNavContainer.elevation = requireContext().dpToPx(8f) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt index 63fc84962..2c8c5a677 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt @@ -2,7 +2,6 @@ package io.github.wulkanowy.ui.modules.timetablewidget import android.annotation.SuppressLint import android.app.PendingIntent -import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager.ACTION_APPWIDGET_DELETED import android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE @@ -25,8 +24,9 @@ import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.services.HiltBroadcastReceiver import io.github.wulkanowy.services.widgets.TimetableWidgetService import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.AnalyticsHelper +import io.github.wulkanowy.utils.PendingIntentCompat import io.github.wulkanowy.utils.capitalise import io.github.wulkanowy.utils.createNameInitialsDrawable import io.github.wulkanowy.utils.getCompatColor @@ -167,18 +167,20 @@ class TimetableWidgetProvider : HiltBroadcastReceiver() { action = appWidgetId.toString() } val accountIntent = PendingIntent.getActivity( - context, -Int.MAX_VALUE + appWidgetId, + context, + -Int.MAX_VALUE + appWidgetId, Intent(context, TimetableWidgetConfigureActivity::class.java).apply { addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK) putExtra(EXTRA_APPWIDGET_ID, appWidgetId) putExtra(EXTRA_FROM_PROVIDER, true) - }, FLAG_UPDATE_CURRENT + }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) val appIntent = PendingIntent.getActivity( context, TIMETABLE_PENDING_INTENT_ID, - MainActivity.getStartIntent(context, Destination.Timetable(), true), - FLAG_UPDATE_CURRENT + SplashActivity.getStartIntent(context, Destination.Timetable(), true), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) val remoteView = RemoteViews(context.packageName, layoutId).apply { @@ -222,16 +224,16 @@ class TimetableWidgetProvider : HiltBroadcastReceiver() { code: Int, appWidgetId: Int, buttonType: String - ): PendingIntent { - return PendingIntent.getBroadcast( - context, code, - Intent(context, TimetableWidgetProvider::class.java).apply { - action = ACTION_APPWIDGET_UPDATE - putExtra(EXTRA_BUTTON_TYPE, buttonType) - putExtra(EXTRA_TOGGLED_WIDGET_ID, appWidgetId) - }, FLAG_UPDATE_CURRENT - ) - } + ) = PendingIntent.getBroadcast( + context, + code, + Intent(context, TimetableWidgetProvider::class.java).apply { + action = ACTION_APPWIDGET_UPDATE + putExtra(EXTRA_BUTTON_TYPE, buttonType) + putExtra(EXTRA_TOGGLED_WIDGET_ID, appWidgetId) + }, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) private suspend fun getStudent(studentId: Long, appWidgetId: Int) = try { val students = studentRepository.getSavedStudents(false) diff --git a/app/src/main/java/io/github/wulkanowy/ui/widgets/FittedScrollableTabLayout.kt b/app/src/main/java/io/github/wulkanowy/ui/widgets/FittedScrollableTabLayout.kt index 0f121dc5b..6b7fb4aa9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/widgets/FittedScrollableTabLayout.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/widgets/FittedScrollableTabLayout.kt @@ -3,17 +3,15 @@ package io.github.wulkanowy.ui.widgets import android.content.Context import android.util.AttributeSet import android.view.ViewGroup +import com.google.android.material.tabs.TabLayout /** * @see Tabs don't fit to screen with tabmode=scrollable, Even with a Custom Tab Layout */ -class FittedScrollableTabLayout : MaterialTabLayout { - - constructor(context: Context) : super(context) - - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) +class FittedScrollableTabLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : TabLayout(context, attrs) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { setMeasuredDimension(widthMeasureSpec, heightMeasureSpec) diff --git a/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialLinearLayout.kt b/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialLinearLayout.kt index a04922e5d..4e1ca1a97 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialLinearLayout.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialLinearLayout.kt @@ -1,24 +1,19 @@ package io.github.wulkanowy.ui.widgets import android.content.Context -import android.os.Build.VERSION.SDK_INT -import android.os.Build.VERSION_CODES.LOLLIPOP import android.util.AttributeSet import android.widget.LinearLayout import androidx.core.view.ViewCompat -import com.google.android.material.elevation.ElevationOverlayProvider import com.google.android.material.shape.MaterialShapeDrawable -class MaterialLinearLayout : LinearLayout { - - constructor(context: Context) : super(context) - - constructor(context: Context, attr: AttributeSet) : super(context, attr) - - constructor(context: Context, attr: AttributeSet, defStyleAttr: Int) : super(context, attr, defStyleAttr) +class MaterialLinearLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : LinearLayout(context, attrs) { init { - val drawable = MaterialShapeDrawable.createWithElevationOverlay(context, ViewCompat.getElevation(this)) + val drawable = + MaterialShapeDrawable.createWithElevationOverlay(context, ViewCompat.getElevation(this)) ViewCompat.setBackground(this, drawable) } @@ -28,12 +23,4 @@ class MaterialLinearLayout : LinearLayout { (background as MaterialShapeDrawable).elevation = elevation } } - - fun setElevationCompat(elevation: Float) { - if (SDK_INT >= LOLLIPOP) { - setElevation(elevation) - } else { - setBackgroundColor(ElevationOverlayProvider(context).compositeOverlayWithThemeSurfaceColorIfNeeded(elevation)) - } - } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialTabLayout.kt b/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialTabLayout.kt deleted file mode 100644 index e19d01116..000000000 --- a/app/src/main/java/io/github/wulkanowy/ui/widgets/MaterialTabLayout.kt +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.wulkanowy.ui.widgets - -import android.content.Context -import android.os.Build.VERSION.SDK_INT -import android.os.Build.VERSION_CODES.LOLLIPOP -import android.util.AttributeSet -import com.google.android.material.elevation.ElevationOverlayProvider -import com.google.android.material.tabs.TabLayout - -open class MaterialTabLayout : TabLayout { - - constructor(context: Context) : super(context) - - constructor(context: Context, attr: AttributeSet) : super(context, attr) - - constructor(context: Context, attr: AttributeSet, defStyleAttr: Int) : super(context, attr, defStyleAttr) - - fun setElevationCompat(elevation: Float) { - if (SDK_INT >= LOLLIPOP) { - setElevation(elevation) - } else { - setBackgroundColor(ElevationOverlayProvider(context).compositeOverlayWithThemeSurfaceColorIfNeeded(elevation)) - } - } -} diff --git a/app/src/main/java/io/github/wulkanowy/utils/PendingIntentCompat.kt b/app/src/main/java/io/github/wulkanowy/utils/PendingIntentCompat.kt new file mode 100644 index 000000000..45ee431a2 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/utils/PendingIntentCompat.kt @@ -0,0 +1,11 @@ +package io.github.wulkanowy.utils + +import android.app.PendingIntent +import android.os.Build + +object PendingIntentCompat { + + val FLAG_IMMUTABLE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PendingIntent.FLAG_IMMUTABLE + } else 0 +} diff --git a/app/src/main/java/io/github/wulkanowy/utils/StringExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/StringExtension.kt index 5c888f30a..bddd7df4c 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/StringExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/StringExtension.kt @@ -4,6 +4,4 @@ inline fun String?.ifNullOrBlank(defaultValue: () -> String) = if (isNullOrBlank()) defaultValue() else this fun String.capitalise() = - replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } - -fun String.decapitalise() = replaceFirstChar { it.lowercase() } + replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt b/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt index 74ae19326..c994ebab6 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt @@ -2,10 +2,8 @@ package io.github.wulkanowy.utils.security -import android.annotation.TargetApi import android.content.Context import android.os.Build.VERSION.SDK_INT -import android.os.Build.VERSION_CODES.JELLY_BEAN_MR2 import android.os.Build.VERSION_CODES.M import android.security.KeyPairGeneratorSpec import android.security.keystore.KeyGenParameterSpec @@ -116,7 +114,6 @@ fun decrypt(cipherText: String): String { } } -@TargetApi(JELLY_BEAN_MR2) private fun generateKeyPair(context: Context) { (if (SDK_INT >= M) { KeyGenParameterSpec.Builder(KEY_ALIAS, PURPOSE_DECRYPT or PURPOSE_ENCRYPT) diff --git a/app/src/main/res/drawable-v23/compat_splash_screen_no_icon_background.xml b/app/src/main/res/drawable-v23/compat_splash_screen_no_icon_background.xml new file mode 100644 index 000000000..dad56a171 --- /dev/null +++ b/app/src/main/res/drawable-v23/compat_splash_screen_no_icon_background.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-v23/img_splash_logo.png b/app/src/main/res/drawable-v23/img_splash_logo.png deleted file mode 100644 index 61489d81b973aae9ed13341ea9bc0f0902168b82..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19474 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 - - - - - - - diff --git a/app/src/main/res/drawable/ic_splash_logo.xml b/app/src/main/res/drawable/ic_splash_logo.xml new file mode 100644 index 000000000..e2e747316 --- /dev/null +++ b/app/src/main/res/drawable/ic_splash_logo.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/img_splash_logo.png b/app/src/main/res/drawable/img_splash_logo.png deleted file mode 100644 index fb521bf65ee01a4c75b85550fd214c7c606f4444..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20340 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.w?|NW*LK_QYcQ79sb63d!aAc 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 diff --git a/app/src/main/res/drawable/layer_splash_background.xml b/app/src/main/res/drawable/layer_splash_background.xml deleted file mode 100644 index 2cf46d1d0..000000000 --- a/app/src/main/res/drawable/layer_splash_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_attendance.xml b/app/src/main/res/layout/fragment_attendance.xml index 8016081b8..6f6f648a8 100644 --- a/app/src/main/res/layout/fragment_attendance.xml +++ b/app/src/main/res/layout/fragment_attendance.xml @@ -142,8 +142,8 @@ android:paddingRight="12dp" android:paddingBottom="8dp" android:scaleType="fitStart" - android:tint="?colorPrimary" - app:srcCompat="@drawable/ic_chevron_left" /> + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/fragment_exam.xml b/app/src/main/res/layout/fragment_exam.xml index ca88849c5..0c62aab5f 100644 --- a/app/src/main/res/layout/fragment_exam.xml +++ b/app/src/main/res/layout/fragment_exam.xml @@ -128,8 +128,8 @@ android:paddingRight="12dp" android:paddingBottom="8dp" android:scaleType="fitStart" - android:tint="?colorPrimary" - app:srcCompat="@drawable/ic_chevron_left" /> + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/fragment_lucky_number_history.xml b/app/src/main/res/layout/fragment_lucky_number_history.xml index 5f50d1261..a5698e2e4 100644 --- a/app/src/main/res/layout/fragment_lucky_number_history.xml +++ b/app/src/main/res/layout/fragment_lucky_number_history.xml @@ -118,8 +118,8 @@ android:paddingRight="12dp" android:paddingBottom="8dp" android:scaleType="fitStart" - android:tint="?colorPrimary" - app:srcCompat="@drawable/ic_chevron_left" /> + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/fragment_message.xml b/app/src/main/res/layout/fragment_message.xml index f6fe39ddb..5269d95e8 100644 --- a/app/src/main/res/layout/fragment_message.xml +++ b/app/src/main/res/layout/fragment_message.xml @@ -5,7 +5,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + android:layout_gravity="center_vertical" + app:tint="?colorPrimary" /> + android:layout_gravity="center_vertical" + app:tint="?colorPrimary" /> - + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/fragment_timetable_additional.xml b/app/src/main/res/layout/fragment_timetable_additional.xml index 61eb44454..a71f7545b 100644 --- a/app/src/main/res/layout/fragment_timetable_additional.xml +++ b/app/src/main/res/layout/fragment_timetable_additional.xml @@ -131,8 +131,8 @@ android:paddingRight="12dp" android:paddingBottom="8dp" android:scaleType="fitStart" - android:tint="?colorPrimary" - app:srcCompat="@drawable/ic_chevron_left" /> + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/fragment_timetable_completed.xml b/app/src/main/res/layout/fragment_timetable_completed.xml index 1a890fe12..e089275d9 100644 --- a/app/src/main/res/layout/fragment_timetable_completed.xml +++ b/app/src/main/res/layout/fragment_timetable_completed.xml @@ -130,8 +130,8 @@ android:paddingRight="12dp" android:paddingBottom="8dp" android:scaleType="fitStart" - android:tint="?colorPrimary" - app:srcCompat="@drawable/ic_chevron_left" /> + app:srcCompat="@drawable/ic_chevron_left" + app:tint="?colorPrimary" /> + app:srcCompat="@drawable/ic_chevron_right" + app:tint="?colorPrimary" /> diff --git a/app/src/main/res/layout/subitem_dashboard_conferences.xml b/app/src/main/res/layout/subitem_dashboard_conferences.xml index e80809365..8da2e19b4 100644 --- a/app/src/main/res/layout/subitem_dashboard_conferences.xml +++ b/app/src/main/res/layout/subitem_dashboard_conferences.xml @@ -2,7 +2,8 @@ + android:layout_height="wrap_content" + xmlns:tools="http://schemas.android.com/tools"> + - - \ No newline at end of file diff --git a/app/src/main/res/values-v26/styles.xml b/app/src/main/res/values-v26/styles.xml index 55413c053..3fb0a5dd8 100644 --- a/app/src/main/res/values-v26/styles.xml +++ b/app/src/main/res/values-v26/styles.xml @@ -5,11 +5,4 @@ false @android:color/darker_gray - - diff --git a/app/src/main/res/values-v28/styles.xml b/app/src/main/res/values-v28/styles.xml index ee77091d3..a936566f0 100644 --- a/app/src/main/res/values-v28/styles.xml +++ b/app/src/main/res/values-v28/styles.xml @@ -6,12 +6,4 @@ true @android:color/white - - \ No newline at end of file diff --git a/app/src/main/res/values-v29/styles.xml b/app/src/main/res/values-v29/styles.xml index ee77091d3..a936566f0 100644 --- a/app/src/main/res/values-v29/styles.xml +++ b/app/src/main/res/values-v29/styles.xml @@ -6,12 +6,4 @@ true @android:color/white - - \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 45382e6d2..7cd0f7258 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -23,10 +23,11 @@ true - @@ -50,8 +50,6 @@ 11sp -