From 4d1218d1d31e79dd91ac1a03843c05c9b8d98ddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Wed, 3 Jan 2024 14:53:16 +0100 Subject: [PATCH 01/51] Version 2.3.1 --- app/build.gradle | 6 +++--- app/src/main/play/release-notes/pl-PL/default.txt | 3 +-- build.gradle | 1 + 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 316d1178..60ef0f3c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,8 +27,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 34 - versionCode 140 - versionName "2.3.0" + versionCode 141 + versionName "2.3.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" @@ -193,7 +193,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.3.1' + implementation 'io.github.wulkanowy:sdk:2.3.3-SNAPSHOT' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index 89f97bdc..2fd7dbee 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,6 +1,5 @@ -Wersja 2.3.0 +Wersja 2.3.1 — poprawiliśmy kilka usterek przy odświeżaniu danych (ale pewnie nie wszystkie) -— zaktualizowaliśmy sposób pytania o zgodę na personalizowane reklamy Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases diff --git a/build.gradle b/build.gradle index 59735b61..a0f434e3 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,7 @@ buildscript { allprojects { repositories { + mavenLocal() mavenCentral() google() maven { url "https://jitpack.io" } From 0aa83b020e884044f8ca00d446051617da1fbcb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Wed, 3 Jan 2024 16:01:30 +0100 Subject: [PATCH 02/51] Bump sdk to 2.3.3 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 60ef0f3c..9f1506bf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -162,7 +162,7 @@ play { defaultToAppBundles = false track = 'production' releaseStatus = ReleaseStatus.IN_PROGRESS - userFraction = 0.15d + userFraction = 0.99d updatePriority = 3 enabled.set(false) } @@ -193,7 +193,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.3.3-SNAPSHOT' + implementation 'com.github.wulkanowy:sdk:2.3.3' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' From a3596c35b84268a5c8d63d904a0e2f8ec38f0986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 4 Jan 2024 09:33:51 +0100 Subject: [PATCH 03/51] Update AGP and Gradle (#2385) --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 63375 -> 63721 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 17 +++++++++-------- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/build.gradle b/build.gradle index a0f434e3..095d1b72 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ buildscript { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$kotlin_version-1.0.16" - classpath 'com.android.tools.build:gradle:8.2.0' + classpath 'com.android.tools.build:gradle:8.2.1' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.4.0' classpath 'com.huawei.agconnect:agcp:1.9.1.303' diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 033e24c4cdf41af1ab109bc7f253b2b887023340..7f93135c49b765f8051ef9d0a6055ff8e46073d8 100644 GIT binary patch delta 28216 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8838ba97..e6aba251 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index fcb6fca1..1aa94a42 100755 --- a/gradlew +++ b/gradlew @@ -83,7 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -201,11 +202,11 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ From 40df80371cc914e70fd188f523c01cae8f72e7fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Fri, 5 Jan 2024 16:03:50 +0100 Subject: [PATCH 04/51] Use forked slf4j-timber to fix logging problems with slf4j v2 (#2387) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 9f1506bf..acfc1bdc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -240,7 +240,7 @@ dependencies { implementation "com.squareup.okhttp3:logging-interceptor:4.12.0" implementation "com.jakewharton.timber:timber:5.0.1" - implementation "at.favre.lib:slf4j-timber:1.0.1" + implementation 'com.github.Faierbel:slf4j-timber:2.0' implementation 'com.github.bastienpaulfr:Treessence:1.1.2' implementation "com.mikepenz:aboutlibraries-core:$about_libraries" implementation 'io.coil-kt:coil:2.5.0' From 6ee38e9259ca62ec3d3ea030782d157aa0621546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sat, 6 Jan 2024 00:01:33 +0100 Subject: [PATCH 05/51] Add clearing all data and key entry when decryption failed (#2386) --- .../wulkanowy/data/db/dao/StudentDao.kt | 12 +- .../data/repositories/StudentRepository.kt | 30 ++- .../github/wulkanowy/ui/base/BaseActivity.kt | 13 +- .../wulkanowy/ui/base/BaseDialogFragment.kt | 8 +- .../github/wulkanowy/ui/base/BaseFragment.kt | 8 +- .../github/wulkanowy/ui/base/BasePresenter.kt | 29 ++- .../io/github/wulkanowy/ui/base/BaseView.kt | 4 +- .../github/wulkanowy/ui/base/ErrorHandler.kt | 10 +- .../ui/modules/dashboard/DashboardFragment.kt | 7 +- .../ui/modules/settings/SettingsFragment.kt | 4 +- .../settings/advanced/AdvancedFragment.kt | 8 +- .../settings/appearance/AppearanceFragment.kt | 8 +- .../notifications/NotificationsFragment.kt | 8 +- .../ui/modules/settings/sync/SyncFragment.kt | 8 +- .../wulkanowy/utils/security/Scrambler.kt | 193 ++++++++++-------- app/src/main/res/values/strings.xml | 2 + .../ui/modules/settings/ads/AdsFragment.kt | 8 +- 17 files changed, 231 insertions(+), 129 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt index d7847c24..d9326ff6 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/StudentDao.kt @@ -1,11 +1,16 @@ package io.github.wulkanowy.data.db.dao -import androidx.room.* +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.StudentName import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar -import io.github.wulkanowy.data.db.entities.StudentWithSemesters import javax.inject.Singleton @Singleton @@ -47,6 +52,9 @@ abstract class StudentDao { @Query("UPDATE Students SET is_current = 0") abstract suspend fun resetCurrent() + @Query("DELETE FROM Students WHERE email = :email AND user_name = :userName") + abstract suspend fun deleteByEmailAndUserName(email: String, userName: String) + @Transaction open suspend fun switchCurrent(id: Long) { resetCurrent() 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 2e04224a..bfad12a8 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt @@ -1,8 +1,6 @@ package io.github.wulkanowy.data.repositories -import android.content.Context import androidx.room.withTransaction -import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.dao.SemesterDao import io.github.wulkanowy.data.db.dao.StudentDao @@ -17,20 +15,19 @@ import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.DispatchersProvider import io.github.wulkanowy.utils.init -import io.github.wulkanowy.utils.security.decrypt -import io.github.wulkanowy.utils.security.encrypt +import io.github.wulkanowy.utils.security.Scrambler import kotlinx.coroutines.withContext import javax.inject.Inject import javax.inject.Singleton @Singleton class StudentRepository @Inject constructor( - @ApplicationContext private val context: Context, private val dispatchers: DispatchersProvider, private val studentDb: StudentDao, private val semesterDb: SemesterDao, private val sdk: Sdk, - private val appDatabase: AppDatabase + private val appDatabase: AppDatabase, + private val scrambler: Scrambler, ) { suspend fun isCurrentStudentSet() = studentDb.loadCurrent()?.isCurrent ?: false @@ -68,7 +65,7 @@ class StudentRepository @Inject constructor( student = student.apply { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { student.password = withContext(dispatchers.io) { - decrypt(student.password) + scrambler.decrypt(student.password) } } }, @@ -86,7 +83,7 @@ class StudentRepository @Inject constructor( }.apply { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { student.password = withContext(dispatchers.io) { - decrypt(student.password) + scrambler.decrypt(student.password) } } } @@ -96,7 +93,7 @@ class StudentRepository @Inject constructor( if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { student.password = withContext(dispatchers.io) { - decrypt(student.password) + scrambler.decrypt(student.password) } } return student @@ -107,7 +104,7 @@ class StudentRepository @Inject constructor( if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { student.password = withContext(dispatchers.io) { - decrypt(student.password) + scrambler.decrypt(student.password) } } return student @@ -120,7 +117,7 @@ class StudentRepository @Inject constructor( it.apply { if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.HEBE) { password = withContext(dispatchers.io) { - encrypt(password, context) + scrambler.encrypt(password) } } } @@ -166,4 +163,15 @@ class StudentRepository @Inject constructor( studentDb.update(studentName) } + + suspend fun deleteStudentsAssociatedWithAccount(student: Student) { + studentDb.deleteByEmailAndUserName(student.email, student.userName) + } + + suspend fun clearAll() { + withContext(dispatchers.io) { + scrambler.clearKeyPair() + appDatabase.clearAllTables() + } + } } 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 f622209a..026d38de 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 @@ -68,11 +68,20 @@ abstract class BaseActivity, VB : ViewBinding> : } else Toast.makeText(this, text, Toast.LENGTH_LONG).show() } - override fun showExpiredDialog() { + override fun showExpiredCredentialsDialog() { + MaterialAlertDialogBuilder(this) + .setTitle(R.string.main_expired_credentials_title) + .setMessage(R.string.main_expired_credentials_description) + .setPositiveButton(R.string.main_log_in) { _, _ -> presenter.onConfirmExpiredCredentialsSelected() } + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .show() + } + + override fun showDecryptionFailedDialog() { MaterialAlertDialogBuilder(this) .setTitle(R.string.main_session_expired) .setMessage(R.string.main_session_relogin) - .setPositiveButton(R.string.main_log_in) { _, _ -> presenter.onExpiredLoginSelected() } + .setPositiveButton(R.string.main_log_in) { _, _ -> presenter.onConfirmDecryptionFailedSelected() } .setNegativeButton(android.R.string.cancel) { _, _ -> } .show() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt index 84540b1c..50e4b05d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt @@ -28,8 +28,12 @@ abstract class BaseDialogFragment : DialogFragment(), BaseView Toast.makeText(context, text, Toast.LENGTH_LONG).show() } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } override fun openClearLoginView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt index b25346a7..cec2670b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt @@ -39,8 +39,12 @@ abstract class BaseFragment(@LayoutRes layoutId: Int) : Fragme } } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } override fun showAuthDialog() { 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 2d913103..ee92e4fc 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 @@ -28,20 +28,37 @@ open class BasePresenter( this.view = view errorHandler.apply { showErrorMessage = view::showError - onSessionExpired = view::showExpiredDialog + onExpiredCredentials = view::showExpiredCredentialsDialog + onDecryptionFailed = view::showDecryptionFailedDialog onNoCurrentStudent = view::openClearLoginView onPasswordChangeRequired = view::showChangePasswordSnackbar onAuthorizationRequired = view::showAuthDialog } } - fun onExpiredLoginSelected() { - Timber.i("Attempt to switch the student after the session expires") + fun onConfirmDecryptionFailedSelected() { + Timber.i("Attempt to clear all data") + + presenterScope.launch { + runCatching { studentRepository.clearAll() } + .onFailure { + Timber.i("Clear data result: An exception occurred") + errorHandler.dispatch(it) + } + .onSuccess { + Timber.i("Clear data result: Open login view") + view?.openClearLoginView() + } + } + } + + fun onConfirmExpiredCredentialsSelected() { + Timber.i("Attempt to delete students associated with the account and switch to new student") presenterScope.launch { runCatching { val student = studentRepository.getCurrentStudent(false) - studentRepository.logoutStudent(student) + studentRepository.deleteStudentsAssociatedWithAccount(student) val students = studentRepository.getSavedStudents(false) if (students.isNotEmpty()) { @@ -50,11 +67,11 @@ open class BasePresenter( } } .onFailure { - Timber.i("Switch student result: An exception occurred") + Timber.i("Delete students result: An exception occurred") errorHandler.dispatch(it) } .onSuccess { - Timber.i("Switch student result: Open login view") + Timber.i("Delete students result: Open login view") view?.openClearLoginView() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt index b31737e2..e97a6ab9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt @@ -6,7 +6,9 @@ interface BaseView { fun showMessage(text: String) - fun showExpiredDialog() + fun showExpiredCredentialsDialog() + + fun showDecryptionFailedDialog() fun showAuthDialog() 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 0a41a47b..56905709 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 @@ -15,7 +15,9 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co var showErrorMessage: (String, Throwable) -> Unit = { _, _ -> } - var onSessionExpired: () -> Unit = {} + var onExpiredCredentials: () -> Unit = {} + + var onDecryptionFailed: () -> Unit = {} var onNoCurrentStudent: () -> Unit = {} @@ -32,7 +34,8 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co showErrorMessage(context.resources.getErrorString(error), error) when (error) { is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl) - is ScramblerException, is BadCredentialsException -> onSessionExpired() + is ScramblerException -> onDecryptionFailed() + is BadCredentialsException -> onExpiredCredentials() is NoCurrentStudentException -> onNoCurrentStudent() is AuthorizationRequiredException -> onAuthorizationRequired() } @@ -40,7 +43,8 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co open fun clear() { showErrorMessage = { _, _ -> } - onSessionExpired = {} + onExpiredCredentials = {} + onDecryptionFailed = {} onNoCurrentStudent = {} onPasswordChangeRequired = {} onAuthorizationRequired = {} 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 ce17c763..301262a0 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 @@ -30,7 +30,12 @@ import io.github.wulkanowy.ui.modules.message.MessageFragment import io.github.wulkanowy.ui.modules.notificationscenter.NotificationsCenterFragment import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment import io.github.wulkanowy.ui.modules.timetable.TimetableFragment -import io.github.wulkanowy.utils.* +import io.github.wulkanowy.utils.capitalise +import io.github.wulkanowy.utils.dpToPx +import io.github.wulkanowy.utils.getErrorString +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 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 21f56498..19c4ef6b 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 @@ -24,7 +24,9 @@ class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView, Settin override fun showMessage(text: String) {} - override fun showExpiredDialog() {} + override fun showExpiredCredentialsDialog() {} + + override fun showDecryptionFailedDialog() {} override fun openClearLoginView() {} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt index 1b8d1a8f..256b1337 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt @@ -47,8 +47,12 @@ class AdvancedFragment : PreferenceFragmentCompat(), (activity as? BaseActivity<*, *>)?.showMessage(text) } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } override fun showChangePasswordSnackbar(redirectUrl: String) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt index 70dd694c..20423eb9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt @@ -63,8 +63,12 @@ class AppearanceFragment : PreferenceFragmentCompat(), (activity as? BaseActivity<*, *>)?.showMessage(text) } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } override fun showChangePasswordSnackbar(redirectUrl: String) { 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 af4c4e6a..2ae983c2 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 @@ -133,8 +133,12 @@ class NotificationsFragment : PreferenceFragmentCompat(), (activity as? BaseActivity<*, *>)?.showMessage(text) } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } override fun showChangePasswordSnackbar(redirectUrl: String) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt index f48abe9b..133b1ff4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt @@ -84,8 +84,12 @@ class SyncFragment : PreferenceFragmentCompat(), } } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } override fun showChangePasswordSnackbar(redirectUrl: String) { 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 c994ebab..db16a256 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 @@ -16,6 +16,7 @@ import android.util.Base64.DEFAULT import android.util.Base64.decode import android.util.Base64.encode import android.util.Base64.encodeToString +import dagger.hilt.android.qualifiers.ApplicationContext import timber.log.Timber import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream @@ -33,108 +34,124 @@ import javax.crypto.CipherInputStream import javax.crypto.CipherOutputStream import javax.crypto.spec.OAEPParameterSpec import javax.crypto.spec.PSource.PSpecified +import javax.inject.Inject +import javax.inject.Singleton import javax.security.auth.x500.X500Principal -private const val KEYSTORE_NAME = "AndroidKeyStore" +@Singleton +class Scrambler @Inject constructor( + @ApplicationContext private val context: Context, +) { + private val keyCharset = Charset.forName("UTF-8") -private const val KEY_ALIAS = "wulkanowy_password" + private val isKeyPairExists: Boolean + get() = keyStore.getKey(KEY_ALIAS, null) != null -private val KEY_CHARSET = Charset.forName("UTF-8") + private val keyStore: KeyStore + get() = KeyStore.getInstance(KEYSTORE_NAME).apply { load(null) } -private val isKeyPairExists: Boolean - get() = keyStore.getKey(KEY_ALIAS, null) != null + private val cipher: Cipher + get() { + return if (SDK_INT >= M) Cipher.getInstance( + "RSA/ECB/OAEPWithSHA-256AndMGF1Padding", + "AndroidKeyStoreBCWorkaround" + ) + else Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL") + } -private val keyStore: KeyStore - get() = KeyStore.getInstance(KEYSTORE_NAME).apply { load(null) } + fun encrypt(plainText: String): String { + if (plainText.isEmpty()) throw ScramblerException("Text to be encrypted is empty") -private val cipher: Cipher - get() { - return if (SDK_INT >= M) Cipher.getInstance( - "RSA/ECB/OAEPWithSHA-256AndMGF1Padding", - "AndroidKeyStoreBCWorkaround" - ) - else Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL") + return try { + if (!isKeyPairExists) generateKeyPair() + + cipher.let { + if (SDK_INT >= M) { + OAEPParameterSpec("SHA-256", "MGF1", SHA1, PSpecified.DEFAULT).let { spec -> + it.init(ENCRYPT_MODE, keyStore.getCertificate(KEY_ALIAS).publicKey, spec) + } + } else it.init(ENCRYPT_MODE, keyStore.getCertificate(KEY_ALIAS).publicKey) + + ByteArrayOutputStream().let { output -> + CipherOutputStream(output, it).apply { + write(plainText.toByteArray(keyCharset)) + close() + } + encodeToString(output.toByteArray(), DEFAULT) + } + } + } catch (exception: Exception) { + Timber.e(exception, "An error occurred while encrypting text") + String(encode(plainText.toByteArray(keyCharset), DEFAULT), keyCharset) + } } -fun encrypt(plainText: String, context: Context): String { - if (plainText.isEmpty()) throw ScramblerException("Text to be encrypted is empty") + fun decrypt(cipherText: String): String { + if (cipherText.isEmpty()) throw ScramblerException("Text to be encrypted is empty") - return try { - if (!isKeyPairExists) generateKeyPair(context) + return try { + if (!isKeyPairExists) throw ScramblerException("KeyPair doesn't exist") - cipher.let { - if (SDK_INT >= M) { - OAEPParameterSpec("SHA-256", "MGF1", SHA1, PSpecified.DEFAULT).let { spec -> - it.init(ENCRYPT_MODE, keyStore.getCertificate(KEY_ALIAS).publicKey, spec) + cipher.let { + if (SDK_INT >= M) { + OAEPParameterSpec("SHA-256", "MGF1", SHA1, PSpecified.DEFAULT).let { spec -> + it.init(DECRYPT_MODE, keyStore.getKey(KEY_ALIAS, null), spec) + } + } else it.init(DECRYPT_MODE, keyStore.getKey(KEY_ALIAS, null)) + + CipherInputStream( + ByteArrayInputStream(decode(cipherText, DEFAULT)), + it + ).let { input -> + val values = ArrayList() + var nextByte: Int + while (run { nextByte = input.read(); nextByte } != -1) { + values.add(nextByte.toByte()) + } + val bytes = ByteArray(values.size) + for (i in bytes.indices) { + bytes[i] = values[i] + } + String(bytes, 0, bytes.size, keyCharset) } - } else it.init(ENCRYPT_MODE, keyStore.getCertificate(KEY_ALIAS).publicKey) + } + } catch (e: Exception) { + throw ScramblerException("An error occurred while decrypting text", e) + } + } - ByteArrayOutputStream().let { output -> - CipherOutputStream(output, it).apply { - write(plainText.toByteArray(KEY_CHARSET)) - close() - } - encodeToString(output.toByteArray(), DEFAULT) + private fun generateKeyPair() { + (if (SDK_INT >= M) { + KeyGenParameterSpec.Builder(KEY_ALIAS, PURPOSE_DECRYPT or PURPOSE_ENCRYPT) + .setDigests(DIGEST_SHA256, DIGEST_SHA512) + .setEncryptionPaddings(ENCRYPTION_PADDING_RSA_OAEP) + .setCertificateSerialNumber(BigInteger.TEN) + .setCertificateSubject(X500Principal("CN=Wulkanowy")) + .build() + } else { + KeyPairGeneratorSpec.Builder(context) + .setAlias(KEY_ALIAS) + .setSubject(X500Principal("CN=Wulkanowy")) + .setSerialNumber(BigInteger.TEN) + .setStartDate(Calendar.getInstance().time) + .setEndDate(Calendar.getInstance().apply { add(YEAR, 99) }.time) + .build() + }).let { + KeyPairGenerator.getInstance("RSA", KEYSTORE_NAME).apply { + initialize(it) + genKeyPair() } } - } catch (exception: Exception) { - Timber.e(exception, "An error occurred while encrypting text") - String(encode(plainText.toByteArray(KEY_CHARSET), DEFAULT), KEY_CHARSET) + Timber.i("A new KeyPair has been generated") + } + + fun clearKeyPair() { + keyStore.deleteEntry(KEY_ALIAS) + Timber.i("KeyPair has been cleared") + } + + private companion object { + private const val KEYSTORE_NAME = "AndroidKeyStore" + private const val KEY_ALIAS = "wulkanowy_password" } } - -fun decrypt(cipherText: String): String { - if (cipherText.isEmpty()) throw ScramblerException("Text to be encrypted is empty") - - return try { - if (!isKeyPairExists) throw ScramblerException("KeyPair doesn't exist") - - cipher.let { - if (SDK_INT >= M) { - OAEPParameterSpec("SHA-256", "MGF1", SHA1, PSpecified.DEFAULT).let { spec -> - it.init(DECRYPT_MODE, keyStore.getKey(KEY_ALIAS, null), spec) - } - } else it.init(DECRYPT_MODE, keyStore.getKey(KEY_ALIAS, null)) - - CipherInputStream(ByteArrayInputStream(decode(cipherText, DEFAULT)), it).let { input -> - val values = ArrayList() - var nextByte: Int - while (run { nextByte = input.read(); nextByte } != -1) { - values.add(nextByte.toByte()) - } - val bytes = ByteArray(values.size) - for (i in bytes.indices) { - bytes[i] = values[i] - } - String(bytes, 0, bytes.size, KEY_CHARSET) - } - } - } catch (e: Exception) { - throw ScramblerException("An error occurred while decrypting text", e) - } -} - -private fun generateKeyPair(context: Context) { - (if (SDK_INT >= M) { - KeyGenParameterSpec.Builder(KEY_ALIAS, PURPOSE_DECRYPT or PURPOSE_ENCRYPT) - .setDigests(DIGEST_SHA256, DIGEST_SHA512) - .setEncryptionPaddings(ENCRYPTION_PADDING_RSA_OAEP) - .setCertificateSerialNumber(BigInteger.TEN) - .setCertificateSubject(X500Principal("CN=Wulkanowy")) - .build() - } else { - KeyPairGeneratorSpec.Builder(context) - .setAlias(KEY_ALIAS) - .setSubject(X500Principal("CN=Wulkanowy")) - .setSerialNumber(BigInteger.TEN) - .setStartDate(Calendar.getInstance().time) - .setEndDate(Calendar.getInstance().apply { add(YEAR, 99) }.time) - .build() - }).let { - KeyPairGenerator.getInstance("RSA", KEYSTORE_NAME).apply { - initialize(it) - genKeyPair() - } - } - Timber.i("A new KeyPair has been generated") -} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 27c454ad..72910b85 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -107,6 +107,8 @@ Log in Session expired Session expired, log in again + Your account password has been changed. You need to log in to Wulkanowy again + Password changed Application support Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time Enable ads diff --git a/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt index ec6027e9..d7d83e6c 100644 --- a/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt +++ b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt @@ -101,8 +101,12 @@ class AdsFragment : PreferenceFragmentCompat(), MainView.TitledView, AdsView { (activity as? BaseActivity<*, *>)?.showMessage(text) } - override fun showExpiredDialog() { - (activity as? BaseActivity<*, *>)?.showExpiredDialog() + override fun showExpiredCredentialsDialog() { + (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() + } + + override fun showDecryptionFailedDialog() { + (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } override fun showChangePasswordSnackbar(redirectUrl: String) { From 81e80181f264b375470777b22e1047968f8aceda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Mon, 8 Jan 2024 16:32:06 +0100 Subject: [PATCH 06/51] New Crowdin updates (#2388) --- app/src/main/res/values-cs/strings.xml | 4 +++- app/src/main/res/values-da-rDK/strings.xml | 2 ++ app/src/main/res/values-de/strings.xml | 2 ++ app/src/main/res/values-es-rES/strings.xml | 2 ++ app/src/main/res/values-it-rIT/strings.xml | 2 ++ app/src/main/res/values-pl/strings.xml | 2 ++ app/src/main/res/values-ru/strings.xml | 2 ++ app/src/main/res/values-sk/strings.xml | 4 +++- app/src/main/res/values-uk/strings.xml | 2 ++ 9 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 3f0940b5..8e60b7a6 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -96,6 +96,8 @@ Přihlásit se Relace vypršela Relace vypršela. Přihlaste se prosím znovu + Heslo k vašemu účtu bylo změněno. Musíte se znovu přihlásit do Wulkanového + Heslo bylo změněno Podpora aplikace Líbí se Vám tato aplikace? Podpořte její vývoj tím, že povolíte neinvazivní reklamy, které můžete kdykoliv vypnout Zapnout reklamy @@ -760,7 +762,7 @@ Podpora Ochrana osobních údajů Souhlasy - Show consent to data processing + Zobrazit souhlas se zpracováním údajů Zobrazit reklamy v aplikaci Podívejte se na jednu reklamu pro podporu projektu Souhlas se zpracováním dat diff --git a/app/src/main/res/values-da-rDK/strings.xml b/app/src/main/res/values-da-rDK/strings.xml index 51275063..01306662 100644 --- a/app/src/main/res/values-da-rDK/strings.xml +++ b/app/src/main/res/values-da-rDK/strings.xml @@ -96,6 +96,8 @@ Log in Session expired Session expired, log in again + Your account password has been changed. You need to log in to Wulkanowy again + Password changed Application support Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time Enable ads diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index bfc194c0..09173d38 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -96,6 +96,8 @@ Anmelden Die Sitzung ist abgelaufen Die Sitzung ist abgelaufen, bitte loggen Sie sich erneut ein + Your account password has been changed. You need to log in to Wulkanowy again + Password changed Anwendungsunterstützung Gefällt Ihnen diese App? Unterstützen Sie ihre Entwicklung, indem Sie nicht-invasive Werbung aktivieren, die Sie jederzeit deaktivieren können Werbung aktivieren diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 51275063..01306662 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -96,6 +96,8 @@ Log in Session expired Session expired, log in again + Your account password has been changed. You need to log in to Wulkanowy again + Password changed Application support Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time Enable ads diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml index 51275063..01306662 100644 --- a/app/src/main/res/values-it-rIT/strings.xml +++ b/app/src/main/res/values-it-rIT/strings.xml @@ -96,6 +96,8 @@ Log in Session expired Session expired, log in again + Your account password has been changed. You need to log in to Wulkanowy again + Password changed Application support Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time Enable ads diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 2872e28e..fb9d170a 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -96,6 +96,8 @@ Zaloguj się Sesja wygasła Sesja wygasła, zaloguj się ponownie + Hasło do Twojego konta zostało zmienione. Musisz zalogować się ponownie do Wulkanowego + Hasło zostało zmienione Wparcie aplikacji Podoba Ci się ta aplikacja? Wspieraj jej rozwój poprzez włączenie nieinwazyjnych reklam, które możesz wyłączyć w dowolnym momencie Włącz reklamy diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 592e9ee8..c604cd8b 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -96,6 +96,8 @@ Войти Сеанс истёк Сеанс истёк, авторизуйтесь снова + Your account password has been changed. You need to log in to Wulkanowy again + Password changed Поддержка приложения Вам нравится это приложение? Поддержите его разработку, включив неинвазивную рекламу, которую можно отключить в любое время Включить рекламу diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index c9ad645e..e02b1542 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -96,6 +96,8 @@ Prihlásiť sa Relácia vypršala Relácia vypršala. Prihláste sa prosím znovu + Heslo k vášmu účtu bolo zmenené. Musíte sa znovu prihlásiť do Wulkanového + Heslo bolo zmenené Podpora aplikácie Páči sa Vám táto aplikácia? Podporte jej vývoj tým, že povolíte neinvazívne reklamy, ktoré môžete kedykoľvek vypnúť Zapnúť reklamy @@ -760,7 +762,7 @@ Podpora Ochrana osobných údajov Súhlasy - Show consent to data processing + Zobraziť súhlas so spracovaním údajov Zobraziť reklamy v aplikácii Pozrite sa na jednu reklamu pre podporu projektu Súhlas so spracovaním dát diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 86ee0910..32617f42 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -96,6 +96,8 @@ Увійти Минув термін дії сесії Минув термін дії сесії, авторизуйтеся знову + Your account password has been changed. You need to log in to Wulkanowy again + Password changed Підтримка додатку Вам подобається цей додаток? Підтримайте його розвиток, увімкнувши неінвазивну рекламу, яку ви можете відключити в будь-який час Увімкнути рекламу From 5316e3e1bf04291bcd0b5b7e14e0af010018a1a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 15:32:30 +0000 Subject: [PATCH 07/51] Bump mockk from 1.13.8 to 1.13.9 (#2389) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index acfc1bdc..70b54235 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -188,7 +188,7 @@ ext { android_hilt = "1.1.0" room = "2.6.1" chucker = "4.0.0" - mockk = "1.13.8" + mockk = "1.13.9" coroutines = "1.7.3" } From 8324a9cac3f6a22ba3c8733618b7e85addf66c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Tue, 9 Jan 2024 19:00:37 +0100 Subject: [PATCH 08/51] Use emptyCookieJarInterceptor in SDK configuration (#2390) --- app/build.gradle | 2 +- app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 70b54235..2281848b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -193,7 +193,7 @@ ext { } dependencies { - implementation 'com.github.wulkanowy:sdk:2.3.3' + implementation 'io.github.wulkanowy:sdk:2.3.4-SNAPSHOT' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' diff --git a/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt index 889d64ea..df99be98 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt @@ -11,6 +11,7 @@ fun Sdk.init(student: Student): Sdk { schoolSymbol = student.schoolSymbol studentId = student.studentId classId = student.classId + emptyCookieJarInterceptor = true if (Sdk.Mode.valueOf(student.loginMode) == Sdk.Mode.HEBE) { mobileBaseUrl = student.mobileBaseUrl From 9dee7f01f63d853ae24f467bcd018723e8ff1980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Tue, 9 Jan 2024 19:07:46 +0100 Subject: [PATCH 09/51] Avoid deleting luckynumber when SDK returns null (#2391) --- .../data/repositories/LuckyNumberRepository.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt index 87e8410f..4ff4517d 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt @@ -35,12 +35,15 @@ class LuckyNumberRepository @Inject constructor( fetch = { sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student) }, - saveFetchResult = { old, new -> - if (new != old) { - old?.let { luckyNumberDb.deleteAll(listOfNotNull(it)) } - luckyNumberDb.insertAll(listOfNotNull((new?.apply { - if (notify) isNotified = false - }))) + saveFetchResult = { oldLuckyNumber, newLuckyNumber -> + newLuckyNumber ?: return@networkBoundResource + + if (newLuckyNumber != oldLuckyNumber) { + val updatedLuckNumberList = + listOf(newLuckyNumber.apply { if (notify) isNotified = false }) + + oldLuckyNumber?.let { luckyNumberDb.deleteAll(listOfNotNull(it)) } + luckyNumberDb.insertAll(updatedLuckNumberList) } } ) From cff08d63221bf353cb91d069e1a05ab722dab253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Tue, 9 Jan 2024 19:27:03 +0100 Subject: [PATCH 10/51] Version 2.3.2 --- app/build.gradle | 6 +++--- app/src/main/play/release-notes/pl-PL/default.txt | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2281848b..dba4dc2f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,8 +27,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 34 - versionCode 141 - versionName "2.3.1" + versionCode 142 + versionName "2.3.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" @@ -193,7 +193,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.3.4-SNAPSHOT' + implementation 'io.github.wulkanowy:sdk:2.3.4' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index 2fd7dbee..46fac4ac 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,5 +1,5 @@ -Wersja 2.3.1 +Wersja 2.3.2 -— poprawiliśmy kilka usterek przy odświeżaniu danych (ale pewnie nie wszystkie) +— poprawiliśmy kolejne usterki przy odświeżaniu danych (teraz to powinno działać już dużo lepiej) Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases From ddbcc7a04c28617765739d2b5774812f235ed611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Tue, 9 Jan 2024 21:45:59 +0100 Subject: [PATCH 11/51] Version 2.3.3 --- app/build.gradle | 6 +++--- app/src/main/play/release-notes/pl-PL/default.txt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index dba4dc2f..180df1a6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,8 +27,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 34 - versionCode 142 - versionName "2.3.2" + versionCode 143 + versionName "2.3.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" @@ -193,7 +193,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.3.4' + implementation 'io.github.wulkanowy:sdk:2.3.5' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index 46fac4ac..0a2eb68f 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,4 +1,4 @@ -Wersja 2.3.2 +Wersja 2.3.3 — poprawiliśmy kolejne usterki przy odświeżaniu danych (teraz to powinno działać już dużo lepiej) From a98e8398fd0a3783af729d6d61965f214c636f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Fri, 12 Jan 2024 18:34:43 +0100 Subject: [PATCH 12/51] Add webview to obtain cloudflare captcha cookies for okhttp (#2392) --- app/build.gradle | 3 +- .../io/github/wulkanowy/data/DataModule.kt | 2 + .../github/wulkanowy/ui/base/BaseActivity.kt | 5 ++ .../wulkanowy/ui/base/BaseDialogFragment.kt | 4 ++ .../github/wulkanowy/ui/base/BaseFragment.kt | 4 ++ .../github/wulkanowy/ui/base/BasePresenter.kt | 1 + .../io/github/wulkanowy/ui/base/BaseView.kt | 2 + .../github/wulkanowy/ui/base/ErrorHandler.kt | 4 ++ .../ui/modules/captcha/CaptchaDialog.kt | 72 +++++++++++++++++++ .../ui/modules/settings/SettingsFragment.kt | 3 + .../settings/advanced/AdvancedFragment.kt | 4 ++ .../settings/appearance/AppearanceFragment.kt | 4 ++ .../notifications/NotificationsFragment.kt | 4 ++ .../ui/modules/settings/sync/SyncFragment.kt | 4 ++ .../utils/WebkitCookieManagerProxy.kt | 39 ++++++++++ app/src/main/res/layout/dialog_captcha.xml | 12 ++++ .../ui/modules/settings/ads/AdsFragment.kt | 4 ++ 17 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt create mode 100644 app/src/main/java/io/github/wulkanowy/utils/WebkitCookieManagerProxy.kt create mode 100644 app/src/main/res/layout/dialog_captcha.xml diff --git a/app/build.gradle b/app/build.gradle index 180df1a6..7069672a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -193,7 +193,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.3.5' + implementation 'io.github.wulkanowy:sdk:2.3.6-SNAPSHOT' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' @@ -238,6 +238,7 @@ dependencies { implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0" implementation "com.squareup.okhttp3:logging-interceptor:4.12.0" + implementation "com.squareup.okhttp3:okhttp-urlconnection:4.12.0" implementation "com.jakewharton.timber:timber:5.0.1" implementation 'com.github.Faierbel:slf4j-timber:2.0' diff --git a/app/src/main/java/io/github/wulkanowy/data/DataModule.kt b/app/src/main/java/io/github/wulkanowy/data/DataModule.kt index bea3f706..950e817b 100644 --- a/app/src/main/java/io/github/wulkanowy/data/DataModule.kt +++ b/app/src/main/java/io/github/wulkanowy/data/DataModule.kt @@ -21,6 +21,7 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.RemoteConfigHelper +import io.github.wulkanowy.utils.WebkitCookieManagerProxy import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient @@ -43,6 +44,7 @@ internal class DataModule { buildTag = android.os.Build.MODEL userAgentTemplate = remoteConfig.userAgentTemplate setSimpleHttpLogger { Timber.d(it) } + setAdditionalCookieManager(WebkitCookieManagerProxy()) // for debug only addInterceptor(chuckerInterceptor, network = 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 026d38de..29996db7 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 @@ -11,6 +11,7 @@ import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar.LENGTH_LONG import io.github.wulkanowy.R import io.github.wulkanowy.ui.modules.auth.AuthDialog +import io.github.wulkanowy.ui.modules.captcha.CaptchaDialog import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.utils.FragmentLifecycleLogger import io.github.wulkanowy.utils.getThemeAttrColor @@ -77,6 +78,10 @@ abstract class BaseActivity, VB : ViewBinding> : .show() } + override fun onCaptchaVerificationRequired(url: String?) { + CaptchaDialog.newInstance(url).show(supportFragmentManager, "captcha_dialog") + } + override fun showDecryptionFailedDialog() { MaterialAlertDialogBuilder(this) .setTitle(R.string.main_session_expired) diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt index 50e4b05d..cb85fd8a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt @@ -32,6 +32,10 @@ abstract class BaseDialogFragment : DialogFragment(), BaseView (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() } + override fun onCaptchaVerificationRequired(url: String?) { + (activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url) + } + override fun showDecryptionFailedDialog() { (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt index cec2670b..4f919f45 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt @@ -43,6 +43,10 @@ abstract class BaseFragment(@LayoutRes layoutId: Int) : Fragme (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() } + override fun onCaptchaVerificationRequired(url: String?) { + (activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url) + } + override fun showDecryptionFailedDialog() { (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } 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 ee92e4fc..d4cb20ca 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 @@ -29,6 +29,7 @@ open class BasePresenter( errorHandler.apply { showErrorMessage = view::showError onExpiredCredentials = view::showExpiredCredentialsDialog + onCaptchaVerificationRequired = view::onCaptchaVerificationRequired onDecryptionFailed = view::showDecryptionFailedDialog onNoCurrentStudent = view::openClearLoginView onPasswordChangeRequired = view::showChangePasswordSnackbar diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt index e97a6ab9..88d5754f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseView.kt @@ -8,6 +8,8 @@ interface BaseView { fun showExpiredCredentialsDialog() + fun onCaptchaVerificationRequired(url: String?) + fun showDecryptionFailedDialog() fun showAuthDialog() 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 56905709..e17c0c9e 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 @@ -4,6 +4,7 @@ import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.sdk.scrapper.exception.AuthorizationRequiredException +import io.github.wulkanowy.sdk.scrapper.exception.CloudflareVerificationException import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException import io.github.wulkanowy.utils.getErrorString @@ -25,6 +26,8 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co var onAuthorizationRequired: () -> Unit = {} + var onCaptchaVerificationRequired: (url: String?) -> Unit = {} + fun dispatch(error: Throwable) { Timber.e(error, "An exception occurred while the Wulkanowy was running") proceed(error) @@ -38,6 +41,7 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co is BadCredentialsException -> onExpiredCredentials() is NoCurrentStudentException -> onNoCurrentStudent() is AuthorizationRequiredException -> onAuthorizationRequired() + is CloudflareVerificationException -> onCaptchaVerificationRequired(error.originalUrl) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt new file mode 100644 index 00000000..6c4d6420 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt @@ -0,0 +1,72 @@ +package io.github.wulkanowy.ui.modules.captcha + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.webkit.WebResourceError +import android.webkit.WebResourceRequest +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.core.os.bundleOf +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.databinding.DialogCaptchaBinding +import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.ui.base.BaseDialogFragment +import timber.log.Timber +import javax.inject.Inject + +@AndroidEntryPoint +class CaptchaDialog : BaseDialogFragment() { + + @Inject + lateinit var sdk: Sdk + + companion object { + private const val CAPTCHA_URL = "captcha_url" + fun newInstance(url: String?): CaptchaDialog { + return CaptchaDialog().apply { + arguments = bundleOf(CAPTCHA_URL to url) + } + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = DialogCaptchaBinding.inflate(inflater).apply { binding = this }.root + + @SuppressLint("SetJavaScriptEnabled") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + with(binding.captchaWebview) { + with(settings) { + javaScriptEnabled = true + userAgentString = sdk.userAgent + } + + webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + view?.evaluateJavascript("document.getElementById('challenge-running') == undefined") { + if (it == "true") { + dismiss() + } else Timber.e("JS result: $it") + } + } + + override fun onReceivedError( + view: WebView?, + request: WebResourceRequest?, + error: WebResourceError? + ) { + super.onReceivedError(view, request, error) + } + } + + loadUrl(arguments?.getString(CAPTCHA_URL).orEmpty()) + } + } +} 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 19c4ef6b..f8d1323c 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 @@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.settings import android.os.Bundle import androidx.preference.PreferenceFragmentCompat import io.github.wulkanowy.R +import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.modules.main.MainView import timber.log.Timber @@ -26,6 +27,8 @@ class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView, Settin override fun showExpiredCredentialsDialog() {} + override fun onCaptchaVerificationRequired(url: String?) = Unit + override fun showDecryptionFailedDialog() {} override fun openClearLoginView() {} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt index 256b1337..a1d00227 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt @@ -51,6 +51,10 @@ class AdvancedFragment : PreferenceFragmentCompat(), (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() } + override fun onCaptchaVerificationRequired(url: String?) { + (activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url) + } + override fun showDecryptionFailedDialog() { (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt index 20423eb9..b9b35019 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt @@ -67,6 +67,10 @@ class AppearanceFragment : PreferenceFragmentCompat(), (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() } + override fun onCaptchaVerificationRequired(url: String?) { + (activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url) + } + override fun showDecryptionFailedDialog() { (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } 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 2ae983c2..fdc4a24d 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 @@ -137,6 +137,10 @@ class NotificationsFragment : PreferenceFragmentCompat(), (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() } + override fun onCaptchaVerificationRequired(url: String?) { + (activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url) + } + override fun showDecryptionFailedDialog() { (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt index 133b1ff4..1e81e58a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt @@ -88,6 +88,10 @@ class SyncFragment : PreferenceFragmentCompat(), (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() } + override fun onCaptchaVerificationRequired(url: String?) { + (activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url) + } + override fun showDecryptionFailedDialog() { (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } diff --git a/app/src/main/java/io/github/wulkanowy/utils/WebkitCookieManagerProxy.kt b/app/src/main/java/io/github/wulkanowy/utils/WebkitCookieManagerProxy.kt new file mode 100644 index 00000000..509f39f5 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/utils/WebkitCookieManagerProxy.kt @@ -0,0 +1,39 @@ +package io.github.wulkanowy.utils + +import java.net.CookiePolicy +import java.net.URI +import android.webkit.CookieManager as WebkitCookieManager +import java.net.CookieManager as JavaCookieManager + +class WebkitCookieManagerProxy : JavaCookieManager(null, CookiePolicy.ACCEPT_ALL) { + + private val webkitCookieManager: WebkitCookieManager = WebkitCookieManager.getInstance() + + override fun put(uri: URI?, responseHeaders: Map>?) { + if (uri == null || responseHeaders == null) return + val url = uri.toString() + for (headerKey in responseHeaders.keys) { + if (headerKey == null || !( + headerKey.equals("Set-Cookie2", ignoreCase = true) || + headerKey.equals("Set-Cookie", ignoreCase = true) + ) + ) continue + + // process each of the headers + for (headerValue in responseHeaders[headerKey].orEmpty()) { + webkitCookieManager.setCookie(url, headerValue) + } + } + } + + override operator fun get( + uri: URI?, + requestHeaders: Map?>? + ): Map> { + require(!(uri == null || requestHeaders == null)) { "Argument is null" } + val res = mutableMapOf>() + val cookie = webkitCookieManager.getCookie(uri.toString()) + if (cookie != null) res["Cookie"] = listOf(cookie) + return res + } +} diff --git a/app/src/main/res/layout/dialog_captcha.xml b/app/src/main/res/layout/dialog_captcha.xml new file mode 100644 index 00000000..2df18066 --- /dev/null +++ b/app/src/main/res/layout/dialog_captcha.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt index d7d83e6c..30b9e6b7 100644 --- a/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt +++ b/app/src/play/java/io/github/wulkanowy/ui/modules/settings/ads/AdsFragment.kt @@ -105,6 +105,10 @@ class AdsFragment : PreferenceFragmentCompat(), MainView.TitledView, AdsView { (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog() } + override fun onCaptchaVerificationRequired(url: String?) { + (activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url) + } + override fun showDecryptionFailedDialog() { (activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog() } From 096fe359e72abb67dc3cd7608428c5f91d29f334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sun, 14 Jan 2024 14:09:04 +0100 Subject: [PATCH 13/51] Make some improvements in captcha dialog (#2393) * Add improvements retrying after captcha solved * Add showAuthDialog from BaseActivity instead of displaying this dialog manually * Add getCookieStore() with removeAll impl in WebkitCookieManagerProxy * Add debounce to captcha dialog showing logic * Add refresh button to captcha dialog * Destroy webview along with captcha dialog * Add clear webkit cookies button to debug menu * Add captcha error message * Update captcha verified message --- .../TimetableNotificationSchedulerHelper.kt | 2 - .../wulkanowy/ui/base/BaseDialogFragment.kt | 3 +- .../github/wulkanowy/ui/base/BaseFragment.kt | 3 +- .../ui/modules/captcha/CaptchaDialog.kt | 40 ++++++++++++------ .../ui/modules/dashboard/DashboardFragment.kt | 12 ++++++ .../modules/dashboard/DashboardPresenter.kt | 8 ++++ .../ui/modules/dashboard/DashboardView.kt | 3 +- .../ui/modules/debug/DebugFragment.kt | 5 +++ .../ui/modules/debug/DebugPresenter.kt | 2 + .../wulkanowy/ui/modules/debug/DebugView.kt | 2 + .../modules/login/form/LoginFormFragment.kt | 9 ++++ .../modules/login/form/LoginFormPresenter.kt | 4 ++ .../wulkanowy/ui/modules/main/MainActivity.kt | 34 +++++++++++++++ .../settings/advanced/AdvancedFragment.kt | 3 +- .../settings/appearance/AppearanceFragment.kt | 3 +- .../notifications/NotificationsFragment.kt | 3 +- .../ui/modules/settings/sync/SyncFragment.kt | 3 +- .../wulkanowy/utils/ExceptionExtension.kt | 2 + .../utils/WebkitCookieManagerProxy.kt | 19 +++++++++ app/src/main/res/layout/dialog_captcha.xml | 41 ++++++++++++++++++- app/src/main/res/values/strings.xml | 7 ++++ 21 files changed, 179 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt b/app/src/main/java/io/github/wulkanowy/services/alarm/TimetableNotificationSchedulerHelper.kt index 42078d03..aae7882f 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 @@ -65,8 +65,6 @@ class TimetableNotificationSchedulerHelper @Inject constructor( range = lesson.start..lesson.end, requestCode = getRequestCode(lesson.start, studentId) ) - - Timber.d("TimetableNotification canceled: type 1 & 2, subject: ${lesson.subject}, start: ${lesson.start}, student: $studentId") } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt index cb85fd8a..e63887b8 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseDialogFragment.kt @@ -8,7 +8,6 @@ import android.widget.Toast import androidx.fragment.app.DialogFragment import androidx.viewbinding.ViewBinding import com.google.android.material.elevation.SurfaceColors -import io.github.wulkanowy.ui.modules.auth.AuthDialog import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.lifecycleAwareVariable import javax.inject.Inject @@ -49,7 +48,7 @@ abstract class BaseDialogFragment : DialogFragment(), BaseView } override fun showAuthDialog() { - AuthDialog.newInstance().show(childFragmentManager, "auth_dialog") + (activity as? BaseActivity<*, *>)?.showAuthDialog() } override fun showErrorDetailsDialog(error: Throwable) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt index 4f919f45..ba346131 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BaseFragment.kt @@ -7,7 +7,6 @@ import androidx.viewbinding.ViewBinding import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar.LENGTH_LONG import io.github.wulkanowy.R -import io.github.wulkanowy.ui.modules.auth.AuthDialog import io.github.wulkanowy.utils.lifecycleAwareVariable abstract class BaseFragment(@LayoutRes layoutId: Int) : Fragment(layoutId), @@ -52,7 +51,7 @@ abstract class BaseFragment(@LayoutRes layoutId: Int) : Fragme } override fun showAuthDialog() { - AuthDialog.newInstance().show(childFragmentManager, "auth_dialog") + (activity as? BaseActivity<*, *>)?.showAuthDialog() } override fun openClearLoginView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt index 6c4d6420..098d08ed 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt @@ -5,12 +5,11 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.webkit.WebResourceError -import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient import androidx.core.os.bundleOf import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R import io.github.wulkanowy.databinding.DialogCaptchaBinding import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.ui.base.BaseDialogFragment @@ -23,8 +22,13 @@ class CaptchaDialog : BaseDialogFragment() { @Inject lateinit var sdk: Sdk + private var webView: WebView? = null + companion object { + const val CAPTCHA_SUCCESS = "captcha_success" private const val CAPTCHA_URL = "captcha_url" + private const val CAPTCHA_CHECK_JS = "document.getElementById('challenge-running') == null" + fun newInstance(url: String?): CaptchaDialog { return CaptchaDialog().apply { arguments = bundleOf(CAPTCHA_URL to url) @@ -41,8 +45,14 @@ class CaptchaDialog : BaseDialogFragment() { @SuppressLint("SetJavaScriptEnabled") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + isCancelable = false + binding.captchaRefresh.setOnClickListener { + binding.captchaWebview.loadUrl(arguments?.getString(CAPTCHA_URL).orEmpty()) + } + binding.captchaClose.setOnClickListener { dismiss() } with(binding.captchaWebview) { + webView = this with(settings) { javaScriptEnabled = true userAgentString = sdk.userAgent @@ -50,23 +60,27 @@ class CaptchaDialog : BaseDialogFragment() { webViewClient = object : WebViewClient() { override fun onPageFinished(view: WebView?, url: String?) { - view?.evaluateJavascript("document.getElementById('challenge-running') == undefined") { + view?.evaluateJavascript(CAPTCHA_CHECK_JS) { if (it == "true") { - dismiss() - } else Timber.e("JS result: $it") + onChallengeAccepted() + } } } - - override fun onReceivedError( - view: WebView?, - request: WebResourceRequest?, - error: WebResourceError? - ) { - super.onReceivedError(view, request, error) - } } loadUrl(arguments?.getString(CAPTCHA_URL).orEmpty()) } } + + private fun onChallengeAccepted() { + runCatching { parentFragmentManager.setFragmentResult(CAPTCHA_SUCCESS, bundleOf()) } + .onFailure { Timber.e(it) } + showMessage(getString(R.string.captcha_verified_message)) + dismiss() + } + + override fun onDestroy() { + webView?.destroy() + super.onDestroy() + } } 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 301262a0..bedbce23 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 @@ -18,6 +18,7 @@ import io.github.wulkanowy.databinding.FragmentDashboardBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsFragment import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment +import io.github.wulkanowy.ui.modules.captcha.CaptchaDialog.Companion.CAPTCHA_SUCCESS import io.github.wulkanowy.ui.modules.conference.ConferenceFragment import io.github.wulkanowy.ui.modules.dashboard.adapters.DashboardAdapter import io.github.wulkanowy.ui.modules.exam.ExamFragment @@ -36,6 +37,7 @@ import io.github.wulkanowy.utils.getErrorString import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.openInternetBrowser import io.github.wulkanowy.utils.toFormattedString +import timber.log.Timber import java.time.LocalDate import javax.inject.Inject @@ -62,6 +64,9 @@ class DashboardFragment : BaseFragment(R.layout.fragme return ((recyclerWidth - margin) / resources.displayMetrics.density).toInt() } + override val isViewEmpty + get() = dashboardAdapter.itemCount == 0 + companion object { fun newInstance() = DashboardFragment() @@ -77,6 +82,13 @@ class DashboardFragment : BaseFragment(R.layout.fragme super.onViewCreated(view, savedInstanceState) binding = FragmentDashboardBinding.bind(view) presenter.onAttachView(this) + initializeCaptchaResultObserver() + } + + private fun initializeCaptchaResultObserver() { + childFragmentManager.setFragmentResultListener(CAPTCHA_SUCCESS, this) { _, _ -> + presenter.onRetryAfterCaptcha() + } } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { 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 c93dd9e7..d7add2c0 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 @@ -239,6 +239,14 @@ class DashboardPresenter @Inject constructor( loadData(selectedDashboardTiles, forceRefresh = true) } + fun onRetryAfterCaptcha() { + view?.run { + showErrorView(false) + showProgress(true) + } + loadData(selectedDashboardTiles, forceRefresh = true) + } + fun onViewReselected() { Timber.i("Dashboard view is reselected") view?.run { 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 76788543..fe011c92 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 @@ -6,6 +6,8 @@ interface DashboardView : BaseView { val tileWidth: Int + val isViewEmpty: Boolean + fun initView() fun updateData(data: List) @@ -27,6 +29,5 @@ interface DashboardView : BaseView { fun popViewToRoot() fun openNotificationsCenterView() - fun openInternetBrowser(url: String) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugFragment.kt index 000916b1..9db01a30 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugFragment.kt @@ -2,6 +2,7 @@ package io.github.wulkanowy.ui.modules.debug import android.os.Bundle import android.view.View +import android.webkit.CookieManager import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -58,6 +59,10 @@ class DebugFragment : BaseFragment(R.layout.fragment_debug (activity as? MainActivity)?.pushView(NotificationDebugFragment.newInstance()) } + override fun clearWebkitCookies() { + CookieManager.getInstance().removeAllCookies(null) + } + override fun onDestroyView() { presenter.onDetachView() super.onDestroyView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugPresenter.kt index 67ac8886..816b5985 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugPresenter.kt @@ -15,6 +15,7 @@ class DebugPresenter @Inject constructor( val items = listOf( DebugItem(R.string.logviewer_title), DebugItem(R.string.notification_debug_title), + DebugItem(R.string.debug_cookies_clear), ) override fun onAttachView(view: DebugView) { @@ -31,6 +32,7 @@ class DebugPresenter @Inject constructor( when (item.title) { R.string.logviewer_title -> view?.openLogViewer() R.string.notification_debug_title -> view?.openNotificationsDebug() + R.string.debug_cookies_clear -> view?.clearWebkitCookies() else -> Timber.d("Unknown debug item: $item") } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugView.kt index 9396ec6a..792d63d9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/DebugView.kt @@ -11,4 +11,6 @@ interface DebugView : BaseView { fun openLogViewer() fun openNotificationsDebug() + + fun clearWebkitCookies() } 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 8e9b86fa..975cad18 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 @@ -7,6 +7,7 @@ import android.view.View.GONE import android.view.View.VISIBLE import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged +import androidx.fragment.app.setFragmentResultListener import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.AdminMessage @@ -14,6 +15,7 @@ import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.databinding.FragmentLoginFormBinding import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.captcha.CaptchaDialog import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.LoginData @@ -72,6 +74,13 @@ class LoginFormFragment : BaseFragment(R.layout.fragme super.onViewCreated(view, savedInstanceState) binding = FragmentLoginFormBinding.bind(view) presenter.onAttachView(this) + initializeCaptchaResultObserver() + } + + private fun initializeCaptchaResultObserver() { + setFragmentResultListener(CaptchaDialog.CAPTCHA_SUCCESS) { _, _ -> + presenter.onRetryAfterCaptcha() + } } override fun initView() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt index ad535c38..c9ae4f27 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt @@ -152,6 +152,10 @@ class LoginFormPresenter @Inject constructor( ) } + fun onRetryAfterCaptcha() { + onSignInClick() + } + fun onSignInClick() { val loginData = getLoginData() 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 ba0ef405..62c16257 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 @@ -16,6 +16,7 @@ import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -30,6 +31,8 @@ import io.github.wulkanowy.databinding.ActivityMainBinding 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.auth.AuthDialog +import io.github.wulkanowy.ui.modules.captcha.CaptchaDialog import io.github.wulkanowy.ui.modules.settings.appearance.menuorder.AppMenuItem import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AppInfo @@ -40,10 +43,17 @@ import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.nickOrName import io.github.wulkanowy.utils.safelyPopFragments import io.github.wulkanowy.utils.setOnViewChangeListener +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import timber.log.Timber import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds @AndroidEntryPoint class MainActivity : BaseActivity(), MainView, @@ -73,6 +83,8 @@ class MainActivity : BaseActivity(), MainVie private val navController = FragNavController(supportFragmentManager, R.id.main_fragment_container) + private val captchaVerificationEvent = MutableSharedFlow() + companion object { private const val EXTRA_START_DESTINATION = "start_destination_json" @@ -144,6 +156,7 @@ class MainActivity : BaseActivity(), MainVie initializeToolbar() initializeBottomNavigation(startMenuIndex, rootAppMenuItems) initializeNavController(startMenuIndex, rootUpdatedDestinations) + initializeCaptchaVerificationEvent() } private fun initializeNavController( @@ -323,6 +336,27 @@ class MainActivity : BaseActivity(), MainVie .show() } + @OptIn(FlowPreview::class) + private fun initializeCaptchaVerificationEvent() { + captchaVerificationEvent + .debounce(1.seconds) + .onEach { url -> + Timber.d("Showing captcha dialog for: $url") + showDialogFragment(CaptchaDialog.newInstance(url)) + } + .launchIn(lifecycleScope) + } + + override fun onCaptchaVerificationRequired(url: String?) { + lifecycleScope.launch { + captchaVerificationEvent.emit(url) + } + } + + override fun showAuthDialog() { + showDialogFragment(AuthDialog.newInstance()) + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) navController.onSaveInstanceState(outState) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt index a1d00227..3ef1a80a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/advanced/AdvancedFragment.kt @@ -8,7 +8,6 @@ import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.ErrorDialog -import io.github.wulkanowy.ui.modules.auth.AuthDialog import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.AppInfo import javax.inject.Inject @@ -72,7 +71,7 @@ class AdvancedFragment : PreferenceFragmentCompat(), } override fun showAuthDialog() { - AuthDialog.newInstance().show(childFragmentManager, "auth_dialog") + (activity as? BaseActivity<*, *>)?.showAuthDialog() } override fun onResume() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt index b9b35019..3d0c8052 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt @@ -9,7 +9,6 @@ import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.ErrorDialog -import io.github.wulkanowy.ui.modules.auth.AuthDialog import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.AppInfo import javax.inject.Inject @@ -88,7 +87,7 @@ class AppearanceFragment : PreferenceFragmentCompat(), } override fun showAuthDialog() { - AuthDialog.newInstance().show(childFragmentManager, "auth_dialog") + (activity as? BaseActivity<*, *>)?.showAuthDialog() } override fun onResume() { 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 fdc4a24d..0bf9ddad 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/notifications/NotificationsFragment.kt @@ -21,7 +21,6 @@ import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.ErrorDialog -import io.github.wulkanowy.ui.modules.auth.AuthDialog import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.openInternetBrowser @@ -158,7 +157,7 @@ class NotificationsFragment : PreferenceFragmentCompat(), } override fun showAuthDialog() { - AuthDialog.newInstance().show(childFragmentManager, "auth_dialog") + (activity as? BaseActivity<*, *>)?.showAuthDialog() } override fun showFixSyncDialog() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt index 1e81e58a..d5714483 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/sync/SyncFragment.kt @@ -10,7 +10,6 @@ import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.ErrorDialog -import io.github.wulkanowy.ui.modules.auth.AuthDialog import io.github.wulkanowy.ui.modules.main.MainView import javax.inject.Inject @@ -109,7 +108,7 @@ class SyncFragment : PreferenceFragmentCompat(), } override fun showAuthDialog() { - AuthDialog.newInstance().show(childFragmentManager, "auth_dialog") + (activity as? BaseActivity<*, *>)?.showAuthDialog() } override fun onResume() { diff --git a/app/src/main/java/io/github/wulkanowy/utils/ExceptionExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/ExceptionExtension.kt index a4c2537a..18fc10bb 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/ExceptionExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/ExceptionExtension.kt @@ -3,6 +3,7 @@ package io.github.wulkanowy.utils import android.content.res.Resources import io.github.wulkanowy.R import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException +import io.github.wulkanowy.sdk.scrapper.exception.CloudflareVerificationException import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException import io.github.wulkanowy.sdk.scrapper.exception.ScrapperException import io.github.wulkanowy.sdk.scrapper.exception.ServiceUnavailableException @@ -34,6 +35,7 @@ fun Resources.getErrorString(error: Throwable): String = when (error) { is FeatureNotAvailableException -> R.string.error_feature_not_available is VulcanException -> R.string.error_unknown_uonet is ScrapperException -> R.string.error_unknown_app + is CloudflareVerificationException -> R.string.error_cloudflare_captcha is SSLHandshakeException -> when { error.isCausedByCertificateNotValidNow() -> R.string.error_invalid_device_datetime else -> R.string.error_timeout diff --git a/app/src/main/java/io/github/wulkanowy/utils/WebkitCookieManagerProxy.kt b/app/src/main/java/io/github/wulkanowy/utils/WebkitCookieManagerProxy.kt index 509f39f5..a5497871 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/WebkitCookieManagerProxy.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/WebkitCookieManagerProxy.kt @@ -1,6 +1,8 @@ package io.github.wulkanowy.utils import java.net.CookiePolicy +import java.net.CookieStore +import java.net.HttpCookie import java.net.URI import android.webkit.CookieManager as WebkitCookieManager import java.net.CookieManager as JavaCookieManager @@ -36,4 +38,21 @@ class WebkitCookieManagerProxy : JavaCookieManager(null, CookiePolicy.ACCEPT_ALL if (cookie != null) res["Cookie"] = listOf(cookie) return res } + + override fun getCookieStore(): CookieStore { + val cookies = super.getCookieStore() + return object : CookieStore { + override fun add(uri: URI?, cookie: HttpCookie?) = cookies.add(uri, cookie) + override fun get(uri: URI?): List = cookies.get(uri) + override fun getCookies(): List = cookies.cookies + override fun getURIs(): List = cookies.urIs + override fun remove(uri: URI?, cookie: HttpCookie?): Boolean = + cookies.remove(uri, cookie) + + override fun removeAll(): Boolean { + webkitCookieManager.removeAllCookies(null) + return true + } + } + } } diff --git a/app/src/main/res/layout/dialog_captcha.xml b/app/src/main/res/layout/dialog_captcha.xml index 2df18066..539aa0cc 100644 --- a/app/src/main/res/layout/dialog_captcha.xml +++ b/app/src/main/res/layout/dialog_captcha.xml @@ -1,12 +1,51 @@ + + + + + + + android:layout_height="match_parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toBottomOf="@id/captcha_close" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 72910b85..60d85606 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,6 +14,7 @@ Log viewer Debug Notification debug + Clear webview cookies Contributors Licenses Messages @@ -833,6 +834,11 @@ Skip for now + + Verification is in progress. Wait… + Verified successfully + + No internet connection An error occurred. Check your device clock @@ -842,6 +848,7 @@ Maintenance underway UONET + register. Try again later Unknown UONET + register error. Try again later Unknown application error. Please try again later + Captcha verification required An unexpected error occurred Feature disabled by your school Feature not available. Login in a mode other than Mobile API From 9ececeb4e92b3adb2ac801097159d1bedd1a2de9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sun, 14 Jan 2024 16:41:57 +0100 Subject: [PATCH 14/51] New Crowdin updates (#2394) --- app/src/main/res/values-cs/strings.xml | 5 +++++ app/src/main/res/values-da-rDK/strings.xml | 5 +++++ app/src/main/res/values-de/strings.xml | 5 +++++ app/src/main/res/values-es-rES/strings.xml | 5 +++++ app/src/main/res/values-it-rIT/strings.xml | 5 +++++ app/src/main/res/values-pl/strings.xml | 5 +++++ app/src/main/res/values-ru/strings.xml | 5 +++++ app/src/main/res/values-sk/strings.xml | 5 +++++ app/src/main/res/values-uk/strings.xml | 5 +++++ 9 files changed, 45 insertions(+) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 8e60b7a6..b4f1f878 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -13,6 +13,7 @@ Prohlížeč protokolů Ladění Ladění oznámení + Vymazat soubory cookie webview Tvůrci Licence Zprávy @@ -833,6 +834,9 @@ Autorizace Pro provoz aplikace potřebujeme potvrdit vaši identitu. Zadejte PESEL žáka <b>%1$s</b> v níže uvedeném poli Zatím přeskočit + + Probíhá ověřování. Počkejte… + Úspěšně ověřeno Žádné internetové připojení Vyskytla se chyba. Zkontrolujte hodiny svého zařízení @@ -842,6 +846,7 @@ Probíhá údržba deníku UONET+. Zkuste to později znovu Neznámá chyba deniku UONET+. Prosím zkuste to znovu později Neznámá chyba aplikace. Prosím zkuste to znovu později + Vyžadováno ověření Captcha Vyskytla se neočekávaná chyba Funkce je deaktivována přes vaší školou Funkce není k dispozici. Přihlaste se v jiném režimu než Mobile API diff --git a/app/src/main/res/values-da-rDK/strings.xml b/app/src/main/res/values-da-rDK/strings.xml index 01306662..ac616418 100644 --- a/app/src/main/res/values-da-rDK/strings.xml +++ b/app/src/main/res/values-da-rDK/strings.xml @@ -13,6 +13,7 @@ Log viewer Debug Notification debug + Clear webview cookies Contributors Licenses Messages @@ -743,6 +744,9 @@ Authorization To operate the application, we need to confirm your identity. Please enter the student\'s PESEL <b>%1$s</b> in the field below Skip for now + + Verification is in progress. Wait… + Verified successfully No internet connection An error occurred. Check your device clock @@ -752,6 +756,7 @@ Maintenance underway UONET + register. Try again later Unknown UONET + register error. Try again later Unknown application error. Please try again later + Captcha verification required An unexpected error occurred Feature disabled by your school Feature not available. Login in a mode other than Mobile API diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 09173d38..ec6aa655 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -13,6 +13,7 @@ Log Viewer Debuggen Benachrichtigungen debuggen + Clear webview cookies Mitarbeiter Lizenzen Nachrichten @@ -743,6 +744,9 @@ Authorization To operate the application, we need to confirm your identity. Please enter the student\'s PESEL <b>%1$s</b> in the field below Skip for now + + Verification is in progress. Wait… + Verified successfully Keine Internetverbindung Es ist ein Fehler aufgetreten. Überprüfen Sie Ihre Geräteuhr @@ -752,6 +756,7 @@ Wartung im Gange UONET + Klassenbuch. Versuchen Sie es später noch einmal Unbekannter UONET + Registerfehler. Versuchen Sie es später erneut Unbekannter Anwendungsfehler. Bitte versuchen Sie es später noch einmal + Captcha verification required Ein unerwarteter Fehler ist aufgetreten Funktion, die von Ihrer Schule deaktiviert wurde Feature in diesem Modus nicht verfügbar diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 01306662..ac616418 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -13,6 +13,7 @@ Log viewer Debug Notification debug + Clear webview cookies Contributors Licenses Messages @@ -743,6 +744,9 @@ Authorization To operate the application, we need to confirm your identity. Please enter the student\'s PESEL <b>%1$s</b> in the field below Skip for now + + Verification is in progress. Wait… + Verified successfully No internet connection An error occurred. Check your device clock @@ -752,6 +756,7 @@ Maintenance underway UONET + register. Try again later Unknown UONET + register error. Try again later Unknown application error. Please try again later + Captcha verification required An unexpected error occurred Feature disabled by your school Feature not available. Login in a mode other than Mobile API diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml index 01306662..ac616418 100644 --- a/app/src/main/res/values-it-rIT/strings.xml +++ b/app/src/main/res/values-it-rIT/strings.xml @@ -13,6 +13,7 @@ Log viewer Debug Notification debug + Clear webview cookies Contributors Licenses Messages @@ -743,6 +744,9 @@ Authorization To operate the application, we need to confirm your identity. Please enter the student\'s PESEL <b>%1$s</b> in the field below Skip for now + + Verification is in progress. Wait… + Verified successfully No internet connection An error occurred. Check your device clock @@ -752,6 +756,7 @@ Maintenance underway UONET + register. Try again later Unknown UONET + register error. Try again later Unknown application error. Please try again later + Captcha verification required An unexpected error occurred Feature disabled by your school Feature not available. Login in a mode other than Mobile API diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index fb9d170a..1b4fbe66 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -13,6 +13,7 @@ Przeglądarka logów Debugowanie Debugowanie powiadomień + Wyczyść ciasteczka webview Twórcy Licencje Wiadomości @@ -833,6 +834,9 @@ Autoryzacja Rodzicu, musimy mieć pewność, że Twój adres e-mail został powiązany z prawidłowym kontem ucznia. W celu autoryzacji konta podaj numer PESEL ucznia <b>%1$s</b> w polu poniżej Na razie pomiń + + Trwa weryfikacja. Czekaj… + Pomyślnie zweryfikowano Brak połączenia z internetem Wystąpił błąd. Sprawdź poprawność daty w urządzeniu @@ -842,6 +846,7 @@ Trwa przerwa techniczna dziennika UONET+. Spróbuj ponownie później Nieznany błąd dziennika UONET+. Spróbuj ponownie później Nieznany błąd aplikacji. Spróbuj ponownie później + Wymagana weryfikacja captcha Wystąpił nieoczekiwany błąd Funkcja wyłączona przez szkołę Funkcja niedostępna. Zaloguj się w trybie innym niż Mobilne API diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index c604cd8b..feb08a03 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -13,6 +13,7 @@ Просмотр журнала Отладка Отладка уведомлений + Clear webview cookies Разработчики Лицензии Сообщения @@ -833,6 +834,9 @@ Авторизация Для работы приложения нам необходимо подтвердить вашу личность. Введите PESEL учащегося <b>%1$s</b> в поле ниже Пропустить сейчас + + Verification is in progress. Wait… + Verified successfully Интернет-соединение отсутствует Произошла ошибка. Проверьте время на вашем устройстве @@ -842,6 +846,7 @@ UONET+ проводит техническое обслуживание, повторите попытку позже Неизвестная ошибка дневника UONET+, повторите попытку позже Неизвестная ошибка приложения, повторите попытку позже + Captcha verification required Произошла непредвиденная ошибка Функция отключена вашей школой Функция недоступна в режиме Mobile API. Воспользуйтесь другим режимом diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index e02b1542..aaf04bc8 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -13,6 +13,7 @@ Prehliadač protokolov Ladenie Ladenie oznámení + Vymazať súbory cookie webview Tvorcovia Licencie Správy @@ -833,6 +834,9 @@ Autorizácia Na prevádzku aplikácie potrebujeme potvrdiť vašu identitu. Zadajte PESEL žiaka <b>%1$s</b> v nižšie uvedenom poli Zatiaľ preskočiť + + Overovanie prebieha. Počkajte… + Úspešne overené Žiadne internetové pripojenie Vyskytla sa chyba. Skontrolujte hodiny svojho zariadenia @@ -842,6 +846,7 @@ Prebieha údržba denníka UONET+. Skúste to neskôr znova Neznáma chyba dennika UONET+. Prosím skúste to znova neskôr Neznáma chyba aplikácie. Prosím skúste to znova neskôr + Vyžaduje sa overenie Captcha Vyskytla sa neočakávaná chyba Funkcia je deaktivovaná cez vašou školou Funkcia nie je k dispozícii. Prihláste sa v inom režime než Mobile API diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 32617f42..fffae003 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -13,6 +13,7 @@ Переглядач логів Відладка Відладка сповіщень + Очистити кукі веб - перегляду Розробники Ліцензії Листи @@ -833,6 +834,9 @@ Авторизувати Для роботи програми нам потрібно підтвердити вашу особу. Будь ласка, введіть число PESEL <b>%1$s</b> студента в поле нижче Поки що пропустити + + Верифікація в процесі. Чекайте… + Верифікація завершена Немає з\'єднання з інтернетом Сталася помилка. Перевірте годинник пристрою @@ -842,6 +846,7 @@ UONET+ проводить технічне осблуговування, спробуйте пізніше Невідома помилка щоденника UONET+, спробуйте пізніше Невідома помилка програми, спробуйте пізніше + Необхідна перевірка Captcha Відбулася несподівана помилка Функція вимкнена вашою школою Функція недоступна в режимі Mobile API. Увійдіть в інший режим From 976eb5a7720fdc2e74356015016f19783ca53e92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sun, 14 Jan 2024 16:45:30 +0100 Subject: [PATCH 15/51] Fix cancelling dashboard jobs (#2395) --- .../java/io/github/wulkanowy/data/Resource.kt | 16 ++++++++-- .../ui/modules/captcha/CaptchaDialog.kt | 2 +- .../modules/dashboard/DashboardPresenter.kt | 30 ++++++++++++++++--- 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/data/Resource.kt b/app/src/main/java/io/github/wulkanowy/data/Resource.kt index 2c5bf0ea..d7c2aeed 100644 --- a/app/src/main/java/io/github/wulkanowy/data/Resource.kt +++ b/app/src/main/java/io/github/wulkanowy/data/Resource.kt @@ -1,6 +1,16 @@ package io.github.wulkanowy.data -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import timber.log.Timber @@ -131,7 +141,7 @@ inline fun networkBoundResource( query().map { Resource.Success(filterResult(it)) } } catch (throwable: Throwable) { onFetchFailed(throwable) - query().map { Resource.Error(throwable) } + flowOf(Resource.Error(throwable)) } } else { query().map { Resource.Success(filterResult(it)) } @@ -165,7 +175,7 @@ inline fun networkBoundResource( query().map { Resource.Success(mapResult(it)) } } catch (throwable: Throwable) { onFetchFailed(throwable) - query().map { Resource.Error(throwable) } + flowOf(Resource.Error(throwable)) } } else { query().map { Resource.Success(mapResult(it)) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt index 098d08ed..ed8293a9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/captcha/CaptchaDialog.kt @@ -76,7 +76,7 @@ class CaptchaDialog : BaseDialogFragment() { runCatching { parentFragmentManager.setFragmentResult(CAPTCHA_SUCCESS, bundleOf()) } .onFailure { Timber.e(it) } showMessage(getString(R.string.captcha_verified_message)) - dismiss() + dismissAllowingStateLoss() } override fun onDestroy() { 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 d7add2c0..74b427e7 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 @@ -324,7 +324,7 @@ class DashboardPresenter @Inject constructor( ) { luckyNumberResource, messageResource, attendanceResource -> val resList = listOf(luckyNumberResource, messageResource, attendanceResource) - DashboardItem.HorizontalGroup( + resList to DashboardItem.HorizontalGroup( isLoading = resList.any { it is Resource.Loading }, error = resList.map { it.errorOrNull }.let { errors -> if (errors.all { it != null }) { @@ -349,9 +349,9 @@ class DashboardPresenter @Inject constructor( ) }) } - .filterNot { it.isLoading && forceRefresh } + .filterNot { (_, it) -> it.isLoading && forceRefresh } .distinctUntilChanged() - .onEach { + .onEach { (_, it) -> updateData(it, forceRefresh) if (it.isLoading) { @@ -369,7 +369,7 @@ class DashboardPresenter @Inject constructor( ) errorHandler.dispatch(it) } - .launch("horizontal_group ${if (forceRefresh) "-forceRefresh" else ""}") + .launchWithUniqueRefreshJob("horizontal_group", forceRefresh) } private fun loadGrades(student: Student, forceRefresh: Boolean) { @@ -862,6 +862,28 @@ class DashboardPresenter @Inject constructor( onEach { if (it is Resource.Success) { cancelJobs(jobName) + } else if (it is Resource.Error) { + cancelJobs(jobName) + } + }.launch(jobName) + } else { + launch(jobName) + } + } + + @JvmName("launchWithUniqueRefreshJobHorizontalGroup") + private fun Flow>, *>>.launchWithUniqueRefreshJob( + name: String, + forceRefresh: Boolean + ) { + val jobName = if (forceRefresh) "$name-forceRefresh" else name + + if (forceRefresh) { + onEach { (resources, _) -> + if (resources.all { it is Resource.Success<*> }) { + cancelJobs(jobName) + } else if (resources.any { it is Resource.Error<*> }) { + cancelJobs(jobName) } }.launch(jobName) } else { From 497acf9d685102064b670a07038ecae6b2f47098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sun, 14 Jan 2024 17:32:41 +0100 Subject: [PATCH 16/51] Version 2.3.4 --- app/build.gradle | 8 ++++---- app/src/main/play/release-notes/pl-PL/default.txt | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 7069672a..8d10ce92 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,8 +27,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 34 - versionCode 143 - versionName "2.3.3" + versionCode 144 + versionName "2.3.4" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" @@ -163,7 +163,7 @@ play { track = 'production' releaseStatus = ReleaseStatus.IN_PROGRESS userFraction = 0.99d - updatePriority = 3 + updatePriority = 1 enabled.set(false) } @@ -193,7 +193,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.3.6-SNAPSHOT' + implementation 'io.github.wulkanowy:sdk:2.3.6' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index 0a2eb68f..c2c30883 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,5 +1,6 @@ -Wersja 2.3.3 +Wersja 2.3.4 -— poprawiliśmy kolejne usterki przy odświeżaniu danych (teraz to powinno działać już dużo lepiej) +— dodaliśmy obsługę captchy, co umożliwi używanie apki np. na odmianie ResMan Rzeszów +— naprawiliśmy wyświetlanie frekwencji w szkołach używających eduOne (piszcie, jeśli nadal nie działa) Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases From e58c15596109075691dfcb7a7cee974ff037d1ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sun, 14 Jan 2024 18:02:15 +0100 Subject: [PATCH 17/51] New Crowdin updates (#2396) --- app/src/main/res/values-da-rDK/strings.xml | 764 --------------------- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-es-rES/strings.xml | 764 --------------------- app/src/main/res/values-it-rIT/strings.xml | 764 --------------------- app/src/main/res/values-ru/strings.xml | 2 +- 5 files changed, 2 insertions(+), 2294 deletions(-) delete mode 100644 app/src/main/res/values-da-rDK/strings.xml delete mode 100644 app/src/main/res/values-es-rES/strings.xml delete mode 100644 app/src/main/res/values-it-rIT/strings.xml diff --git a/app/src/main/res/values-da-rDK/strings.xml b/app/src/main/res/values-da-rDK/strings.xml deleted file mode 100644 index ac616418..00000000 --- a/app/src/main/res/values-da-rDK/strings.xml +++ /dev/null @@ -1,764 +0,0 @@ - - - - Login - Wulkanowy - Grades - Attendance - Exams - Timetable - Settings - More - About - Log viewer - Debug - Notification debug - Clear webview cookies - Contributors - Licenses - Messages - New message - New homework - Notes and achievements - Homework - Accounts manager - Select account - Account details - Student info - Dashboard - Notifications center - Menu configuartion - - Semester %1$d, %2$d/%3$d - - Sign in with the student or parent account - Enter the symbol from the register page for account: <b>%1$s</b> - Username - Email - Login, PESEL or e-mail - Password - UONET+ register variant - Custom domain suffix - Mobile API - Scraper - Hybrid - Token - PIN - Symbol - E.g. \"lodz\" or \"powiatjaroslawski\" - Sign in - Password too short - Login details are incorrect - %1$s. Make sure the correct UONET+ register variation is selected below - Invalid PIN - Invalid token - Token expired - Invalid email - Use the assigned login instead of email - Use the assigned login or email in @%1$s - Invalid symbol. If you cannot find it, please contact the school - Don\'t make this up! If you cannot find it, please contact the school - Student not found. Validate the symbol and the chosen variation of the UONET+ register - Selected student is already logged in - The symbol can be found on the register page in Uczeń → Dostęp Mobilny → Wygeneruj kod dostępu.\n\nMake sure that you have set the appropriate register variant in the UONET+ register variant field on the first login screen - Select students to log in to the application - Other options - In this mode, a lucky number does not work, a class grade stats, summary of attendance, excuse for absence, completed lessons, school information and preview of the list of registered devices - This mode displays the same data as it appears on the register website - The combination of the best features of the other two modes. It works faster than scraper and provides features not available in the Mobile API mode. It is in the experimental phase - Privacy policy - Trouble signing in? Contact us! - Email - Discord - Send email - Make sure you select the correct UONET+ register variation! - Reset password - Recover your account - Recover - Student is already signed in - Standard - Other search locations - No active students found - Enter a different symbol - Get help - Full school name with the town (required) - Np. ZSTiO Jarosław lub SP nr 99 w Łodzi - Enter correct name of the school - Additional information in Polish (optional) - Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\" - Submit - - Enable notifications - Enable notifications so you don\'t miss message from teacher or new grade - Skip - Enable - - Account manager - Log in - Session expired - Session expired, log in again - Your account password has been changed. You need to log in to Wulkanowy again - Password changed - Application support - Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time - Enable ads - - Grade - Semester %d - Change semester - No grades - Weight - Weight: %s - Comment - Number of new ratings: %1$d - Average: %1$.2f - Points: %s - No average - Total points - Final grade - Predicted grade - Calculated average - How does Calculated Average work? - The Calculated Average is the arithmetic average calculated from the subjects averages. It allows you to know the approximate final average. It is calculated in a way selected by the user in the application settings. It is recommended that you choose the appropriate option. This is because the calculation of school averages differs. Additionally, if your school reports the average of the subjects on the Vulcan page, the application downloads them and does not calculate these averages. This can be changed by forcing the calculation of the average in the application settings.\n\nAverage of grades only from selected semester:\n1. Calculating the weighted average for each subject in a given semester\n2.Adding calculated averages\n3. Calculation of the arithmetic average of the summed averages\n\nAverage of averages from both semesters:\n1.Calculating the weighted average for each subject in semester 1 and 2\n2. Calculating the arithmetic average of the calculated averages for semesters 1 and 2 for each subject.\n3. Adding calculated averages\n4. Calculation of the arithmetic average of the summed averages\n\nAverage of grades from the whole year:\n1. Calculating weighted average over the year for each subject. The final average in the 1st semester is irrelevant.\n2. Adding calculated averages\n3. Calculating the arithmetic average of summed averages - How does the Final Average work? - The Final Average is the arithmetic average calculated from all currently available final grades in the given semester.\n\nThe calculation scheme consists of the following steps:\n1. Summing up the final grades given by teachers\n2. Divide by the number of subjects that have already been graded - Final average - from %1$d of %2$d subjects - Summary - Class - Mark as read - Partial - Semester - Points - Legend - Class average: %1$s - Your average: %1$s - Your grade: %1$s - Class - Student - - %d grade - %d grades - - - New grade - New grades - - - New predicted grade - New predicted grades - - - New final grade - New final grades - - - You received %1$d grade - You received %1$d grades - - - You received %1$d predicted grade - You received %1$d predicted grades - - - You received %1$d final grade - You received %1$d final grades - - - Lesson - Room - Group - Hours - Changes - No lessons this day - %s min - %s sec - %1$s left - in %1$s - Finished - Now: %s - Next: %s - Later: %s - %1$s lesson %2$d - %3$s - Change of room from %1$s to %2$s - Change of teacher from %1$s to %2$s - Change of subject from %1$s to %2$s - - No lesson - No lessons - - - Timetable change - Timetable changes - - - %1$s - %2$d change in timetable - %1$s - %2$d changes in timetable - - - %1$d change in timetable - %1$d changes in timetable - - - %d change - %d changes - - - Completed lessons - Show completed lessons - No info about completed lessons - Topic - Absence - Resources - - Additional lessons - Show additional lessons - No info about additional lessons - New lesson - New additional lesson - Additional lesson added successfully - Additional lesson deleted successfully - Repeat weekly - Delete additional lesson - Just this lesson - All in the series - Start time - End time - End time must be greater than start time - - Attendance summary - Absent for school reasons - Excused absence - Unexcused absence - Exemption - Excused lateness - Unexcused lateness - Present - Deleted - Unknown - Number of lesson - No entries - Absence reason (optional) - Send - Absence excuse request sent successfully! - You must select at least one absence! - Excuse - - New attendance - New attendance - - - %1$d new attendance - %1$d attendance - - - %d attendance - %d attendance - - - Total - - No exams this week - Type - Entry date - - New exam - New exams - - - %d new exam - %d new exams - - - %d exam - %d exams - - - Inbox - Sent - Trash - (no subject) - No messages - From: - To: - Date: %1$s - Reply - Forward - Select all - Unselect all - Move to trash - Delete permanently - Message deleted successfully - student - parent - guardian - employee - Share - Print - Subject - Content - Message sent successfully - Message does not exist - You need to choose at least 1 recipient - The message content must be at least 3 characters - All mailboxes - Only unread - Only with attachments - Read: %s - Read by: %1$d of %2$d people - - %1$d message - %1$d messages - - - New message - New messages - - Do you want to restore draft message? - Do you want to restore draft message with recipients: %s? - - You received %1$d message - You received %1$d messages - - - %1$d selected - %1$d selected - - Messages deleted - Choose mailbox - Incognito mode is on - Thanks to incognito mode sender is not notified when you read the message - - No info about notes - Points - - %d note - %d notes - - - New note - New notes - - - You received %1$d note - You received %1$d notes - - - - %d praise - %d praises - - - New praise - New praises - - - You received %1$d praise - You received %1$d praises - - - - %d neutral note - %d neutral notes - - - New neutral note - New neutral notes - - - You received %1$d neutral note - You received %1$d neutral notes - - - No info about homework - Mark as done - Mark as undone - Add homework - Homework added successfully - Homework deleted successfully - Attachments - - New homework - New homework - - - You received %d new homework - You received %d new homework - - - %d homework - %d homework - - - Lucky number - Today\'s lucky number is - No info about the lucky number - Lucky number for today - Today\'s lucky number is: %s - Show history - - Lucky number history - No info about lucky numbers - - Mobile devices - No devices - Deregister - Device removed - QR code - Token - Symbol - PIN - - School and teachers - - School - No info about school - School name - School address - Telephone - Name of headmaster - Name of pedagogue - Show on map - Call - - Teachers - No info about teachers - No subject - - Conferences - No info about conferences - - %d conference - %d conferences - - - New conference - New conferences - - - You have %1$d new conference - You have %1$d new conferences - - Present at conference - Agenda - Place - Topic - - School announcements - No school announcements - - %d school announcement - %d school announcements - - - New school announcement - New school announcements - - - You have %1$d new school announcement - You have %1$d new school announcements - - - Add account - Logout - Do you want to log out this student? - Student logout - Student account - Parent account - Edit data - Accounts manager - Select student - Family - Contact - Residence details - Personal information - - App version - Contributors - List of Wulkanowy developers - Report a bug - Send a bug report via e-mail - FAQ - Read Frequently Asked Questions - Discord server - Join the Wulkanowy community - Facebook fanpage - Twitter page - Follow us on twitter - Like our facebook fanpage - Privacy policy - Rules for collecting personal data - System settings - Open system settings - Homepage - Visit the website and help develop the application - Licenses - Licenses of libraries used in the application - - License - - Avatar - See more on GitHub - - No info about student or student family - Name - Second name - Gender - Polish citizenship - Family name - Mother\'s and father\'s names - Phone - Cellphone - E-mail - Address of residence - Address of registration - Correspondence address - Surname and first name - Degree of kinship - Address - Phones - Male - Female - Last name - Guardian - - Nick - Add nick - Choose avatar color - - Share logs - Refresh - - Lessons - (Tomorrow) - (Today and tomorrow) - In a moment: - Soon: - First: - Now: - End of lessons - Next: - Later: - - %1$d more lesson - %1$d more lessons - - until %1$s - No upcoming lessons - An error occurred while loading the lessons - Homework - No homework to do - An error occurred while loading the homework - - %1$d more homework - %1$d more homework - - due %1$s - Last grades - No new grades - An error occurred while loading the grades - School announcements - No current announcements - An error occurred while loading the announcements - - %1$d more announcement - %1$d more announcements - - Exams - No upcoming exams - An error occurred while loading the exams - - %1$d more exam - %1$d more exams - - Conferences - No upcoming conferences - An error occurred while loading the conferences - - %1$d more conference - %1$d more conferences - - An error occurred while loading data - None - - Check for updates - Before reporting a bug, check first if an update with the bug fix is available - - Content - Retry - Description - No description - Teacher - Date - Entry date - Color - Details - Category - Close - No data - Subject - Prev - Next - Search - Search… - Yes - No - Save - Title - Add - Copied - Undo - Change - Add to calendar - Cancel - - No lessons - Synchronized on %1$s at %2$s - Choose theme - Light - Dark - System Theme - - App - Default view - Calculated average options - Force average calculation by app - Show presence - Theme - Grades expanding - Show groups next to subjects - Show empty tiles where there\'s no lesson - Show chart list in class grades - Show subjects without grades - Grades color scheme - Subjects sorting - Language - Menu configuration - Set the order of functions in the menu - Notifications - Other - Show notifications - Show upcoming lesson notifications - Make upcoming lesson notification persistent - Turn off when notification is not showing in your watch/band - Open system notification settings - Fix synchronization & notifications issues - Your device may have data synchronization issues and with notifications.\n\nTo fix them, you need to add Wulkanowy to the autostart and turn off battery optimization/saving in the phone settings. - Show debug notifications - Synchronization is disabled - Official app notifications - Capture official app notifications - Remove official app notifications after capture - Capture notifications - With this feature you can gain a substitute of push notifications like in the official app. All you need to do is allow Wulkanowy to receive all notifications in your system settings.\n\nHow it works?\nWhen you get a notification in Dziennik VULCAN, Wulkanowy will be notified (that\'s what these extra permissions are for) and will trigger a sync so that can send its own notification.\n\nFOR ADVANCED USERS ONLY - Upcoming lesson notifications - You must allow the Wulkanowy app to set alarms and reminders in your system settings to use this feature. - Go to settings - Synchronization - Automatic update - Suspended on holidays - Updates interval - Wi-Fi only - Sync now - Synced! - Sync failed - Sync in progress - Last full sync: %s - Value of the plus - Value of the minus - Reply with message history - Show arithmetic average when no weights provided - Incognito mode - Do not inform about reading the message - Support - Privacy Policy - Agreements - Show consent to data processing - Show ads in app - Watch single ad to support project - Consent to data processing - To view an advertisement you must agree to the data processing terms of our Privacy Policy - Agree - Privacy policy - Ad is loading - Thank you for your support, come back later for more ads - Advanced - Appearance & Behavior - Notifications - Synchronization - Advertisements - Grades - Dashboard - Tiles visibility - Attendance - Timetable - Grades - Calculated average - Messages - Appearance & Behavior - Languages, themes, subjects sorting - App notifications, fix problems - Notifications - Synchronization - Automatic update, synchronization interval - Plus and minus values, average calculation - Advanced - App version, contributors, social portals - Displaying advertisements, project support - - New grades - New homework - New conferences - New exams - Lucky number - New messages - New notes - New school announcements - Push notifications - Upcoming lessons - Debug - Timetable change - New attendance - - Black - Red - Blue - Green - Purple - No color - - Download of updates has started… - An update has just been downloaded. - Restart - Update failed! Wulkanowy may not function properly. Consider updating - - Application restart - The application must restart for the changes to be saved - Restart - - Authorization has been rejected. The data provided does not match the records in the secretary\'s office. - Invalid PESEL - PESEL - Authorize - Authorization completed successfully - Authorization - To operate the application, we need to confirm your identity. Please enter the student\'s PESEL <b>%1$s</b> in the field below - Skip for now - - Verification is in progress. Wait… - Verified successfully - - No internet connection - An error occurred. Check your device clock - Connection to register failed. Servers can be overloaded. Please try again later - Loading data failed. Please try again later - Register password change required - Maintenance underway UONET + register. Try again later - Unknown UONET + register error. Try again later - Unknown application error. Please try again later - Captcha verification required - An unexpected error occurred - Feature disabled by your school - Feature not available. Login in a mode other than Mobile API - This field is required - diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index ec6aa655..7e0ce868 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -10,7 +10,7 @@ Einstellungen Mehr Über die Applikation - Log Viewer + Log viewer Debuggen Benachrichtigungen debuggen Clear webview cookies diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml deleted file mode 100644 index ac616418..00000000 --- a/app/src/main/res/values-es-rES/strings.xml +++ /dev/null @@ -1,764 +0,0 @@ - - - - Login - Wulkanowy - Grades - Attendance - Exams - Timetable - Settings - More - About - Log viewer - Debug - Notification debug - Clear webview cookies - Contributors - Licenses - Messages - New message - New homework - Notes and achievements - Homework - Accounts manager - Select account - Account details - Student info - Dashboard - Notifications center - Menu configuartion - - Semester %1$d, %2$d/%3$d - - Sign in with the student or parent account - Enter the symbol from the register page for account: <b>%1$s</b> - Username - Email - Login, PESEL or e-mail - Password - UONET+ register variant - Custom domain suffix - Mobile API - Scraper - Hybrid - Token - PIN - Symbol - E.g. \"lodz\" or \"powiatjaroslawski\" - Sign in - Password too short - Login details are incorrect - %1$s. Make sure the correct UONET+ register variation is selected below - Invalid PIN - Invalid token - Token expired - Invalid email - Use the assigned login instead of email - Use the assigned login or email in @%1$s - Invalid symbol. If you cannot find it, please contact the school - Don\'t make this up! If you cannot find it, please contact the school - Student not found. Validate the symbol and the chosen variation of the UONET+ register - Selected student is already logged in - The symbol can be found on the register page in Uczeń → Dostęp Mobilny → Wygeneruj kod dostępu.\n\nMake sure that you have set the appropriate register variant in the UONET+ register variant field on the first login screen - Select students to log in to the application - Other options - In this mode, a lucky number does not work, a class grade stats, summary of attendance, excuse for absence, completed lessons, school information and preview of the list of registered devices - This mode displays the same data as it appears on the register website - The combination of the best features of the other two modes. It works faster than scraper and provides features not available in the Mobile API mode. It is in the experimental phase - Privacy policy - Trouble signing in? Contact us! - Email - Discord - Send email - Make sure you select the correct UONET+ register variation! - Reset password - Recover your account - Recover - Student is already signed in - Standard - Other search locations - No active students found - Enter a different symbol - Get help - Full school name with the town (required) - Np. ZSTiO Jarosław lub SP nr 99 w Łodzi - Enter correct name of the school - Additional information in Polish (optional) - Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\" - Submit - - Enable notifications - Enable notifications so you don\'t miss message from teacher or new grade - Skip - Enable - - Account manager - Log in - Session expired - Session expired, log in again - Your account password has been changed. You need to log in to Wulkanowy again - Password changed - Application support - Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time - Enable ads - - Grade - Semester %d - Change semester - No grades - Weight - Weight: %s - Comment - Number of new ratings: %1$d - Average: %1$.2f - Points: %s - No average - Total points - Final grade - Predicted grade - Calculated average - How does Calculated Average work? - The Calculated Average is the arithmetic average calculated from the subjects averages. It allows you to know the approximate final average. It is calculated in a way selected by the user in the application settings. It is recommended that you choose the appropriate option. This is because the calculation of school averages differs. Additionally, if your school reports the average of the subjects on the Vulcan page, the application downloads them and does not calculate these averages. This can be changed by forcing the calculation of the average in the application settings.\n\nAverage of grades only from selected semester:\n1. Calculating the weighted average for each subject in a given semester\n2.Adding calculated averages\n3. Calculation of the arithmetic average of the summed averages\n\nAverage of averages from both semesters:\n1.Calculating the weighted average for each subject in semester 1 and 2\n2. Calculating the arithmetic average of the calculated averages for semesters 1 and 2 for each subject.\n3. Adding calculated averages\n4. Calculation of the arithmetic average of the summed averages\n\nAverage of grades from the whole year:\n1. Calculating weighted average over the year for each subject. The final average in the 1st semester is irrelevant.\n2. Adding calculated averages\n3. Calculating the arithmetic average of summed averages - How does the Final Average work? - The Final Average is the arithmetic average calculated from all currently available final grades in the given semester.\n\nThe calculation scheme consists of the following steps:\n1. Summing up the final grades given by teachers\n2. Divide by the number of subjects that have already been graded - Final average - from %1$d of %2$d subjects - Summary - Class - Mark as read - Partial - Semester - Points - Legend - Class average: %1$s - Your average: %1$s - Your grade: %1$s - Class - Student - - %d grade - %d grades - - - New grade - New grades - - - New predicted grade - New predicted grades - - - New final grade - New final grades - - - You received %1$d grade - You received %1$d grades - - - You received %1$d predicted grade - You received %1$d predicted grades - - - You received %1$d final grade - You received %1$d final grades - - - Lesson - Room - Group - Hours - Changes - No lessons this day - %s min - %s sec - %1$s left - in %1$s - Finished - Now: %s - Next: %s - Later: %s - %1$s lesson %2$d - %3$s - Change of room from %1$s to %2$s - Change of teacher from %1$s to %2$s - Change of subject from %1$s to %2$s - - No lesson - No lessons - - - Timetable change - Timetable changes - - - %1$s - %2$d change in timetable - %1$s - %2$d changes in timetable - - - %1$d change in timetable - %1$d changes in timetable - - - %d change - %d changes - - - Completed lessons - Show completed lessons - No info about completed lessons - Topic - Absence - Resources - - Additional lessons - Show additional lessons - No info about additional lessons - New lesson - New additional lesson - Additional lesson added successfully - Additional lesson deleted successfully - Repeat weekly - Delete additional lesson - Just this lesson - All in the series - Start time - End time - End time must be greater than start time - - Attendance summary - Absent for school reasons - Excused absence - Unexcused absence - Exemption - Excused lateness - Unexcused lateness - Present - Deleted - Unknown - Number of lesson - No entries - Absence reason (optional) - Send - Absence excuse request sent successfully! - You must select at least one absence! - Excuse - - New attendance - New attendance - - - %1$d new attendance - %1$d attendance - - - %d attendance - %d attendance - - - Total - - No exams this week - Type - Entry date - - New exam - New exams - - - %d new exam - %d new exams - - - %d exam - %d exams - - - Inbox - Sent - Trash - (no subject) - No messages - From: - To: - Date: %1$s - Reply - Forward - Select all - Unselect all - Move to trash - Delete permanently - Message deleted successfully - student - parent - guardian - employee - Share - Print - Subject - Content - Message sent successfully - Message does not exist - You need to choose at least 1 recipient - The message content must be at least 3 characters - All mailboxes - Only unread - Only with attachments - Read: %s - Read by: %1$d of %2$d people - - %1$d message - %1$d messages - - - New message - New messages - - Do you want to restore draft message? - Do you want to restore draft message with recipients: %s? - - You received %1$d message - You received %1$d messages - - - %1$d selected - %1$d selected - - Messages deleted - Choose mailbox - Incognito mode is on - Thanks to incognito mode sender is not notified when you read the message - - No info about notes - Points - - %d note - %d notes - - - New note - New notes - - - You received %1$d note - You received %1$d notes - - - - %d praise - %d praises - - - New praise - New praises - - - You received %1$d praise - You received %1$d praises - - - - %d neutral note - %d neutral notes - - - New neutral note - New neutral notes - - - You received %1$d neutral note - You received %1$d neutral notes - - - No info about homework - Mark as done - Mark as undone - Add homework - Homework added successfully - Homework deleted successfully - Attachments - - New homework - New homework - - - You received %d new homework - You received %d new homework - - - %d homework - %d homework - - - Lucky number - Today\'s lucky number is - No info about the lucky number - Lucky number for today - Today\'s lucky number is: %s - Show history - - Lucky number history - No info about lucky numbers - - Mobile devices - No devices - Deregister - Device removed - QR code - Token - Symbol - PIN - - School and teachers - - School - No info about school - School name - School address - Telephone - Name of headmaster - Name of pedagogue - Show on map - Call - - Teachers - No info about teachers - No subject - - Conferences - No info about conferences - - %d conference - %d conferences - - - New conference - New conferences - - - You have %1$d new conference - You have %1$d new conferences - - Present at conference - Agenda - Place - Topic - - School announcements - No school announcements - - %d school announcement - %d school announcements - - - New school announcement - New school announcements - - - You have %1$d new school announcement - You have %1$d new school announcements - - - Add account - Logout - Do you want to log out this student? - Student logout - Student account - Parent account - Edit data - Accounts manager - Select student - Family - Contact - Residence details - Personal information - - App version - Contributors - List of Wulkanowy developers - Report a bug - Send a bug report via e-mail - FAQ - Read Frequently Asked Questions - Discord server - Join the Wulkanowy community - Facebook fanpage - Twitter page - Follow us on twitter - Like our facebook fanpage - Privacy policy - Rules for collecting personal data - System settings - Open system settings - Homepage - Visit the website and help develop the application - Licenses - Licenses of libraries used in the application - - License - - Avatar - See more on GitHub - - No info about student or student family - Name - Second name - Gender - Polish citizenship - Family name - Mother\'s and father\'s names - Phone - Cellphone - E-mail - Address of residence - Address of registration - Correspondence address - Surname and first name - Degree of kinship - Address - Phones - Male - Female - Last name - Guardian - - Nick - Add nick - Choose avatar color - - Share logs - Refresh - - Lessons - (Tomorrow) - (Today and tomorrow) - In a moment: - Soon: - First: - Now: - End of lessons - Next: - Later: - - %1$d more lesson - %1$d more lessons - - until %1$s - No upcoming lessons - An error occurred while loading the lessons - Homework - No homework to do - An error occurred while loading the homework - - %1$d more homework - %1$d more homework - - due %1$s - Last grades - No new grades - An error occurred while loading the grades - School announcements - No current announcements - An error occurred while loading the announcements - - %1$d more announcement - %1$d more announcements - - Exams - No upcoming exams - An error occurred while loading the exams - - %1$d more exam - %1$d more exams - - Conferences - No upcoming conferences - An error occurred while loading the conferences - - %1$d more conference - %1$d more conferences - - An error occurred while loading data - None - - Check for updates - Before reporting a bug, check first if an update with the bug fix is available - - Content - Retry - Description - No description - Teacher - Date - Entry date - Color - Details - Category - Close - No data - Subject - Prev - Next - Search - Search… - Yes - No - Save - Title - Add - Copied - Undo - Change - Add to calendar - Cancel - - No lessons - Synchronized on %1$s at %2$s - Choose theme - Light - Dark - System Theme - - App - Default view - Calculated average options - Force average calculation by app - Show presence - Theme - Grades expanding - Show groups next to subjects - Show empty tiles where there\'s no lesson - Show chart list in class grades - Show subjects without grades - Grades color scheme - Subjects sorting - Language - Menu configuration - Set the order of functions in the menu - Notifications - Other - Show notifications - Show upcoming lesson notifications - Make upcoming lesson notification persistent - Turn off when notification is not showing in your watch/band - Open system notification settings - Fix synchronization & notifications issues - Your device may have data synchronization issues and with notifications.\n\nTo fix them, you need to add Wulkanowy to the autostart and turn off battery optimization/saving in the phone settings. - Show debug notifications - Synchronization is disabled - Official app notifications - Capture official app notifications - Remove official app notifications after capture - Capture notifications - With this feature you can gain a substitute of push notifications like in the official app. All you need to do is allow Wulkanowy to receive all notifications in your system settings.\n\nHow it works?\nWhen you get a notification in Dziennik VULCAN, Wulkanowy will be notified (that\'s what these extra permissions are for) and will trigger a sync so that can send its own notification.\n\nFOR ADVANCED USERS ONLY - Upcoming lesson notifications - You must allow the Wulkanowy app to set alarms and reminders in your system settings to use this feature. - Go to settings - Synchronization - Automatic update - Suspended on holidays - Updates interval - Wi-Fi only - Sync now - Synced! - Sync failed - Sync in progress - Last full sync: %s - Value of the plus - Value of the minus - Reply with message history - Show arithmetic average when no weights provided - Incognito mode - Do not inform about reading the message - Support - Privacy Policy - Agreements - Show consent to data processing - Show ads in app - Watch single ad to support project - Consent to data processing - To view an advertisement you must agree to the data processing terms of our Privacy Policy - Agree - Privacy policy - Ad is loading - Thank you for your support, come back later for more ads - Advanced - Appearance & Behavior - Notifications - Synchronization - Advertisements - Grades - Dashboard - Tiles visibility - Attendance - Timetable - Grades - Calculated average - Messages - Appearance & Behavior - Languages, themes, subjects sorting - App notifications, fix problems - Notifications - Synchronization - Automatic update, synchronization interval - Plus and minus values, average calculation - Advanced - App version, contributors, social portals - Displaying advertisements, project support - - New grades - New homework - New conferences - New exams - Lucky number - New messages - New notes - New school announcements - Push notifications - Upcoming lessons - Debug - Timetable change - New attendance - - Black - Red - Blue - Green - Purple - No color - - Download of updates has started… - An update has just been downloaded. - Restart - Update failed! Wulkanowy may not function properly. Consider updating - - Application restart - The application must restart for the changes to be saved - Restart - - Authorization has been rejected. The data provided does not match the records in the secretary\'s office. - Invalid PESEL - PESEL - Authorize - Authorization completed successfully - Authorization - To operate the application, we need to confirm your identity. Please enter the student\'s PESEL <b>%1$s</b> in the field below - Skip for now - - Verification is in progress. Wait… - Verified successfully - - No internet connection - An error occurred. Check your device clock - Connection to register failed. Servers can be overloaded. Please try again later - Loading data failed. Please try again later - Register password change required - Maintenance underway UONET + register. Try again later - Unknown UONET + register error. Try again later - Unknown application error. Please try again later - Captcha verification required - An unexpected error occurred - Feature disabled by your school - Feature not available. Login in a mode other than Mobile API - This field is required - diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml deleted file mode 100644 index ac616418..00000000 --- a/app/src/main/res/values-it-rIT/strings.xml +++ /dev/null @@ -1,764 +0,0 @@ - - - - Login - Wulkanowy - Grades - Attendance - Exams - Timetable - Settings - More - About - Log viewer - Debug - Notification debug - Clear webview cookies - Contributors - Licenses - Messages - New message - New homework - Notes and achievements - Homework - Accounts manager - Select account - Account details - Student info - Dashboard - Notifications center - Menu configuartion - - Semester %1$d, %2$d/%3$d - - Sign in with the student or parent account - Enter the symbol from the register page for account: <b>%1$s</b> - Username - Email - Login, PESEL or e-mail - Password - UONET+ register variant - Custom domain suffix - Mobile API - Scraper - Hybrid - Token - PIN - Symbol - E.g. \"lodz\" or \"powiatjaroslawski\" - Sign in - Password too short - Login details are incorrect - %1$s. Make sure the correct UONET+ register variation is selected below - Invalid PIN - Invalid token - Token expired - Invalid email - Use the assigned login instead of email - Use the assigned login or email in @%1$s - Invalid symbol. If you cannot find it, please contact the school - Don\'t make this up! If you cannot find it, please contact the school - Student not found. Validate the symbol and the chosen variation of the UONET+ register - Selected student is already logged in - The symbol can be found on the register page in Uczeń → Dostęp Mobilny → Wygeneruj kod dostępu.\n\nMake sure that you have set the appropriate register variant in the UONET+ register variant field on the first login screen - Select students to log in to the application - Other options - In this mode, a lucky number does not work, a class grade stats, summary of attendance, excuse for absence, completed lessons, school information and preview of the list of registered devices - This mode displays the same data as it appears on the register website - The combination of the best features of the other two modes. It works faster than scraper and provides features not available in the Mobile API mode. It is in the experimental phase - Privacy policy - Trouble signing in? Contact us! - Email - Discord - Send email - Make sure you select the correct UONET+ register variation! - Reset password - Recover your account - Recover - Student is already signed in - Standard - Other search locations - No active students found - Enter a different symbol - Get help - Full school name with the town (required) - Np. ZSTiO Jarosław lub SP nr 99 w Łodzi - Enter correct name of the school - Additional information in Polish (optional) - Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\" - Submit - - Enable notifications - Enable notifications so you don\'t miss message from teacher or new grade - Skip - Enable - - Account manager - Log in - Session expired - Session expired, log in again - Your account password has been changed. You need to log in to Wulkanowy again - Password changed - Application support - Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time - Enable ads - - Grade - Semester %d - Change semester - No grades - Weight - Weight: %s - Comment - Number of new ratings: %1$d - Average: %1$.2f - Points: %s - No average - Total points - Final grade - Predicted grade - Calculated average - How does Calculated Average work? - The Calculated Average is the arithmetic average calculated from the subjects averages. It allows you to know the approximate final average. It is calculated in a way selected by the user in the application settings. It is recommended that you choose the appropriate option. This is because the calculation of school averages differs. Additionally, if your school reports the average of the subjects on the Vulcan page, the application downloads them and does not calculate these averages. This can be changed by forcing the calculation of the average in the application settings.\n\nAverage of grades only from selected semester:\n1. Calculating the weighted average for each subject in a given semester\n2.Adding calculated averages\n3. Calculation of the arithmetic average of the summed averages\n\nAverage of averages from both semesters:\n1.Calculating the weighted average for each subject in semester 1 and 2\n2. Calculating the arithmetic average of the calculated averages for semesters 1 and 2 for each subject.\n3. Adding calculated averages\n4. Calculation of the arithmetic average of the summed averages\n\nAverage of grades from the whole year:\n1. Calculating weighted average over the year for each subject. The final average in the 1st semester is irrelevant.\n2. Adding calculated averages\n3. Calculating the arithmetic average of summed averages - How does the Final Average work? - The Final Average is the arithmetic average calculated from all currently available final grades in the given semester.\n\nThe calculation scheme consists of the following steps:\n1. Summing up the final grades given by teachers\n2. Divide by the number of subjects that have already been graded - Final average - from %1$d of %2$d subjects - Summary - Class - Mark as read - Partial - Semester - Points - Legend - Class average: %1$s - Your average: %1$s - Your grade: %1$s - Class - Student - - %d grade - %d grades - - - New grade - New grades - - - New predicted grade - New predicted grades - - - New final grade - New final grades - - - You received %1$d grade - You received %1$d grades - - - You received %1$d predicted grade - You received %1$d predicted grades - - - You received %1$d final grade - You received %1$d final grades - - - Lesson - Room - Group - Hours - Changes - No lessons this day - %s min - %s sec - %1$s left - in %1$s - Finished - Now: %s - Next: %s - Later: %s - %1$s lesson %2$d - %3$s - Change of room from %1$s to %2$s - Change of teacher from %1$s to %2$s - Change of subject from %1$s to %2$s - - No lesson - No lessons - - - Timetable change - Timetable changes - - - %1$s - %2$d change in timetable - %1$s - %2$d changes in timetable - - - %1$d change in timetable - %1$d changes in timetable - - - %d change - %d changes - - - Completed lessons - Show completed lessons - No info about completed lessons - Topic - Absence - Resources - - Additional lessons - Show additional lessons - No info about additional lessons - New lesson - New additional lesson - Additional lesson added successfully - Additional lesson deleted successfully - Repeat weekly - Delete additional lesson - Just this lesson - All in the series - Start time - End time - End time must be greater than start time - - Attendance summary - Absent for school reasons - Excused absence - Unexcused absence - Exemption - Excused lateness - Unexcused lateness - Present - Deleted - Unknown - Number of lesson - No entries - Absence reason (optional) - Send - Absence excuse request sent successfully! - You must select at least one absence! - Excuse - - New attendance - New attendance - - - %1$d new attendance - %1$d attendance - - - %d attendance - %d attendance - - - Total - - No exams this week - Type - Entry date - - New exam - New exams - - - %d new exam - %d new exams - - - %d exam - %d exams - - - Inbox - Sent - Trash - (no subject) - No messages - From: - To: - Date: %1$s - Reply - Forward - Select all - Unselect all - Move to trash - Delete permanently - Message deleted successfully - student - parent - guardian - employee - Share - Print - Subject - Content - Message sent successfully - Message does not exist - You need to choose at least 1 recipient - The message content must be at least 3 characters - All mailboxes - Only unread - Only with attachments - Read: %s - Read by: %1$d of %2$d people - - %1$d message - %1$d messages - - - New message - New messages - - Do you want to restore draft message? - Do you want to restore draft message with recipients: %s? - - You received %1$d message - You received %1$d messages - - - %1$d selected - %1$d selected - - Messages deleted - Choose mailbox - Incognito mode is on - Thanks to incognito mode sender is not notified when you read the message - - No info about notes - Points - - %d note - %d notes - - - New note - New notes - - - You received %1$d note - You received %1$d notes - - - - %d praise - %d praises - - - New praise - New praises - - - You received %1$d praise - You received %1$d praises - - - - %d neutral note - %d neutral notes - - - New neutral note - New neutral notes - - - You received %1$d neutral note - You received %1$d neutral notes - - - No info about homework - Mark as done - Mark as undone - Add homework - Homework added successfully - Homework deleted successfully - Attachments - - New homework - New homework - - - You received %d new homework - You received %d new homework - - - %d homework - %d homework - - - Lucky number - Today\'s lucky number is - No info about the lucky number - Lucky number for today - Today\'s lucky number is: %s - Show history - - Lucky number history - No info about lucky numbers - - Mobile devices - No devices - Deregister - Device removed - QR code - Token - Symbol - PIN - - School and teachers - - School - No info about school - School name - School address - Telephone - Name of headmaster - Name of pedagogue - Show on map - Call - - Teachers - No info about teachers - No subject - - Conferences - No info about conferences - - %d conference - %d conferences - - - New conference - New conferences - - - You have %1$d new conference - You have %1$d new conferences - - Present at conference - Agenda - Place - Topic - - School announcements - No school announcements - - %d school announcement - %d school announcements - - - New school announcement - New school announcements - - - You have %1$d new school announcement - You have %1$d new school announcements - - - Add account - Logout - Do you want to log out this student? - Student logout - Student account - Parent account - Edit data - Accounts manager - Select student - Family - Contact - Residence details - Personal information - - App version - Contributors - List of Wulkanowy developers - Report a bug - Send a bug report via e-mail - FAQ - Read Frequently Asked Questions - Discord server - Join the Wulkanowy community - Facebook fanpage - Twitter page - Follow us on twitter - Like our facebook fanpage - Privacy policy - Rules for collecting personal data - System settings - Open system settings - Homepage - Visit the website and help develop the application - Licenses - Licenses of libraries used in the application - - License - - Avatar - See more on GitHub - - No info about student or student family - Name - Second name - Gender - Polish citizenship - Family name - Mother\'s and father\'s names - Phone - Cellphone - E-mail - Address of residence - Address of registration - Correspondence address - Surname and first name - Degree of kinship - Address - Phones - Male - Female - Last name - Guardian - - Nick - Add nick - Choose avatar color - - Share logs - Refresh - - Lessons - (Tomorrow) - (Today and tomorrow) - In a moment: - Soon: - First: - Now: - End of lessons - Next: - Later: - - %1$d more lesson - %1$d more lessons - - until %1$s - No upcoming lessons - An error occurred while loading the lessons - Homework - No homework to do - An error occurred while loading the homework - - %1$d more homework - %1$d more homework - - due %1$s - Last grades - No new grades - An error occurred while loading the grades - School announcements - No current announcements - An error occurred while loading the announcements - - %1$d more announcement - %1$d more announcements - - Exams - No upcoming exams - An error occurred while loading the exams - - %1$d more exam - %1$d more exams - - Conferences - No upcoming conferences - An error occurred while loading the conferences - - %1$d more conference - %1$d more conferences - - An error occurred while loading data - None - - Check for updates - Before reporting a bug, check first if an update with the bug fix is available - - Content - Retry - Description - No description - Teacher - Date - Entry date - Color - Details - Category - Close - No data - Subject - Prev - Next - Search - Search… - Yes - No - Save - Title - Add - Copied - Undo - Change - Add to calendar - Cancel - - No lessons - Synchronized on %1$s at %2$s - Choose theme - Light - Dark - System Theme - - App - Default view - Calculated average options - Force average calculation by app - Show presence - Theme - Grades expanding - Show groups next to subjects - Show empty tiles where there\'s no lesson - Show chart list in class grades - Show subjects without grades - Grades color scheme - Subjects sorting - Language - Menu configuration - Set the order of functions in the menu - Notifications - Other - Show notifications - Show upcoming lesson notifications - Make upcoming lesson notification persistent - Turn off when notification is not showing in your watch/band - Open system notification settings - Fix synchronization & notifications issues - Your device may have data synchronization issues and with notifications.\n\nTo fix them, you need to add Wulkanowy to the autostart and turn off battery optimization/saving in the phone settings. - Show debug notifications - Synchronization is disabled - Official app notifications - Capture official app notifications - Remove official app notifications after capture - Capture notifications - With this feature you can gain a substitute of push notifications like in the official app. All you need to do is allow Wulkanowy to receive all notifications in your system settings.\n\nHow it works?\nWhen you get a notification in Dziennik VULCAN, Wulkanowy will be notified (that\'s what these extra permissions are for) and will trigger a sync so that can send its own notification.\n\nFOR ADVANCED USERS ONLY - Upcoming lesson notifications - You must allow the Wulkanowy app to set alarms and reminders in your system settings to use this feature. - Go to settings - Synchronization - Automatic update - Suspended on holidays - Updates interval - Wi-Fi only - Sync now - Synced! - Sync failed - Sync in progress - Last full sync: %s - Value of the plus - Value of the minus - Reply with message history - Show arithmetic average when no weights provided - Incognito mode - Do not inform about reading the message - Support - Privacy Policy - Agreements - Show consent to data processing - Show ads in app - Watch single ad to support project - Consent to data processing - To view an advertisement you must agree to the data processing terms of our Privacy Policy - Agree - Privacy policy - Ad is loading - Thank you for your support, come back later for more ads - Advanced - Appearance & Behavior - Notifications - Synchronization - Advertisements - Grades - Dashboard - Tiles visibility - Attendance - Timetable - Grades - Calculated average - Messages - Appearance & Behavior - Languages, themes, subjects sorting - App notifications, fix problems - Notifications - Synchronization - Automatic update, synchronization interval - Plus and minus values, average calculation - Advanced - App version, contributors, social portals - Displaying advertisements, project support - - New grades - New homework - New conferences - New exams - Lucky number - New messages - New notes - New school announcements - Push notifications - Upcoming lessons - Debug - Timetable change - New attendance - - Black - Red - Blue - Green - Purple - No color - - Download of updates has started… - An update has just been downloaded. - Restart - Update failed! Wulkanowy may not function properly. Consider updating - - Application restart - The application must restart for the changes to be saved - Restart - - Authorization has been rejected. The data provided does not match the records in the secretary\'s office. - Invalid PESEL - PESEL - Authorize - Authorization completed successfully - Authorization - To operate the application, we need to confirm your identity. Please enter the student\'s PESEL <b>%1$s</b> in the field below - Skip for now - - Verification is in progress. Wait… - Verified successfully - - No internet connection - An error occurred. Check your device clock - Connection to register failed. Servers can be overloaded. Please try again later - Loading data failed. Please try again later - Register password change required - Maintenance underway UONET + register. Try again later - Unknown UONET + register error. Try again later - Unknown application error. Please try again later - Captcha verification required - An unexpected error occurred - Feature disabled by your school - Feature not available. Login in a mode other than Mobile API - This field is required - diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index feb08a03..2ca66928 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -829,7 +829,7 @@ Авторизация отклонена. Предоставленные данные не соответствуют записям в кабинете секретаря. Неправильный номер PESEL Номер PESEL - Authorize + Авторизовать Авторизация прошла успешно Авторизация Для работы приложения нам необходимо подтвердить вашу личность. Введите PESEL учащегося <b>%1$s</b> в поле ниже From 725668f855eb0ea41217e1a57d2b72547634ceed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jan 2024 09:22:34 +0000 Subject: [PATCH 18/51] Bump androidx.lifecycle:lifecycle-livedata-ktx from 2.6.2 to 2.7.0 (#2398) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 8d10ce92..cd8a1df2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -221,7 +221,7 @@ dependencies { implementation "androidx.work:work-runtime:$work_manager" playImplementation "androidx.work:work-gcm:$work_manager" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.2" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.7.0" implementation "androidx.room:room-runtime:$room" implementation "androidx.room:room-ktx:$room" From 9dfb282e881aa0dc6278777993352740d84b43f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sun, 21 Jan 2024 11:39:55 +0100 Subject: [PATCH 19/51] Add X to close admin message (#2401) --- .../58.json | 2451 +++++++++++++++++ .../github/wulkanowy/data/db/AppDatabase.kt | 124 +- .../data/db/entities/AdminMessage.kt | 7 +- .../data/db/migrations/Migration58.kt | 10 + .../viewholders/AdminMessageViewHolder.kt | 10 +- app/src/main/res/drawable/ic_close.xml | 10 + .../layout/item_dashboard_admin_message.xml | 15 +- 7 files changed, 2617 insertions(+), 10 deletions(-) create mode 100644 app/schemas/io.github.wulkanowy.data.db.AppDatabase/58.json create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration58.kt create mode 100644 app/src/main/res/drawable/ic_close.xml diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/58.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/58.json new file mode 100644 index 00000000..e6e71229 --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/58.json @@ -0,0 +1,2451 @@ +{ + "formatVersion": 1, + "database": { + "version": 58, + "identityHash": "cd1d4f8f2b6e3860fbc1de93d4f5ca42", + "entities": [ + { + "tableName": "Students", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrapper_base_url` TEXT NOT NULL, `scrapper_domain_suffix` TEXT NOT NULL DEFAULT '', `mobile_base_url` TEXT NOT NULL, `login_type` TEXT NOT NULL, `login_mode` TEXT NOT NULL, `certificate_key` TEXT NOT NULL, `private_key` TEXT NOT NULL, `is_parent` INTEGER NOT NULL, `email` TEXT NOT NULL, `password` TEXT NOT NULL, `symbol` TEXT NOT NULL, `student_id` INTEGER NOT NULL, `user_login_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `student_name` TEXT NOT NULL, `school_id` TEXT NOT NULL, `school_short` TEXT NOT NULL, `school_name` TEXT NOT NULL, `class_name` TEXT NOT NULL, `class_id` INTEGER NOT NULL, `is_current` INTEGER NOT NULL, `registration_date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `nick` TEXT NOT NULL, `avatar_color` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "scrapperBaseUrl", + "columnName": "scrapper_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scrapperDomainSuffix", + "columnName": "scrapper_domain_suffix", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "mobileBaseUrl", + "columnName": "mobile_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginType", + "columnName": "login_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginMode", + "columnName": "login_mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificateKey", + "columnName": "certificate_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "private_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isParent", + "columnName": "is_parent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "user_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "student_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolSymbol", + "columnName": "school_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "school_short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolName", + "columnName": "school_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "className", + "columnName": "class_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCurrent", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registrationDate", + "columnName": "registration_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nick", + "columnName": "nick", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarColor", + "columnName": "avatar_color", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Students_email_symbol_student_id_school_id_class_id", + "unique": true, + "columnNames": [ + "email", + "symbol", + "student_id", + "school_id", + "class_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `kindergarten_diary_id` INTEGER NOT NULL DEFAULT 0, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "kindergartenDiaryId", + "columnName": "kindergarten_diary_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "diaryName", + "columnName": "diary_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolYear", + "columnName": "school_year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterName", + "columnName": "semester_name", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "kindergarten_diary_id", + "semester_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `kindergarten_diary_id`, `semester_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Exams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Timetable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `number` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `subjectOld` TEXT NOT NULL, `group` TEXT NOT NULL, `room` TEXT NOT NULL, `roomOld` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacherOld` TEXT NOT NULL, `info` TEXT NOT NULL, `student_plan` INTEGER NOT NULL, `changes` INTEGER NOT NULL, `canceled` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectOld", + "columnName": "subjectOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "room", + "columnName": "room", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roomOld", + "columnName": "roomOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherOld", + "columnName": "teacherOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isStudentPlan", + "columnName": "student_plan", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "changes", + "columnName": "changes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canceled", + "columnName": "canceled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Attendance", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `time_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `excusable` INTEGER NOT NULL, `excuse_status` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeId", + "columnName": "time_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excusable", + "columnName": "excusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excuseStatus", + "columnName": "excuse_status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AttendanceSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `subject_id` INTEGER NOT NULL, `month` INTEGER NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `absence_excused` INTEGER NOT NULL, `absence_for_school_reasons` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `lateness_excused` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subject_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "month", + "columnName": "month", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceExcused", + "columnName": "absence_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceForSchoolReasons", + "columnName": "absence_for_school_reasons", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latenessExcused", + "columnName": "lateness_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Grades", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `entry` TEXT NOT NULL, `value` REAL NOT NULL, `modifier` REAL NOT NULL, `comment` TEXT NOT NULL, `color` TEXT NOT NULL, `grade_symbol` TEXT NOT NULL, `description` TEXT NOT NULL, `weight` TEXT NOT NULL, `weightValue` REAL NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entry", + "columnName": "entry", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "modifier", + "columnName": "modifier", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gradeSymbol", + "columnName": "grade_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weightValue", + "columnName": "weightValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `subject` TEXT NOT NULL, `predicted_grade` TEXT NOT NULL, `final_grade` TEXT NOT NULL, `proposed_points` TEXT NOT NULL, `final_points` TEXT NOT NULL, `points_sum` TEXT NOT NULL, `average` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_predicted_grade_notified` INTEGER NOT NULL, `is_final_grade_notified` INTEGER NOT NULL, `predicted_grade_last_change` INTEGER NOT NULL, `final_grade_last_change` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "predictedGrade", + "columnName": "predicted_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalGrade", + "columnName": "final_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "proposedPoints", + "columnName": "proposed_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalPoints", + "columnName": "final_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pointsSum", + "columnName": "points_sum", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "average", + "columnName": "average", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPredictedGradeNotified", + "columnName": "is_predicted_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFinalGradeNotified", + "columnName": "is_final_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "predictedGradeLastChange", + "columnName": "predicted_grade_last_change", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "finalGradeLastChange", + "columnName": "final_grade_last_change", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradePartialStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `class_average` TEXT NOT NULL, `student_average` TEXT NOT NULL, `class_amounts` TEXT NOT NULL, `student_amounts` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAverage", + "columnName": "class_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAverage", + "columnName": "student_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAmounts", + "columnName": "class_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAmounts", + "columnName": "student_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesPointsStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `others` REAL NOT NULL, `student` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "others", + "columnName": "others", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "student", + "columnName": "student", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradeSemesterStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `amounts` TEXT NOT NULL, `student_grade` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amounts", + "columnName": "amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentGrade", + "columnName": "student_grade", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `message_global_key` TEXT NOT NULL, `mailbox_key` TEXT NOT NULL, `message_id` INTEGER NOT NULL, `correspondents` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `read_by` INTEGER, `unread_by` INTEGER, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `content` TEXT NOT NULL, `sender` TEXT, `recipients` TEXT)", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageGlobalKey", + "columnName": "message_global_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mailboxKey", + "columnName": "mailbox_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "correspondents", + "columnName": "correspondents", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasAttachments", + "columnName": "has_attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recipients", + "columnName": "recipients", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`message_global_key` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`message_global_key`, `url`, `filename`))", + "fields": [ + { + "fieldPath": "messageGlobalKey", + "columnName": "message_global_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "message_global_key", + "url", + "filename" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `category` TEXT NOT NULL, `category_type` INTEGER NOT NULL, `is_points_show` INTEGER NOT NULL, `points` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryType", + "columnName": "category_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPointsShow", + "columnName": "is_points_show", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Homework", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `attachments` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_done` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL, `is_added_by_user` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "is_done", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Subjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "LuckyNumbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `lucky_number` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "luckyNumber", + "columnName": "lucky_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CompletedLesson", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `topic` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `substitution` TEXT NOT NULL, `absence` TEXT NOT NULL, `resources` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "topic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "substitution", + "columnName": "substitution", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "resources", + "columnName": "resources", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Mailboxes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`globalKey` TEXT NOT NULL, `email` TEXT NOT NULL, `symbol` TEXT NOT NULL, `schoolId` TEXT NOT NULL, `fullName` TEXT NOT NULL, `userName` TEXT NOT NULL, `studentName` TEXT NOT NULL, `schoolNameShort` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`globalKey`))", + "fields": [ + { + "fieldPath": "globalKey", + "columnName": "globalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolId", + "columnName": "schoolId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "studentName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolNameShort", + "columnName": "schoolNameShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "globalKey" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mailboxGlobalKey` TEXT NOT NULL, `studentMailboxGlobalKey` TEXT NOT NULL, `fullName` TEXT NOT NULL, `userName` TEXT NOT NULL, `schoolShortName` TEXT NOT NULL, `type` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "mailboxGlobalKey", + "columnName": "mailboxGlobalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentMailboxGlobalKey", + "columnName": "studentMailboxGlobalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "schoolShortName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MobileDevices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_login_id` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Teachers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `short_name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "School", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `contact` TEXT NOT NULL, `headmaster` TEXT NOT NULL, `pedagogue` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contact", + "columnName": "contact", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headmaster", + "columnName": "headmaster", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pedagogue", + "columnName": "pedagogue", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Conferences", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `subject` TEXT NOT NULL, `agenda` TEXT NOT NULL, `present_on_conference` TEXT NOT NULL, `conference_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agenda", + "columnName": "agenda", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presentOnConference", + "columnName": "present_on_conference", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conference_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableAdditional", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `repeat_id` BLOB DEFAULT NULL, `is_added_by_user` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatId", + "columnName": "repeat_id", + "affinity": "BLOB", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StudentInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `full_name` TEXT NOT NULL, `first_name` TEXT NOT NULL, `second_name` TEXT NOT NULL, `surname` TEXT NOT NULL, `birth_date` INTEGER NOT NULL, `birth_place` TEXT NOT NULL, `gender` TEXT NOT NULL, `has_polish_citizenship` INTEGER NOT NULL, `family_name` TEXT NOT NULL, `parents_names` TEXT NOT NULL, `address` TEXT NOT NULL, `registered_address` TEXT NOT NULL, `correspondence_address` TEXT NOT NULL, `phone_number` TEXT NOT NULL, `cell_phone_number` TEXT NOT NULL, `email` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `first_guardian_full_name` TEXT, `first_guardian_kinship` TEXT, `first_guardian_address` TEXT, `first_guardian_phones` TEXT, `first_guardian_email` TEXT, `second_guardian_full_name` TEXT, `second_guardian_kinship` TEXT, `second_guardian_address` TEXT, `second_guardian_phones` TEXT, `second_guardian_email` TEXT)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "full_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondName", + "columnName": "second_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "surname", + "columnName": "surname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "birthDate", + "columnName": "birth_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthPlace", + "columnName": "birth_place", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gender", + "columnName": "gender", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPolishCitizenship", + "columnName": "has_polish_citizenship", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "familyName", + "columnName": "family_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentsNames", + "columnName": "parents_names", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "registeredAddress", + "columnName": "registered_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "correspondenceAddress", + "columnName": "correspondence_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cellPhoneNumber", + "columnName": "cell_phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstGuardian.fullName", + "columnName": "first_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.kinship", + "columnName": "first_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.address", + "columnName": "first_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.phones", + "columnName": "first_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.email", + "columnName": "first_guardian_email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.fullName", + "columnName": "second_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.kinship", + "columnName": "second_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.address", + "columnName": "second_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.phones", + "columnName": "second_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.email", + "columnName": "second_guardian_email", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableHeaders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SchoolAnnouncements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_login_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `type` TEXT NOT NULL, `destination` TEXT NOT NULL DEFAULT '{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}', `date` INTEGER NOT NULL, `data` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "destination", + "columnName": "destination", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}'" + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AdminMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `version_name` INTEGER, `version_max` INTEGER, `target_register_host` TEXT, `target_flavor` TEXT, `destination_url` TEXT, `priority` TEXT NOT NULL, `types` TEXT NOT NULL DEFAULT '[]', `is_ok_visible` INTEGER NOT NULL DEFAULT 0, `is_x_visible` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "versionMin", + "columnName": "version_name", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMax", + "columnName": "version_max", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "targetRegisterHost", + "columnName": "target_register_host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "targetFlavor", + "columnName": "target_flavor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "destinationUrl", + "columnName": "destination_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "types", + "columnName": "types", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'[]'" + }, + { + "fieldPath": "isOkVisible", + "columnName": "is_ok_visible", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isXVisible", + "columnName": "is_x_visible", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cd1d4f8f2b6e3860fbc1de93d4f5ca42')" + ] + } +} \ 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 48a2942c..a2f230f4 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 @@ -1,11 +1,124 @@ package io.github.wulkanowy.data.db import android.content.Context -import androidx.room.* +import androidx.room.AutoMigration +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase import androidx.room.RoomDatabase.JournalMode.TRUNCATE -import io.github.wulkanowy.data.db.dao.* -import io.github.wulkanowy.data.db.entities.* -import io.github.wulkanowy.data.db.migrations.* +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 +import io.github.wulkanowy.data.db.dao.ConferenceDao +import io.github.wulkanowy.data.db.dao.ExamDao +import io.github.wulkanowy.data.db.dao.GradeDao +import io.github.wulkanowy.data.db.dao.GradePartialStatisticsDao +import io.github.wulkanowy.data.db.dao.GradePointsStatisticsDao +import io.github.wulkanowy.data.db.dao.GradeSemesterStatisticsDao +import io.github.wulkanowy.data.db.dao.GradeSummaryDao +import io.github.wulkanowy.data.db.dao.HomeworkDao +import io.github.wulkanowy.data.db.dao.LuckyNumberDao +import io.github.wulkanowy.data.db.dao.MailboxDao +import io.github.wulkanowy.data.db.dao.MessageAttachmentDao +import io.github.wulkanowy.data.db.dao.MessagesDao +import io.github.wulkanowy.data.db.dao.MobileDeviceDao +import io.github.wulkanowy.data.db.dao.NoteDao +import io.github.wulkanowy.data.db.dao.NotificationDao +import io.github.wulkanowy.data.db.dao.RecipientDao +import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao +import io.github.wulkanowy.data.db.dao.SchoolDao +import io.github.wulkanowy.data.db.dao.SemesterDao +import io.github.wulkanowy.data.db.dao.StudentDao +import io.github.wulkanowy.data.db.dao.StudentInfoDao +import io.github.wulkanowy.data.db.dao.SubjectDao +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 +import io.github.wulkanowy.data.db.entities.Conference +import io.github.wulkanowy.data.db.entities.Exam +import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.data.db.entities.GradePartialStatistics +import io.github.wulkanowy.data.db.entities.GradePointsStatistics +import io.github.wulkanowy.data.db.entities.GradeSemesterStatistics +import io.github.wulkanowy.data.db.entities.GradeSummary +import io.github.wulkanowy.data.db.entities.Homework +import io.github.wulkanowy.data.db.entities.LuckyNumber +import io.github.wulkanowy.data.db.entities.Mailbox +import io.github.wulkanowy.data.db.entities.Message +import io.github.wulkanowy.data.db.entities.MessageAttachment +import io.github.wulkanowy.data.db.entities.MobileDevice +import io.github.wulkanowy.data.db.entities.Note +import io.github.wulkanowy.data.db.entities.Notification +import io.github.wulkanowy.data.db.entities.Recipient +import io.github.wulkanowy.data.db.entities.School +import io.github.wulkanowy.data.db.entities.SchoolAnnouncement +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.StudentInfo +import io.github.wulkanowy.data.db.entities.Subject +import io.github.wulkanowy.data.db.entities.Teacher +import io.github.wulkanowy.data.db.entities.Timetable +import io.github.wulkanowy.data.db.entities.TimetableAdditional +import io.github.wulkanowy.data.db.entities.TimetableHeader +import io.github.wulkanowy.data.db.migrations.Migration10 +import io.github.wulkanowy.data.db.migrations.Migration11 +import io.github.wulkanowy.data.db.migrations.Migration12 +import io.github.wulkanowy.data.db.migrations.Migration13 +import io.github.wulkanowy.data.db.migrations.Migration14 +import io.github.wulkanowy.data.db.migrations.Migration15 +import io.github.wulkanowy.data.db.migrations.Migration16 +import io.github.wulkanowy.data.db.migrations.Migration17 +import io.github.wulkanowy.data.db.migrations.Migration18 +import io.github.wulkanowy.data.db.migrations.Migration19 +import io.github.wulkanowy.data.db.migrations.Migration2 +import io.github.wulkanowy.data.db.migrations.Migration20 +import io.github.wulkanowy.data.db.migrations.Migration21 +import io.github.wulkanowy.data.db.migrations.Migration22 +import io.github.wulkanowy.data.db.migrations.Migration23 +import io.github.wulkanowy.data.db.migrations.Migration24 +import io.github.wulkanowy.data.db.migrations.Migration25 +import io.github.wulkanowy.data.db.migrations.Migration26 +import io.github.wulkanowy.data.db.migrations.Migration27 +import io.github.wulkanowy.data.db.migrations.Migration28 +import io.github.wulkanowy.data.db.migrations.Migration29 +import io.github.wulkanowy.data.db.migrations.Migration3 +import io.github.wulkanowy.data.db.migrations.Migration30 +import io.github.wulkanowy.data.db.migrations.Migration31 +import io.github.wulkanowy.data.db.migrations.Migration32 +import io.github.wulkanowy.data.db.migrations.Migration33 +import io.github.wulkanowy.data.db.migrations.Migration34 +import io.github.wulkanowy.data.db.migrations.Migration35 +import io.github.wulkanowy.data.db.migrations.Migration36 +import io.github.wulkanowy.data.db.migrations.Migration37 +import io.github.wulkanowy.data.db.migrations.Migration38 +import io.github.wulkanowy.data.db.migrations.Migration39 +import io.github.wulkanowy.data.db.migrations.Migration4 +import io.github.wulkanowy.data.db.migrations.Migration40 +import io.github.wulkanowy.data.db.migrations.Migration41 +import io.github.wulkanowy.data.db.migrations.Migration42 +import io.github.wulkanowy.data.db.migrations.Migration43 +import io.github.wulkanowy.data.db.migrations.Migration44 +import io.github.wulkanowy.data.db.migrations.Migration46 +import io.github.wulkanowy.data.db.migrations.Migration49 +import io.github.wulkanowy.data.db.migrations.Migration5 +import io.github.wulkanowy.data.db.migrations.Migration50 +import io.github.wulkanowy.data.db.migrations.Migration51 +import io.github.wulkanowy.data.db.migrations.Migration53 +import io.github.wulkanowy.data.db.migrations.Migration54 +import io.github.wulkanowy.data.db.migrations.Migration55 +import io.github.wulkanowy.data.db.migrations.Migration57 +import io.github.wulkanowy.data.db.migrations.Migration58 +import io.github.wulkanowy.data.db.migrations.Migration6 +import io.github.wulkanowy.data.db.migrations.Migration7 +import io.github.wulkanowy.data.db.migrations.Migration8 +import io.github.wulkanowy.data.db.migrations.Migration9 import io.github.wulkanowy.utils.AppInfo import javax.inject.Singleton @@ -51,6 +164,7 @@ import javax.inject.Singleton AutoMigration(from = 54, to = 55, spec = Migration55::class), AutoMigration(from = 55, to = 56), AutoMigration(from = 56, to = 57, spec = Migration57::class), + AutoMigration(from = 57, to = 58, spec = Migration58::class), ], version = AppDatabase.VERSION_SCHEMA, exportSchema = true @@ -59,7 +173,7 @@ import javax.inject.Singleton abstract class AppDatabase : RoomDatabase() { companion object { - const val VERSION_SCHEMA = 57 + const val VERSION_SCHEMA = 58 fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( Migration2(), diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt index 875c2a3a..0c8f1a5d 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt @@ -37,6 +37,9 @@ data class AdminMessage( @ColumnInfo(name = "types", defaultValue = "[]") val types: List = emptyList(), - @ColumnInfo(name = "is_dismissible") - val isDismissible: Boolean = false + @ColumnInfo(name = "is_ok_visible", defaultValue = "0") + val isOkVisible: Boolean = false, + + @ColumnInfo(name = "is_x_visible", defaultValue = "0") + val isXVisible: Boolean = false ) diff --git a/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration58.kt b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration58.kt new file mode 100644 index 00000000..c440d58d --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/migrations/Migration58.kt @@ -0,0 +1,10 @@ +package io.github.wulkanowy.data.db.migrations + +import androidx.room.DeleteColumn +import androidx.room.migration.AutoMigrationSpec + +@DeleteColumn( + tableName = "AdminMessages", + columnName = "is_dismissible", +) +class Migration58 : AutoMigrationSpec diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/viewholders/AdminMessageViewHolder.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/viewholders/AdminMessageViewHolder.kt index 81099801..1e0f0bdb 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/viewholders/AdminMessageViewHolder.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/viewholders/AdminMessageViewHolder.kt @@ -7,7 +7,6 @@ import androidx.recyclerview.widget.RecyclerView import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.databinding.ItemDashboardAdminMessageBinding -import io.github.wulkanowy.ui.modules.dashboard.DashboardItem import io.github.wulkanowy.utils.getThemeAttrColor class AdminMessageViewHolder( @@ -25,9 +24,11 @@ class AdminMessageViewHolder( context.getThemeAttrColor(R.attr.colorMessageHigh) to context.getThemeAttrColor(R.attr.colorOnMessageHigh) } + "MEDIUM" -> { context.getThemeAttrColor(R.attr.colorMessageMedium) to Color.BLACK } + else -> null to context.getThemeAttrColor(R.attr.colorOnSurface) } @@ -37,11 +38,16 @@ class AdminMessageViewHolder( dashboardAdminMessageItemDescription.text = item.content dashboardAdminMessageItemDescription.setTextColor(textColor) dashboardAdminMessageItemIcon.setColorFilter(textColor) - dashboardAdminMessageItemDismiss.isVisible = item.isDismissible + dashboardAdminMessageItemDismiss.isVisible = item.isOkVisible + dashboardAdminMessageItemClose.isVisible = item.isXVisible dashboardAdminMessageItemDismiss.setTextColor(textColor) + dashboardAdminMessageItemClose.imageTintList = ColorStateList.valueOf(textColor) dashboardAdminMessageItemDismiss.setOnClickListener { onAdminMessageDismissClickListener(item) } + dashboardAdminMessageItemClose.setOnClickListener { + onAdminMessageDismissClickListener(item) + } root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) }) item.destinationUrl?.let { url -> diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 00000000..1d6c0046 --- /dev/null +++ b/app/src/main/res/drawable/ic_close.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 index e12241df..407e1292 100644 --- a/app/src/main/res/layout/item_dashboard_admin_message.xml +++ b/app/src/main/res/layout/item_dashboard_admin_message.xml @@ -34,11 +34,24 @@ android:layout_marginEnd="16dp" android:textSize="18sp" android:textStyle="bold" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toStartOf="@id/dashboard_admin_message_item_close" app:layout_constraintStart_toEndOf="@id/dashboard_admin_message_item_icon" app:layout_constraintTop_toTopOf="parent" tools:text="@tools:sample/lorem" /> + + Date: Sun, 21 Jan 2024 12:39:23 +0100 Subject: [PATCH 20/51] Add admin message to error view in dashboard (#2400) --- app/build.gradle | 2 +- .../ui/modules/dashboard/DashboardFragment.kt | 13 +++++-- .../modules/dashboard/DashboardPresenter.kt | 20 ++++++----- .../ui/modules/dashboard/DashboardView.kt | 3 +- .../main/res/layout/fragment_dashboard.xml | 36 +++++++++++++++---- 5 files changed, 54 insertions(+), 20 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index cd8a1df2..a67adb6f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -193,7 +193,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.3.6' + implementation 'io.github.wulkanowy:sdk:2.3.7-SNAPSHOT' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' 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 bedbce23..b7a0796c 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 @@ -21,6 +21,7 @@ import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragme import io.github.wulkanowy.ui.modules.captcha.CaptchaDialog.Companion.CAPTCHA_SUCCESS import io.github.wulkanowy.ui.modules.conference.ConferenceFragment import io.github.wulkanowy.ui.modules.dashboard.adapters.DashboardAdapter +import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder import io.github.wulkanowy.ui.modules.exam.ExamFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment import io.github.wulkanowy.ui.modules.homework.HomeworkFragment @@ -37,7 +38,6 @@ import io.github.wulkanowy.utils.getErrorString import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.openInternetBrowser import io.github.wulkanowy.utils.toFormattedString -import timber.log.Timber import java.time.LocalDate import javax.inject.Inject @@ -199,8 +199,17 @@ class DashboardFragment : BaseFragment(R.layout.fragme binding.dashboardRecycler.isVisible = show } - override fun showErrorView(show: Boolean) { + override fun showErrorView(show: Boolean, adminMessageItem: DashboardItem.AdminMessages?) { binding.dashboardErrorContainer.isVisible = show + binding.dashboardErrorAdminMessage.root.isVisible = adminMessageItem != null + + if (adminMessageItem != null) { + AdminMessageViewHolder( + binding = binding.dashboardErrorAdminMessage, + onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed, + onAdminMessageClickListener = presenter::onAdminMessageSelected, + ).bind(adminMessageItem.adminMessage) + } } override fun setErrorDetails(error: Throwable) { 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 74b427e7..4e158743 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 @@ -403,7 +403,7 @@ class DashboardPresenter @Inject constructor( subjectWithGrades = it.dataOrNull, gradeTheme = preferencesRepository.gradeColorTheme, isLoading = true - ), forceRefresh + ), false ) if (!it.dataOrNull.isNullOrEmpty()) { @@ -452,7 +452,7 @@ class DashboardPresenter @Inject constructor( if (forceRefresh) return@onEach updateData( DashboardItem.Lessons(it.dataOrNull, isLoading = true), - forceRefresh + false ) if (!it.dataOrNull?.lessons.isNullOrEmpty()) { @@ -509,7 +509,7 @@ class DashboardPresenter @Inject constructor( val data = it.dataOrNull.orEmpty() updateData( DashboardItem.Homework(data, isLoading = true), - forceRefresh + false ) if (data.isNotEmpty()) { @@ -543,7 +543,7 @@ class DashboardPresenter @Inject constructor( if (forceRefresh) return@onEach updateData( DashboardItem.Announcements(it.dataOrNull.orEmpty(), isLoading = true), - forceRefresh + false ) if (!it.dataOrNull.isNullOrEmpty()) { @@ -586,7 +586,7 @@ class DashboardPresenter @Inject constructor( if (forceRefresh) return@onEach updateData( DashboardItem.Exams(it.dataOrNull.orEmpty(), isLoading = true), - forceRefresh + false ) if (!it.dataOrNull.isNullOrEmpty()) { @@ -627,7 +627,7 @@ class DashboardPresenter @Inject constructor( if (forceRefresh) return@onEach updateData( DashboardItem.Conferences(it.dataOrNull.orEmpty(), isLoading = true), - forceRefresh + false ) if (!it.dataOrNull.isNullOrEmpty()) { @@ -662,7 +662,7 @@ class DashboardPresenter @Inject constructor( is Resource.Loading -> { Timber.i("Loading dashboard admin message data started") if (forceRefresh) return@onEach - updateData(DashboardItem.AdminMessages(), forceRefresh) + updateData(DashboardItem.AdminMessages(), false) } is Resource.Success -> { @@ -692,7 +692,7 @@ class DashboardPresenter @Inject constructor( private fun loadAds(forceRefresh: Boolean) { presenterScope.launch { if (!forceRefresh) { - updateData(DashboardItem.Ads(), forceRefresh) + updateData(DashboardItem.Ads(), false) } val dashboardAdItem = @@ -813,6 +813,8 @@ class DashboardPresenter @Inject constructor( val filteredItems = itemsLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT || it.type == DashboardItem.Type.ADMIN_MESSAGE } + val dataLoadedAdminMessageItem = + itemsLoadedList.find { it.type == DashboardItem.Type.ADMIN_MESSAGE && it.isDataLoaded } as DashboardItem.AdminMessages? val isAccountItemError = itemsLoadedList.find { it.type == DashboardItem.Type.ACCOUNT }?.error != null val isGeneralError = @@ -834,7 +836,7 @@ class DashboardPresenter @Inject constructor( showRefresh(false) if ((forceRefresh && wasGeneralError) || !forceRefresh) { showContent(false) - showErrorView(true) + showErrorView(true, dataLoadedAdminMessageItem) setErrorDetails(lastError) } } 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 fe011c92..56a0a773 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 @@ -20,7 +20,7 @@ interface DashboardView : BaseView { fun showRefresh(show: Boolean) - fun showErrorView(show: Boolean) + fun showErrorView(show: Boolean, adminMessageItem: DashboardItem.AdminMessages? = null) fun setErrorDetails(error: Throwable) @@ -29,5 +29,6 @@ interface DashboardView : BaseView { fun popViewToRoot() fun openNotificationsCenterView() + fun openInternetBrowser(url: String) } diff --git a/app/src/main/res/layout/fragment_dashboard.xml b/app/src/main/res/layout/fragment_dashboard.xml index 348602d7..9de44a70 100644 --- a/app/src/main/res/layout/fragment_dashboard.xml +++ b/app/src/main/res/layout/fragment_dashboard.xml @@ -18,7 +18,8 @@ + android:layout_height="match_parent" + tools:visibility="gone"> - + + @@ -55,14 +70,21 @@ android:gravity="center" android:padding="8dp" android:text="@string/error_unknown" - android:textSize="20sp" /> + android:textSize="20sp" + app:layout_constraintBottom_toTopOf="@id/dashboard_error_buttons" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/dashboard_error_image" /> + android:orientation="horizontal" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toBottomOf="@id/dashboard_error_message"> - + From a51a54dc7ac444f73bb525c5a54c26d9c281ce46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sun, 21 Jan 2024 18:59:54 +0100 Subject: [PATCH 21/51] Normalize synchronization date ranges to fix weird notification issues (#2403) --- .../data/repositories/ExamRepository.kt | 18 ++++++++++-------- .../data/repositories/HomeworkRepository.kt | 7 ++++++- .../services/sync/works/AttendanceWork.kt | 13 ++++++++++--- .../wulkanowy/services/sync/works/ExamWork.kt | 16 +++++++++++++--- .../services/sync/works/HomeworkWork.kt | 16 +++++++++++++--- .../services/sync/works/TimetableWork.kt | 13 ++++++++++--- .../ui/modules/dashboard/DashboardPresenter.kt | 4 ++-- 7 files changed, 64 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt index 013c0951..59f42242 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt @@ -67,14 +67,16 @@ class ExamRepository @Inject constructor( filterResult = { it.filter { item -> item.date in start..end } } ) - fun getExamsFromDatabase(semester: Semester, start: LocalDate): Flow> { - return examDb.loadAll( - diaryId = semester.diaryId, - studentId = semester.studentId, - from = start.startExamsDay, - end = start.endExamsDay - ) - } + fun getExamsFromDatabase( + semester: Semester, + start: LocalDate, + end: LocalDate + ): Flow> = examDb.loadAll( + diaryId = semester.diaryId, + studentId = semester.studentId, + from = start, + end = end, + ) suspend fun updateExam(exam: List) = examDb.updateAll(exam) } 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 f564824d..048c95a3 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 @@ -7,7 +7,12 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.utils.* +import io.github.wulkanowy.utils.AutoRefreshHelper +import io.github.wulkanowy.utils.getRefreshKey +import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.monday +import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.sync.Mutex import java.time.LocalDate import javax.inject.Inject 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 657f6963..4fc09749 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 @@ -16,17 +16,24 @@ class AttendanceWork @Inject constructor( ) : Work { override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { + val startDate = now().previousOrSameSchoolDay + val endDate = startDate.plusDays(7) + attendanceRepository.getAttendance( student = student, semester = semester, - start = now().previousOrSameSchoolDay, - end = now().previousOrSameSchoolDay, + start = startDate, + end = endDate, forceRefresh = true, notify = notify, ) .waitForResult() - attendanceRepository.getAttendanceFromDatabase(semester, now().minusDays(7), now()) + attendanceRepository.getAttendanceFromDatabase( + semester = semester, + start = startDate, + end = endDate, + ) .first() .filterNot { it.isNotified } .let { diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/ExamWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/ExamWork.kt index 7071bce2..4b0b1bdb 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/ExamWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/ExamWork.kt @@ -5,6 +5,8 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.ExamRepository import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.services.sync.notifications.NewExamNotification +import io.github.wulkanowy.utils.endExamsDay +import io.github.wulkanowy.utils.startExamsDay import kotlinx.coroutines.flow.first import java.time.LocalDate.now import javax.inject.Inject @@ -15,16 +17,24 @@ class ExamWork @Inject constructor( ) : Work { override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { + val startDate = now().startExamsDay + val endDate = startDate.endExamsDay + examRepository.getExams( student = student, semester = semester, - start = now(), - end = now(), + start = startDate, + end = endDate, forceRefresh = true, notify = notify, ).waitForResult() - examRepository.getExamsFromDatabase(semester, now()).first() + examRepository.getExamsFromDatabase( + semester = semester, + start = startDate, + end = endDate, + ) + .first() .filter { !it.isNotified }.let { if (it.isNotEmpty()) newExamNotification.notify(it, student) diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/HomeworkWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/HomeworkWork.kt index 4cfe27d0..ddff3af7 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/HomeworkWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/HomeworkWork.kt @@ -5,7 +5,9 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.HomeworkRepository import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.services.sync.notifications.NewHomeworkNotification +import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.nextOrSameSchoolDay +import io.github.wulkanowy.utils.sunday import kotlinx.coroutines.flow.first import java.time.LocalDate.now import javax.inject.Inject @@ -16,16 +18,24 @@ class HomeworkWork @Inject constructor( ) : Work { override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { + val startDate = now().nextOrSameSchoolDay.monday + val endDate = startDate.sunday + homeworkRepository.getHomework( student = student, semester = semester, - start = now().nextOrSameSchoolDay, - end = now().nextOrSameSchoolDay, + start = startDate, + end = endDate, forceRefresh = true, notify = notify, ).waitForResult() - homeworkRepository.getHomeworkFromDatabase(semester, now(), now().plusDays(7)).first() + homeworkRepository.getHomeworkFromDatabase( + semester = semester, + start = startDate, + end = endDate + ) + .first() .filter { !it.isNotified }.let { if (it.isNotEmpty()) newHomeworkNotification.notify(it, student) 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 29b1f13c..ac9a8eb4 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 @@ -16,17 +16,24 @@ class TimetableWork @Inject constructor( ) : Work { override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { + val startDate = now().nextOrSameSchoolDay + val endDate = startDate.plusDays(7) + timetableRepository.getTimetable( student = student, semester = semester, - start = now().nextOrSameSchoolDay, - end = now().nextOrSameSchoolDay, + start = startDate, + end = endDate, forceRefresh = true, notify = notify, ) .waitForResult() - timetableRepository.getTimetableFromDatabase(semester, now(), now().plusDays(7)) + timetableRepository.getTimetableFromDatabase( + semester = semester, + from = startDate, + end = endDate, + ) .first() .filterNot { it.isNotified } .let { 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 4e158743..ad604499 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 @@ -435,13 +435,13 @@ class DashboardPresenter @Inject constructor( private fun loadLessons(student: Student, forceRefresh: Boolean) { flatResourceFlow { val semester = semesterRepository.getCurrentSemester(student) - val date = LocalDate.now() + val date = LocalDate.now().nextOrSameSchoolDay timetableRepository.getTimetable( student = student, semester = semester, start = date, - end = date.plusDays(1), + end = date, forceRefresh = forceRefresh ) } From e0f4cad7fb4e35cc38772b02e7b9a8a62de51996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sun, 21 Jan 2024 20:01:00 +0100 Subject: [PATCH 22/51] Add missing unitId to sdk switchSemester call (#2402) --- .../data/repositories/AttendanceRepository.kt | 12 +++++++++--- .../data/repositories/AttendanceSummaryRepository.kt | 3 ++- .../data/repositories/CompletedLessonsRepository.kt | 2 +- .../data/repositories/ConferenceRepository.kt | 3 ++- .../wulkanowy/data/repositories/ExamRepository.kt | 10 ++++++++-- .../wulkanowy/data/repositories/GradeRepository.kt | 2 +- .../data/repositories/GradeStatisticsRepository.kt | 7 ++++--- .../data/repositories/HomeworkRepository.kt | 2 +- .../data/repositories/MobileDeviceRepository.kt | 7 ++++--- .../wulkanowy/data/repositories/NoteRepository.kt | 2 +- .../wulkanowy/data/repositories/SchoolRepository.kt | 3 ++- .../wulkanowy/data/repositories/SchoolsRepository.kt | 7 ++----- .../data/repositories/StudentInfoRepository.kt | 3 ++- .../wulkanowy/data/repositories/StudentRepository.kt | 5 +++-- .../wulkanowy/data/repositories/SubjectRepository.kt | 6 ++++-- .../wulkanowy/data/repositories/TeacherRepository.kt | 3 ++- .../data/repositories/TimetableRepository.kt | 2 +- .../java/io/github/wulkanowy/utils/SdkExtension.kt | 10 ++++++++++ 18 files changed, 59 insertions(+), 30 deletions(-) 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 3afb9907..6d782047 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 @@ -9,7 +9,13 @@ import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.pojo.Absent -import io.github.wulkanowy.utils.* +import io.github.wulkanowy.utils.AutoRefreshHelper +import io.github.wulkanowy.utils.getRefreshKey +import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.monday +import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.utils.switchSemester +import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex @@ -58,7 +64,7 @@ class AttendanceRepository @Inject constructor( ) } sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getAttendance(start.monday, end.sunday) .mapToEntities(semester, lessons) }, @@ -97,7 +103,7 @@ class AttendanceRepository @Inject constructor( ) } sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .excuseForAbsence(items, reason) } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt index 8e070913..6bdcf9d7 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AttendanceSummaryRepository.kt @@ -9,6 +9,7 @@ import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.switchSemester import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.sync.Mutex import javax.inject.Inject @@ -40,7 +41,7 @@ class AttendanceSummaryRepository @Inject constructor( query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, subjectId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getAttendanceSummary(subjectId) .mapToEntities(semester, subjectId) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt index 8f393cad..1579ae62 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/CompletedLessonsRepository.kt @@ -48,7 +48,7 @@ class CompletedLessonsRepository @Inject constructor( }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getCompletedLessons(start.monday, end.sunday) .mapToEntities(semester) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt index 83204cab..7eb37f0b 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/ConferenceRepository.kt @@ -10,6 +10,7 @@ import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.switchSemester import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex @@ -46,7 +47,7 @@ class ConferenceRepository @Inject constructor( }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getConferences() .mapToEntities(semester) .filter { it.date >= startDate } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt index 59f42242..96026a55 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt @@ -7,7 +7,13 @@ import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.utils.* +import io.github.wulkanowy.utils.AutoRefreshHelper +import io.github.wulkanowy.utils.endExamsDay +import io.github.wulkanowy.utils.getRefreshKey +import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.startExamsDay +import io.github.wulkanowy.utils.switchSemester +import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex import java.time.LocalDate @@ -51,7 +57,7 @@ class ExamRepository @Inject constructor( }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getExams(start.startExamsDay, start.endExamsDay) .mapToEntities(semester) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt index f5f895d8..b2bb7b90 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt @@ -52,7 +52,7 @@ class GradeRepository @Inject constructor( }, fetch = { val (details, summary) = sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getGrades(semester.semesterId) details.mapToEntities(semester) to summary.mapToEntities(semester) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt index 9fa06c49..23d7b858 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeStatisticsRepository.kt @@ -16,6 +16,7 @@ import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.switchSemester import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.sync.Mutex import java.util.* @@ -56,7 +57,7 @@ class GradeStatisticsRepository @Inject constructor( query = { gradePartialStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getGradesPartialStatistics(semester.semesterId) .mapToEntities(semester) }, @@ -101,7 +102,7 @@ class GradeStatisticsRepository @Inject constructor( query = { gradeSemesterStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getGradesSemesterStatistics(semester.semesterId) .mapToEntities(semester) }, @@ -157,7 +158,7 @@ class GradeStatisticsRepository @Inject constructor( query = { gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getGradesPointsStatistics(semester.semesterId) .mapToEntities(semester) }, 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 048c95a3..d4899b19 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 @@ -55,7 +55,7 @@ class HomeworkRepository @Inject constructor( }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getHomework(start.monday, end.sunday) .mapToEntities(semester) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt index 07c6959e..412f9e7f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MobileDeviceRepository.kt @@ -12,6 +12,7 @@ import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.switchSemester import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.sync.Mutex import javax.inject.Inject @@ -42,7 +43,7 @@ class MobileDeviceRepository @Inject constructor( query = { mobileDb.loadAll(student.userLoginId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getRegisteredDevices() .mapToEntities(student) }, @@ -56,7 +57,7 @@ class MobileDeviceRepository @Inject constructor( suspend fun unregisterDevice(student: Student, semester: Semester, device: MobileDevice) { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .unregisterDevice(device.deviceId) mobileDb.deleteAll(listOf(device)) @@ -64,7 +65,7 @@ class MobileDeviceRepository @Inject constructor( suspend fun getToken(student: Student, semester: Semester): MobileDeviceToken { return sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getToken() .mapToMobileDeviceToken() } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt index 4101803f..eeb1d53e 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/NoteRepository.kt @@ -41,7 +41,7 @@ class NoteRepository @Inject constructor( query = { noteDb.loadAll(student.studentId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getNotes() .mapToEntities(semester) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt index 7972ed08..f757ef04 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolRepository.kt @@ -9,6 +9,7 @@ import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.switchSemester import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -40,7 +41,7 @@ class SchoolRepository @Inject constructor( query = { schoolDb.load(semester.studentId, semester.classId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getSchool() .mapToEntity(semester) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolsRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolsRepository.kt index 9c642934..216a8c11 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolsRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SchoolsRepository.kt @@ -11,6 +11,7 @@ import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.utils.IntegrityHelper import io.github.wulkanowy.utils.getCurrentOrLast import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.switchSemester import kotlinx.coroutines.withTimeout import timber.log.Timber import java.util.UUID @@ -42,11 +43,7 @@ class SchoolsRepository @Inject constructor( val schoolInfo = sdk .init(student.copy(password = loginData.password)) - .switchDiary( - diaryId = semester.diaryId, - kindergartenDiaryId = semester.kindergartenDiaryId, - schoolYear = semester.schoolYear - ) + .switchSemester(semester) .getSchool() schoolsService.logLoginEvent( diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt index efc82a77..d6cd25c8 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentInfoRepository.kt @@ -7,6 +7,7 @@ import io.github.wulkanowy.data.mappers.mapToEntity import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.switchSemester import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -30,7 +31,7 @@ class StudentInfoRepository @Inject constructor( query = { studentInfoDao.loadStudentInfo(student.studentId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getStudentInfo().mapToEntity(semester) }, saveFetchResult = { old, new -> 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 bfad12a8..e063840c 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 @@ -16,6 +16,7 @@ import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.DispatchersProvider import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.security.Scrambler +import io.github.wulkanowy.utils.switchSemester import kotlinx.coroutines.withContext import javax.inject.Inject import javax.inject.Singleton @@ -149,12 +150,12 @@ class StudentRepository @Inject constructor( suspend fun authorizePermission(student: Student, semester: Semester, pesel: String) = sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .authorizePermission(pesel) suspend fun refreshStudentName(student: Student, semester: Semester) { val newCurrentApiStudent = sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getCurrentStudent() ?: return val studentName = StudentName( diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt index 3926122b..98cb181a 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SubjectRepository.kt @@ -9,6 +9,7 @@ import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.switchSemester import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.sync.Mutex import javax.inject.Inject @@ -39,8 +40,9 @@ class SubjectRepository @Inject constructor( query = { subjectDao.loadAll(semester.diaryId, semester.studentId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) - .getSubjects().mapToEntities(semester) + .switchSemester(semester) + .getSubjects() + .mapToEntities(semester) }, saveFetchResult = { old, new -> subjectDao.deleteAll(old uniqueSubtract new) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt index 4e3b40f9..42698f92 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TeacherRepository.kt @@ -9,6 +9,7 @@ import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.switchSemester import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.sync.Mutex import javax.inject.Inject @@ -39,7 +40,7 @@ class TeacherRepository @Inject constructor( query = { teacherDb.loadAll(semester.studentId, semester.classId) }, fetch = { sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getTeachers() .mapToEntities(semester) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt index 136fb8d5..9305d3b3 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 @@ -65,7 +65,7 @@ class TimetableRepository @Inject constructor( query = { getFullTimetableFromDatabase(student, semester, start, end) }, fetch = { val timetableFull = sdk.init(student) - .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) + .switchSemester(semester) .getTimetable(start.monday, end.sunday) timetableFull.mapToEntities(semester) diff --git a/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt index df99be98..9b6ca706 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/SdkExtension.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.utils +import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.sdk.Sdk import timber.log.Timber @@ -30,3 +31,12 @@ fun Sdk.init(student: Student): Sdk { return this } + +fun Sdk.switchSemester(semester: Semester): Sdk { + return switchDiary( + diaryId = semester.diaryId, + kindergartenDiaryId = semester.kindergartenDiaryId, + schoolYear = semester.schoolYear, + unitId = semester.unitId, + ) +} From dc59f4ffa35c85bea6db3d0eef81b1b2e484f94f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sun, 21 Jan 2024 21:04:23 +0100 Subject: [PATCH 23/51] Version 2.3.5 --- app/build.gradle | 8 ++++---- .../wulkanowy/data/repositories/HomeworkRepository.kt | 1 + app/src/main/play/release-notes/pl-PL/default.txt | 6 +++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index a67adb6f..7b290e63 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,8 +27,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 34 - versionCode 144 - versionName "2.3.4" + versionCode 145 + versionName "2.3.5" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" @@ -162,7 +162,7 @@ play { defaultToAppBundles = false track = 'production' releaseStatus = ReleaseStatus.IN_PROGRESS - userFraction = 0.99d + userFraction = 0.15d updatePriority = 1 enabled.set(false) } @@ -193,7 +193,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.3.7-SNAPSHOT' + implementation 'io.github.wulkanowy:sdk:2.3.7' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' 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 d4899b19..010cf845 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 @@ -12,6 +12,7 @@ import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.utils.switchSemester import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.sync.Mutex import java.time.LocalDate diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index c2c30883..04f3ba46 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,6 +1,6 @@ -Wersja 2.3.4 +Wersja 2.3.5 -— dodaliśmy obsługę captchy, co umożliwi używanie apki np. na odmianie ResMan Rzeszów -— naprawiliśmy wyświetlanie frekwencji w szkołach używających eduOne (piszcie, jeśli nadal nie działa) +— naprawiliśmy ładowanie frekwencji dla szkół używających eduOne +— naprawiliśmy wielokrotne wysyłanie powiadomień o zmianach w planie lekcji Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases From 098af9884a4179e2ea44a22330b068b58ae3c2da Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jan 2024 19:17:29 +0000 Subject: [PATCH 24/51] Bump com.google.firebase:firebase-bom from 32.7.0 to 32.7.1 (#2404) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 7b290e63..54659cdd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -250,7 +250,7 @@ dependencies { implementation 'com.fredporciuncula:flow-preferences:1.9.1' implementation 'org.apache.commons:commons-text:1.11.0' - playImplementation platform('com.google.firebase:firebase-bom:32.7.0') + playImplementation platform('com.google.firebase:firebase-bom:32.7.1') playImplementation 'com.google.firebase:firebase-analytics' playImplementation 'com.google.firebase:firebase-messaging' playImplementation 'com.google.firebase:firebase-crashlytics:' From 10add8a70ed105a214579d6ee019dc770765f42c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Jan 2024 10:21:30 +0000 Subject: [PATCH 25/51] Bump com.android.tools.build:gradle from 8.2.1 to 8.2.2 (#2406) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 095d1b72..e6e090b8 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ buildscript { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$kotlin_version-1.0.16" - classpath 'com.android.tools.build:gradle:8.2.1' + classpath 'com.android.tools.build:gradle:8.2.2' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.4.0' classpath 'com.huawei.agconnect:agcp:1.9.1.303' From 88043569ac8cb7a6fe1b418ece97aaec1b29e6f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Jan 2024 10:23:01 +0000 Subject: [PATCH 26/51] Bump com.huawei.hms:hianalytics from 6.12.0.300 to 6.12.0.301 (#2407) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 54659cdd..c40bef8e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -262,7 +262,7 @@ dependencies { playImplementation 'com.google.android.play:review-ktx:2.0.1' playImplementation "com.google.android.ump:user-messaging-platform:2.1.0" - hmsImplementation 'com.huawei.hms:hianalytics:6.12.0.300' + hmsImplementation 'com.huawei.hms:hianalytics:6.12.0.301' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.1.303' releaseImplementation "com.github.chuckerteam.chucker:library-no-op:$chucker" From fc91936884715cc1d46d5af17086364961f8631d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Jan 2024 10:30:36 +0000 Subject: [PATCH 27/51] Bump com.google.android.ump:user-messaging-platform from 2.1.0 to 2.2.0 (#2408) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index c40bef8e..c0caf1dd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -260,7 +260,7 @@ dependencies { playImplementation "com.google.android.play:integrity:1.3.0" playImplementation 'com.google.android.play:app-update-ktx:2.1.0' playImplementation 'com.google.android.play:review-ktx:2.0.1' - playImplementation "com.google.android.ump:user-messaging-platform:2.1.0" + playImplementation "com.google.android.ump:user-messaging-platform:2.2.0" hmsImplementation 'com.huawei.hms:hianalytics:6.12.0.301' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.1.303' From e58a60410c3a6ee0dda506d2422905fe9cc8ee4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sun, 4 Feb 2024 10:48:59 +0100 Subject: [PATCH 28/51] Fix android tests (#2410) --- .gitignore | 2 ++ .idea/migrations.xml | 10 ---------- app/build.gradle | 4 +++- .../wulkanowy/utils/security/ScramblerTest.kt | 15 +++++++++------ 4 files changed, 14 insertions(+), 17 deletions(-) delete mode 100644 .idea/migrations.xml diff --git a/.gitignore b/.gitignore index 921bd0a9..980085e3 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,8 @@ captures/ .idea/uiDesigner.xml .idea/runConfigurations.xml .idea/discord.xml +.idea/migrations.xml +.idea/androidTestResultsUserPreferences.xml # Keystore files *.jks diff --git a/.idea/migrations.xml b/.idea/migrations.xml deleted file mode 100644 index f8051a6f..00000000 --- a/.idea/migrations.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index c0caf1dd..b60dc3b4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -142,7 +142,9 @@ android { packagingOptions { resources { excludes += ['META-INF/library_release.kotlin_module', - 'META-INF/library-core_release.kotlin_module'] + 'META-INF/library-core_release.kotlin_module', + 'META-INF/LICENSE.md', + 'META-INF/LICENSE-notice.md'] } } diff --git a/app/src/androidTest/java/io/github/wulkanowy/utils/security/ScramblerTest.kt b/app/src/androidTest/java/io/github/wulkanowy/utils/security/ScramblerTest.kt index 0c47e6bb..1b0319f6 100644 --- a/app/src/androidTest/java/io/github/wulkanowy/utils/security/ScramblerTest.kt +++ b/app/src/androidTest/java/io/github/wulkanowy/utils/security/ScramblerTest.kt @@ -14,34 +14,37 @@ import kotlin.test.assertFailsWith @RunWith(AndroidJUnit4::class) class ScramblerTest { + private val scrambler = Scrambler(ApplicationProvider.getApplicationContext()) + @Test fun encryptDecryptTest() { - assertEquals("TEST", decrypt(encrypt("TEST", - ApplicationProvider.getApplicationContext()))) + assertEquals( + "TEST", scrambler.decrypt(scrambler.encrypt("TEST")) + ) } @Test fun emptyTextEncryptTest() { assertFailsWith { - decrypt("") + scrambler.decrypt("") } assertFailsWith { - encrypt("", ApplicationProvider.getApplicationContext()) + scrambler.encrypt("") } } @Test @SdkSuppress(minSdkVersion = 18) fun emptyKeyStoreTest() { - val text = encrypt("test", ApplicationProvider.getApplicationContext()) + val text = scrambler.encrypt("test") val keyStore = KeyStore.getInstance("AndroidKeyStore") keyStore.load(null) keyStore.deleteEntry("wulkanowy_password") assertFailsWith { - decrypt(text) + scrambler.decrypt(text) } } } From a05f1f70f7f8c7b7dbcc0aefa6f98cd021522943 Mon Sep 17 00:00:00 2001 From: JestemKamil <84380834+JestemKamil@users.noreply.github.com> Date: Tue, 6 Feb 2024 18:21:56 +0100 Subject: [PATCH 29/51] Add colors to attendance and ! to unexcused lateness (#2412) --- .../modules/attendance/AttendanceAdapter.kt | 32 ++++++++++++++++++- .../ui/modules/attendance/AttendanceDialog.kt | 12 +++++++ app/src/main/res/values-night-v31/styles.xml | 3 ++ app/src/main/res/values-night/styles.xml | 2 ++ app/src/main/res/values-v31/styles.xml | 3 ++ app/src/main/res/values/attrs.xml | 2 ++ app/src/main/res/values/colors.xml | 6 ++++ app/src/main/res/values/styles.xml | 2 ++ 8 files changed, 61 insertions(+), 1 deletion(-) 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 39f376f6..4e9baac3 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 @@ -1,5 +1,6 @@ package io.github.wulkanowy.ui.modules.attendance +import android.graphics.Typeface import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -10,6 +11,7 @@ import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.enums.SentExcuseStatus import io.github.wulkanowy.databinding.ItemAttendanceBinding import io.github.wulkanowy.utils.descriptionRes +import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.isExcusableOrNotExcused import javax.inject.Inject @@ -39,7 +41,33 @@ class AttendanceAdapter @Inject constructor() : root.context.getString(R.string.all_no_data) } attendanceItemDescription.setText(item.descriptionRes) - attendanceItemAlert.isVisible = item.let { it.absence && !it.excused } + + attendanceItemDescription.setTextColor( + root.context.getThemeAttrColor( + when { + item.absence && !item.excused -> R.attr.colorAttendanceAbsence + item.lateness && !item.excused -> R.attr.colorAttendanceLateness + else -> android.R.attr.textColorSecondary + } + ) + ) + + if (item.exemption || item.excused) { + attendanceItemDescription.setTypeface(null, Typeface.BOLD) + } else { + attendanceItemDescription.setTypeface(null, Typeface.NORMAL) + } + + attendanceItemAlert.isVisible = + item.let { (it.absence && !it.excused) || (it.lateness && !it.excused) } + + attendanceItemAlert.setColorFilter(root.context.getThemeAttrColor( + when{ + item.absence && !item.excused -> R.attr.colorAttendanceAbsence + item.lateness && !item.excused -> R.attr.colorAttendanceLateness + else -> android.R.attr.colorPrimary + } + )) attendanceItemNumber.visibility = View.GONE attendanceItemExcuseInfo.visibility = View.GONE attendanceItemExcuseCheckbox.visibility = View.GONE @@ -54,10 +82,12 @@ class AttendanceAdapter @Inject constructor() : attendanceItemExcuseInfo.visibility = View.VISIBLE attendanceItemAlert.visibility = View.INVISIBLE } + SentExcuseStatus.DENIED -> { attendanceItemExcuseInfo.setImageResource(R.drawable.ic_excuse_denied) attendanceItemExcuseInfo.visibility = View.VISIBLE } + else -> { if (item.isExcusableOrNotExcused && excuseActionMode) { attendanceItemNumber.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 c0026bee..63503313 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 @@ -6,10 +6,12 @@ import android.view.View import androidx.core.os.bundleOf import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.databinding.DialogAttendanceBinding import io.github.wulkanowy.ui.base.BaseDialogFragment import io.github.wulkanowy.utils.descriptionRes +import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.serializable import io.github.wulkanowy.utils.toFormattedString @@ -44,6 +46,16 @@ class AttendanceDialog : BaseDialogFragment() { with(binding) { attendanceDialogSubjectValue.text = attendance.subject attendanceDialogDescriptionValue.setText(attendance.descriptionRes) + attendanceDialogDescriptionValue.setTextColor( + root.context.getThemeAttrColor( + when { + attendance.absence && !attendance.excused -> R.attr.colorAttendanceAbsence + attendance.lateness && !attendance.excused -> R.attr.colorAttendanceLateness + else -> android.R.attr.textColorSecondary + } + ) + ) + attendanceDialogDateValue.text = attendance.date.toFormattedString() attendanceDialogNumberValue.text = attendance.number.toString() attendanceDialogClose.setOnClickListener { dismiss() } diff --git a/app/src/main/res/values-night-v31/styles.xml b/app/src/main/res/values-night-v31/styles.xml index 6e6c4d79..808bc714 100644 --- a/app/src/main/res/values-night-v31/styles.xml +++ b/app/src/main/res/values-night-v31/styles.xml @@ -32,6 +32,9 @@ @color/material_dynamic_primary40 @color/timetable_canceled_dark @color/timetable_change_dark + @color/attendance_absence_dark + @color/attendance_lateness_dark + @color/colorErrorLight @color/colorDividerInverse @color/material_dynamic_secondary20 diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml index 5d9aa22a..5840a051 100644 --- a/app/src/main/res/values-night/styles.xml +++ b/app/src/main/res/values-night/styles.xml @@ -21,6 +21,8 @@ @color/colorSurfaceDark @color/timetable_canceled_dark @color/timetable_change_dark + @color/attendance_absence_dark + @color/attendance_lateness_dark @color/colorErrorLight @color/colorDividerInverse @color/colorSwipeRefreshDark diff --git a/app/src/main/res/values-v31/styles.xml b/app/src/main/res/values-v31/styles.xml index bb47b22e..35880668 100644 --- a/app/src/main/res/values-v31/styles.xml +++ b/app/src/main/res/values-v31/styles.xml @@ -34,6 +34,9 @@ @color/material_dynamic_primary80 @color/timetable_canceled_light @color/timetable_change_light + @color/attendance_absence_light + @color/attendance_lateness_light + @color/colorError @color/colorDivider @color/material_dynamic_secondary90 diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index aa58fa09..3c3fdb8e 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -7,4 +7,6 @@ + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 87057c61..8ad27ad8 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -49,6 +49,12 @@ #ff8f00 #ffd54f + #d32f2f + #e57373 + + #cd2a01 + #f05d0e + #1f000000 #1fffffff diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index a0023dda..b5b02950 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -19,6 +19,8 @@ @color/colorSurface @color/timetable_canceled_light @color/timetable_change_light + @color/attendance_absence_light + @color/attendance_lateness_light @color/colorError @color/colorDivider @color/colorSwipeRefresh From ed5166333ae3a574bc974737686afb0db994806a Mon Sep 17 00:00:00 2001 From: PoProstuSever <88079246+PoProstuSever@users.noreply.github.com> Date: Tue, 6 Feb 2024 19:48:31 +0100 Subject: [PATCH 30/51] Add the ability to dynamically expand the application window (#2413) --- app/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 174c9a1f..f43dfdd2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -44,6 +44,7 @@ android:networkSecurityConfig="@xml/network_security_config" android:supportsRtl="false" android:theme="@style/WulkanowyTheme" + android:resizeableActivity="true" tools:ignore="DataExtractionRules,UnusedAttribute"> Date: Thu, 8 Feb 2024 07:49:17 +0100 Subject: [PATCH 31/51] Fix displaying lessons for tomorrow if there is no more lessons for today (#2416) --- .../IsStudentHasLessonsOnWeekendUseCase.kt | 33 +++++++++++++++++++ .../timetable/IsWeekendHasLessonsUseCase.kt | 17 ++++++++++ .../modules/dashboard/DashboardPresenter.kt | 12 +++++-- .../modules/timetable/TimetablePresenter.kt | 32 ++++-------------- 4 files changed, 65 insertions(+), 29 deletions(-) create mode 100644 app/src/main/java/io/github/wulkanowy/domain/timetable/IsStudentHasLessonsOnWeekendUseCase.kt create mode 100644 app/src/main/java/io/github/wulkanowy/domain/timetable/IsWeekendHasLessonsUseCase.kt diff --git a/app/src/main/java/io/github/wulkanowy/domain/timetable/IsStudentHasLessonsOnWeekendUseCase.kt b/app/src/main/java/io/github/wulkanowy/domain/timetable/IsStudentHasLessonsOnWeekendUseCase.kt new file mode 100644 index 00000000..efe928e2 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/domain/timetable/IsStudentHasLessonsOnWeekendUseCase.kt @@ -0,0 +1,33 @@ +package io.github.wulkanowy.domain.timetable + +import io.github.wulkanowy.data.dataOrNull +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.repositories.TimetableRepository +import io.github.wulkanowy.data.toFirstResult +import io.github.wulkanowy.utils.monday +import io.github.wulkanowy.utils.sunday +import java.time.LocalDate +import javax.inject.Inject + +class IsStudentHasLessonsOnWeekendUseCase @Inject constructor( + private val timetableRepository: TimetableRepository, + private val isWeekendHasLessonsUseCase: IsWeekendHasLessonsUseCase, +) { + + suspend operator fun invoke( + student: Student, + semester: Semester, + currentDate: LocalDate = LocalDate.now(), + ): Boolean { + val lessons = timetableRepository.getTimetable( + student = student, + semester = semester, + start = currentDate.monday, + end = currentDate.sunday, + forceRefresh = false, + timetableType = TimetableRepository.TimetableType.NORMAL + ).toFirstResult().dataOrNull?.lessons.orEmpty() + return isWeekendHasLessonsUseCase(lessons) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/domain/timetable/IsWeekendHasLessonsUseCase.kt b/app/src/main/java/io/github/wulkanowy/domain/timetable/IsWeekendHasLessonsUseCase.kt new file mode 100644 index 00000000..908c9df7 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/domain/timetable/IsWeekendHasLessonsUseCase.kt @@ -0,0 +1,17 @@ +package io.github.wulkanowy.domain.timetable + +import io.github.wulkanowy.data.db.entities.Timetable +import java.time.DayOfWeek +import javax.inject.Inject + +class IsWeekendHasLessonsUseCase @Inject constructor() { + + operator fun invoke( + lessons: List, + ): Boolean = lessons.any { + it.date.dayOfWeek in listOf( + DayOfWeek.SATURDAY, + DayOfWeek.SUNDAY, + ) + } +} 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 ad604499..1e6f1c19 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 @@ -24,11 +24,13 @@ import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.TimetableRepository import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase +import io.github.wulkanowy.domain.timetable.IsStudentHasLessonsOnWeekendUseCase import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.AdsHelper import io.github.wulkanowy.utils.calculatePercentage import io.github.wulkanowy.utils.nextOrSameSchoolDay +import io.github.wulkanowy.utils.sunday import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine @@ -56,6 +58,7 @@ class DashboardPresenter @Inject constructor( private val messageRepository: MessageRepository, private val attendanceSummaryRepository: AttendanceSummaryRepository, private val timetableRepository: TimetableRepository, + private val isStudentHasLessonsOnWeekendUseCase: IsStudentHasLessonsOnWeekendUseCase, private val homeworkRepository: HomeworkRepository, private val examRepository: ExamRepository, private val conferenceRepository: ConferenceRepository, @@ -435,14 +438,17 @@ class DashboardPresenter @Inject constructor( private fun loadLessons(student: Student, forceRefresh: Boolean) { flatResourceFlow { val semester = semesterRepository.getCurrentSemester(student) - val date = LocalDate.now().nextOrSameSchoolDay + val date = when (isStudentHasLessonsOnWeekendUseCase(student, semester)) { + true -> LocalDate.now() + else -> LocalDate.now().nextOrSameSchoolDay + } timetableRepository.getTimetable( student = student, semester = semester, start = date, - end = date, - forceRefresh = forceRefresh + end = date.sunday, + forceRefresh = forceRefresh, ) } .onEach { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt index 6b442d1c..7e8c876e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt @@ -2,7 +2,6 @@ package io.github.wulkanowy.ui.modules.timetable import android.os.Handler import android.os.Looper -import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Timetable @@ -20,8 +19,8 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.TimetableRepository -import io.github.wulkanowy.data.toFirstResult -import io.github.wulkanowy.data.waitForResult +import io.github.wulkanowy.domain.timetable.IsStudentHasLessonsOnWeekendUseCase +import io.github.wulkanowy.domain.timetable.IsWeekendHasLessonsUseCase import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.AnalyticsHelper @@ -31,16 +30,12 @@ import io.github.wulkanowy.utils.isHolidays import io.github.wulkanowy.utils.isJustFinished import io.github.wulkanowy.utils.isShowTimeUntil import io.github.wulkanowy.utils.left -import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.nextOrSameSchoolDay import io.github.wulkanowy.utils.nextSchoolDay import io.github.wulkanowy.utils.previousSchoolDay -import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.until -import kotlinx.coroutines.flow.firstOrNull import timber.log.Timber -import java.time.DayOfWeek import java.time.Instant import java.time.LocalDate import java.time.LocalDate.now @@ -54,6 +49,8 @@ class TimetablePresenter @Inject constructor( errorHandler: ErrorHandler, studentRepository: StudentRepository, private val timetableRepository: TimetableRepository, + private val isStudentHasLessonsOnWeekendUseCase: IsStudentHasLessonsOnWeekendUseCase, + private val isWeekendHasLessonsUseCase: IsWeekendHasLessonsUseCase, private val semesterRepository: SemesterRepository, private val prefRepository: PreferencesRepository, private val analytics: AnalyticsHelper, @@ -165,7 +162,7 @@ class TimetablePresenter @Inject constructor( } .logResourceStatus("load timetable data") .onResourceData { - isWeekendHasLessons = isWeekendHasLessons || isWeekendHasLessons(it.lessons) + isWeekendHasLessons = isWeekendHasLessons || isWeekendHasLessonsUseCase(it.lessons) view?.run { enableSwipe(true) @@ -199,15 +196,7 @@ class TimetablePresenter @Inject constructor( private suspend fun checkInitialAndCurrentDate(student: Student, semester: Semester) { if (initialDate == null) { - val lessons = timetableRepository.getTimetable( - student = student, - semester = semester, - start = now().monday, - end = now().sunday, - forceRefresh = false, - timetableType = TimetableRepository.TimetableType.NORMAL - ).toFirstResult().dataOrNull?.lessons.orEmpty() - isWeekendHasLessons = isWeekendHasLessons(lessons) + isWeekendHasLessons = isStudentHasLessonsOnWeekendUseCase(student, semester) initialDate = getInitialDate(semester) } @@ -216,15 +205,6 @@ class TimetablePresenter @Inject constructor( } } - private fun isWeekendHasLessons( - lessons: List, - ): Boolean = lessons.any { - it.date.dayOfWeek in listOf( - DayOfWeek.SATURDAY, - DayOfWeek.SUNDAY, - ) - } - private fun getInitialDate(semester: Semester): LocalDate { val now = now() From 22f72981cb7eea613f6b6ac234fbb7c050ccccf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Thu, 8 Feb 2024 09:16:09 +0100 Subject: [PATCH 32/51] Add descriptive grades (#2411) --- .../59.json | 2501 +++++++++++++++++ .../io/github/wulkanowy/data/DataModule.kt | 4 + .../github/wulkanowy/data/db/AppDatabase.kt | 10 +- .../data/db/dao/GradeDescriptiveDao.kt | 15 + .../data/db/entities/GradeDescriptive.kt | 27 + .../wulkanowy/data/mappers/GradeMapper.kt | 16 +- .../data/repositories/GradeRepository.kt | 63 +- .../notifications/NewGradeNotification.kt | 26 +- .../sync/notifications/NotificationType.kt | 4 + .../services/sync/works/GradeWork.kt | 10 + .../NotificationDebugPresenter.kt | 9 + .../notification/mock/gradeDescriptive.kt | 48 + .../ui/modules/grade/GradeAverageProvider.kt | 93 +- .../ui/modules/grade/GradeSubject.kt | 2 + .../grade/details/GradeDetailsPresenter.kt | 25 +- .../grade/summary/GradeSummaryAdapter.kt | 52 +- .../grade/summary/GradeSummaryFragment.kt | 4 +- .../modules/grade/summary/GradeSummaryItem.kt | 9 + .../grade/summary/GradeSummaryPresenter.kt | 36 +- .../modules/grade/summary/GradeSummaryView.kt | 3 +- .../main/res/layout/item_grade_summary.xml | 46 +- app/src/main/res/values/strings.xml | 9 + .../data/repositories/GradeRepositoryTest.kt | 14 +- .../modules/grade/GradeAverageProviderTest.kt | 446 ++- 24 files changed, 3248 insertions(+), 224 deletions(-) create mode 100644 app/schemas/io.github.wulkanowy.data.db.AppDatabase/59.json create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/dao/GradeDescriptiveDao.kt create mode 100644 app/src/main/java/io/github/wulkanowy/data/db/entities/GradeDescriptive.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/gradeDescriptive.kt create mode 100644 app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryItem.kt diff --git a/app/schemas/io.github.wulkanowy.data.db.AppDatabase/59.json b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/59.json new file mode 100644 index 00000000..a3f2e0dc --- /dev/null +++ b/app/schemas/io.github.wulkanowy.data.db.AppDatabase/59.json @@ -0,0 +1,2501 @@ +{ + "formatVersion": 1, + "database": { + "version": 59, + "identityHash": "3bd95e40b587e8131a2a2c23aee538c1", + "entities": [ + { + "tableName": "Students", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrapper_base_url` TEXT NOT NULL, `scrapper_domain_suffix` TEXT NOT NULL DEFAULT '', `mobile_base_url` TEXT NOT NULL, `login_type` TEXT NOT NULL, `login_mode` TEXT NOT NULL, `certificate_key` TEXT NOT NULL, `private_key` TEXT NOT NULL, `is_parent` INTEGER NOT NULL, `email` TEXT NOT NULL, `password` TEXT NOT NULL, `symbol` TEXT NOT NULL, `student_id` INTEGER NOT NULL, `user_login_id` INTEGER NOT NULL, `user_name` TEXT NOT NULL, `student_name` TEXT NOT NULL, `school_id` TEXT NOT NULL, `school_short` TEXT NOT NULL, `school_name` TEXT NOT NULL, `class_name` TEXT NOT NULL, `class_id` INTEGER NOT NULL, `is_current` INTEGER NOT NULL, `registration_date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `nick` TEXT NOT NULL, `avatar_color` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "scrapperBaseUrl", + "columnName": "scrapper_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scrapperDomainSuffix", + "columnName": "scrapper_domain_suffix", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "mobileBaseUrl", + "columnName": "mobile_base_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginType", + "columnName": "login_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "loginMode", + "columnName": "login_mode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificateKey", + "columnName": "certificate_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "privateKey", + "columnName": "private_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isParent", + "columnName": "is_parent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "user_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "student_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolSymbol", + "columnName": "school_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "school_short", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolName", + "columnName": "school_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "className", + "columnName": "class_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isCurrent", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registrationDate", + "columnName": "registration_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nick", + "columnName": "nick", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatarColor", + "columnName": "avatar_color", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Students_email_symbol_student_id_school_id_class_id", + "unique": true, + "columnNames": [ + "email", + "symbol", + "student_id", + "school_id", + "class_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Students_email_symbol_student_id_school_id_class_id` ON `${TABLE_NAME}` (`email`, `symbol`, `student_id`, `school_id`, `class_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Semesters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `kindergarten_diary_id` INTEGER NOT NULL DEFAULT 0, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "kindergartenDiaryId", + "columnName": "kindergarten_diary_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "diaryName", + "columnName": "diary_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolYear", + "columnName": "school_year", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterName", + "columnName": "semester_name", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unitId", + "columnName": "unit_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "current", + "columnName": "is_current", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id", + "unique": true, + "columnNames": [ + "student_id", + "diary_id", + "kindergarten_diary_id", + "semester_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id` ON `${TABLE_NAME}` (`student_id`, `diary_id`, `kindergarten_diary_id`, `semester_id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "Exams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `group` TEXT NOT NULL, `type` TEXT NOT NULL, `description` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Timetable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `number` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `subjectOld` TEXT NOT NULL, `group` TEXT NOT NULL, `room` TEXT NOT NULL, `roomOld` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacherOld` TEXT NOT NULL, `info` TEXT NOT NULL, `student_plan` INTEGER NOT NULL, `changes` INTEGER NOT NULL, `canceled` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subjectOld", + "columnName": "subjectOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "group", + "columnName": "group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "room", + "columnName": "room", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roomOld", + "columnName": "roomOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherOld", + "columnName": "teacherOld", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isStudentPlan", + "columnName": "student_plan", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "changes", + "columnName": "changes", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canceled", + "columnName": "canceled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Attendance", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `time_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `excused` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `excusable` INTEGER NOT NULL, `excuse_status` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeId", + "columnName": "time_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excused", + "columnName": "excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excusable", + "columnName": "excusable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "excuseStatus", + "columnName": "excuse_status", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AttendanceSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `subject_id` INTEGER NOT NULL, `month` INTEGER NOT NULL, `presence` INTEGER NOT NULL, `absence` INTEGER NOT NULL, `absence_excused` INTEGER NOT NULL, `absence_for_school_reasons` INTEGER NOT NULL, `lateness` INTEGER NOT NULL, `lateness_excused` INTEGER NOT NULL, `exemption` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subject_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "month", + "columnName": "month", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "presence", + "columnName": "presence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceExcused", + "columnName": "absence_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "absenceForSchoolReasons", + "columnName": "absence_for_school_reasons", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lateness", + "columnName": "lateness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latenessExcused", + "columnName": "lateness_excused", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exemption", + "columnName": "exemption", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Grades", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `entry` TEXT NOT NULL, `value` REAL NOT NULL, `modifier` REAL NOT NULL, `comment` TEXT NOT NULL, `color` TEXT NOT NULL, `grade_symbol` TEXT NOT NULL, `description` TEXT NOT NULL, `weight` TEXT NOT NULL, `weightValue` REAL NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entry", + "columnName": "entry", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "modifier", + "columnName": "modifier", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gradeSymbol", + "columnName": "grade_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "weightValue", + "columnName": "weightValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesSummary", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `position` INTEGER NOT NULL, `subject` TEXT NOT NULL, `predicted_grade` TEXT NOT NULL, `final_grade` TEXT NOT NULL, `proposed_points` TEXT NOT NULL, `final_points` TEXT NOT NULL, `points_sum` TEXT NOT NULL, `average` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_predicted_grade_notified` INTEGER NOT NULL, `is_final_grade_notified` INTEGER NOT NULL, `predicted_grade_last_change` INTEGER NOT NULL, `final_grade_last_change` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "predictedGrade", + "columnName": "predicted_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalGrade", + "columnName": "final_grade", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "proposedPoints", + "columnName": "proposed_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "finalPoints", + "columnName": "final_points", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pointsSum", + "columnName": "points_sum", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "average", + "columnName": "average", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPredictedGradeNotified", + "columnName": "is_predicted_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isFinalGradeNotified", + "columnName": "is_final_grade_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "predictedGradeLastChange", + "columnName": "predicted_grade_last_change", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "finalGradeLastChange", + "columnName": "final_grade_last_change", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradePartialStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `class_average` TEXT NOT NULL, `student_average` TEXT NOT NULL, `class_amounts` TEXT NOT NULL, `student_amounts` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAverage", + "columnName": "class_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAverage", + "columnName": "student_average", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "classAmounts", + "columnName": "class_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentAmounts", + "columnName": "student_amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesPointsStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `others` REAL NOT NULL, `student` REAL NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "others", + "columnName": "others", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "student", + "columnName": "student", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradeSemesterStatistics", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `amounts` TEXT NOT NULL, `student_grade` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "amounts", + "columnName": "amounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentGrade", + "columnName": "student_grade", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `message_global_key` TEXT NOT NULL, `mailbox_key` TEXT NOT NULL, `message_id` INTEGER NOT NULL, `correspondents` TEXT NOT NULL, `subject` TEXT NOT NULL, `date` INTEGER NOT NULL, `folder_id` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `read_by` INTEGER, `unread_by` INTEGER, `has_attachments` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL, `content` TEXT NOT NULL, `sender` TEXT, `recipients` TEXT)", + "fields": [ + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageGlobalKey", + "columnName": "message_global_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mailboxKey", + "columnName": "mailbox_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "message_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "correspondents", + "columnName": "correspondents", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folderId", + "columnName": "folder_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readBy", + "columnName": "read_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unreadBy", + "columnName": "unread_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasAttachments", + "columnName": "has_attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sender", + "columnName": "sender", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recipients", + "columnName": "recipients", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageAttachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`message_global_key` TEXT NOT NULL, `url` TEXT NOT NULL, `filename` TEXT NOT NULL, PRIMARY KEY(`message_global_key`, `url`, `filename`))", + "fields": [ + { + "fieldPath": "messageGlobalKey", + "columnName": "message_global_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "message_global_key", + "url", + "filename" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `category` TEXT NOT NULL, `category_type` INTEGER NOT NULL, `is_points_show` INTEGER NOT NULL, `points` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_read` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryType", + "columnName": "category_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPointsShow", + "columnName": "is_points_show", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "points", + "columnName": "points", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isRead", + "columnName": "is_read", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Homework", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `entry_date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `attachments` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_done` INTEGER NOT NULL, `is_notified` INTEGER NOT NULL, `is_added_by_user` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "semesterId", + "columnName": "semester_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entryDate", + "columnName": "entry_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "is_done", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Subjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `real_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "realId", + "columnName": "real_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "LuckyNumbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `lucky_number` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "luckyNumber", + "columnName": "lucky_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CompletedLesson", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `number` INTEGER NOT NULL, `subject` TEXT NOT NULL, `topic` TEXT NOT NULL, `teacher` TEXT NOT NULL, `teacher_symbol` TEXT NOT NULL, `substitution` TEXT NOT NULL, `absence` TEXT NOT NULL, `resources` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "topic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacher", + "columnName": "teacher", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "teacherSymbol", + "columnName": "teacher_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "substitution", + "columnName": "substitution", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "absence", + "columnName": "absence", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "resources", + "columnName": "resources", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Mailboxes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`globalKey` TEXT NOT NULL, `email` TEXT NOT NULL, `symbol` TEXT NOT NULL, `schoolId` TEXT NOT NULL, `fullName` TEXT NOT NULL, `userName` TEXT NOT NULL, `studentName` TEXT NOT NULL, `schoolNameShort` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`globalKey`))", + "fields": [ + { + "fieldPath": "globalKey", + "columnName": "globalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolId", + "columnName": "schoolId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentName", + "columnName": "studentName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolNameShort", + "columnName": "schoolNameShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "globalKey" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Recipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`mailboxGlobalKey` TEXT NOT NULL, `studentMailboxGlobalKey` TEXT NOT NULL, `fullName` TEXT NOT NULL, `userName` TEXT NOT NULL, `schoolShortName` TEXT NOT NULL, `type` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "mailboxGlobalKey", + "columnName": "mailboxGlobalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentMailboxGlobalKey", + "columnName": "studentMailboxGlobalKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userName", + "columnName": "userName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "schoolShortName", + "columnName": "schoolShortName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MobileDevices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_login_id` INTEGER NOT NULL, `device_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Teachers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `short_name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "School", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `contact` TEXT NOT NULL, `headmaster` TEXT NOT NULL, `pedagogue` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "classId", + "columnName": "class_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contact", + "columnName": "contact", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "headmaster", + "columnName": "headmaster", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pedagogue", + "columnName": "pedagogue", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Conferences", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `subject` TEXT NOT NULL, `agenda` TEXT NOT NULL, `present_on_conference` TEXT NOT NULL, `conference_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "agenda", + "columnName": "agenda", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "presentOnConference", + "columnName": "present_on_conference", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "conferenceId", + "columnName": "conference_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableAdditional", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `repeat_id` BLOB DEFAULT NULL, `is_added_by_user` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatId", + "columnName": "repeat_id", + "affinity": "BLOB", + "notNull": false, + "defaultValue": "NULL" + }, + { + "fieldPath": "isAddedByUser", + "columnName": "is_added_by_user", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "StudentInfo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `full_name` TEXT NOT NULL, `first_name` TEXT NOT NULL, `second_name` TEXT NOT NULL, `surname` TEXT NOT NULL, `birth_date` INTEGER NOT NULL, `birth_place` TEXT NOT NULL, `gender` TEXT NOT NULL, `has_polish_citizenship` INTEGER NOT NULL, `family_name` TEXT NOT NULL, `parents_names` TEXT NOT NULL, `address` TEXT NOT NULL, `registered_address` TEXT NOT NULL, `correspondence_address` TEXT NOT NULL, `phone_number` TEXT NOT NULL, `cell_phone_number` TEXT NOT NULL, `email` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `first_guardian_full_name` TEXT, `first_guardian_kinship` TEXT, `first_guardian_address` TEXT, `first_guardian_phones` TEXT, `first_guardian_email` TEXT, `second_guardian_full_name` TEXT, `second_guardian_kinship` TEXT, `second_guardian_address` TEXT, `second_guardian_phones` TEXT, `second_guardian_email` TEXT)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "full_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondName", + "columnName": "second_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "surname", + "columnName": "surname", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "birthDate", + "columnName": "birth_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "birthPlace", + "columnName": "birth_place", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gender", + "columnName": "gender", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPolishCitizenship", + "columnName": "has_polish_citizenship", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "familyName", + "columnName": "family_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentsNames", + "columnName": "parents_names", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "registeredAddress", + "columnName": "registered_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "correspondenceAddress", + "columnName": "correspondence_address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cellPhoneNumber", + "columnName": "cell_phone_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstGuardian.fullName", + "columnName": "first_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.kinship", + "columnName": "first_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.address", + "columnName": "first_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.phones", + "columnName": "first_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstGuardian.email", + "columnName": "first_guardian_email", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.fullName", + "columnName": "second_guardian_full_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.kinship", + "columnName": "second_guardian_kinship", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.address", + "columnName": "second_guardian_address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.phones", + "columnName": "second_guardian_phones", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "secondGuardian.email", + "columnName": "second_guardian_email", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimetableHeaders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "diaryId", + "columnName": "diary_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SchoolAnnouncements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_login_id` INTEGER NOT NULL, `date` INTEGER NOT NULL, `subject` TEXT NOT NULL, `content` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "userLoginId", + "columnName": "user_login_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`student_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `type` TEXT NOT NULL, `destination` TEXT NOT NULL DEFAULT '{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}', `date` INTEGER NOT NULL, `data` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "studentId", + "columnName": "student_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "destination", + "columnName": "destination", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'{\"type\":\"io.github.wulkanowy.ui.modules.Destination.Dashboard\"}'" + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AdminMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `version_name` INTEGER, `version_max` INTEGER, `target_register_host` TEXT, `target_flavor` TEXT, `destination_url` TEXT, `priority` TEXT NOT NULL, `types` TEXT NOT NULL DEFAULT '[]', `is_ok_visible` INTEGER NOT NULL DEFAULT 0, `is_x_visible` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "versionMin", + "columnName": "version_name", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMax", + "columnName": "version_max", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "targetRegisterHost", + "columnName": "target_register_host", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "targetFlavor", + "columnName": "target_flavor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "destinationUrl", + "columnName": "destination_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "types", + "columnName": "types", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'[]'" + }, + { + "fieldPath": "isOkVisible", + "columnName": "is_ok_visible", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isXVisible", + "columnName": "is_x_visible", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "GradesDescriptive", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`semester_id` INTEGER NOT NULL, `student_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `description` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT 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": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isNotified", + "columnName": "is_notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "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, '3bd95e40b587e8131a2a2c23aee538c1')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/wulkanowy/data/DataModule.kt b/app/src/main/java/io/github/wulkanowy/data/DataModule.kt index 950e817b..7c9cf9a3 100644 --- a/app/src/main/java/io/github/wulkanowy/data/DataModule.kt +++ b/app/src/main/java/io/github/wulkanowy/data/DataModule.kt @@ -253,4 +253,8 @@ internal class DataModule { @Singleton @Provides fun provideAdminMessageDao(database: AppDatabase) = database.adminMessagesDao + + @Singleton + @Provides + fun provideGradeDescriptiveDao(database: AppDatabase) = database.gradeDescriptiveDao } 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 a2f230f4..8e5841fe 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 @@ -14,6 +14,7 @@ import io.github.wulkanowy.data.db.dao.CompletedLessonsDao import io.github.wulkanowy.data.db.dao.ConferenceDao import io.github.wulkanowy.data.db.dao.ExamDao import io.github.wulkanowy.data.db.dao.GradeDao +import io.github.wulkanowy.data.db.dao.GradeDescriptiveDao import io.github.wulkanowy.data.db.dao.GradePartialStatisticsDao import io.github.wulkanowy.data.db.dao.GradePointsStatisticsDao import io.github.wulkanowy.data.db.dao.GradeSemesterStatisticsDao @@ -44,6 +45,7 @@ import io.github.wulkanowy.data.db.entities.CompletedLesson import io.github.wulkanowy.data.db.entities.Conference import io.github.wulkanowy.data.db.entities.Exam import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.data.db.entities.GradeDescriptive import io.github.wulkanowy.data.db.entities.GradePartialStatistics import io.github.wulkanowy.data.db.entities.GradePointsStatistics import io.github.wulkanowy.data.db.entities.GradeSemesterStatistics @@ -154,7 +156,8 @@ import javax.inject.Singleton TimetableHeader::class, SchoolAnnouncement::class, Notification::class, - AdminMessage::class + AdminMessage::class, + GradeDescriptive::class, ], autoMigrations = [ AutoMigration(from = 44, to = 45), @@ -165,6 +168,7 @@ import javax.inject.Singleton AutoMigration(from = 55, to = 56), AutoMigration(from = 56, to = 57, spec = Migration57::class), AutoMigration(from = 57, to = 58, spec = Migration58::class), + AutoMigration(from = 58, to = 59), ], version = AppDatabase.VERSION_SCHEMA, exportSchema = true @@ -173,7 +177,7 @@ import javax.inject.Singleton abstract class AppDatabase : RoomDatabase() { companion object { - const val VERSION_SCHEMA = 58 + const val VERSION_SCHEMA = 59 fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( Migration2(), @@ -298,4 +302,6 @@ abstract class AppDatabase : RoomDatabase() { abstract val notificationDao: NotificationDao abstract val adminMessagesDao: AdminMessageDao + + abstract val gradeDescriptiveDao: GradeDescriptiveDao } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/GradeDescriptiveDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/GradeDescriptiveDao.kt new file mode 100644 index 00000000..6282c080 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/GradeDescriptiveDao.kt @@ -0,0 +1,15 @@ +package io.github.wulkanowy.data.db.dao + +import androidx.room.Dao +import androidx.room.Query +import io.github.wulkanowy.data.db.entities.GradeDescriptive +import kotlinx.coroutines.flow.Flow +import javax.inject.Singleton + +@Singleton +@Dao +interface GradeDescriptiveDao : BaseDao { + + @Query("SELECT * FROM GradesDescriptive WHERE semester_id = :semesterId AND student_id = :studentId") + fun loadAll(semesterId: Int, studentId: Int): Flow> +} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeDescriptive.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeDescriptive.kt new file mode 100644 index 00000000..9aec9599 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/GradeDescriptive.kt @@ -0,0 +1,27 @@ +package io.github.wulkanowy.data.db.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import java.io.Serializable + +@Entity(tableName = "GradesDescriptive") +data class GradeDescriptive( + + @ColumnInfo(name = "semester_id") + val semesterId: Int, + + @ColumnInfo(name = "student_id") + val studentId: Int, + + val subject: String, + + val description: String, +) : Serializable { + + @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/mappers/GradeMapper.kt b/app/src/main/java/io/github/wulkanowy/data/mappers/GradeMapper.kt index 178de682..66e92217 100644 --- a/app/src/main/java/io/github/wulkanowy/data/mappers/GradeMapper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/mappers/GradeMapper.kt @@ -1,10 +1,12 @@ package io.github.wulkanowy.data.mappers import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.data.db.entities.GradeDescriptive import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Semester -import io.github.wulkanowy.sdk.pojo.GradeSummary as SdkGradeSummary import io.github.wulkanowy.sdk.pojo.Grade as SdkGrade +import io.github.wulkanowy.sdk.pojo.GradeDescriptive as SdkGradeDescriptive +import io.github.wulkanowy.sdk.pojo.GradeSummary as SdkGradeSummary fun List.mapToEntities(semester: Semester) = map { Grade( @@ -40,3 +42,15 @@ fun List.mapToEntities(semester: Semester) = map { average = it.average ) } + +@JvmName("mapGradeDescriptiveToEntities") +fun List.mapToEntities(semester: Semester) = map { + GradeDescriptive( + semesterId = semester.semesterId, + studentId = semester.studentId, + subject = it.subject, + description = it.description + ) +} + + diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt index b2bb7b90..1e2ea935 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt @@ -1,15 +1,22 @@ package io.github.wulkanowy.data.repositories import io.github.wulkanowy.data.db.dao.GradeDao +import io.github.wulkanowy.data.db.dao.GradeDescriptiveDao import io.github.wulkanowy.data.db.dao.GradeSummaryDao import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.data.db.entities.GradeDescriptive import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.utils.* +import io.github.wulkanowy.utils.AutoRefreshHelper +import io.github.wulkanowy.utils.getRefreshKey +import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.switchSemester +import io.github.wulkanowy.utils.toLocalDate +import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map @@ -22,14 +29,13 @@ import javax.inject.Singleton class GradeRepository @Inject constructor( private val gradeDb: GradeDao, private val gradeSummaryDb: GradeSummaryDao, + private val gradeDescriptiveDb: GradeDescriptiveDao, private val sdk: Sdk, private val refreshHelper: AutoRefreshHelper, ) { private val saveFetchResultMutex = Mutex() - private val cacheKey = "grade" - fun getGrades( student: Student, semester: Semester, @@ -41,30 +47,52 @@ class GradeRepository @Inject constructor( //When details is empty and summary is not, app will not use summary cache - edge case it.first.isEmpty() }, - shouldFetch = { (details, summaries) -> - val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester)) - details.isEmpty() || summaries.isEmpty() || forceRefresh || isExpired + shouldFetch = { (details, summaries, descriptive) -> + val isExpired = + refreshHelper.shouldBeRefreshed(getRefreshKey(GRADE_CACHE_KEY, semester)) + details.isEmpty() || (summaries.isEmpty() && descriptive.isEmpty()) || forceRefresh || isExpired }, query = { val detailsFlow = gradeDb.loadAll(semester.semesterId, semester.studentId) val summaryFlow = gradeSummaryDb.loadAll(semester.semesterId, semester.studentId) - detailsFlow.combine(summaryFlow) { details, summaries -> details to summaries } + val descriptiveFlow = + gradeDescriptiveDb.loadAll(semester.semesterId, semester.studentId) + + combine(detailsFlow, summaryFlow, descriptiveFlow) { details, summaries, descriptive -> + Triple(details, summaries, descriptive) + } }, fetch = { - val (details, summary) = sdk.init(student) + val (details, summary, descriptive) = sdk.init(student) .switchSemester(semester) .getGrades(semester.semesterId) - details.mapToEntities(semester) to summary.mapToEntities(semester) + Triple( + details.mapToEntities(semester), + summary.mapToEntities(semester), + descriptive.mapToEntities(semester) + ) }, - saveFetchResult = { (oldDetails, oldSummary), (newDetails, newSummary) -> + saveFetchResult = { (oldDetails, oldSummary, oldDescriptive), (newDetails, newSummary, newDescriptive) -> refreshGradeDetails(student, oldDetails, newDetails, notify) refreshGradeSummaries(oldSummary, newSummary, notify) + refreshGradeDescriptions(oldDescriptive, newDescriptive, notify) - refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester)) + refreshHelper.updateLastRefreshTimestamp(getRefreshKey(GRADE_CACHE_KEY, semester)) } ) + private suspend fun refreshGradeDescriptions( + old: List, + new: List, + notify: Boolean + ) { + gradeDescriptiveDb.deleteAll(old uniqueSubtract new) + gradeDescriptiveDb.insertAll((new uniqueSubtract old).onEach { + if (notify) it.isNotified = false + }) + } + private suspend fun refreshGradeDetails( student: Student, oldGrades: List, @@ -132,6 +160,10 @@ class GradeRepository @Inject constructor( return gradeSummaryDb.loadAll(semester.semesterId, semester.studentId) } + fun getGradesDescriptiveFromDatabase(semester: Semester): Flow> { + return gradeDescriptiveDb.loadAll(semester.semesterId, semester.studentId) + } + suspend fun updateGrade(grade: Grade) { return gradeDb.updateAll(listOf(grade)) } @@ -143,4 +175,13 @@ class GradeRepository @Inject constructor( suspend fun updateGradesSummary(gradesSummary: List) { return gradeSummaryDb.updateAll(gradesSummary) } + + suspend fun updateGradesDescriptive(gradesDescriptive: List) { + return gradeDescriptiveDb.updateAll(gradesDescriptive) + } + + private companion object { + + private const val GRADE_CACHE_KEY = "grade" + } } 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 9b49ed17..7f90bbdd 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 @@ -4,12 +4,12 @@ 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.GradeDescriptive import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.pojos.GroupNotificationData import io.github.wulkanowy.data.pojos.NotificationData import io.github.wulkanowy.ui.modules.Destination -import io.github.wulkanowy.ui.modules.splash.SplashActivity import io.github.wulkanowy.utils.getPlural import javax.inject.Inject @@ -88,4 +88,28 @@ class NewGradeNotification @Inject constructor( appNotificationManager.sendMultipleNotifications(groupNotificationData, student) } + + suspend fun notifyDescriptive(items: List, student: Student) { + val notificationDataList = items.map { + NotificationData( + title = context.getPlural(R.plurals.grade_new_items_descriptive, 1), + content = "${it.subject}: ${it.description}", + destination = Destination.Grade, + ) + } + + val groupNotificationData = GroupNotificationData( + notificationDataList = notificationDataList, + title = context.getPlural(R.plurals.grade_new_items_descriptive, items.size), + content = context.getPlural( + R.plurals.grade_notify_new_items_descriptive, + items.size, + items.size + ), + destination = Destination.Grade, + type = NotificationType.NEW_GRADE_DESCRIPTIVE + ) + + 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 023ae2e4..4e7f2735 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 @@ -37,6 +37,10 @@ enum class NotificationType( channel = NewGradesChannel.CHANNEL_ID, icon = R.drawable.ic_stat_grade, ), + NEW_GRADE_DESCRIPTIVE( + channel = NewGradesChannel.CHANNEL_ID, + icon = R.drawable.ic_stat_grade, + ), NEW_HOMEWORK( channel = NewHomeworkChannel.CHANNEL_ID, icon = R.drawable.ic_more_homework, diff --git a/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeWork.kt b/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeWork.kt index ba21b860..b62ad94b 100644 --- a/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeWork.kt +++ b/app/src/main/java/io/github/wulkanowy/services/sync/works/GradeWork.kt @@ -45,5 +45,15 @@ class GradeWork @Inject constructor( grade.isFinalGradeNotified = true }) } + + gradeRepository.getGradesDescriptiveFromDatabase(semester).first() + .filter { !it.isNotified } + .let { + if (it.isNotEmpty()) newGradeNotification.notifyDescriptive(it, student) + + gradeRepository.updateGradesDescriptive(it.onEach { grade -> + grade.isNotified = true + }) + } } } 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 d0dfcd69..cdd186b9 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 @@ -18,6 +18,7 @@ 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.debugGradeDescriptiveItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugGradeDetailsItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugGradeSummaryItems import io.github.wulkanowy.ui.modules.debug.notification.mock.debugHomeworkItems @@ -55,6 +56,14 @@ class NotificationDebugPresenter @Inject constructor( NotificationDebugItem(R.string.grade_summary_final_grade) { n -> withStudent { newGradeNotification.notifyFinal(debugGradeSummaryItems.take(n), it) } }, + NotificationDebugItem(R.string.grade_summary_descriptive) { n -> + withStudent { + newGradeNotification.notifyDescriptive( + debugGradeDescriptiveItems.take(n), + it + ) + } + }, NotificationDebugItem(R.string.homework_title) { n -> withStudent { newHomeworkNotification.notify(debugHomeworkItems.take(n), it) } }, diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/gradeDescriptive.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/gradeDescriptive.kt new file mode 100644 index 00000000..d5a0c089 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/debug/notification/mock/gradeDescriptive.kt @@ -0,0 +1,48 @@ +package io.github.wulkanowy.ui.modules.debug.notification.mock + +import io.github.wulkanowy.data.db.entities.GradeDescriptive + +val debugGradeDescriptiveItems = listOf( + generateGradeDescriptive( + "Matematyka", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + ), + generateGradeDescriptive("Fizyka", "Lorem ipsum dolor sit amet, consectetur adipiscing elit."), + generateGradeDescriptive( + "Geografia", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + ), + generateGradeDescriptive( + "Sieci komputerowe", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + ), + generateGradeDescriptive( + "Systemy operacyjne", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + ), + generateGradeDescriptive( + "Język polski", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + ), + generateGradeDescriptive( + "Język angielski", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + ), + generateGradeDescriptive("Religia", "Lorem ipsum dolor sit amet, consectetur adipiscing elit."), + generateGradeDescriptive( + "Język niemiecki", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + ), + generateGradeDescriptive( + "Wychowanie fizyczne", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + ), +) + +private fun generateGradeDescriptive(subject: String, description: String) = + GradeDescriptive( + semesterId = 0, + studentId = 0, + subject = subject, + description = description + ) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt index ec4bd8e8..e8a5fa25 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProvider.kt @@ -1,15 +1,23 @@ package io.github.wulkanowy.ui.modules.grade -import io.github.wulkanowy.data.* +import io.github.wulkanowy.data.Resource +import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.data.db.entities.GradeDescriptive import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.errorOrNull +import io.github.wulkanowy.data.flatResourceFlow +import io.github.wulkanowy.data.mapData +import io.github.wulkanowy.data.mapResourceData import io.github.wulkanowy.data.repositories.GradeRepository import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.sdk.Sdk -import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.* +import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.ALL_YEAR +import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.BOTH_SEMESTERS +import io.github.wulkanowy.ui.modules.grade.GradeAverageMode.ONE_SEMESTER import io.github.wulkanowy.utils.calcAverage import io.github.wulkanowy.utils.changeModifier import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -62,6 +70,7 @@ class GradeAverageProvider @Inject constructor( forceRefresh = forceRefresh, params = params, ) + BOTH_SEMESTERS -> calculateCombinedAverage( student = student, semesters = semesters, @@ -69,6 +78,7 @@ class GradeAverageProvider @Inject constructor( forceRefresh = forceRefresh, config = params, ) + ALL_YEAR -> calculateCombinedAverage( student = student, semesters = semesters, @@ -189,36 +199,73 @@ class GradeAverageProvider @Inject constructor( ): Flow>> { return gradeRepository.getGrades(student, semester, forceRefresh = forceRefresh) .mapResourceData { res -> - val (details, summaries) = res + val (details, summaries, descriptives) = res val isAnyAverage = summaries.any { it.average != .0 } val allGrades = details.groupBy { it.subject } + val descriptiveGradesBySubject = descriptives.associateBy { it.subject } - val items = summaries.emulateEmptySummaries( - student = student, - semester = semester, - grades = allGrades.toList(), - calcAverage = isAnyAverage, - params = params, - ).map { summary -> - val grades = allGrades[summary.subject].orEmpty() - GradeSubject( - subject = summary.subject, - average = if (!isAnyAverage || params.forceAverageCalc) { - grades.updateModifiers(student, params) - .calcAverage(params.isOptionalArithmeticAverage) - } else summary.average, - points = summary.pointsSum, - summary = summary, - grades = grades, - isVulcanAverage = isAnyAverage + val items = summaries + .createEmptySummariesByGradesIfNeeded( + student = student, + semester = semester, + grades = allGrades.toList(), + calcAverage = isAnyAverage, + params = params, ) - } + .createEmptySummariesByDescriptiveGradesIfNeeded( + student = student, + semester = semester, + descriptives = descriptives, + ) + .map { summary -> + val grades = allGrades[summary.subject].orEmpty() + val descriptiveGrade = descriptiveGradesBySubject[summary.subject] + + GradeSubject( + subject = summary.subject, + average = if (!isAnyAverage || params.forceAverageCalc) { + grades.updateModifiers(student, params) + .calcAverage(params.isOptionalArithmeticAverage) + } else summary.average, + points = summary.pointsSum, + summary = summary, + grades = grades, + descriptive = descriptiveGrade, + isVulcanAverage = isAnyAverage + ) + } items } } - private fun List.emulateEmptySummaries( + private fun List.createEmptySummariesByDescriptiveGradesIfNeeded( + student: Student, + semester: Semester, + descriptives: List + ): List { + val summarySubjects = this.map { it.subject } + val gradeSummaryToAdd = descriptives.mapNotNull { gradeDescriptive -> + if (gradeDescriptive.subject in summarySubjects) return@mapNotNull null + + GradeSummary( + studentId = student.studentId, + semesterId = semester.semesterId, + position = 0, + subject = gradeDescriptive.subject, + predictedGrade = "", + finalGrade = "", + proposedPoints = "", + finalPoints = "", + pointsSum = "", + average = .0 + ) + } + + return this + gradeSummaryToAdd + } + + private fun List.createEmptySummariesByGradesIfNeeded( student: Student, semester: Semester, grades: List>>, diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeSubject.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeSubject.kt index 57be55ee..a465551f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeSubject.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeSubject.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.ui.modules.grade import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.data.db.entities.GradeDescriptive import io.github.wulkanowy.data.db.entities.GradeSummary data class GradeSubject( @@ -8,6 +9,7 @@ data class GradeSubject( val average: Double, val points: String, val summary: GradeSummary, + val descriptive: GradeDescriptive?, val grades: List, val isVulcanAverage: Boolean ) 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 4261c507..d9621f51 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 @@ -1,13 +1,22 @@ package io.github.wulkanowy.ui.modules.grade.details -import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.enums.GradeExpandMode -import io.github.wulkanowy.data.enums.GradeSortingMode.* +import io.github.wulkanowy.data.enums.GradeSortingMode.ALPHABETIC +import io.github.wulkanowy.data.enums.GradeSortingMode.AVERAGE +import io.github.wulkanowy.data.enums.GradeSortingMode.DATE +import io.github.wulkanowy.data.flatResourceFlow +import io.github.wulkanowy.data.logResourceStatus +import io.github.wulkanowy.data.onResourceData +import io.github.wulkanowy.data.onResourceError +import io.github.wulkanowy.data.onResourceIntermediate +import io.github.wulkanowy.data.onResourceNotLoading +import io.github.wulkanowy.data.onResourceSuccess import io.github.wulkanowy.data.repositories.GradeRepository import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider @@ -207,20 +216,20 @@ class GradeDetailsPresenter @Inject constructor( AVERAGE -> gradeSubjects.sortedByDescending { it.average } } } - .map { (subject, average, points, _, grades) -> - val subItems = grades + .map { gradeSubject -> + val subItems = gradeSubject.grades .sortedByDescending { it.date } .map { GradeDetailsItem(it, ViewType.ITEM) } val gradeDetailsItems = listOf( GradeDetailsItem( GradeDetailsHeader( - subject = subject, - average = average, - pointsSum = points, + subject = gradeSubject.subject, + average = gradeSubject.average, + pointsSum = gradeSubject.points, grades = subItems ).apply { - newGrades = grades.filter { grade -> !grade.isRead }.size + newGrades = gradeSubject.grades.filter { grade -> !grade.isRead }.size }, ViewType.HEADER ) ) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt index 8dcade56..95cf97be 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryAdapter.kt @@ -2,16 +2,16 @@ package io.github.wulkanowy.ui.modules.grade.summary import android.annotation.SuppressLint import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import io.github.wulkanowy.R -import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.databinding.ItemGradeSummaryBinding import io.github.wulkanowy.databinding.ScrollableHeaderGradeSummaryBinding import io.github.wulkanowy.sdk.scrapper.grades.isGradeValid import io.github.wulkanowy.utils.calcFinalAverage +import io.github.wulkanowy.utils.ifNullOrBlank import java.util.Locale import javax.inject.Inject @@ -24,7 +24,7 @@ class GradeSummaryAdapter @Inject constructor( ITEM(2) } - var items = emptyList() + var items = emptyList() var onCalculatedHelpClickListener: () -> Unit = {} @@ -44,9 +44,11 @@ class GradeSummaryAdapter @Inject constructor( ViewType.HEADER.id -> HeaderViewHolder( ScrollableHeaderGradeSummaryBinding.inflate(inflater, parent, false) ) + ViewType.ITEM.id -> ItemViewHolder( ItemGradeSummaryBinding.inflate(inflater, parent, false) ) + else -> throw IllegalStateException() } } @@ -60,19 +62,23 @@ class GradeSummaryAdapter @Inject constructor( private fun bindHeaderViewHolder(binding: ScrollableHeaderGradeSummaryBinding) { if (items.isEmpty()) return + val gradeSummaries = items + .filter { it.gradeDescriptive == null } + .map { it.gradeSummary } val context = binding.root.context - val finalItemsCount = items.count { isGradeValid(it.finalGrade) } - val calculatedItemsCount = items.count { value -> value.average != 0.0 } - val allItemsCount = items.count { !it.subject.equals("zachowanie", true) } - val finalAverage = items.calcFinalAverage( + val finalItemsCount = gradeSummaries.count { isGradeValid(it.finalGrade) } + val calculatedItemsCount = gradeSummaries.count { value -> value.average != 0.0 } + val allItemsCount = gradeSummaries.count { !it.subject.equals("zachowanie", true) } + val finalAverage = gradeSummaries.calcFinalAverage( preferencesRepository.gradePlusModifier, preferencesRepository.gradeMinusModifier ) - val calculatedAverage = items.filter { value -> value.average != 0.0 } + val calculatedAverage = gradeSummaries.filter { value -> value.average != 0.0 } .map { values -> values.average } .reversed() // fix average precision .average() + .let { if (it.isNaN()) 0.0 else it } with(binding) { gradeSummaryScrollableHeaderFinal.text = formatAverage(finalAverage) @@ -95,16 +101,28 @@ class GradeSummaryAdapter @Inject constructor( } @SuppressLint("SetTextI18n") - private fun bindItemViewHolder(binding: ItemGradeSummaryBinding, item: GradeSummary) { - with(binding) { - gradeSummaryItemTitle.text = item.subject - gradeSummaryItemPoints.text = item.pointsSum - gradeSummaryItemAverage.text = formatAverage(item.average, "") - gradeSummaryItemPredicted.text = "${item.predictedGrade} ${item.proposedPoints}".trim() - gradeSummaryItemFinal.text = "${item.finalGrade} ${item.finalPoints}".trim() + private fun bindItemViewHolder(binding: ItemGradeSummaryBinding, item: GradeSummaryItem) { + val (gradeSummary, gradeDescriptive) = item - gradeSummaryItemPointsContainer.visibility = - if (item.pointsSum.isBlank()) View.GONE else View.VISIBLE + with(binding) { + gradeSummaryItemTitle.text = gradeSummary.subject + gradeSummaryItemPoints.text = gradeSummary.pointsSum + gradeSummaryItemAverage.text = formatAverage(gradeSummary.average, "") + gradeSummaryItemPredicted.text = + "${gradeSummary.predictedGrade} ${gradeSummary.proposedPoints}".trim() + gradeSummaryItemFinal.text = + "${gradeSummary.finalGrade} ${gradeSummary.finalPoints}".trim() + gradeSummaryItemDescriptive.text = gradeDescriptive?.description.ifNullOrBlank { + root.context.getString(R.string.all_no_data) + } + + gradeSummaryItemFinalDivider.isVisible = gradeDescriptive == null + gradeSummaryItemPredictedDivider.isVisible = gradeDescriptive == null + gradeSummaryItemPointsDivider.isVisible = gradeDescriptive == null + gradeSummaryItemPredictedContainer.isVisible = gradeDescriptive == null + gradeSummaryItemFinalContainer.isVisible = gradeDescriptive == null + gradeSummaryItemDescriptiveContainer.isVisible = gradeDescriptive != null + gradeSummaryItemPointsContainer.isVisible = gradeSummary.pointsSum.isNotBlank() } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt index abd0b13c..35b2edd5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryFragment.kt @@ -5,12 +5,10 @@ import android.view.View import android.view.View.GONE import android.view.View.INVISIBLE import android.view.View.VISIBLE -import androidx.appcompat.app.AlertDialog import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R -import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.databinding.FragmentGradeSummaryBinding import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment @@ -72,7 +70,7 @@ class GradeSummaryFragment : } } - override fun updateData(data: List) { + override fun updateData(data: List) { with(gradeSummaryAdapter) { items = data notifyDataSetChanged() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryItem.kt new file mode 100644 index 00000000..cf0f1d92 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryItem.kt @@ -0,0 +1,9 @@ +package io.github.wulkanowy.ui.modules.grade.summary + +import io.github.wulkanowy.data.db.entities.GradeDescriptive +import io.github.wulkanowy.data.db.entities.GradeSummary + +data class GradeSummaryItem( + val gradeSummary: GradeSummary, + val gradeDescriptive: GradeDescriptive? +) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt index 32508ff6..d762df02 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryPresenter.kt @@ -1,9 +1,16 @@ package io.github.wulkanowy.ui.modules.grade.summary -import io.github.wulkanowy.data.* -import io.github.wulkanowy.data.db.entities.GradeSummary -import io.github.wulkanowy.data.enums.GradeSortingMode -import io.github.wulkanowy.data.enums.GradeSortingMode.* +import io.github.wulkanowy.data.enums.GradeSortingMode.ALPHABETIC +import io.github.wulkanowy.data.enums.GradeSortingMode.AVERAGE +import io.github.wulkanowy.data.enums.GradeSortingMode.DATE +import io.github.wulkanowy.data.flatResourceFlow +import io.github.wulkanowy.data.logResourceStatus +import io.github.wulkanowy.data.mapResourceData +import io.github.wulkanowy.data.onResourceData +import io.github.wulkanowy.data.onResourceError +import io.github.wulkanowy.data.onResourceIntermediate +import io.github.wulkanowy.data.onResourceNotLoading +import io.github.wulkanowy.data.onResourceSuccess import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.ui.base.BasePresenter @@ -128,7 +135,7 @@ class GradeSummaryPresenter @Inject constructor( view?.showFinalAverageHelpDialog() } - private fun createGradeSummaryItems(items: List): List { + private fun createGradeSummaryItems(items: List): List { return items .filter { !checkEmpty(it) } .let { gradeSubjects -> @@ -136,21 +143,32 @@ class GradeSummaryPresenter @Inject constructor( DATE -> gradeSubjects.sortedByDescending { gradeDetailsWithAverage -> gradeDetailsWithAverage.grades.maxByOrNull { it.date }?.date } + ALPHABETIC -> gradeSubjects.sortedBy { gradeDetailsWithAverage -> gradeDetailsWithAverage.subject.lowercase() } + AVERAGE -> gradeSubjects.sortedByDescending { it.average } } } - .map { it.summary.copy(average = it.average) } + .map { + val gradeSummary = it.summary.copy(average = it.average) + val descriptive = it.descriptive + GradeSummaryItem( + gradeSummary = gradeSummary, + gradeDescriptive = descriptive, + ) + } + } private fun checkEmpty(gradeSummary: GradeSubject): Boolean { return gradeSummary.run { summary.finalGrade.isBlank() - && summary.predictedGrade.isBlank() - && average == .0 - && points.isBlank() + && summary.predictedGrade.isBlank() + && average == .0 + && points.isBlank() + && descriptive == null } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryView.kt index 156731c3..36bd6142 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/summary/GradeSummaryView.kt @@ -1,6 +1,5 @@ package io.github.wulkanowy.ui.modules.grade.summary -import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.ui.base.BaseView interface GradeSummaryView : BaseView { @@ -13,7 +12,7 @@ interface GradeSummaryView : BaseView { fun initView() - fun updateData(data: List) + fun updateData(data: List) fun resetView() diff --git a/app/src/main/res/layout/item_grade_summary.xml b/app/src/main/res/layout/item_grade_summary.xml index 30aa6e77..2c8c4ea3 100644 --- a/app/src/main/res/layout/item_grade_summary.xml +++ b/app/src/main/res/layout/item_grade_summary.xml @@ -64,10 +64,12 @@ + android:layout_height="1dp" + android:id="@+id/gradeSummaryItemPointsDivider" + android:background="@drawable/ic_all_divider" /> + android:id="@+id/gradeSummaryItemPredictedDivider" + android:layout_height="1dp" + android:background="@drawable/ic_all_divider" /> + android:layout_height="1dp" + android:id="@+id/gradeSummaryItemFinalDivider" + android:background="@drawable/ic_all_divider" /> + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 60d85606..f1fa3ce7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -130,6 +130,7 @@ Total points Final grade Predicted grade + Descriptive grade Calculated average How does Calculated Average work? The Calculated Average is the arithmetic average calculated from the subjects averages. It allows you to know the approximate final average. It is calculated in a way selected by the user in the application settings. It is recommended that you choose the appropriate option. This is because the calculation of school averages differs. Additionally, if your school reports the average of the subjects on the Vulcan page, the application downloads them and does not calculate these averages. This can be changed by forcing the calculation of the average in the application settings.\n\nAverage of grades only from selected semester:\n1. Calculating the weighted average for each subject in a given semester\n2.Adding calculated averages\n3. Calculation of the arithmetic average of the summed averages\n\nAverage of averages from both semesters:\n1.Calculating the weighted average for each subject in semester 1 and 2\n2. Calculating the arithmetic average of the calculated averages for semesters 1 and 2 for each subject.\n3. Adding calculated averages\n4. Calculation of the arithmetic average of the summed averages\n\nAverage of grades from the whole year:\n1. Calculating weighted average over the year for each subject. The final average in the 1st semester is irrelevant.\n2. Adding calculated averages\n3. Calculating the arithmetic average of summed averages @@ -165,6 +166,10 @@ New final grade New final grades + + New descriptive grade + New descriptive grades + You received %1$d grade You received %1$d grades @@ -177,6 +182,10 @@ You received %1$d final grade You received %1$d final grades + + You received %1$d descriptive grade + You received %1$d descriptive grades + diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt index 5a1877cc..515b0d66 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/GradeRepositoryTest.kt @@ -2,6 +2,7 @@ package io.github.wulkanowy.data.repositories import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.dao.GradeDao +import io.github.wulkanowy.data.db.dao.GradeDescriptiveDao import io.github.wulkanowy.data.db.dao.GradeSummaryDao import io.github.wulkanowy.data.errorOrNull import io.github.wulkanowy.data.mappers.mapToEntities @@ -42,6 +43,9 @@ class GradeRepositoryTest { @MockK private lateinit var gradeSummaryDb: GradeSummaryDao + @MockK + private lateinit var gradeDescriptiveDb: GradeDescriptiveDao + @MockK(relaxUnitFun = true) private lateinit var refreshHelper: AutoRefreshHelper @@ -56,7 +60,8 @@ class GradeRepositoryTest { MockKAnnotations.init(this) every { refreshHelper.shouldBeRefreshed(any()) } returns false - gradeRepository = GradeRepository(gradeDb, gradeSummaryDb, sdk, refreshHelper) + gradeRepository = + GradeRepository(gradeDb, gradeSummaryDb, gradeDescriptiveDb, sdk, refreshHelper) coEvery { gradeDb.deleteAll(any()) } just Runs coEvery { gradeDb.insertAll(any()) } returns listOf() @@ -68,6 +73,13 @@ class GradeRepositoryTest { ) coEvery { gradeSummaryDb.deleteAll(any()) } just Runs coEvery { gradeSummaryDb.insertAll(any()) } returns listOf() + + coEvery { gradeDescriptiveDb.loadAll(any(), any()) } returnsMany listOf( + flowOf(listOf()), + ) + + coEvery { gradeDescriptiveDb.deleteAll(any()) } just Runs + coEvery { gradeDescriptiveDb.insertAll(any()) } returns listOf() } @Test diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt index 31ea3322..6a717f6f 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/grade/GradeAverageProviderTest.kt @@ -1,12 +1,16 @@ package io.github.wulkanowy.ui.modules.grade -import io.github.wulkanowy.data.* +import io.github.wulkanowy.data.Resource +import io.github.wulkanowy.data.dataOrNull 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.errorOrNull import io.github.wulkanowy.data.repositories.GradeRepository import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SemesterRepository +import io.github.wulkanowy.data.resourceFlow +import io.github.wulkanowy.data.toFirstResult import io.github.wulkanowy.getSemesterEntity import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.utils.Status @@ -158,7 +162,9 @@ class GradeAverageProviderTest { semesters[2], true ) - } returns resourceFlow { noWeightGrades to noWeightGradesSummary } + } returns resourceFlow { + Triple(noWeightGrades, noWeightGradesSummary, emptyList()) + } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -186,7 +192,9 @@ class GradeAverageProviderTest { semesters[2], true ) - } returns resourceFlow { noWeightGrades to noWeightGradesArithmeticSummary } + } returns resourceFlow { + Triple(noWeightGrades, noWeightGradesArithmeticSummary, emptyList()) + } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -211,8 +219,24 @@ class GradeAverageProviderTest { coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flow { emit(Resource.Loading()) - emit(Resource.Intermediate(secondGradeWithModifier to secondSummariesWithModifier)) - emit(Resource.Success(secondGradeWithModifier to secondSummariesWithModifier)) + emit( + Resource.Intermediate( + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + ) + ) + emit( + Resource.Success( + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + ) + ) } val items = runBlocking { @@ -253,11 +277,27 @@ class GradeAverageProviderTest { coEvery { gradeRepository.getGrades(student, semesters[2], false) } returns flow { emit(Resource.Loading()) delay(1000) - emit(Resource.Success(secondGradeWithModifier to secondSummariesWithModifier)) + emit( + Resource.Success( + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + ) + ) } coEvery { gradeRepository.getGrades(student, semesters[1], false) } returns flow { emit(Resource.Loading()) - emit(Resource.Success(secondGradeWithModifier to secondSummariesWithModifier)) + emit( + Resource.Success( + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + ) + ) } val items = runBlocking { @@ -296,7 +336,13 @@ class GradeAverageProviderTest { semesters[1], false ) - } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } + } returns resourceFlow { + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + } coEvery { gradeRepository.getGrades( student, @@ -304,8 +350,10 @@ class GradeAverageProviderTest { false ) } returns resourceFlow { - listOf(getGrade(semesters[2].semesterId, "Język polski", .0, .0, .0)) to listOf( - getSummary(semesters[2].semesterId, "Język polski", 2.5) + Triple( + listOf(getGrade(semesters[2].semesterId, "Język polski", .0, .0, .0)), + listOf(getSummary(semesters[2].semesterId, "Język polski", 2.5)), + emptyList() ) } @@ -332,7 +380,13 @@ class GradeAverageProviderTest { semesters[1], false ) - } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } + } returns resourceFlow { + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + } coEvery { gradeRepository.getGrades( student, @@ -340,12 +394,14 @@ class GradeAverageProviderTest { false ) } returns resourceFlow { - emptyList() to listOf( - getSummary( - 24, - "Język polski", - .0 - ) + Triple( + emptyList(), listOf( + getSummary( + 24, + "Język polski", + .0 + ) + ), emptyList() ) } @@ -372,14 +428,22 @@ class GradeAverageProviderTest { semesters[1], true ) - } returns resourceFlow { emptyList() to emptyList() } + } returns resourceFlow { + Triple( + emptyList(), + emptyList(), + emptyList() + ) + } coEvery { gradeRepository.getGrades( student, semesters[2], true ) - } returns resourceFlow { emptyList() to emptyList() } + } returns resourceFlow { + Triple(emptyList(), emptyList(), emptyList()) + } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -404,7 +468,13 @@ class GradeAverageProviderTest { semesters[2], true ) - } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } + } returns resourceFlow { + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -438,7 +508,13 @@ class GradeAverageProviderTest { semesters[2], true ) - } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } + } returns resourceFlow { + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -472,7 +548,13 @@ class GradeAverageProviderTest { semesters[2], true ) - } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } + } returns resourceFlow { + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -506,7 +588,13 @@ class GradeAverageProviderTest { semesters[2], true ) - } returns resourceFlow { secondGradeWithModifier to secondSummariesWithModifier } + } returns resourceFlow { + Triple( + secondGradeWithModifier, + secondSummariesWithModifier, + emptyList() + ) + } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -534,7 +622,7 @@ class GradeAverageProviderTest { semesters[2], true ) - } returns resourceFlow { secondGrades to secondSummaries } + } returns resourceFlow { Triple(secondGrades, secondSummaries, emptyList()) } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -564,7 +652,7 @@ class GradeAverageProviderTest { semesters[2], true ) - } returns resourceFlow { secondGrades to secondSummaries } + } returns resourceFlow { Triple(secondGrades, secondSummaries, emptyList()) } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -594,7 +682,7 @@ class GradeAverageProviderTest { semesters[1], true ) - } returns resourceFlow { firstGrades to firstSummaries } + } returns resourceFlow { Triple(firstGrades, firstSummaries, emptyList()) } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -625,8 +713,8 @@ class GradeAverageProviderTest { coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flow { emit(Resource.Loading()) - emit(Resource.Intermediate(firstGrades to firstSummaries)) - emit(Resource.Success(firstGrades to firstSummaries)) + emit(Resource.Intermediate(Triple(firstGrades, firstSummaries, emptyList()))) + emit(Resource.Success(Triple(firstGrades, firstSummaries, emptyList()))) } val items = runBlocking { @@ -675,9 +763,11 @@ class GradeAverageProviderTest { true ) } returns resourceFlow { - firstGrades to listOf( - getSummary(22, "Matematyka", 3.0), - getSummary(22, "Fizyka", 3.5) + Triple( + firstGrades, listOf( + getSummary(22, "Matematyka", 3.0), + getSummary(22, "Fizyka", 3.5) + ), emptyList() ) } coEvery { @@ -687,9 +777,13 @@ class GradeAverageProviderTest { true ) } returns resourceFlow { - secondGrades to listOf( - getSummary(22, "Matematyka", 3.5), - getSummary(22, "Fizyka", 4.0) + Triple( + secondGrades, + listOf( + getSummary(22, "Matematyka", 3.5), + getSummary(22, "Fizyka", 4.0) + ), + emptyList() ) } @@ -723,17 +817,21 @@ class GradeAverageProviderTest { emit(Resource.Loading()) emit( Resource.Intermediate( - firstGrades to listOf( - getSummary(22, "Matematyka", 3.0), - getSummary(22, "Fizyka", 3.5) + Triple( + firstGrades, listOf( + getSummary(22, "Matematyka", 3.0), + getSummary(22, "Fizyka", 3.5) + ), emptyList() ) ) ) emit( Resource.Success( - firstGrades to listOf( - getSummary(22, "Matematyka", 3.0), - getSummary(22, "Fizyka", 3.5) + Triple( + firstGrades, listOf( + getSummary(22, "Matematyka", 3.0), + getSummary(22, "Fizyka", 3.5) + ), emptyList() ) ) ) @@ -742,17 +840,21 @@ class GradeAverageProviderTest { emit(Resource.Loading()) emit( Resource.Intermediate( - secondGrades to listOf( - getSummary(22, "Matematyka", 3.5), - getSummary(22, "Fizyka", 4.0) + Triple( + secondGrades, listOf( + getSummary(22, "Matematyka", 3.5), + getSummary(22, "Fizyka", 4.0) + ), emptyList() ) ) ) emit( Resource.Success( - secondGrades to listOf( - getSummary(22, "Matematyka", 3.5), - getSummary(22, "Fizyka", 4.0) + Triple( + secondGrades, listOf( + getSummary(22, "Matematyka", 3.5), + getSummary(22, "Fizyka", 4.0) + ), emptyList() ) ) ) @@ -803,7 +905,7 @@ class GradeAverageProviderTest { semesters[1], true ) - } returns resourceFlow { firstGrades to firstSummaries } + } returns resourceFlow { Triple(firstGrades, firstSummaries, emptyList()) } coEvery { gradeRepository.getGrades( student, @@ -811,9 +913,11 @@ class GradeAverageProviderTest { true ) } returns resourceFlow { - secondGrades to listOf( - getSummary(22, "Matematyka", 1.1), - getSummary(22, "Fizyka", 7.26) + Triple( + secondGrades, listOf( + getSummary(22, "Matematyka", 1.1), + getSummary(22, "Fizyka", 7.26) + ), emptyList() ) } @@ -850,9 +954,11 @@ class GradeAverageProviderTest { true ) } returns resourceFlow { - firstGrades to listOf( - getSummary(22, "Matematyka", .0), - getSummary(22, "Fizyka", .0) + Triple( + firstGrades, listOf( + getSummary(22, "Matematyka", .0), + getSummary(22, "Fizyka", .0) + ), emptyList() ) } coEvery { @@ -862,9 +968,11 @@ class GradeAverageProviderTest { true ) } returns resourceFlow { - secondGrades to listOf( - getSummary(22, "Matematyka", .0), - getSummary(22, "Fizyka", .0) + Triple( + secondGrades, listOf( + getSummary(22, "Matematyka", .0), + getSummary(22, "Fizyka", .0) + ), emptyList() ) } @@ -889,24 +997,28 @@ class GradeAverageProviderTest { coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns flow { emit(Resource.Loading()) - emit(Resource.Intermediate(firstGrades to firstSummaries)) - emit(Resource.Success(firstGrades to firstSummaries)) + emit(Resource.Intermediate(Triple(firstGrades, firstSummaries, emptyList()))) + emit(Resource.Success(Triple(firstGrades, firstSummaries, emptyList()))) } coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns flow { emit(Resource.Loading()) emit( Resource.Intermediate( - secondGrades to listOf( - getSummary(22, "Matematyka", 1.1), - getSummary(22, "Fizyka", 7.26) + Triple( + secondGrades, listOf( + getSummary(22, "Matematyka", 1.1), + getSummary(22, "Fizyka", 7.26) + ), emptyList() ) ) ) emit( Resource.Success( - secondGrades to listOf( - getSummary(22, "Matematyka", 1.1), - getSummary(22, "Fizyka", 7.26) + Triple( + secondGrades, listOf( + getSummary(22, "Matematyka", 1.1), + getSummary(22, "Fizyka", 7.26) + ), emptyList() ) ) ) @@ -958,14 +1070,14 @@ class GradeAverageProviderTest { semesters[1], true ) - } returns resourceFlow { firstGrades to emptyList() } + } returns resourceFlow { Triple(firstGrades, emptyList(), emptyList()) } coEvery { gradeRepository.getGrades( student, semesters[2], true ) - } returns resourceFlow { secondGrades to emptyList() } + } returns resourceFlow { Triple(secondGrades, emptyList(), emptyList()) } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -1000,14 +1112,14 @@ class GradeAverageProviderTest { semesters[1], true ) - } returns resourceFlow { firstGrades to emptyList() } + } returns resourceFlow { Triple(firstGrades, emptyList(), emptyList()) } coEvery { gradeRepository.getGrades( student, semesters[2], true ) - } returns resourceFlow { secondGrades to emptyList() } + } returns resourceFlow { Triple(secondGrades, emptyList(), emptyList()) } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -1043,8 +1155,10 @@ class GradeAverageProviderTest { true ) } returns resourceFlow { - firstGrades to listOf( - getSummary(22, "Matematyka", 4.0) + Triple( + firstGrades, listOf( + getSummary(22, "Matematyka", 4.0) + ), emptyList() ) } coEvery { @@ -1054,8 +1168,10 @@ class GradeAverageProviderTest { true ) } returns resourceFlow { - secondGrades to listOf( - getSummary(23, "Matematyka", 3.0) + Triple( + secondGrades, listOf( + getSummary(23, "Matematyka", 3.0) + ), emptyList() ) } @@ -1092,14 +1208,20 @@ class GradeAverageProviderTest { semesters[1], true ) - } returns resourceFlow { firstGrades to firstSummaries } + } returns resourceFlow { Triple(firstGrades, firstSummaries, emptyList()) } coEvery { gradeRepository.getGrades( student, semesters[2], true ) - } returns resourceFlow { secondGrades to secondSummaries.dropLast(1) } + } returns resourceFlow { + Triple( + secondGrades, + secondSummaries.dropLast(1), + emptyList() + ) + } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -1134,14 +1256,20 @@ class GradeAverageProviderTest { semesters[1], true ) - } returns resourceFlow { firstGrades to firstSummaries.dropLast(1) } + } returns resourceFlow { + Triple( + firstGrades, + firstSummaries.dropLast(1), + emptyList() + ) + } coEvery { gradeRepository.getGrades( student, semesters[2], true ) - } returns resourceFlow { secondGrades to secondSummaries } + } returns resourceFlow { Triple(secondGrades, secondSummaries, emptyList()) } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -1176,14 +1304,20 @@ class GradeAverageProviderTest { semesters[1], true ) - } returns resourceFlow { firstGrades to firstSummaries.dropLast(1) } + } returns resourceFlow { + Triple( + firstGrades, + firstSummaries.dropLast(1), + emptyList() + ) + } coEvery { gradeRepository.getGrades( student, semesters[2], true ) - } returns resourceFlow { secondGrades to secondSummaries } + } returns resourceFlow { Triple(secondGrades, secondSummaries, emptyList()) } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( @@ -1219,16 +1353,20 @@ class GradeAverageProviderTest { true ) } returns resourceFlow { - listOf( - getGrade(22, "Fizyka", 5.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 5.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0) - ) to listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)) + Triple( + listOf( + getGrade(22, "Fizyka", 5.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 5.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0) + ), + listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)), + emptyList() + ) } coEvery { gradeRepository.getGrades( @@ -1237,11 +1375,15 @@ class GradeAverageProviderTest { true ) } returns resourceFlow { - listOf( - getGrade(23, "Fizyka", 5.0, weight = 1.0), - getGrade(23, "Fizyka", 5.0, weight = 2.0), - getGrade(23, "Fizyka", 4.0, modifier = 0.3, weight = 2.0) - ) to listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)) + Triple( + listOf( + getGrade(23, "Fizyka", 5.0, weight = 1.0), + getGrade(23, "Fizyka", 5.0, weight = 2.0), + getGrade(23, "Fizyka", 4.0, modifier = 0.3, weight = 2.0) + ), + listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)), + emptyList() + ) } val items = runBlocking { @@ -1266,23 +1408,31 @@ class GradeAverageProviderTest { every { preferencesRepository.gradeAverageModeFlow } returns flowOf(GradeAverageMode.ALL_YEAR) coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns resourceFlow { - listOf( - getGrade(22, "Fizyka", 5.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 5.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0) - ) to listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)) + Triple( + listOf( + getGrade(22, "Fizyka", 5.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 5.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0) + ), + listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)), + emptyList() + ) } coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns resourceFlow { - listOf( - getGrade(23, "Fizyka", 5.0, weight = 1.0), - getGrade(23, "Fizyka", 5.0, weight = 2.0), - getGrade(23, "Fizyka", 4.0, modifier = 0.3, weight = 2.0) - ) to listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)) + Triple( + listOf( + getGrade(23, "Fizyka", 5.0, weight = 1.0), + getGrade(23, "Fizyka", 5.0, weight = 2.0), + getGrade(23, "Fizyka", 4.0, modifier = 0.3, weight = 2.0) + ), + listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)), + emptyList() + ) } val items = runBlocking { @@ -1313,23 +1463,31 @@ class GradeAverageProviderTest { coEvery { semesterRepository.getSemesters(student) } returns semesters coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns resourceFlow { - listOf( - getGrade(22, "Fizyka", 5.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 5.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0) - ) to listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)) + Triple( + listOf( + getGrade(22, "Fizyka", 5.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 5.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0) + ), + listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)), + emptyList() + ) } coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns resourceFlow { - listOf( - getGrade(23, "Fizyka", 5.0, weight = 1.0), - getGrade(23, "Fizyka", 5.0, weight = 2.0), - getGrade(23, "Fizyka", 4.0, modifier = 0.33, weight = 2.0) - ) to listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)) + Triple( + listOf( + getGrade(23, "Fizyka", 5.0, weight = 1.0), + getGrade(23, "Fizyka", 5.0, weight = 2.0), + getGrade(23, "Fizyka", 4.0, modifier = 0.33, weight = 2.0) + ), + listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)), + emptyList() + ) } val items = runBlocking { @@ -1366,16 +1524,20 @@ class GradeAverageProviderTest { true ) } returns resourceFlow { - listOf( - getGrade(22, "Fizyka", 5.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 5.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 4.0), - getGrade(22, "Fizyka", 6.0, weight = 2.0) - ) to listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)) + Triple( + listOf( + getGrade(22, "Fizyka", 5.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 5.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0), + getGrade(22, "Fizyka", 6.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 4.0), + getGrade(22, "Fizyka", 6.0, weight = 2.0) + ), + listOf(getSummary(semesterId = 22, subject = "Fizyka", average = .0)), + emptyList() + ) } coEvery { gradeRepository.getGrades( @@ -1384,11 +1546,15 @@ class GradeAverageProviderTest { true ) } returns resourceFlow { - listOf( - getGrade(23, "Fizyka", 5.0, weight = 1.0), - getGrade(23, "Fizyka", 5.0, weight = 2.0), - getGrade(23, "Fizyka", 4.0, modifier = 0.33, weight = 2.0) - ) to listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)) + Triple( + listOf( + getGrade(23, "Fizyka", 5.0, weight = 1.0), + getGrade(23, "Fizyka", 5.0, weight = 2.0), + getGrade(23, "Fizyka", 4.0, modifier = 0.33, weight = 2.0) + ), + listOf(getSummary(semesterId = 23, subject = "Fizyka", average = .0)), + emptyList() + ) } val items = runBlocking { @@ -1413,9 +1579,9 @@ class GradeAverageProviderTest { every { preferencesRepository.isOptionalArithmeticAverageFlow } returns flowOf(false) coEvery { gradeRepository.getGrades(student, semesters[1], true) } returns - resourceFlow { firstGrades to firstSummaries } + resourceFlow { Triple(firstGrades, firstSummaries, emptyList()) } coEvery { gradeRepository.getGrades(student, semesters[2], true) } returns - resourceFlow { listOf() to firstSummaries } + resourceFlow { Triple(listOf(), firstSummaries, emptyList()) } val items = runBlocking { gradeAverageProvider.getGradesDetailsWithAverage( From 3f199cb610016c72baaa89ae369205125c864411 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Fri, 9 Feb 2024 13:40:01 +0100 Subject: [PATCH 33/51] Replace fakelog.cf to wulkanowy.net.pl (#2419) --- .../ui/modules/login/advanced/LoginAdvancedPresenter.kt | 4 ++-- .../wulkanowy/ui/modules/login/form/LoginFormPresenter.kt | 2 +- .../ui/modules/login/recover/LoginRecoverPresenter.kt | 2 +- app/src/main/res/values/api_hosts.xml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt index a17ad003..fc26f376 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt @@ -71,7 +71,7 @@ class LoginAdvancedPresenter @Inject constructor( fun updateUsernameLabel() { view?.apply { - setUsernameLabel(if ("vulcan" in formHostValue || "fakelog" in formHostValue) emailLabel else nicknameLabel) + setUsernameLabel(if ("vulcan" in formHostValue || "wulkanowy" in formHostValue) emailLabel else nicknameLabel) } } @@ -79,7 +79,7 @@ class LoginAdvancedPresenter @Inject constructor( view?.apply { clearPassError() clearUsernameError() - if (formHostValue.contains("fakelog")) { + if (formHostValue.contains("wulkanowy")) { setDefaultCredentials( "jan@fakelog.cf", "jan123", "powiatwulkanowy", "FK100000", "999999" ) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt index c9ae4f27..26b15bff 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt @@ -90,7 +90,7 @@ class LoginFormPresenter @Inject constructor( clearPassError() clearUsernameError() clearHostError() - if (formHostValue.contains("fakelog")) { + if (formHostValue.contains("wulkanowy")) { setCredentials("jan@fakelog.cf", "jan123") } else if (formUsernameValue == "jan@fakelog.cf" && formPassValue == "jan123") { setCredentials("", "") diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverPresenter.kt index a424df40..879055a9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverPresenter.kt @@ -38,7 +38,7 @@ class LoginRecoverPresenter @Inject constructor( fun onHostSelected() { view?.run { - if ("fakelog" in recoverHostValue) setDefaultCredentials("jan@fakelog.cf") + if ("wulkanowy" in recoverHostValue) setDefaultCredentials("jan@fakelog.cf") clearUsernameError() updateFields() } diff --git a/app/src/main/res/values/api_hosts.xml b/app/src/main/res/values/api_hosts.xml index 522b6e11..94ef8abd 100644 --- a/app/src/main/res/values/api_hosts.xml +++ b/app/src/main/res/values/api_hosts.xml @@ -44,7 +44,7 @@ https://vulcan.net.pl/?login https://vulcan.net.pl/?login https://vulcan.net.pl/?email&customSuffix - https://fakelog.cf/?email + https://wulkanowy.net.pl/?email Default From 8183d7d5a0f9f62e7d63a8a42f15817525e11d7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Fri, 9 Feb 2024 16:35:18 +0100 Subject: [PATCH 34/51] Change default symbol for standard register variant (#2421) --- app/build.gradle | 2 +- .../java/io/github/wulkanowy/ui/modules/login/LoginData.kt | 3 ++- .../ui/modules/login/advanced/LoginAdvancedPresenter.kt | 2 +- .../wulkanowy/ui/modules/login/form/LoginFormPresenter.kt | 4 ++-- .../ui/modules/login/recover/LoginRecoverPresenter.kt | 4 ++-- .../login/studentselect/LoginStudentSelectFragment.kt | 2 -- .../login/studentselect/LoginStudentSelectPresenter.kt | 6 +++--- .../ui/modules/login/support/LoginSupportDialog.kt | 2 +- .../ui/modules/login/symbol/LoginSymbolPresenter.kt | 6 +++--- app/src/main/res/values/api_hosts.xml | 4 ++-- app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt | 1 + .../login/studentselect/LoginStudentSelectPresenterTest.kt | 2 +- 12 files changed, 19 insertions(+), 19 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index b60dc3b4..65a42ce1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -195,7 +195,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.3.7' + implementation 'io.github.wulkanowy:sdk:2.3.8-SNAPSHOT' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginData.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginData.kt index 2c11bb6d..b066cceb 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginData.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginData.kt @@ -7,5 +7,6 @@ data class LoginData( val password: String, val baseUrl: String, val domainSuffix: String, - val symbol: String?, + val defaultSymbol: String, + val userEnteredSymbol: String? = null, ) : Serializable diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt index fc26f376..009f26e1 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/advanced/LoginAdvancedPresenter.kt @@ -155,7 +155,7 @@ class LoginAdvancedPresenter @Inject constructor( password = view?.formPassValue.orEmpty().trim(), baseUrl = view?.formHostValue.orEmpty().trim(), domainSuffix = view?.formDomainSuffix.orEmpty().trim(), - symbol = view?.formSymbolValue.orEmpty().trim().getNormalizedSymbol(), + defaultSymbol = view?.formSymbolValue.orEmpty().trim().getNormalizedSymbol(), ) when (it.data.symbols.size) { 0 -> view?.navigateToSymbol(loginData) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt index 26b15bff..af89f147 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt @@ -148,7 +148,7 @@ class LoginFormPresenter @Inject constructor( password = password, baseUrl = host, domainSuffix = domainSuffix, - symbol = symbol + defaultSymbol = symbol ) } @@ -167,7 +167,7 @@ class LoginFormPresenter @Inject constructor( password = loginData.password, scrapperBaseUrl = loginData.baseUrl, domainSuffix = loginData.domainSuffix, - symbol = loginData.symbol.orEmpty(), + symbol = loginData.defaultSymbol, ) } .logResourceStatus("login") diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverPresenter.kt index 879055a9..18902e01 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/recover/LoginRecoverPresenter.kt @@ -60,7 +60,7 @@ class LoginRecoverPresenter @Inject constructor( resourceFlow { recoverRepository.getReCaptchaSiteKey( host, - symbol.ifBlank { "Default" }) + symbol.ifBlank { "default" }) }.onEach { when (it) { is Resource.Loading -> view?.run { @@ -103,7 +103,7 @@ class LoginRecoverPresenter @Inject constructor( fun onReCaptchaVerified(reCaptchaResponse: String) { val username = view?.recoverNameValue.orEmpty() val host = view?.recoverHostValue.orEmpty() - val symbol = view?.formHostSymbol.ifNullOrBlank { "Default" } + val symbol = view?.formHostSymbol.ifNullOrBlank { "default" } resourceFlow { recoverRepository.sendRecoverRequest( 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 06efd8d9..0fe36aa9 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt @@ -10,13 +10,11 @@ import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.databinding.FragmentLoginStudentSelectBinding import io.github.wulkanowy.ui.base.BaseFragment -import io.github.wulkanowy.ui.modules.auth.AuthDialog import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo import io.github.wulkanowy.utils.AppInfo -import io.github.wulkanowy.utils.openEmailClient import io.github.wulkanowy.utils.openInternetBrowser import io.github.wulkanowy.utils.serializable import javax.inject.Inject diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt index 6cbdfbb8..34441418 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt @@ -111,8 +111,8 @@ class LoginStudentSelectPresenter @Inject constructor( val notEmptySymbols = registerUser.symbols.filter { it.schools.isNotEmpty() } val emptySymbols = registerUser.symbols.filter { it.schools.isEmpty() } - if (emptySymbols.isNotEmpty() && notEmptySymbols.isNotEmpty() && emptySymbols.any { it.symbol == loginData.symbol }) { - add(createEmptySymbolItem(emptySymbols.first { it.symbol == loginData.symbol })) + if (emptySymbols.isNotEmpty() && notEmptySymbols.isNotEmpty() && emptySymbols.any { it.symbol == loginData.userEnteredSymbol }) { + add(createEmptySymbolItem(emptySymbols.first { it.symbol == loginData.userEnteredSymbol })) } addAll(createNotEmptySymbolItems(notEmptySymbols, students)) @@ -317,7 +317,7 @@ class LoginStudentSelectPresenter @Inject constructor( loginData = loginData, registerUser = registerUser, lastErrorMessage = lastError?.message, - enteredSymbol = loginData.symbol, + enteredSymbol = loginData.userEnteredSymbol, ) ) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/support/LoginSupportDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/support/LoginSupportDialog.kt index fcf7f51c..4be2dbaa 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/support/LoginSupportDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/support/LoginSupportDialog.kt @@ -105,7 +105,7 @@ class LoginSupportDialog : BaseDialogFragment() { "${appInfo.systemManufacturer} ${appInfo.systemModel}", appInfo.systemVersion.toString(), "${appInfo.versionName}-${appInfo.buildFlavor}", - supportInfo.loginData.baseUrl + "/" + supportInfo.loginData.symbol, + supportInfo.loginData.let { "${it.baseUrl}/${it.defaultSymbol}/${it.userEnteredSymbol}" }, preferencesRepository.installationId, getLastErrorFromStudentSelectScreen(), dialogLoginSupportSchoolInput.text.takeIf { !it.isNullOrBlank() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt index 02bfde5d..cc88b09e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt @@ -60,7 +60,7 @@ class LoginSymbolPresenter @Inject constructor( } loginData = loginData.copy( - symbol = view?.symbolValue?.getNormalizedSymbol(), + userEnteredSymbol = view?.symbolValue?.getNormalizedSymbol(), ) resourceFlow { studentRepository.getUserSubjectsFromScrapper( @@ -68,7 +68,7 @@ class LoginSymbolPresenter @Inject constructor( password = loginData.password, scrapperBaseUrl = loginData.baseUrl, domainSuffix = loginData.domainSuffix, - symbol = loginData.symbol.orEmpty(), + symbol = loginData.userEnteredSymbol.orEmpty(), ) }.onEach { user -> registerUser = user.dataOrNull @@ -93,7 +93,7 @@ class LoginSymbolPresenter @Inject constructor( else -> { val enteredSymbolDetails = user.data.symbols .firstOrNull() - ?.takeIf { it.symbol == loginData.symbol } + ?.takeIf { it.symbol == loginData.userEnteredSymbol } if (enteredSymbolDetails?.error is InvalidSymbolException) { view?.run { diff --git a/app/src/main/res/values/api_hosts.xml b/app/src/main/res/values/api_hosts.xml index 94ef8abd..6439b462 100644 --- a/app/src/main/res/values/api_hosts.xml +++ b/app/src/main/res/values/api_hosts.xml @@ -47,7 +47,7 @@ https://wulkanowy.net.pl/?email - Default + warszawa opole gdansk lublin @@ -66,7 +66,7 @@ gminaulanmajorat gminaozorkow gminalopiennikgorny - Default + warszawa powiatwulkanowy diff --git a/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt b/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt index eac1389f..9f5d731b 100644 --- a/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt +++ b/app/src/test/java/io/github/wulkanowy/TestEnityCreator.kt @@ -42,6 +42,7 @@ fun getSemesterPojo(diaryId: Int, semesterId: Int, start: LocalDate, end: LocalD diaryName = "$semesterId", schoolYear = 1970, classId = 0, + className = "Ti", semesterNumber = semesterName, unitId = 1, start = start, diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt index fad6436d..34965f00 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt @@ -58,7 +58,7 @@ class LoginStudentSelectPresenterTest { login = "", password = "", baseUrl = "", - symbol = null, + defaultSymbol = "warszawa", domainSuffix = "", ) From cd853e4d5720881623a18b8e1dbfbfb234640ca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Fri, 9 Feb 2024 19:34:04 +0100 Subject: [PATCH 35/51] New Crowdin updates (#2417) --- app/src/main/res/values-cs/strings.xml | 13 +++++++++++++ app/src/main/res/values-de/strings.xml | 9 +++++++++ app/src/main/res/values-pl/strings.xml | 13 +++++++++++++ app/src/main/res/values-ru/strings.xml | 13 +++++++++++++ app/src/main/res/values-sk/strings.xml | 13 +++++++++++++ app/src/main/res/values-uk/strings.xml | 17 +++++++++++++++-- 6 files changed, 76 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index b4f1f878..85a67f9b 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -117,6 +117,7 @@ Součet bodů Konečná známka Předpokládaná známka + Popisná známka Vypočítaný průměr Jak funguje vypočítaný průměr? Vypočítaný průměr je aritmetický průměr vypočítaný z průměrů předmětů. Umožňuje vám to znát přibližný konečný průměr. Vypočítává se způsobem zvoleným uživatelem v nastavení aplikaci. Doporučuje se vybrat příslušnou možnost. Důvodem je rozdílný výpočet školních průměrů. Pokud vaše škola navíc uvádí průměr předmětů na stránce deníku Vulcan, aplikace si je stáhne a tyto průměry nepočítá. To lze změnit vynucením výpočtu průměru v nastavení aplikaci.\n\nPrůměr známek pouze z vybraného semestru:\n1. Výpočet váženého průměru pro každý předmět v daném semestru\n2. Sčítání vypočítaných průměrů\n3. Výpočet aritmetického průměru součtených průměrů\n\nPrůměr průměrů z obou semestrů:\n1. Výpočet váženého průměru pro každý předmět v semestru 1 a 2\n2. Výpočet aritmetického průměru vypočítaných průměrů za semestry 1 a 2 pro každý předmět.\n3. Sčítání vypočítaných průměrů\n4. Výpočet aritmetického průměru sečtených průměrů\n\nPrůměr známek z celého roku:\n1. Výpočet váženého průměru za rok pro každý předmět. Konečný průměr v 1. semestru je nepodstatný.\n2. Sčítání vypočítaných průměrů\n3. Výpočet aritmetického průměru součtených průměrů @@ -160,6 +161,12 @@ Nové konečné známky Nové konečné známky + + Nová popisná známka + Nové popisné známky + Nové popisné známky + Nové popisné známky + Máte %1$d novou známku Máte %1$d nové známky @@ -178,6 +185,12 @@ Máte %1$d nových konečných známek Máte %1$d nových konečných známek + + Máte %1$d novou popisnou známku + Máte %1$d nové popisné známky + Máte %1$d nových popisných známek + Máte %1$d nových popisných známek + Lekce Učebna diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 7e0ce868..0fbba392 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -117,6 +117,7 @@ Gesamtpunkte Finaler Note Vorhergesagte Note + Descriptive grade Berechnender Durchschnitt Wie funktioniert der berechnete Durchschnitt? Der berechnete Mittelwert ist das arithmetische Mittel, das aus den Durchschnittswerten der Probanden errechnet wird. Es erlaubt Ihnen, den ungefähre endgültigen Durchschnitt zu kennen. Sie wird auf eine vom Anwender in den Anwendungseinstellungen gewählte Weise berechnet. Es wird empfohlen, die entsprechende Option zu wählen. Das liegt daran, dass die Berechnung der Schuldurchschnitte unterschiedlich ist. Wenn Ihre Schule den Durchschnitt der Fächer auf der Vulcan-Seite angibt, lädt die Anwendung diese Fächer herunter und berechnet nicht den Durchschnitt. Dies kann geändert werden, indem die Berechnung des Durchschnitts in den Anwendungseinstellungen erzwungen wird. \n\nDurchschnitt der Noten nur aus dem ausgewählten Semester :\n1. Berechnung des gewichteten Durchschnitts für jedes Fach in einem bestimmten Semester\n2. Addition der berechneten Durchschnittswerte\n3. Berechnung des arithmetischen Mittels der summierten Durchschnitte\nDurchschnitt der Durchschnitte aus beiden Semestern:\n1. Berechnung des gewichteten Durchschnitts für jedes Fach in Semester 1 und 2\n2. Berechnung des arithmetischen Mittels der berechneten Durchschnitte für Semester 1 und 2 für jedes Fach. \n3. Hinzufügen von berechneten Durchschnittswerten\n4. Berechnung des arithmetischen Mittels der summierten Durchschnitte\nDurchschnitt der Noten aus dem ganzen Jahr:\n1. Berechnung des gewichteten Jahresdurchschnitts für jedes Fach. Der Abschlussdurchschnitt im 1. Semester ist irrelevant. \n2. Addition der berechneten Durchschnittswerte\n3. Berechnung des arithmetischen Mittels der summierten Mittelwerte @@ -152,6 +153,10 @@ Neue Abschlussnote Neue Abschlussnoten + + New descriptive grade + New descriptive grades + Du hast %1$d Note bekommen Du hast %1$d Noten bekommen @@ -164,6 +169,10 @@ Sie haben %1$d Abschlussnote bekommen Sie haben %1$d Abschlussnoten bekommen + + You received %1$d descriptive grade + You received %1$d descriptive grades + Lektion Klassenzimmer diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 1b4fbe66..df9ef8db 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -117,6 +117,7 @@ Suma punktów Ocena końcowa Przewidywana ocena + Ocena opisowa Obliczona średnia Jak działa obliczona średnia? Obliczona średnia jest średnią arytmetyczną obliczoną ze średnich przedmiotów. Pozwala ona na poznanie przybliżonej średniej końcowej. Jest obliczana w sposób wybrany przez użytkownika w ustawieniach aplikacji. Zaleca się wybranie odpowiedniej opcji. Dzieje się tak dlatego, że obliczanie średnich w szkołach różni się. Dodatkowo, jeśli twoja szkoła ma włączone średnie przedmiotów na stronie dziennika Vulcan, aplikacja pobiera je i ich nie oblicza. Można to zmienić, wymuszając obliczanie średniej w ustawieniach aplikacji.\n\nŚrednia ocen tylko z wybranego semestru:\n1. Obliczanie średniej arytmetycznej każdego przedmiotu w danym semestrze\n2. Zsumowanie obliczonych średnich\n3. Obliczanie średniej arytmetycznej zsumowanych średnich\n\nŚrednia ze średnich z obu semestrów:\n1.Obliczanie średniej arytmetycznej każdego przedmiotu w semestrze 1 i 2\n2. Obliczanie średniej arytmetycznej obliczonych średnich w semestrze 1 i 2 każdego przedmiotu.\n3. Zsumowanie obliczonych średnich\n4. Obliczanie średniej arytmetycznej zsumowanych średnich\n\nŚrednia wszystkich ocen z całego roku:\n1. Obliczanie średniej arytmetycznej z każdego przedmiotu w ciągu całego roku. Końcowa ocena w 1 semestrze jest bez znaczenia.\n2. Zsumowanie obliczonych średnich\n3. Obliczanie średniej arytmetycznej z zsumowanych średnich @@ -160,6 +161,12 @@ Nowe oceny końcowe Nowe oceny końcowe + + Nowa ocena opisowa + Nowe oceny opisowe + Nowe oceny opisowe + Nowe oceny opisowe + Masz %1$d nową ocenę Masz %1$d nowe oceny @@ -178,6 +185,12 @@ Masz %1$d nowych końcowych ocen Masz %1$d nowych końcowych ocen + + Masz %1$d nową ocenę opisową + Masz %1$d nowe oceny opisowe + Masz %1$d nowych ocen opisowych + Masz %1$d nowych ocen opisowych + Lekcja Sala diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 2ca66928..fffc5ce1 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -117,6 +117,7 @@ Сумма баллов Итоговая оценка Ожидаемая оценка + Descriptive grade Рассчитанная средняя оценка Как работает \"Рассчитанная средняя оценка\"? Рассчитанная средняя оценка - это среднее арифметическое, рассчитанное на основе средних оценок по предметам. Это позволяет узнать приблизительную итоговую среднюю оценку. Она рассчитывается способом, выбранным пользователем в настройках приложения. Рекомендуется выбрать подходящий вариант, так как каждая школа по разному считает среднюю оценку. Кроме того, если ваша школа выставляет средние оценки по предметам на странице Vulcan, приложение просто загрузит их. Это можно изменить, заставив приложение считать среднюю оценку в настройках.\n\nСредняя из оценок выбранного семестра:\n1. Вычисление средневзвешенного значения по каждому предмету за семестр\n2.Суммирование вычисленных значений\n3. Вычисление среднего арифметического суммированных значений\n\nСредняя из средних оценок семестров:\n1.Расчет средневзвешенного значения для каждого предмета в семестрах. \n2. Вычисление среднего арифметического из средневзвешенных значений для каждого предмета в семестрах.\n3. Суммирование средних арифметических\n4. Вычисление среднего арифматического из суммированных значений\n\nСредняя из оценок со всего года:\n1. Расчет средневзвешенного значения по каждому предмету за год. Итоговое среднее значение за 1 семестр не имеет значения.\n2. Суммирование вычисленных средних\n3. Расчет среднего арифметического суммированных чисел @@ -160,6 +161,12 @@ Новые итоговые оценки Новые итоговые оценки + + New descriptive grade + New descriptive grades + New descriptive grades + New descriptive grades + Вы получили %1$d новую оценку Вы получили %1$d новые оценки @@ -178,6 +185,12 @@ Вы получили %1$d новых итоговых оценок Вы получили %1$d новых итоговые оценки + + You received %1$d descriptive grade + You received %1$d descriptive grades + You received %1$d descriptive grades + You received %1$d descriptive grades + Урок Аудитория diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index aaf04bc8..1e822890 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -117,6 +117,7 @@ Súčet bodov Konečná známka Predpokladaná známka + Popisná známka Vypočítaný priemer Ako funguje vypočítaný priemer? Vypočítaný priemer je aritmetický priemer vypočítaný z priemerov predmetov. Umožňuje vám to poznať približný konečný priemer. Vypočítava sa spôsobom zvoleným užívateľom v nastaveniach aplikácii. Odporúča sa vybrať príslušnú možnosť. Dôvodom je rozdielny výpočet školských priemerov. Ak vaša škola navyše uvádza priemer predmetov na stránke denníka Vulcan, aplikácia si ich stiahne a tieto priemery nepočíta. To možno zmeniť vynútením výpočtu priemeru v nastavení aplikácii.\n\nPriemer známok iba z vybraného semestra:\n1. Výpočet váženého priemeru pre každý predmet v danom semestri\n2. Sčítanie vypočítaných priemerov\n3. Výpočet aritmetického priemeru součtených priemerov\n\nPriemer priemerov z oboch semestrov:\n1. Výpočet váženého priemeru pre každý predmet v semestri 1 a 2\n2. Výpočet aritmetického priemeru vypočítaných priemerov za semestre 1 a 2 pre každý predmet.\n3. Sčítanie vypočítaných priemerov\n4. Výpočet aritmetického priemeru součtených priemerov\n\nPriemer známok z celého roka:\n1. Výpočet váženého priemeru za rok pre každý predmet. Konečný priemer v 1. semestri je nepodstatný.\n2. Sčítanie vypočítaných priemerov\n3. Výpočet aritmetického priemeru součtených priemerov @@ -160,6 +161,12 @@ Nové konečné známky Nové konečné známky + + Nová popisná známka + Nové popisné známky + Nové popisné známky + Nové popisné známky + Máte %1$d novú známku Máte %1$d nové známky @@ -178,6 +185,12 @@ Máte %1$d nových konečných známok Máte %1$d nových konečných známok + + Máte %1$d novú popisnú známku + Máte %1$d nové popisné známky + Máte %1$d nových popisných známok + Máte %1$d nových popisných známok + Lekcia Učebňa diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index fffae003..47034de6 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -97,8 +97,8 @@ Увійти Минув термін дії сесії Минув термін дії сесії, авторизуйтеся знову - Your account password has been changed. You need to log in to Wulkanowy again - Password changed + Пароль вашого облікового запису був змінений. Ви повинні увійти в Wulkanowy знову + Пароль змінено Підтримка додатку Вам подобається цей додаток? Підтримайте його розвиток, увімкнувши неінвазивну рекламу, яку ви можете відключити в будь-який час Увімкнути рекламу @@ -117,6 +117,7 @@ Всього балів Підсумкова оцінка Передбачувана оцінка + Описова оцінка Розрахована середня оцінка Як працює \"Розрахована середня оцінка\"? Розрахована середня оцінка - це середнє арифметичне, обчислене з середніх оцінок з предметів. Це дозволяє дізнатися приблизну кінцеву середню оцінку. Вона розраховується спосібом, обраним користувачем у налаштуваннях програми. Рекомендується вибрати відповідний варіант, тому що кожна школа по різному розраховує середню оцінку. Крім того, якщо у вашій школі повідомляється середня оцінка з предметів на сторінці Vulcan, програма тільки завантажує ці оцінки і не розраховує їх самостійно. Це можна змінити шляхом примусового розрахунку середньоЇ оцінки в налаштуваннях програми.\n\nСередні оцінки тільки за обраний семестр:\n1. Розрахунок середньозваженого числа для кожного предмета в даному семестрі\n2. Сумування розрахованих числ\n3. Розрахунок середнього арифметичного з сумованих чисел\n\nСереднє значення з обох семестрів:\n1. Обчислення середньозваженого числа для кожного предмета у 1 та 2 семестрі\n2. Обчислення середнього арифметичного з розрахованих середньозважених числ за 1 та 2 семестри для кожного предмета.\n3. Додавання розрахованих середніх\n4. Розрахунок середнього арифметичного підсумованих середніх значень\n\nСереднє значення оцінок за весь рік: \n1. Розрахунок середньозваженого числа за рік для кожного предмета. Підсумковий середній показник у 1-му семестрі не має значення.\n2. Сумування розрахованих середніх\n3. Обчислення середнього арифметичного з суммованих середніх @@ -160,6 +161,12 @@ Нові підсумкові оцінки Нові підсумкові оцінки + + Нова описова оцінка + Нових описових оцінок + Описових оцінок + Нові описові оцінки + Ви отримали %1$d нову оцінку Ви отримали %1$d нові оцінки @@ -178,6 +185,12 @@ Ви отримали %1$d нових підсумкових оцінок Ви отримали %1$d нових підсумкових оцінок + + Ви отримали %1$d описову оцінку + Ви отримали %1$d нові описові оцінки + Ви отримали %1$d нових описових оцінок + Ви отримали %1$d нових описових оцінок + Урок Аудиторія From 2d4a1bff830b172232b6ca7074f097057bc5a608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Fri, 9 Feb 2024 19:44:55 +0100 Subject: [PATCH 36/51] Version 2.4.0 --- app/build.gradle | 8 ++++---- app/src/main/play/release-notes/pl-PL/default.txt | 8 +++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 65a42ce1..766bee8d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,8 +27,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 34 - versionCode 145 - versionName "2.3.5" + versionCode 146 + versionName "2.4.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" @@ -164,7 +164,7 @@ play { defaultToAppBundles = false track = 'production' releaseStatus = ReleaseStatus.IN_PROGRESS - userFraction = 0.15d + userFraction = 0.50d updatePriority = 1 enabled.set(false) } @@ -195,7 +195,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.3.8-SNAPSHOT' + implementation 'io.github.wulkanowy:sdk:2.4.0' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index 04f3ba46..012bbd26 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,6 +1,8 @@ -Wersja 2.3.5 +Wersja 2.4.0 -— naprawiliśmy ładowanie frekwencji dla szkół używających eduOne -— naprawiliśmy wielokrotne wysyłanie powiadomień o zmianach w planie lekcji +— naprawiliśmy logowanie do aplikacji na odmianie standardowej +— naprawiliśmy wyświetlanie lekcji na kolejny dzień w kafelku na ekranie Start +— dodaliśmy oceny opisowe +— dodaliśmy kolorowe opisy we frekwencji we wpisach innych niż obecność Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases From cc01525f167d4d313bc8f1a48a1d8e895e6db431 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 18:49:46 +0000 Subject: [PATCH 37/51] Bump com.google.gms:google-services from 4.4.0 to 4.4.1 (#2423) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e6e090b8..f7f3d209 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ buildscript { classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$kotlin_version-1.0.16" classpath 'com.android.tools.build:gradle:8.2.2' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" - classpath 'com.google.gms:google-services:4.4.0' + classpath 'com.google.gms:google-services:4.4.1' classpath 'com.huawei.agconnect:agcp:1.9.1.303' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9' classpath "com.github.triplet.gradle:play-publisher:3.8.4" From b5e17c4ff7b607eed9d1f9b65e82114c7b00d01a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 18:50:07 +0000 Subject: [PATCH 38/51] Bump com.google.firebase:firebase-bom from 32.7.1 to 32.7.2 (#2424) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 766bee8d..5ca8c3b5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -252,7 +252,7 @@ dependencies { implementation 'com.fredporciuncula:flow-preferences:1.9.1' implementation 'org.apache.commons:commons-text:1.11.0' - playImplementation platform('com.google.firebase:firebase-bom:32.7.1') + playImplementation platform('com.google.firebase:firebase-bom:32.7.2') playImplementation 'com.google.firebase:firebase-analytics' playImplementation 'com.google.firebase:firebase-messaging' playImplementation 'com.google.firebase:firebase-crashlytics:' From 6f4a8d5534916eabfa32f21dbd34139bbac1ced7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sat, 17 Feb 2024 11:41:32 +0100 Subject: [PATCH 39/51] Add missing symbol and custom domain suffix validation (#2425) --- app/build.gradle | 2 +- .../modules/login/form/LoginFormFragment.kt | 11 ++++++ .../modules/login/form/LoginFormPresenter.kt | 36 +++++++++++++------ .../ui/modules/login/form/LoginFormView.kt | 4 +++ .../login/symbol/LoginSymbolPresenter.kt | 15 +++++--- .../main/res/layout/fragment_login_form.xml | 1 + app/src/main/res/values/strings.xml | 1 + 7 files changed, 55 insertions(+), 15 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 5ca8c3b5..f40b74db 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -195,7 +195,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.4.0' + implementation 'io.github.wulkanowy:sdk:2.4.1-SNAPSHOT' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' 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 975cad18..1c492069 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 @@ -94,6 +94,7 @@ class LoginFormFragment : BaseFragment(R.layout.fragme loginFormUsername.doOnTextChanged { _, _, _, _ -> presenter.onUsernameTextChanged() } loginFormPass.doOnTextChanged { _, _, _, _ -> presenter.onPassTextChanged() } loginFormHost.setOnItemClickListener { _, _, _, _ -> presenter.onHostSelected() } + loginFormDomainSuffix.doOnTextChanged { _, _, _, _ -> presenter.onDomainSuffixChanged() } loginFormSignIn.setOnClickListener { presenter.onSignInClick() } loginFormAdvancedButton.setOnClickListener { presenter.onAdvancedLoginClick() } loginFormPrivacyLink.setOnClickListener { presenter.onPrivacyLinkClick() } @@ -188,6 +189,12 @@ class LoginFormFragment : BaseFragment(R.layout.fragme } } + override fun setDomainSuffixInvalid() { + with(binding.loginFormDomainSuffixLayout) { + error = getString(R.string.login_invalid_domain_suffix) + } + } + override fun clearUsernameError() { binding.loginFormUsernameLayout.error = null binding.loginFormErrorBox.isVisible = false @@ -206,6 +213,10 @@ class LoginFormFragment : BaseFragment(R.layout.fragme binding.loginFormErrorBox.isVisible = false } + override fun clearDomainSuffixError() { + binding.loginFormDomainSuffixLayout.error = null + } + override fun showSoftKeyboard() { activity?.showSoftInput() } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt index af89f147..69e1d027 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt @@ -101,6 +101,12 @@ class LoginFormPresenter @Inject constructor( } } + fun onDomainSuffixChanged() { + view?.apply { + clearDomainSuffixError() + } + } + fun updateCustomDomainSuffixVisibility() { view?.run { showDomainSuffixInput("customSuffix" in formHostValue) @@ -159,7 +165,7 @@ class LoginFormPresenter @Inject constructor( fun onSignInClick() { val loginData = getLoginData() - if (!validateCredentials(loginData.login, loginData.password, loginData.baseUrl)) return + if (!validateCredentials(loginData)) return resourceFlow { studentRepository.getUserSubjectsFromScrapper( @@ -229,24 +235,29 @@ class LoginFormPresenter @Inject constructor( view?.onRecoverClick() } - private fun validateCredentials(login: String, password: String, host: String): Boolean { + private fun validateCredentials(loginData: LoginData): Boolean { var isCorrect = true - if (login.isEmpty()) { + if (loginData.login.isEmpty()) { view?.setErrorUsernameRequired() isCorrect = false } else { - if ("@" in login && "login" in host) { + if ("@" in loginData.login && "login" in loginData.baseUrl) { view?.setErrorLoginRequired() isCorrect = false } - if ("@" !in login && "email" in host) { + if ("@" !in loginData.login && "email" in loginData.baseUrl) { view?.setErrorEmailRequired() isCorrect = false } - if ("@" in login && "||" !in login && "login" !in host && "email" !in host) { - val emailHost = login.substringAfter("@") - val emailDomain = URL(host).host + + val isEmailLogin = "@" in loginData.login + val isEmailWithLogin = "||" !in loginData.login + val isLoginNotRequired = "login" !in loginData.baseUrl + val isEmailNotRequired = "email" !in loginData.baseUrl + if (isEmailLogin && isEmailWithLogin && isLoginNotRequired && isEmailNotRequired) { + val emailHost = loginData.login.substringAfter("@") + val emailDomain = URL(loginData.baseUrl).host if (!emailHost.equals(emailDomain, true)) { view?.setErrorEmailInvalid(domain = emailDomain) isCorrect = false @@ -254,16 +265,21 @@ class LoginFormPresenter @Inject constructor( } } - if (password.isEmpty()) { + if (loginData.password.isEmpty()) { view?.setErrorPassRequired(focus = isCorrect) isCorrect = false } - if (password.length < 6 && password.isNotEmpty()) { + if (loginData.password.length < 6 && loginData.password.isNotEmpty()) { view?.setErrorPassInvalid(focus = isCorrect) isCorrect = false } + if (loginData.domainSuffix !in listOf("", "rc", "kurs")) { + view?.setDomainSuffixInvalid() + isCorrect = false + } + return isCorrect } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt index f2b7b100..6ea22d18 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormView.kt @@ -46,12 +46,16 @@ interface LoginFormView : BaseView { fun setErrorEmailInvalid(domain: String) + fun setDomainSuffixInvalid() + fun clearUsernameError() fun clearPassError() fun clearHostError() + fun clearDomainSuffixError() + fun showSoftKeyboard() fun hideSoftKeyboard() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt index cc88b09e..5c31f14d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt @@ -96,10 +96,7 @@ class LoginSymbolPresenter @Inject constructor( ?.takeIf { it.symbol == loginData.userEnteredSymbol } if (enteredSymbolDetails?.error is InvalidSymbolException) { - view?.run { - setErrorSymbolInvalid() - showContact(true) - } + showInvalidSymbolError() } else { Timber.i("Login with symbol result: Success") view?.navigateToStudentSelect(loginData, requireNotNull(user.data)) @@ -128,6 +125,9 @@ class LoginSymbolPresenter @Inject constructor( loginErrorHandler.dispatch(user.error) lastError = user.error view?.showContact(true) + if (user.error is InvalidSymbolException) { + showInvalidSymbolError() + } } } }.onResourceNotLoading { @@ -145,6 +145,13 @@ class LoginSymbolPresenter @Inject constructor( return normalizedSymbol in definitelyInvalidSymbols } + private fun showInvalidSymbolError() { + view?.run { + setErrorSymbolInvalid() + showContact(true) + } + } + fun onFaqClick() { view?.openFaqPage() } diff --git a/app/src/main/res/layout/fragment_login_form.xml b/app/src/main/res/layout/fragment_login_form.xml index fc5e5f35..10864e64 100644 --- a/app/src/main/res/layout/fragment_login_form.xml +++ b/app/src/main/res/layout/fragment_login_form.xml @@ -261,6 +261,7 @@ android:layout_marginEnd="24dp" android:layout_marginRight="24dp" android:hint="@string/login_domain_suffix_hint" + app:errorEnabled="true" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/loginFormHostLayout" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f1fa3ce7..0a4dcf7f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -61,6 +61,7 @@ Invalid email Use the assigned login instead of email Use the assigned login or email in @%1$s + Invalid domain suffix Invalid symbol. If you cannot find it, please contact the school Don\'t make this up! If you cannot find it, please contact the school Student not found. Validate the symbol and the chosen variation of the UONET+ register From 736d16a7ab475c86c5306df17de5be8457533620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sat, 17 Feb 2024 12:31:14 +0100 Subject: [PATCH 40/51] Make WebkitCookieManagerProxy no-op if webview is not available on the device (#2427) --- .../utils/WebkitCookieManagerProxy.kt | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/utils/WebkitCookieManagerProxy.kt b/app/src/main/java/io/github/wulkanowy/utils/WebkitCookieManagerProxy.kt index a5497871..3d41c711 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/WebkitCookieManagerProxy.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/WebkitCookieManagerProxy.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.utils +import android.util.AndroidRuntimeException import java.net.CookiePolicy import java.net.CookieStore import java.net.HttpCookie @@ -9,7 +10,18 @@ import java.net.CookieManager as JavaCookieManager class WebkitCookieManagerProxy : JavaCookieManager(null, CookiePolicy.ACCEPT_ALL) { - private val webkitCookieManager: WebkitCookieManager = WebkitCookieManager.getInstance() + private val webkitCookieManager: WebkitCookieManager? = getWebkitCookieManager() + + /** + * @see [https://stackoverflow.com/a/70354583/6695449] + */ + private fun getWebkitCookieManager(): WebkitCookieManager? { + return try { + WebkitCookieManager.getInstance() + } catch (e: AndroidRuntimeException) { + null + } + } override fun put(uri: URI?, responseHeaders: Map>?) { if (uri == null || responseHeaders == null) return @@ -23,7 +35,7 @@ class WebkitCookieManagerProxy : JavaCookieManager(null, CookiePolicy.ACCEPT_ALL // process each of the headers for (headerValue in responseHeaders[headerKey].orEmpty()) { - webkitCookieManager.setCookie(url, headerValue) + webkitCookieManager?.setCookie(url, headerValue) } } } @@ -34,7 +46,7 @@ class WebkitCookieManagerProxy : JavaCookieManager(null, CookiePolicy.ACCEPT_ALL ): Map> { require(!(uri == null || requestHeaders == null)) { "Argument is null" } val res = mutableMapOf>() - val cookie = webkitCookieManager.getCookie(uri.toString()) + val cookie = webkitCookieManager?.getCookie(uri.toString()) if (cookie != null) res["Cookie"] = listOf(cookie) return res } @@ -50,7 +62,7 @@ class WebkitCookieManagerProxy : JavaCookieManager(null, CookiePolicy.ACCEPT_ALL cookies.remove(uri, cookie) override fun removeAll(): Boolean { - webkitCookieManager.removeAllCookies(null) + webkitCookieManager?.removeAllCookies(null) ?: return false return true } } From e757585bd38030e09282d1ce6ceb0d083298e6b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Sat, 17 Feb 2024 12:43:52 +0100 Subject: [PATCH 41/51] New Crowdin updates (#2426) --- app/src/main/res/values-cs/strings.xml | 1 + app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values-pl/strings.xml | 1 + app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-sk/strings.xml | 1 + app/src/main/res/values-uk/strings.xml | 1 + 6 files changed, 6 insertions(+) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 85a67f9b..e1cafa6e 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -56,6 +56,7 @@ Neplatný e-mail Místo e-mailu použijte přiřazené přihlašovací údaje Použijte přiřazené přihlašovací nebo e-mail v @%1$s + Invalid domain suffix Neplatný symbol. Pokud jej nemůžete najít, kontaktujte školu Nevymýšlejte si! Pokud symbol nemůžete najít, kontaktujte školu Žák nebyl nalezen. Zkontrolujte správnost symbolu a vybrané varianty deníku UONET+ diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 0fbba392..5bd71bb2 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -56,6 +56,7 @@ Ungültige email Den zugewiesenen Login anstelle von email verwenden Benutze den zugewiesenen Login oder E-Mail in @%1$s + Invalid domain suffix Invalid symbol. If you cannot find it, please contact the school Don\'t make this up! If you cannot find it, please contact the school Schüler nicht gefunden. Überprüfen Sie das Symbol und die gewählte Variation des UONET+ Registers diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index df9ef8db..70d4982b 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -56,6 +56,7 @@ Nieprawidłowy adres e-mail Użyj loginu zamiast adresu e-mail Użyj loginu lub adresu e-mail w @%1$s + Nieprawidłowy sufiks domeny Nieprawidłowy symbol. Jeśli nie możesz go znaleźć, skontaktuj się ze szkołą Nie zmyślaj! Jeśli nie możesz znaleźć symbolu, skontaktuj się ze szkołą Nie znaleziono ucznia. Sprawdź poprawność symbolu i wybranej odmiany dziennika UONET+ diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index fffc5ce1..717e0213 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -56,6 +56,7 @@ Неверный e-mail Используйте назначенный логин вместо e-mail Используйте назначенный логин или email в @%1$s + Invalid domain suffix Invalid symbol. If you cannot find it, please contact the school Don\'t make this up! If you cannot find it, please contact the school Ученик не найден. Проверьте symbol и выбранный тип дненика UONET+ diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 1e822890..368ead9d 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -56,6 +56,7 @@ Neplatný e-mail Namiesto e-mailu použite priradené prihlasovacie údaje Použite priradené prihlasovacie alebo e-mail v @%1$s + Invalid domain suffix Neplatný symbol. Pokiaľ ho nemôžete nájsť, kontaktujte školu Nevymýšľajte si! Pokiaľ symbol nemôžete nájsť, kontaktujte školu Žiak nebol nájdený. Skontrolujte správnosť symbolu a vybrané varianty denníka UONET+ diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 47034de6..3d10f117 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -56,6 +56,7 @@ Недійсна адреса e-mail Використовуйте призначений логін замість адреси e-mail Використовуйте призначений логін або адресу e-mail в @%1$s + Invalid domain suffix Некоректний символ. Якщо ви не можете знайти його, будь ласка, зв\'яжіться зі школою Не вигадуйте! Якщо ви не можете знайти його, будь ласка, зв\'яжіться зі школою Студента не знайдено. Перевірте symbol та обраний тип щоденника UONET+ From 3cf6c295b01512f6acabd27e864d821c3f5a79cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sat, 17 Feb 2024 13:07:45 +0100 Subject: [PATCH 42/51] Version 2.4.1 --- app/build.gradle | 6 +++--- app/src/main/play/release-notes/pl-PL/default.txt | 7 ++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index f40b74db..e6d35f54 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,8 +27,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 34 - versionCode 146 - versionName "2.4.0" + versionCode 147 + versionName "2.4.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" @@ -195,7 +195,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.4.1-SNAPSHOT' + implementation 'io.github.wulkanowy:sdk:2.4.1' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index 012bbd26..5736992b 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,8 +1,5 @@ -Wersja 2.4.0 +Wersja 2.4.1 -— naprawiliśmy logowanie do aplikacji na odmianie standardowej -— naprawiliśmy wyświetlanie lekcji na kolejny dzień w kafelku na ekranie Start -— dodaliśmy oceny opisowe -— dodaliśmy kolorowe opisy we frekwencji we wpisach innych niż obecność +- drobne poprawki stabilności aplikacji i odświeżania danych Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases From 7d8be1b9fc63e7e232ea93754abd385a62bd825c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 18 Feb 2024 14:21:38 +0000 Subject: [PATCH 43/51] Bump coroutines from 1.7.3 to 1.8.0 (#2428) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index e6d35f54..0b9512cb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -191,7 +191,7 @@ ext { room = "2.6.1" chucker = "4.0.0" mockk = "1.13.9" - coroutines = "1.7.3" + coroutines = "1.8.0" } dependencies { From cfec79405fe8c683012ff5583ccbe508bee502c9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 18 Feb 2024 14:21:59 +0000 Subject: [PATCH 44/51] Bump org.jetbrains.kotlinx:kotlinx-serialization-json (#2429) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 0b9512cb..1408fedf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -199,7 +199,7 @@ dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' - implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" implementation 'androidx.core:core-ktx:1.12.0' From ec101c1f52a6ea150d52484314cf66003891c0f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Borcz?= Date: Mon, 19 Feb 2024 11:30:05 +0100 Subject: [PATCH 45/51] Fix error handling in widgets (#2430) --- .../java/io/github/wulkanowy/data/Resource.kt | 11 +++- .../LuckyNumberWidgetProvider.kt | 15 ++--- .../timetablewidget/TimetableWidgetFactory.kt | 59 +++++++++++++------ .../timetablewidget/TimetableWidgetItem.kt | 5 ++ .../layout/item_widget_timetable_error.xml | 12 ++++ app/src/main/res/layout/widget_timetable.xml | 2 - 6 files changed, 72 insertions(+), 32 deletions(-) create mode 100644 app/src/main/res/layout/item_widget_timetable_error.xml diff --git a/app/src/main/java/io/github/wulkanowy/data/Resource.kt b/app/src/main/java/io/github/wulkanowy/data/Resource.kt index d7c2aeed..108b0d58 100644 --- a/app/src/main/java/io/github/wulkanowy/data/Resource.kt +++ b/app/src/main/java/io/github/wulkanowy/data/Resource.kt @@ -30,8 +30,15 @@ val Resource.dataOrNull: T? get() = when (this) { is Resource.Success -> this.data is Resource.Intermediate -> this.data - is Resource.Loading -> null - is Resource.Error -> null + else -> null + } + +val Resource.dataOrThrow: T + get() = when (this) { + is Resource.Success -> this.data + is Resource.Intermediate -> this.data + is Resource.Loading -> throw IllegalStateException("Resource is in loading state") + is Resource.Error -> throw this.error } val Resource.errorOrNull: Throwable? 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 bafb2d7e..1ab079a3 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 @@ -11,10 +11,8 @@ import android.view.View import android.widget.RemoteViews import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R -import io.github.wulkanowy.data.Resource -import io.github.wulkanowy.data.dataOrNull +import io.github.wulkanowy.data.dataOrThrow import io.github.wulkanowy.data.db.SharedPrefProvider -import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.repositories.LuckyNumberRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.toFirstResult @@ -69,8 +67,7 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { appWidgetIds?.forEach { widgetId -> val studentId = sharedPref.getLong(getStudentWidgetKey(widgetId), 0) - val luckyNumberResource = getLuckyNumber(studentId, widgetId) - val luckyNumber = luckyNumberResource.dataOrNull?.luckyNumber?.toString() + val luckyNumber = getLuckyNumber(studentId, widgetId)?.luckyNumber?.toString() val remoteView = RemoteViews(context.packageName, R.layout.widget_luckynumber) .apply { setTextViewText(R.id.luckyNumberWidgetValue, luckyNumber ?: "-") @@ -143,18 +140,18 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { sharedPref.putLong(getStudentWidgetKey(appWidgetId), it.id) } } + else -> null } if (currentStudent != null) { luckyNumberRepository.getLuckyNumber(currentStudent, forceRefresh = false) .toFirstResult() - } else { - Resource.Success(null) - } + .dataOrThrow + } else null } catch (e: Exception) { Timber.e(e, "An error has occurred in lucky number provider") - Resource.Error(e) + null } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt index 4e0578e2..4cfc0322 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt @@ -11,7 +11,7 @@ import android.view.View.VISIBLE import android.widget.RemoteViews import android.widget.RemoteViewsService import io.github.wulkanowy.R -import io.github.wulkanowy.data.dataOrNull +import io.github.wulkanowy.data.dataOrThrow import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student @@ -27,6 +27,7 @@ import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Co import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getStudentWidgetKey import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getTodayLastLessonEndDateTimeWidgetKey import io.github.wulkanowy.utils.getCompatColor +import io.github.wulkanowy.utils.getErrorString import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.toFormattedString import kotlinx.coroutines.runBlocking @@ -67,25 +68,31 @@ class TimetableWidgetFactory( override fun onDestroy() {} override fun onDataSetChanged() { - intent?.extras?.getInt(EXTRA_APPWIDGET_ID)?.let { appWidgetId -> - val date = LocalDate.ofEpochDay(sharedPref.getLong(getDateWidgetKey(appWidgetId), 0)) - val studentId = sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0) + val appWidgetId = intent?.extras?.getInt(EXTRA_APPWIDGET_ID) ?: return + val date = LocalDate.ofEpochDay(sharedPref.getLong(getDateWidgetKey(appWidgetId), 0)) + val studentId = sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0) + items = emptyList() + + runBlocking { runCatching { - runBlocking { - val student = getStudent(studentId) ?: return@runBlocking - val semester = semesterRepository.getCurrentSemester(student) - items = createItems( - lessons = getLessons(student, semester, date), - lastSync = timetableRepository.getLastRefreshTimestamp(semester, date, date) - ) + val student = getStudent(studentId) ?: return@runBlocking + val semester = semesterRepository.getCurrentSemester(student) + val lessons = getLessons(student, semester, date) + val lastSync = timetableRepository.getLastRefreshTimestamp(semester, date, date) + + createItems(lessons, lastSync) + } + .onFailure { + items = listOf(TimetableWidgetItem.Error(it)) + Timber.e(it, "An error has occurred in timetable widget factory") + } + .onSuccess { + items = it if (date == LocalDate.now()) { updateTodayLastLessonEnd(appWidgetId) } } - }.onFailure { - Timber.e(it, "An error has occurred in timetable widget factory") - } } } @@ -98,7 +105,7 @@ class TimetableWidgetFactory( student: Student, semester: Semester, date: LocalDate ): List { val timetable = timetableRepository.getTimetable(student, semester, date, date, false) - val lessons = timetable.toFirstResult().dataOrNull?.lessons.orEmpty() + val lessons = timetable.toFirstResult().dataOrThrow.lessons return lessons.sortedBy { it.number } } @@ -110,6 +117,7 @@ class TimetableWidgetFactory( BETWEEN_AND_BEFORE_LESSONS -> 0 else -> null } + return buildList { lessons.forEach { if (prefRepository.showTimetableGaps != NO_GAPS && prevNum != null && it.number > prevNum!! + 1) { @@ -133,15 +141,12 @@ class TimetableWidgetFactory( sharedPref.putLong(key, todayLastLessonEnd.epochSecond, true) } - companion object { - const val TIME_FORMAT_STYLE = "HH:mm" - } - override fun getViewAt(position: Int): RemoteViews? { return when (val item = items.getOrNull(position) ?: return null) { is TimetableWidgetItem.Normal -> getNormalItemRemoteView(item) is TimetableWidgetItem.Empty -> getEmptyItemRemoteView(item) is TimetableWidgetItem.Synchronized -> getSynchronizedItemRemoteView(item) + is TimetableWidgetItem.Error -> getErrorItemRemoteView(item) } } @@ -213,6 +218,18 @@ class TimetableWidgetFactory( } } + private fun getErrorItemRemoteView(item: TimetableWidgetItem.Error): RemoteViews { + return RemoteViews( + context.packageName, + R.layout.item_widget_timetable_error + ).apply { + setTextViewText( + R.id.timetable_widget_item_error_message, + context.resources.getErrorString(item.error) + ) + } + } + private fun updateTheme() { when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { Configuration.UI_MODE_NIGHT_YES -> { @@ -300,4 +317,8 @@ class TimetableWidgetFactory( synchronizationTime, ) } + + private companion object { + private const val TIME_FORMAT_STYLE = "HH:mm" + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetItem.kt index 166b1a8f..fb02f891 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetItem.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetItem.kt @@ -17,10 +17,15 @@ sealed class TimetableWidgetItem(val type: TimetableWidgetItemType) { data class Synchronized( val timestamp: Instant, ) : TimetableWidgetItem(TimetableWidgetItemType.SYNCHRONIZED) + + data class Error( + val error: Throwable + ) : TimetableWidgetItem(TimetableWidgetItemType.ERROR) } enum class TimetableWidgetItemType { NORMAL, EMPTY, SYNCHRONIZED, + ERROR, } diff --git a/app/src/main/res/layout/item_widget_timetable_error.xml b/app/src/main/res/layout/item_widget_timetable_error.xml new file mode 100644 index 00000000..6f9ab067 --- /dev/null +++ b/app/src/main/res/layout/item_widget_timetable_error.xml @@ -0,0 +1,12 @@ + + diff --git a/app/src/main/res/layout/widget_timetable.xml b/app/src/main/res/layout/widget_timetable.xml index b07cc78f..b438da6c 100644 --- a/app/src/main/res/layout/widget_timetable.xml +++ b/app/src/main/res/layout/widget_timetable.xml @@ -114,7 +114,5 @@ android:text="@string/widget_timetable_no_items" android:textAppearance="?attr/textAppearanceBody1" android:visibility="gone" /> - - From 729e72cddb4c9b04e509914a66e654b9608db3df Mon Sep 17 00:00:00 2001 From: Kacper Majcher Date: Wed, 21 Feb 2024 21:36:20 +0100 Subject: [PATCH 46/51] Fix text pasting into date field in additional lesson add dialog (#2433) --- app/src/main/res/layout/dialog_additional_add.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/dialog_additional_add.xml b/app/src/main/res/layout/dialog_additional_add.xml index 4be436d7..78967394 100644 --- a/app/src/main/res/layout/dialog_additional_add.xml +++ b/app/src/main/res/layout/dialog_additional_add.xml @@ -39,7 +39,7 @@ android:layout_height="wrap_content" android:editable="false" android:focusable="false" - android:inputType="text" + android:inputType="none" tools:ignore="Deprecated" /> @@ -67,7 +67,7 @@ android:layout_height="wrap_content" android:editable="false" android:focusable="false" - android:inputType="text" + android:inputType="none" tools:ignore="Deprecated" /> @@ -87,7 +87,7 @@ android:layout_height="wrap_content" android:editable="false" android:focusable="false" - android:inputType="text" + android:inputType="none" tools:ignore="Deprecated" /> From 2776d019b957062b87a31f93ff6a282f5548124b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Thu, 22 Feb 2024 15:52:40 +0100 Subject: [PATCH 47/51] =?UTF-8?q?Revert=20"Bump=20com.google.android.ump:u?= =?UTF-8?q?ser-messaging-platform=20from=202.1.0=20to=202.2=E2=80=A6"=20(#?= =?UTF-8?q?2434)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit fc91936884715cc1d46d5af17086364961f8631d. --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 1408fedf..c3a7baab 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -262,7 +262,7 @@ dependencies { playImplementation "com.google.android.play:integrity:1.3.0" playImplementation 'com.google.android.play:app-update-ktx:2.1.0' playImplementation 'com.google.android.play:review-ktx:2.0.1' - playImplementation "com.google.android.ump:user-messaging-platform:2.2.0" + playImplementation "com.google.android.ump:user-messaging-platform:2.1.0" hmsImplementation 'com.huawei.hms:hianalytics:6.12.0.301' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.1.303' From b613b844692580874099e0d205919fb11b1d7b7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Thu, 22 Feb 2024 16:15:24 +0100 Subject: [PATCH 48/51] Version 2.4.2 --- app/build.gradle | 8 ++++---- app/src/main/play/release-notes/pl-PL/default.txt | 6 ++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c3a7baab..26c2547e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,8 +27,8 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 34 - versionCode 147 - versionName "2.4.1" + versionCode 148 + versionName "2.4.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" @@ -164,8 +164,8 @@ play { defaultToAppBundles = false track = 'production' releaseStatus = ReleaseStatus.IN_PROGRESS - userFraction = 0.50d - updatePriority = 1 + userFraction = 0.99d + updatePriority = 2 enabled.set(false) } diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index 5736992b..ef6308b6 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,5 +1,7 @@ -Wersja 2.4.1 +Wersja 2.4.2 -- drobne poprawki stabilności aplikacji i odświeżania danych +- naprawiliśmy crash przy przełączaniu uczniów, motywów i języków +- naprawiliśmy crash przy dodawaniu dodatkowych lekcji +- naprawiliśmy obsługę błędów widżetach Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases From 31854fc4b86f3b66f63720709d423a62fa13b2c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sun, 25 Feb 2024 16:35:56 +0100 Subject: [PATCH 49/51] Fix text cut off across the app when text size is set to 200% (#2435) --- app/src/main/res/layout/activity_main.xml | 2 +- .../main/res/layout/dialog_account_edit.xml | 6 ++-- .../main/res/layout/dialog_additional_add.xml | 6 ++-- app/src/main/res/layout/dialog_attendance.xml | 3 +- app/src/main/res/layout/dialog_conference.xml | 3 +- app/src/main/res/layout/dialog_exam.xml | 6 ++-- app/src/main/res/layout/dialog_grade.xml | 3 +- app/src/main/res/layout/dialog_homework.xml | 6 ++-- .../main/res/layout/dialog_homework_add.xml | 6 ++-- .../res/layout/dialog_lesson_completed.xml | 3 +- .../main/res/layout/dialog_mobile_device.xml | 18 +++++----- app/src/main/res/layout/dialog_note.xml | 3 +- .../res/layout/dialog_school_announcement.xml | 3 +- app/src/main/res/layout/dialog_timetable.xml | 3 +- .../main/res/layout/header_grade_details.xml | 15 +++++++++ app/src/main/res/layout/item_timetable.xml | 33 ++++++++++++------- .../layout/subitem_dashboard_small_grade.xml | 4 +++ 17 files changed, 86 insertions(+), 37 deletions(-) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index d14de50a..a9284234 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -16,7 +16,7 @@ + android:layout_height="wrap_content" /> diff --git a/app/src/main/res/layout/dialog_homework.xml b/app/src/main/res/layout/dialog_homework.xml index 8c6cf0a7..10b71907 100644 --- a/app/src/main/res/layout/dialog_homework.xml +++ b/app/src/main/res/layout/dialog_homework.xml @@ -27,7 +27,7 @@ android:id="@+id/homeworkDialogRead" style="@style/Widget.Material3.Button.TextButton.Dialog" android:layout_width="wrap_content" - android:layout_height="36dp" + android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginEnd="8dp" android:layout_marginBottom="24dp" @@ -35,6 +35,7 @@ android:insetTop="0dp" android:insetRight="0dp" android:insetBottom="0dp" + android:minHeight="36dp" android:text="@string/homework_mark_as_done" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/homeworkDialogClose" /> @@ -43,13 +44,14 @@ android:id="@+id/homeworkDialogClose" style="@style/Widget.Material3.Button.TextButton.Dialog" android:layout_width="wrap_content" - android:layout_height="36dp" + android:layout_height="wrap_content" android:layout_marginEnd="24dp" android:layout_marginBottom="24dp" android:insetLeft="0dp" android:insetTop="0dp" android:insetRight="0dp" android:insetBottom="0dp" + android:minHeight="36dp" 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 index e0ff5b74..dc7ae32d 100644 --- a/app/src/main/res/layout/dialog_homework_add.xml +++ b/app/src/main/res/layout/dialog_homework_add.xml @@ -94,7 +94,7 @@ android:id="@+id/homeworkDialogClose" style="@style/Widget.Material3.Button.TextButton.Dialog" android:layout_width="wrap_content" - android:layout_height="36dp" + android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginTop="24dp" android:layout_marginEnd="8dp" @@ -103,6 +103,7 @@ android:insetTop="0dp" android:insetRight="0dp" android:insetBottom="0dp" + android:minHeight="36dp" android:text="@string/all_close" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/homeworkDialogAdd" @@ -112,13 +113,14 @@ android:id="@+id/homeworkDialogAdd" style="@style/Widget.Material3.Button.TextButton.Dialog" android:layout_width="wrap_content" - android:layout_height="36dp" + android:layout_height="wrap_content" android:layout_marginTop="24dp" android:layout_marginBottom="24dp" android:insetLeft="0dp" android:insetTop="0dp" android:insetRight="0dp" android:insetBottom="0dp" + android:minHeight="36dp" android:text="@string/all_add" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/dialog_lesson_completed.xml b/app/src/main/res/layout/dialog_lesson_completed.xml index 3a1d3fd0..fc32a252 100644 --- a/app/src/main/res/layout/dialog_lesson_completed.xml +++ b/app/src/main/res/layout/dialog_lesson_completed.xml @@ -212,7 +212,7 @@ android:id="@+id/completedLessonDialogClose" style="@style/Widget.Material3.Button.TextButton.Dialog" android:layout_width="wrap_content" - android:layout_height="36dp" + android:layout_height="wrap_content" android:layout_marginTop="24dp" android:layout_marginEnd="16dp" android:layout_marginBottom="24dp" @@ -220,6 +220,7 @@ android:insetTop="0dp" android:insetRight="0dp" android:insetBottom="0dp" + android:minHeight="36dp" android:text="@string/all_close" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/dialog_mobile_device.xml b/app/src/main/res/layout/dialog_mobile_device.xml index 9b81737f..c526ed74 100644 --- a/app/src/main/res/layout/dialog_mobile_device.xml +++ b/app/src/main/res/layout/dialog_mobile_device.xml @@ -18,10 +18,10 @@ android:layout_marginTop="24dp" android:adjustViewBounds="true" android:contentDescription="@string/mobile_device_qr" - tools:src="@tools:sample/avatars" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" + tools:src="@tools:sample/avatars" /> + + app:constraint_referenced_ids="mobileDeviceQr,mobileDeviceDialogTokenTitle,mobileDeviceDialogTokenValue,mobileDeviceDialogSymbolTitle,mobileDeviceDialogSymbolValue,mobileDeviceDialogPinTitle,mobileDeviceDialogPinValue,mobileDeviceDialogClose" + tools:visibility="visible" /> + tools:visibility="invisible" /> diff --git a/app/src/main/res/layout/dialog_note.xml b/app/src/main/res/layout/dialog_note.xml index 9c8b18b3..3b88ea5f 100644 --- a/app/src/main/res/layout/dialog_note.xml +++ b/app/src/main/res/layout/dialog_note.xml @@ -180,7 +180,7 @@ android:id="@+id/noteDialogClose" style="@style/Widget.Material3.Button.TextButton.Dialog" android:layout_width="wrap_content" - android:layout_height="36dp" + android:layout_height="wrap_content" android:layout_marginTop="24dp" android:layout_marginEnd="16dp" android:layout_marginBottom="24dp" @@ -188,6 +188,7 @@ android:insetTop="0dp" android:insetRight="0dp" android:insetBottom="0dp" + android:minHeight="36dp" android:text="@string/all_close" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/dialog_school_announcement.xml b/app/src/main/res/layout/dialog_school_announcement.xml index 4e0ef556..a771b772 100644 --- a/app/src/main/res/layout/dialog_school_announcement.xml +++ b/app/src/main/res/layout/dialog_school_announcement.xml @@ -122,7 +122,7 @@ android:id="@+id/announcementDialogClose" style="@style/Widget.Material3.Button.TextButton.Dialog" android:layout_width="wrap_content" - android:layout_height="36dp" + android:layout_height="wrap_content" android:layout_marginTop="24dp" android:layout_marginEnd="16dp" android:layout_marginBottom="24dp" @@ -130,6 +130,7 @@ android:insetTop="0dp" android:insetRight="0dp" android:insetBottom="0dp" + android:minHeight="36dp" android:text="@string/all_close" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/dialog_timetable.xml b/app/src/main/res/layout/dialog_timetable.xml index aeb01b3b..de269648 100644 --- a/app/src/main/res/layout/dialog_timetable.xml +++ b/app/src/main/res/layout/dialog_timetable.xml @@ -263,7 +263,7 @@ android:id="@+id/timetableDialogClose" style="@style/Widget.Material3.Button.TextButton.Dialog" android:layout_width="wrap_content" - android:layout_height="36dp" + android:layout_height="wrap_content" android:layout_marginTop="24dp" android:layout_marginEnd="16dp" android:layout_marginBottom="24dp" @@ -271,6 +271,7 @@ android:insetTop="0dp" android:insetRight="0dp" android:insetBottom="0dp" + android:minHeight="36dp" android:text="@string/all_close" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/header_grade_details.xml b/app/src/main/res/layout/header_grade_details.xml index f2ba9a8c..e43e8993 100644 --- a/app/src/main/res/layout/header_grade_details.xml +++ b/app/src/main/res/layout/header_grade_details.xml @@ -45,6 +45,9 @@ android:textColor="?android:textColorSecondary" android:textSize="12sp" app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/gradeHeaderPointsSum" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintStart_toStartOf="@id/gradeHeaderSubject" app:layout_constraintTop_toBottomOf="@+id/gradeHeaderSubject" tools:text="Average: 6,00" /> @@ -55,8 +58,12 @@ android:layout_height="wrap_content" android:layout_marginStart="10dp" android:layout_marginTop="5dp" + android:ellipsize="end" + android:maxLines="1" android:textColor="?android:textColorSecondary" android:textSize="12sp" + app:layout_constrainedWidth="true" + app:layout_constraintEnd_toStartOf="@id/gradeHeaderNumber" app:layout_constraintStart_toEndOf="@+id/gradeHeaderAverage" app:layout_constraintTop_toBottomOf="@+id/gradeHeaderSubject" tools:text="Points: 123/200 (61,5%)" /> @@ -67,8 +74,13 @@ android:layout_height="wrap_content" android:layout_marginStart="10dp" android:layout_marginTop="5dp" + android:layout_marginEnd="8dp" + android:ellipsize="end" + android:maxLines="1" android:textColor="?android:textColorSecondary" android:textSize="12sp" + app:layout_constrainedWidth="true" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/gradeHeaderPointsSum" app:layout_constraintTop_toBottomOf="@id/gradeHeaderSubject" tools:text="12 grades" /> @@ -85,6 +97,9 @@ android:paddingRight="5dp" android:textColor="?colorOnPrimary" android:textSize="14sp" + app:autoSizeMaxTextSize="16dp" + app:autoSizeMinTextSize="10dp" + app:autoSizeTextType="uniform" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/app/src/main/res/layout/item_timetable.xml b/app/src/main/res/layout/item_timetable.xml index 57af6f7e..b9966c12 100644 --- a/app/src/main/res/layout/item_timetable.xml +++ b/app/src/main/res/layout/item_timetable.xml @@ -1,7 +1,6 @@ @@ -49,8 +49,9 @@ android:maxLines="1" android:textColor="?android:textColorSecondary" android:textSize="13sp" + app:layout_constraintBottom_toTopOf="@id/timetableItemTimeFinish" app:layout_constraintStart_toEndOf="@id/timetableItemNumber" - app:layout_constraintTop_toTopOf="@id/timetableItemNumber" + app:layout_constraintTop_toTopOf="parent" tools:text="11:11" /> @@ -83,13 +91,14 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="10dp" - android:layout_marginTop="0dp" - android:layout_marginEnd="5dp" + android:layout_marginEnd="0dp" + android:ellipsize="end" + android:maxLines="1" android:textColor="?android:textColorSecondary" android:textSize="13sp" app:layout_constraintEnd_toStartOf="@+id/timetableItemTeacher" app:layout_constraintStart_toEndOf="@+id/timetableItemRoom" - app:layout_constraintTop_toTopOf="@+id/timetableItemTimeFinish" + app:layout_constraintTop_toTopOf="@id/timetableItemTimeFinish" tools:text="(2/2)" tools:visibility="visible" /> @@ -98,13 +107,15 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="10dp" - android:layout_marginEnd="16dp" + android:layout_marginEnd="8dp" android:ellipsize="end" android:maxLines="1" android:textColor="?android:textColorSecondary" android:textSize="13sp" - app:layout_constraintBottom_toBottomOf="@+id/timetableItemNumber" + app:layout_constrainedWidth="true" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/timetableItemGroup" + app:layout_constraintTop_toTopOf="@id/timetableItemTimeFinish" tools:text="Agata Kowalska - Błaszczyk" tools:visibility="visible" /> diff --git a/app/src/main/res/layout/subitem_dashboard_small_grade.xml b/app/src/main/res/layout/subitem_dashboard_small_grade.xml index 6800b72e..3684c267 100644 --- a/app/src/main/res/layout/subitem_dashboard_small_grade.xml +++ b/app/src/main/res/layout/subitem_dashboard_small_grade.xml @@ -1,5 +1,6 @@ From e378b4c70adc8b4e4be7f302c51a16c9377066cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sun, 25 Feb 2024 16:36:50 +0100 Subject: [PATCH 50/51] Fix loading timetable and attendance when should be refreshed returns true (#2436) --- .../wulkanowy/data/db/dao/TimetableDao.kt | 2 +- .../data/repositories/AttendanceRepository.kt | 10 +++------ .../data/repositories/TimetableRepository.kt | 22 ++++++++++++++----- .../IsStudentHasLessonsOnWeekendUseCase.kt | 11 ++-------- .../services/sync/works/TimetableWork.kt | 4 +--- .../modules/attendance/AttendancePresenter.kt | 18 ++++++--------- .../modules/dashboard/DashboardPresenter.kt | 2 +- .../modules/timetable/TimetablePresenter.kt | 7 +++--- 8 files changed, 34 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/TimetableDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/TimetableDao.kt index b4b7379f..40d97ea9 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/TimetableDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/TimetableDao.kt @@ -15,5 +15,5 @@ interface TimetableDao : BaseDao { fun loadAll(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): Flow> @Query("SELECT * FROM Timetable WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :from AND date <= :end") - fun load(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): List + suspend fun load(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): List } 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 6d782047..bbf627de 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 @@ -16,10 +16,8 @@ import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.sunday import io.github.wulkanowy.utils.switchSemester import io.github.wulkanowy.utils.uniqueSubtract -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.withContext import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime @@ -58,11 +56,9 @@ class AttendanceRepository @Inject constructor( attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday) }, fetch = { - val lessons = withContext(Dispatchers.IO) { - timetableDb.load( - semester.diaryId, semester.studentId, start.monday, end.sunday - ) - } + val lessons = timetableDb.load( + semester.diaryId, semester.studentId, start.monday, end.sunday + ) sdk.init(student) .switchSemester(semester) .getAttendance(start.monday, end.sunday) 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 9305d3b3..acbd02d1 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 @@ -3,13 +3,23 @@ package io.github.wulkanowy.data.repositories 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.* +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.Timetable +import io.github.wulkanowy.data.db.entities.TimetableAdditional +import io.github.wulkanowy.data.db.entities.TimetableHeader import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.data.pojos.TimetableFull import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper -import io.github.wulkanowy.utils.* +import io.github.wulkanowy.utils.AutoRefreshHelper +import io.github.wulkanowy.utils.getRefreshKey +import io.github.wulkanowy.utils.init +import io.github.wulkanowy.utils.monday +import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.utils.switchSemester +import io.github.wulkanowy.utils.uniqueSubtract import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.sync.Mutex @@ -121,12 +131,12 @@ class TimetableRepository @Inject constructor( } } - fun getTimetableFromDatabase( + suspend fun getTimetableFromDatabase( semester: Semester, - from: LocalDate, + start: LocalDate, end: LocalDate - ): Flow> { - return timetableDb.loadAll(semester.diaryId, semester.studentId, from, end) + ): List { + return timetableDb.load(semester.diaryId, semester.studentId, start, end) } suspend fun updateTimetable(timetable: List) { diff --git a/app/src/main/java/io/github/wulkanowy/domain/timetable/IsStudentHasLessonsOnWeekendUseCase.kt b/app/src/main/java/io/github/wulkanowy/domain/timetable/IsStudentHasLessonsOnWeekendUseCase.kt index efe928e2..ffd00574 100644 --- a/app/src/main/java/io/github/wulkanowy/domain/timetable/IsStudentHasLessonsOnWeekendUseCase.kt +++ b/app/src/main/java/io/github/wulkanowy/domain/timetable/IsStudentHasLessonsOnWeekendUseCase.kt @@ -1,10 +1,7 @@ package io.github.wulkanowy.domain.timetable -import io.github.wulkanowy.data.dataOrNull import io.github.wulkanowy.data.db.entities.Semester -import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.repositories.TimetableRepository -import io.github.wulkanowy.data.toFirstResult import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.sunday import java.time.LocalDate @@ -16,18 +13,14 @@ class IsStudentHasLessonsOnWeekendUseCase @Inject constructor( ) { suspend operator fun invoke( - student: Student, semester: Semester, currentDate: LocalDate = LocalDate.now(), ): Boolean { - val lessons = timetableRepository.getTimetable( - student = student, + val lessons = timetableRepository.getTimetableFromDatabase( semester = semester, start = currentDate.monday, end = currentDate.sunday, - forceRefresh = false, - timetableType = TimetableRepository.TimetableType.NORMAL - ).toFirstResult().dataOrNull?.lessons.orEmpty() + ) return isWeekendHasLessonsUseCase(lessons) } } 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 ac9a8eb4..2d10d925 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 @@ -6,7 +6,6 @@ import io.github.wulkanowy.data.repositories.TimetableRepository import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.services.sync.notifications.ChangeTimetableNotification import io.github.wulkanowy.utils.nextOrSameSchoolDay -import kotlinx.coroutines.flow.first import java.time.LocalDate.now import javax.inject.Inject @@ -31,10 +30,9 @@ class TimetableWork @Inject constructor( timetableRepository.getTimetableFromDatabase( semester = semester, - from = startDate, + start = startDate, end = endDate, ) - .first() .filterNot { it.isNotified } .let { if (it.isNotEmpty()) changeTimetableNotification.notify(it, student) 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 f66479da..82fe69cb 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 @@ -4,18 +4,14 @@ import android.annotation.SuppressLint import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.Semester -import io.github.wulkanowy.data.db.entities.Student -import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.repositories.AttendanceRepository import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.StudentRepository -import io.github.wulkanowy.data.repositories.TimetableRepository import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.utils.* -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.onEach import timber.log.Timber import java.time.DayOfWeek @@ -210,7 +206,7 @@ class AttendancePresenter @Inject constructor( val semester = semesterRepository.getCurrentSemester(student) - checkInitialAndCurrentDate(student, semester) + checkInitialAndCurrentDate(semester) attendanceRepository.getAttendance( student = student, semester = semester, @@ -266,15 +262,13 @@ class AttendancePresenter @Inject constructor( .launch() } - private suspend fun checkInitialAndCurrentDate(student: Student, semester: Semester) { + private suspend fun checkInitialAndCurrentDate(semester: Semester) { if (initialDate == null) { - val lessons = attendanceRepository.getAttendance( - student = student, + val lessons = attendanceRepository.getAttendanceFromDatabase( semester = semester, start = now().monday, end = now().sunday, - forceRefresh = false, - ).toFirstResult().dataOrNull.orEmpty() + ).firstOrNull().orEmpty() isWeekendHasLessons = isWeekendHasLessons(lessons) initialDate = getInitialDate(semester) } @@ -316,6 +310,7 @@ class AttendancePresenter @Inject constructor( showContent(false) showExcuseButton(false) } + is Resource.Success -> { Timber.i("Excusing for absence result: Success") analytics.logEvent("excuse_absence", "items" to attendanceToExcuseList.size) @@ -328,6 +323,7 @@ class AttendancePresenter @Inject constructor( } loadData(forceRefresh = true) } + is Resource.Error -> { Timber.i("Excusing for absence result: An exception occurred") errorHandler.dispatch(it.error) 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 1e6f1c19..784ac112 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 @@ -438,7 +438,7 @@ class DashboardPresenter @Inject constructor( private fun loadLessons(student: Student, forceRefresh: Boolean) { flatResourceFlow { val semester = semesterRepository.getCurrentSemester(student) - val date = when (isStudentHasLessonsOnWeekendUseCase(student, semester)) { + val date = when (isStudentHasLessonsOnWeekendUseCase(semester)) { true -> LocalDate.now() else -> LocalDate.now().nextOrSameSchoolDay } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt index 7e8c876e..e83f2517 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt @@ -3,7 +3,6 @@ package io.github.wulkanowy.ui.modules.timetable import android.os.Handler import android.os.Looper import io.github.wulkanowy.data.db.entities.Semester -import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.enums.TimetableGapsMode.BETWEEN_AND_BEFORE_LESSONS import io.github.wulkanowy.data.enums.TimetableGapsMode.NO_GAPS @@ -150,7 +149,7 @@ class TimetablePresenter @Inject constructor( val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student) - checkInitialAndCurrentDate(student, semester) + checkInitialAndCurrentDate(semester) timetableRepository.getTimetable( student = student, semester = semester, @@ -194,9 +193,9 @@ class TimetablePresenter @Inject constructor( .launch() } - private suspend fun checkInitialAndCurrentDate(student: Student, semester: Semester) { + private suspend fun checkInitialAndCurrentDate(semester: Semester) { if (initialDate == null) { - isWeekendHasLessons = isStudentHasLessonsOnWeekendUseCase(student, semester) + isWeekendHasLessons = isStudentHasLessonsOnWeekendUseCase(semester) initialDate = getInitialDate(semester) } From d5c17285c1ce29c87e3f28cf380b8691d7bb468a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Pich?= Date: Sun, 25 Feb 2024 16:37:28 +0100 Subject: [PATCH 51/51] Fix error handling in login (#2437) --- app/build.gradle | 2 +- .../main/java/io/github/wulkanowy/ui/base/ErrorHandler.kt | 6 +++++- .../io/github/wulkanowy/ui/modules/auth/AuthPresenter.kt | 6 +++++- .../wulkanowy/ui/modules/login/form/LoginFormPresenter.kt | 4 ++++ .../java/io/github/wulkanowy/utils/ExceptionExtension.kt | 2 ++ app/src/main/res/values/api_hosts.xml | 2 +- app/src/main/res/values/strings.xml | 1 + 7 files changed, 19 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 26c2547e..b8123667 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -195,7 +195,7 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.4.1' + implementation 'io.github.wulkanowy:sdk:2.4.2-SNAPSHOT' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' 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 e17c0c9e..7109f1ff 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 @@ -34,7 +34,7 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co } protected open fun proceed(error: Throwable) { - showErrorMessage(context.resources.getErrorString(error), error) + showDefaultMessage(error) when (error) { is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl) is ScramblerException -> onDecryptionFailed() @@ -45,6 +45,10 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co } } + fun showDefaultMessage(error: Throwable) { + showErrorMessage(context.resources.getErrorString(error), error) + } + open fun clear() { showErrorMessage = { _, _ -> } onExpiredCredentials = {} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthPresenter.kt index 8f579712..3c061f49 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthPresenter.kt @@ -62,7 +62,11 @@ class AuthPresenter @Inject constructor( } isSuccess } - .onFailure { errorHandler.dispatch(it) } + .onFailure { + errorHandler.dispatch(it) + view?.showProgress(false) + view?.showContent(true) + } .onSuccess { if (it) { view?.showSuccess(true) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt index 69e1d027..39bc3f02 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/form/LoginFormPresenter.kt @@ -14,6 +14,7 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase +import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginErrorHandler @@ -204,6 +205,9 @@ class LoginFormPresenter @Inject constructor( } .onResourceError { loginErrorHandler.dispatch(it) + if (it is InvalidSymbolException) { + loginErrorHandler.showDefaultMessage(it) + } lastError = it view?.showContact(true) analytics.logEvent( diff --git a/app/src/main/java/io/github/wulkanowy/utils/ExceptionExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/ExceptionExtension.kt index 18fc10bb..1c229051 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/ExceptionExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/ExceptionExtension.kt @@ -3,6 +3,7 @@ package io.github.wulkanowy.utils import android.content.res.Resources import io.github.wulkanowy.R import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException +import io.github.wulkanowy.sdk.scrapper.exception.AccountInactiveException import io.github.wulkanowy.sdk.scrapper.exception.CloudflareVerificationException import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException import io.github.wulkanowy.sdk.scrapper.exception.ScrapperException @@ -33,6 +34,7 @@ fun Resources.getErrorString(error: Throwable): String = when (error) { is ServiceUnavailableException -> R.string.error_service_unavailable is FeatureDisabledException -> R.string.error_feature_disabled is FeatureNotAvailableException -> R.string.error_feature_not_available + is AccountInactiveException -> R.string.error_account_inactive is VulcanException -> R.string.error_unknown_uonet is ScrapperException -> R.string.error_unknown_app is CloudflareVerificationException -> R.string.error_cloudflare_captcha diff --git a/app/src/main/res/values/api_hosts.xml b/app/src/main/res/values/api_hosts.xml index 6439b462..9768329d 100644 --- a/app/src/main/res/values/api_hosts.xml +++ b/app/src/main/res/values/api_hosts.xml @@ -66,7 +66,7 @@ gminaulanmajorat gminaozorkow gminalopiennikgorny - warszawa + saas1 powiatwulkanowy diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0a4dcf7f..faed4d18 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -852,6 +852,7 @@ No internet connection An error occurred. Check your device clock + This account is inactive. Try logging in again Connection to register failed. Servers can be overloaded. Please try again later Loading data failed. Please try again later Register password change required