Compare commits

..

11 Commits

Author SHA1 Message Date
26ad6373e6 [4.13-rc.3] Update build.gradle, signing and changelog. 2022-10-23 23:16:23 +02:00
cac8f94407 [Gradle] Update Chucker to fix Android 12 crash issue. 2022-10-23 23:16:08 +02:00
6628b97faf [API/Usos] Fix detecting term start and end date. 2022-10-23 23:11:49 +02:00
8424414317 [Login] Fix configOverrides NPE during login. 2022-10-23 23:11:20 +02:00
d8bb927703 [4.13-rc.2] Update build.gradle, signing and changelog. 2022-10-22 22:34:24 +02:00
c8e8c172a2 [App] Rework update handling. 2022-10-22 22:10:04 +02:00
0d4dee765a [Lab] Allow setting custom API key. 2022-10-22 12:56:15 +02:00
fd407b2b03 [Config] Set highest data version by default. 2022-10-22 12:31:40 +02:00
40ed5a221f [App] Fix crashes while deserializing AppData and Config. 2022-10-22 12:19:23 +02:00
5150467372 [4.13-rc.1] Update build.gradle, signing and changelog. 2022-10-22 00:02:57 +02:00
63c5720f63 [App] Move per-register settings to JSON resource. Rewrite Config to use delegates. (#150)
* [App] Add base for AppData loading.

* [UI] Fix timetable widget date navigation.

* [UI] Migrate register-specific behavior to use AppData.

* [App] Implement new delegate-based config base.

* [Config] Migrate config and profile config.

* [Config] Remove defaults from migrations.

* [App] Apply event types and config overrides from AppData.

* [Events] Change default event types for university type school.
2022-10-21 23:59:53 +02:00
68 changed files with 1295 additions and 989 deletions

View File

@ -1,9 +1,17 @@
<component name="ProjectCodeStyleConfiguration"> <component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173"> <code_scheme name="Project" version="173">
<JetCodeStyleSettings> <JetCodeStyleSettings>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="ALLOW_TRAILING_COMMA" value="true" /> <option name="ALLOW_TRAILING_COMMA" value="true" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings> </JetCodeStyleSettings>
<codeStyleSettings language="JSON">
<indentOptions>
<option name="INDENT_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="true" />
<option name="SMART_TABS" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="XML"> <codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" /> <option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions> <indentOptions>

View File

@ -5,6 +5,7 @@
<w>ciasteczko</w> <w>ciasteczko</w>
<w>csrf</w> <w>csrf</w>
<w>edziennik</w> <w>edziennik</w>
<w>eggfall</w>
<w>elearning</w> <w>elearning</w>
<w>gson</w> <w>gson</w>
<w>hebe</w> <w>hebe</w>

View File

@ -208,7 +208,7 @@ dependencies {
implementation "com.daimajia.swipelayout:library:1.2.0@aar" implementation "com.daimajia.swipelayout:library:1.2.0@aar"
implementation "com.github.Applandeo:Material-Calendar-View:15de569cbc" // https://github.com/Applandeo/Material-Calendar-View implementation "com.github.Applandeo:Material-Calendar-View:15de569cbc" // https://github.com/Applandeo/Material-Calendar-View
implementation "com.github.CanHub:Android-Image-Cropper:2.2.2" // https://github.com/CanHub/Android-Image-Cropper implementation "com.github.CanHub:Android-Image-Cropper:2.2.2" // https://github.com/CanHub/Android-Image-Cropper
implementation "com.github.ChuckerTeam.Chucker:library:3.0.1" // https://github.com/ChuckerTeam/chucker implementation "com.github.ChuckerTeam.Chucker:library:3.5.2" // https://github.com/ChuckerTeam/chucker
implementation "com.github.antonKozyriatskyi:CircularProgressIndicator:1.2.2" // https://github.com/antonKozyriatskyi/CircularProgressIndicator implementation "com.github.antonKozyriatskyi:CircularProgressIndicator:1.2.2" // https://github.com/antonKozyriatskyi/CircularProgressIndicator
implementation "com.github.bassaer:chatmessageview:2.0.1" // https://github.com/bassaer/ChatMessageView implementation "com.github.bassaer:chatmessageview:2.0.1" // https://github.com/bassaer/ChatMessageView
implementation "com.github.hypertrack:hyperlog-android:0.0.10" // https://github.com/hypertrack/hyperlog-android implementation "com.github.hypertrack:hyperlog-android:0.0.10" // https://github.com/hypertrack/hyperlog-android

View File

@ -22,6 +22,7 @@
-keep class android.support.v7.widget.** { *; } -keep class android.support.v7.widget.** { *; }
-keep class pl.szczodrzynski.edziennik.utils.models.** { *; } -keep class pl.szczodrzynski.edziennik.utils.models.** { *; }
-keep class pl.szczodrzynski.edziennik.data.db.enums.* { *; }
-keep class pl.szczodrzynski.edziennik.data.db.entity.Event { *; } -keep class pl.szczodrzynski.edziennik.data.db.entity.Event { *; }
-keep class pl.szczodrzynski.edziennik.data.db.full.EventFull { *; } -keep class pl.szczodrzynski.edziennik.data.db.full.EventFull { *; }
-keep class pl.szczodrzynski.edziennik.data.db.entity.FeedbackMessage { *; } -keep class pl.szczodrzynski.edziennik.data.db.entity.FeedbackMessage { *; }
@ -31,6 +32,9 @@
-keepnames class pl.szczodrzynski.edziennik.ui.widgets.timetable.WidgetTimetableProvider -keepnames class pl.szczodrzynski.edziennik.ui.widgets.timetable.WidgetTimetableProvider
-keepnames class pl.szczodrzynski.edziennik.ui.widgets.notifications.WidgetNotificationsProvider -keepnames class pl.szczodrzynski.edziennik.ui.widgets.notifications.WidgetNotificationsProvider
-keepnames class pl.szczodrzynski.edziennik.ui.widgets.luckynumber.WidgetLuckyNumberProvider -keepnames class pl.szczodrzynski.edziennik.ui.widgets.luckynumber.WidgetLuckyNumberProvider
-keep class pl.szczodrzynski.edziennik.config.AppData { *; }
-keep class pl.szczodrzynski.edziennik.config.AppData$** { *; }
-keep class pl.szczodrzynski.edziennik.utils.managers.TextStylingManager$HtmlMode { *; }
-keepnames class androidx.appcompat.view.menu.MenuBuilder { setHeaderTitleInt(java.lang.CharSequence); } -keepnames class androidx.appcompat.view.menu.MenuBuilder { setHeaderTitleInt(java.lang.CharSequence); }
-keepnames class androidx.appcompat.view.menu.MenuPopupHelper { showPopup(int, int, boolean, boolean); } -keepnames class androidx.appcompat.view.menu.MenuPopupHelper { showPopup(int, int, boolean, boolean); }

View File

@ -1,10 +1,11 @@
<h3>Wersja 4.13-beta.3, 2022-10-20</h3> <h3>Wersja 4.13-rc.3, 2022-10-23</h3>
<ul> <ul>
<li>Poprawione powiadomienia na Androidzie 13. @santoni0</li> <li>Poprawione powiadomienia na Androidzie 13. @santoni0</li>
<li>Możliwość dostosowania wyświetlania planu lekcji</li> <li>Możliwość dostosowania wyświetlania planu lekcji.</li>
<li>Opcja kolorowania bloków w planie lekcji</li> <li>Opcja kolorowania bloków w planie lekcji.</li>
<li><b>USOS</b> - pierwsza wersja obsługi systemu</li> <li><b>USOS</b> - pierwsza wersja obsługi systemu. Osobne rodzaje wydarzeń (oraz wygląd niektórych części aplikacji) lepiej dostosowany do nauki na studiach.</li>
<li>Poprawione opcje filtrowania powiadomień i wyboru przycisków menu bocznego.</li> <li>Poprawione opcje filtrowania powiadomień i wyboru przycisków menu bocznego.</li>
<li>Ulepszony system pobierania aktualizacji aplikacji.</li>
</ul> </ul>
<br> <br>
<br> <br>

View File

@ -9,7 +9,7 @@
/*secret password - removed for source code publication*/ /*secret password - removed for source code publication*/
static toys AES_IV[16] = { static toys AES_IV[16] = {
0x74, 0xc9, 0x77, 0x6d, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; 0x0f, 0x0d, 0x19, 0x4d, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat); unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat);

View File

@ -12,6 +12,7 @@ import android.graphics.drawable.Icon
import android.os.Build import android.os.Build
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.multidex.MultiDexApplication import androidx.multidex.MultiDexApplication
import androidx.work.Configuration import androidx.work.Configuration
@ -27,10 +28,15 @@ import com.google.gson.Gson
import com.hypertrack.hyperlog.HyperLog import com.hypertrack.hyperlog.HyperLog
import com.mikepenz.iconics.Iconics import com.mikepenz.iconics.Iconics
import im.wangchao.mhttp.MHttp import im.wangchao.mhttp.MHttp
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.leolin.shortcutbadger.ShortcutBadger import me.leolin.shortcutbadger.ShortcutBadger
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.config.AppData
import pl.szczodrzynski.edziennik.config.Config import pl.szczodrzynski.edziennik.config.Config
import pl.szczodrzynski.edziennik.data.api.events.ProfileListEmptyEvent import pl.szczodrzynski.edziennik.data.api.events.ProfileListEmptyEvent
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
@ -48,18 +54,39 @@ import pl.szczodrzynski.edziennik.sync.SyncWorker
import pl.szczodrzynski.edziennik.sync.UpdateWorker import pl.szczodrzynski.edziennik.sync.UpdateWorker
import pl.szczodrzynski.edziennik.ui.base.CrashActivity import pl.szczodrzynski.edziennik.ui.base.CrashActivity
import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget
import pl.szczodrzynski.edziennik.utils.* import pl.szczodrzynski.edziennik.utils.DebugLogFormat
import pl.szczodrzynski.edziennik.utils.PermissionChecker
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.Utils.d import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.managers.* import pl.szczodrzynski.edziennik.utils.managers.AttendanceManager
import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager
import pl.szczodrzynski.edziennik.utils.managers.BuildManager
import pl.szczodrzynski.edziennik.utils.managers.EventManager
import pl.szczodrzynski.edziennik.utils.managers.GradesManager
import pl.szczodrzynski.edziennik.utils.managers.MessageManager
import pl.szczodrzynski.edziennik.utils.managers.NoteManager
import pl.szczodrzynski.edziennik.utils.managers.NotificationChannelsManager
import pl.szczodrzynski.edziennik.utils.managers.PermissionManager
import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager
import pl.szczodrzynski.edziennik.utils.managers.TimetableManager
import pl.szczodrzynski.edziennik.utils.managers.UpdateManager
import pl.szczodrzynski.edziennik.utils.managers.UserActionManager
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.system.exitProcess
class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
companion object { companion object {
@Volatile @Volatile
lateinit var db: AppDb lateinit var db: AppDb
private set
lateinit var config: Config lateinit var config: Config
// private set // for LabFragment
lateinit var profile: Profile lateinit var profile: Profile
private set
lateinit var data: AppData
private set
val profileId val profileId
get() = profile.id get() = profile.id
@ -69,18 +96,19 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
} }
val api by lazy { SzkolnyApi(this) } val api by lazy { SzkolnyApi(this) }
val notificationChannelsManager by lazy { NotificationChannelsManager(this) }
val userActionManager by lazy { UserActionManager(this) }
val gradesManager by lazy { GradesManager(this) }
val timetableManager by lazy { TimetableManager(this) }
val eventManager by lazy { EventManager(this) }
val permissionManager by lazy { PermissionManager(this) }
val attendanceManager by lazy { AttendanceManager(this) } val attendanceManager by lazy { AttendanceManager(this) }
val buildManager by lazy { BuildManager(this) }
val availabilityManager by lazy { AvailabilityManager(this) } val availabilityManager by lazy { AvailabilityManager(this) }
val textStylingManager by lazy { TextStylingManager(this) } val buildManager by lazy { BuildManager(this) }
val eventManager by lazy { EventManager(this) }
val gradesManager by lazy { GradesManager(this) }
val messageManager by lazy { MessageManager(this) } val messageManager by lazy { MessageManager(this) }
val noteManager by lazy { NoteManager(this) } val noteManager by lazy { NoteManager(this) }
val notificationChannelsManager by lazy { NotificationChannelsManager(this) }
val permissionManager by lazy { PermissionManager(this) }
val textStylingManager by lazy { TextStylingManager(this) }
val timetableManager by lazy { TimetableManager(this) }
val updateManager by lazy { UpdateManager(this) }
val userActionManager by lazy { UserActionManager(this) }
val db val db
get() = App.db get() = App.db
@ -90,6 +118,8 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
get() = App.profile get() = App.profile
val profileId val profileId
get() = App.profileId get() = App.profileId
val data
get() = App.data
private val job = Job() private val job = Job()
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
@ -124,9 +154,6 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
SSLProviderInstaller.enableSupportedTls(builder, enableCleartext = true) SSLProviderInstaller.enableSupportedTls(builder, enableCleartext = true)
if (devMode) { if (devMode) {
HyperLog.initialize(this)
HyperLog.setLogLevel(Log.VERBOSE)
HyperLog.setLogFormat(DebugLogFormat(this))
if (enableChucker) { if (enableChucker) {
val chuckerCollector = ChuckerCollector(this, true, RetentionManager.Period.ONE_HOUR) val chuckerCollector = ChuckerCollector(this, true, RetentionManager.Period.ONE_HOUR)
val chuckerInterceptor = ChuckerInterceptor(this, chuckerCollector) val chuckerInterceptor = ChuckerInterceptor(this, chuckerCollector)
@ -181,15 +208,23 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
Iconics.respectFontBoundsDefault = true Iconics.respectFontBoundsDefault = true
// initialize companion object values // initialize companion object values
AppData.read(this)
App.db = AppDb(this) App.db = AppDb(this)
App.config = Config(App.db) App.config = Config(App.db)
App.profile = Profile(0, 0, LoginType.TEMPLATE, "")
debugMode = BuildConfig.DEBUG debugMode = BuildConfig.DEBUG
devMode = config.devMode ?: debugMode devMode = config.devMode ?: debugMode
enableChucker = config.enableChucker ?: devMode enableChucker = config.enableChucker ?: devMode
if (devMode) {
HyperLog.initialize(this)
HyperLog.setLogLevel(Log.VERBOSE)
HyperLog.setLogFormat(DebugLogFormat(this))
}
if (!profileLoadById(config.lastProfileId)) { if (!profileLoadById(config.lastProfileId)) {
db.profileDao().firstId?.let { profileLoadById(it) } val success = db.profileDao().firstId?.let { profileLoadById(it) }
if (success != true)
profileLoad(Profile(0, 0, LoginType.TEMPLATE, ""))
} }
buildHttp() buildHttp()
@ -381,10 +416,22 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
} }
} }
fun profileLoad(profile: Profile) {
App.profile = profile
App.config.lastProfileId = profile.id
try {
App.data = AppData.get(profile.loginStoreType)
d("App", "Loaded AppData: ${App.data}")
} catch (e: Exception) {
Log.e("App", "Cannot load AppData", e)
Toast.makeText(this, R.string.app_cannot_load_data, Toast.LENGTH_LONG).show()
exitProcess(0)
}
}
private fun profileLoadById(profileId: Int): Boolean { private fun profileLoadById(profileId: Int): Boolean {
db.profileDao().getByIdNow(profileId)?.also { db.profileDao().getByIdNow(profileId)?.also {
App.profile = it profileLoad(it)
App.config.lastProfileId = it.id
return true return true
} }
return false return false

View File

@ -42,11 +42,11 @@ import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.entity.Metadata.* import pl.szczodrzynski.edziennik.data.db.entity.Metadata.*
import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.enums.FeatureType import pl.szczodrzynski.edziennik.data.db.enums.FeatureType
import pl.szczodrzynski.edziennik.data.db.enums.LoginType
import pl.szczodrzynski.edziennik.databinding.ActivitySzkolnyBinding import pl.szczodrzynski.edziennik.databinding.ActivitySzkolnyBinding
import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.ext.*
import pl.szczodrzynski.edziennik.sync.AppManagerDetectedEvent import pl.szczodrzynski.edziennik.sync.AppManagerDetectedEvent
import pl.szczodrzynski.edziennik.sync.SyncWorker import pl.szczodrzynski.edziennik.sync.SyncWorker
import pl.szczodrzynski.edziennik.sync.UpdateStateEvent
import pl.szczodrzynski.edziennik.sync.UpdateWorker import pl.szczodrzynski.edziennik.sync.UpdateWorker
import pl.szczodrzynski.edziennik.ui.base.MainSnackbar import pl.szczodrzynski.edziennik.ui.base.MainSnackbar
import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget
@ -57,6 +57,7 @@ import pl.szczodrzynski.edziennik.ui.dialogs.sync.RegisterUnavailableDialog
import pl.szczodrzynski.edziennik.ui.dialogs.sync.ServerMessageDialog import pl.szczodrzynski.edziennik.ui.dialogs.sync.ServerMessageDialog
import pl.szczodrzynski.edziennik.ui.dialogs.sync.SyncViewListDialog import pl.szczodrzynski.edziennik.ui.dialogs.sync.SyncViewListDialog
import pl.szczodrzynski.edziennik.ui.dialogs.sync.UpdateAvailableDialog import pl.szczodrzynski.edziennik.ui.dialogs.sync.UpdateAvailableDialog
import pl.szczodrzynski.edziennik.ui.dialogs.sync.UpdateProgressDialog
import pl.szczodrzynski.edziennik.ui.error.ErrorDetailsDialog import pl.szczodrzynski.edziennik.ui.error.ErrorDetailsDialog
import pl.szczodrzynski.edziennik.ui.error.ErrorSnackbar import pl.szczodrzynski.edziennik.ui.error.ErrorSnackbar
import pl.szczodrzynski.edziennik.ui.event.EventManualDialog import pl.szczodrzynski.edziennik.ui.event.EventManualDialog
@ -427,7 +428,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
launch { launch {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
app.db.profileDao().allNow.forEach { profile -> app.db.profileDao().allNow.forEach { profile ->
if (profile.loginStoreType != LoginType.LIBRUS) if (!profile.getAppData().uiConfig.enableMarkAsReadAnnouncements)
app.db.metadataDao() app.db.metadataDao()
.setAllSeenExceptMessagesAndAnnouncements(profile.id, true) .setAllSeenExceptMessagesAndAnnouncements(profile.id, true)
else else
@ -537,6 +538,14 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
UpdateAvailableDialog(this, event).show() UpdateAvailableDialog(this, event).show()
} }
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onUpdateStateEvent(event: UpdateStateEvent) {
if (!event.running)
return
EventBus.getDefault().removeStickyEvent(event)
UpdateProgressDialog(this, event.update ?: return, event.downloadId).show()
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true) @Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onRegisterAvailabilityEvent(event: RegisterAvailabilityEvent) { fun onRegisterAvailabilityEvent(event: RegisterAvailabilityEvent) {
EventBus.getDefault().removeStickyEvent(event) EventBus.getDefault().removeStickyEvent(event)
@ -695,11 +704,15 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
} }
d(TAG, "}") d(TAG, "}")
val intentProfileId = extras.getIntOrNull("profileId") val intentProfileId = extras.getIntOrNull("profileId").takePositive()
var intentNavTarget = extras.getIntOrNull("fragmentId").asNavTargetOrNull() var intentNavTarget = extras.getIntOrNull("fragmentId").asNavTargetOrNull()
if (extras?.containsKey("action") == true) { if (extras?.containsKey("action") == true) {
val handled = when (extras.getString("action")) { val handled = when (extras.getString("action")) {
"updateRequest" -> {
UpdateAvailableDialog(this, app.config.update).show()
true
}
"serverMessage" -> { "serverMessage" -> {
ServerMessageDialog( ServerMessageDialog(
this, this,
@ -743,8 +756,8 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
} }
if (extras?.containsKey("reloadProfileId") == true) { if (extras?.containsKey("reloadProfileId") == true) {
val reloadProfileId = extras.getIntOrNull("reloadProfileId") val reloadProfileId = extras.getIntOrNull("reloadProfileId").takePositive()
if (reloadProfileId == -1 || app.profile.id == reloadProfileId) { if (reloadProfileId == null || app.profile.id == reloadProfileId) {
reloadTarget() reloadTarget()
return return
} }
@ -767,7 +780,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
navTarget = intentNavTarget, navTarget = intentNavTarget,
args = extras, args = extras,
) )
intentProfileId != -1 -> navigate( intentProfileId != null -> navigate(
profileId = intentProfileId, profileId = intentProfileId,
navTarget = intentNavTarget, navTarget = intentNavTarget,
args = extras, args = extras,
@ -776,6 +789,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
navTarget = intentNavTarget, navTarget = intentNavTarget,
args = extras, args = extras,
) )
navLoading -> navigate()
else -> drawer.currentProfile = app.profile.id else -> drawer.currentProfile = app.profile.id
} }
navLoading = false navLoading = false
@ -923,7 +937,8 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
} }
if (profileChanged) { if (profileChanged) {
App.profile = profile if (App.profileId != profile.id)
app.profileLoad(profile)
MessagesFragment.pageSelection = -1 MessagesFragment.pageSelection = -1
// set new drawer items for this profile // set new drawer items for this profile
setDrawerItems() setDrawerItems()

View File

@ -1,9 +0,0 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-27.
*/
package pl.szczodrzynski.edziennik.config
interface AbstractConfig {
fun set(key: String, value: String?)
}

View File

@ -0,0 +1,69 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-21.
*/
package pl.szczodrzynski.edziennik.config
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.google.gson.stream.JsonReader
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.enums.LoginType
import pl.szczodrzynski.edziennik.ext.getJsonObject
import pl.szczodrzynski.edziennik.ext.mergeWith
import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.HtmlMode
data class AppData(
val configOverrides: Map<String, String>,
val messagesConfig: MessagesConfig,
val uiConfig: UIConfig,
val eventTypes: List<EventType>,
) {
companion object {
private var data: JsonObject? = null
private val appData = mutableMapOf<LoginType, AppData>()
fun read(app: App) {
val res = app.resources.openRawResource(R.raw.app_data)
data = JsonParser.parseReader(JsonReader(res.reader())).asJsonObject
}
fun get(loginType: LoginType): AppData {
if (loginType in appData)
return appData.getValue(loginType)
val json = data?.getJsonObject("base")?.deepCopy()
?: throw NoSuchElementException("Base data not found")
val overrides = setOf(loginType, loginType.schoolType)
for (overrideType in overrides) {
val override = data?.getJsonObject(overrideType.name.lowercase()) ?: continue
json.mergeWith(override)
}
val value = Gson().fromJson(json, AppData::class.java)
appData[loginType] = value
return value
}
}
data class MessagesConfig(
val subjectLength: Int?,
val bodyLength: Int?,
val textStyling: Boolean,
val syncRecipientList: Boolean,
val htmlMode: HtmlMode,
val needsReadStatus: Boolean,
)
data class UIConfig(
val lessonHeight: Int,
val enableMarkAsReadAnnouncements: Boolean,
val enableNoticePoints: Boolean,
)
data class EventType(
val id: Int,
val color: String,
val name: String,
)
}

View File

@ -0,0 +1,45 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-27.
*/
package pl.szczodrzynski.edziennik.config
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import pl.szczodrzynski.edziennik.config.db.ConfigEntry
import pl.szczodrzynski.edziennik.data.db.AppDb
import pl.szczodrzynski.edziennik.ext.takePositive
import kotlin.coroutines.CoroutineContext
abstract class BaseConfig(
val db: AppDb,
val profileId: Int? = null,
protected var entries: List<ConfigEntry>? = null,
) : CoroutineScope {
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Default
val values = hashMapOf<String, String?>()
init {
if (entries == null)
entries = db.configDao().getAllNow()
values.clear()
for ((profileId, key, value) in entries!!) {
if (profileId.takePositive() != this.profileId)
continue
values[key] = value
}
}
fun set(key: String, value: String?) {
values[key] = value
launch(Dispatchers.IO) {
db.configDao().add(ConfigEntry(profileId ?: -1, key, value))
}
}
}

View File

@ -5,151 +5,61 @@
package pl.szczodrzynski.edziennik.config package pl.szczodrzynski.edziennik.config
import com.google.gson.JsonObject import com.google.gson.JsonObject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.BuildConfig import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.config.db.ConfigEntry
import pl.szczodrzynski.edziennik.config.utils.* import pl.szczodrzynski.edziennik.config.utils.*
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
import pl.szczodrzynski.edziennik.data.db.AppDb import pl.szczodrzynski.edziennik.data.db.AppDb
import kotlin.coroutines.CoroutineContext
class Config(val db: AppDb) : CoroutineScope, AbstractConfig { @Suppress("RemoveExplicitTypeArguments")
class Config(db: AppDb) : BaseConfig(db) {
companion object { companion object {
const val DATA_VERSION = 12 const val DATA_VERSION = 12
} }
private val job = Job() private val profileConfigs: HashMap<Int, ProfileConfig> = hashMapOf()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Default
val values: HashMap<String, String?> = hashMapOf()
val ui by lazy { ConfigUI(this) } val ui by lazy { ConfigUI(this) }
val sync by lazy { ConfigSync(this) } val sync by lazy { ConfigSync(this) }
val timetable by lazy { ConfigTimetable(this) } val timetable by lazy { ConfigTimetable(this) }
val grades by lazy { ConfigGrades(this) } val grades by lazy { ConfigGrades(this) }
private var mDataVersion: Int? = null var dataVersion by config<Int>(DATA_VERSION)
var dataVersion: Int var hash by config<String>("")
get() { mDataVersion = mDataVersion ?: values.get("dataVersion", 0); return mDataVersion ?: 0 }
set(value) { set("dataVersion", value); mDataVersion = value }
private var mHash: String? = null var lastProfileId by config<Int>(0)
var hash: String var loginFinished by config<Boolean>(false)
get() { mHash = mHash ?: values.get("hash", ""); return mHash ?: "" } var privacyPolicyAccepted by config<Boolean>(false)
set(value) { set("hash", value); mHash = value } var update by config<Update?>(null)
var updatesChannel by config<String>("release")
private var mLastProfileId: Int? = null var devMode by config<Boolean?>(null)
var lastProfileId: Int var devModePassword by config<String?>(null)
get() { mLastProfileId = mLastProfileId ?: values.get("lastProfileId", 0); return mLastProfileId ?: 0 } var enableChucker by config<Boolean?>(null)
set(value) { set("lastProfileId", value); mLastProfileId = value }
private var mUpdatesChannel: String? = null var apiAvailabilityCheck by config<Boolean>(true)
var updatesChannel: String var apiInvalidCert by config<String?>(null)
get() { mUpdatesChannel = mUpdatesChannel ?: values.get("updatesChannel", "release"); return mUpdatesChannel ?: "release" } var apiKeyCustom by config<String?>(null)
set(value) { set("updatesChannel", value); mUpdatesChannel = value } var appInstalledTime by config<Long>(0L)
private var mUpdate: Update? = null var appRateSnackbarTime by config<Long>(0L)
var update: Update? var appVersion by config<Int>(BuildConfig.VERSION_CODE)
get() { mUpdate = mUpdate ?: values.get("update", null as Update?); return mUpdate ?: null as Update? } var validation by config<String?>(null, "buildValidation")
set(value) { set("update", value); mUpdate = value }
private var mAppVersion: Int? = null var archiverEnabled by config<Boolean>(true)
var appVersion: Int var runSync by config<Boolean>(false)
get() { mAppVersion = mAppVersion ?: values.get("appVersion", BuildConfig.VERSION_CODE); return mAppVersion ?: BuildConfig.VERSION_CODE } var widgetConfigs by config<JsonObject> { JsonObject() }
set(value) { set("appVersion", value); mAppVersion = value }
private var mLoginFinished: Boolean? = null
var loginFinished: Boolean
get() { mLoginFinished = mLoginFinished ?: values.get("loginFinished", false); return mLoginFinished ?: false }
set(value) { set("loginFinished", value); mLoginFinished = value }
private var mPrivacyPolicyAccepted: Boolean? = null
var privacyPolicyAccepted: Boolean
get() { mPrivacyPolicyAccepted = mPrivacyPolicyAccepted ?: values.get("privacyPolicyAccepted", false); return mPrivacyPolicyAccepted ?: false }
set(value) { set("privacyPolicyAccepted", value); mPrivacyPolicyAccepted = value }
private var mDevMode: Boolean? = null
var devMode: Boolean?
get() { mDevMode = mDevMode ?: values.getBooleanOrNull("debugMode"); return mDevMode }
set(value) { set("debugMode", value?.toString()); mDevMode = value }
private var mEnableChucker: Boolean? = null
var enableChucker: Boolean?
get() { mEnableChucker = mEnableChucker ?: values.getBooleanOrNull("enableChucker"); return mEnableChucker }
set(value) { set("enableChucker", value?.toString()); mEnableChucker = value }
private var mDevModePassword: String? = null
var devModePassword: String?
get() { mDevModePassword = mDevModePassword ?: values.get("devModePassword", null as String?); return mDevModePassword }
set(value) { set("devModePassword", value); mDevModePassword = value }
private var mAppInstalledTime: Long? = null
var appInstalledTime: Long
get() { mAppInstalledTime = mAppInstalledTime ?: values.get("appInstalledTime", 0L); return mAppInstalledTime ?: 0L }
set(value) { set("appInstalledTime", value); mAppInstalledTime = value }
private var mAppRateSnackbarTime: Long? = null
var appRateSnackbarTime: Long
get() { mAppRateSnackbarTime = mAppRateSnackbarTime ?: values.get("appRateSnackbarTime", 0L); return mAppRateSnackbarTime ?: 0L }
set(value) { set("appRateSnackbarTime", value); mAppRateSnackbarTime = value }
private var mRunSync: Boolean? = null
var runSync: Boolean
get() { mRunSync = mRunSync ?: values.get("runSync", false); return mRunSync ?: false }
set(value) { set("runSync", value); mRunSync = value }
private var mWidgetConfigs: JsonObject? = null
var widgetConfigs: JsonObject
get() { mWidgetConfigs = mWidgetConfigs ?: values.get("widgetConfigs", JsonObject()); return mWidgetConfigs ?: JsonObject() }
set(value) { set("widgetConfigs", value); mWidgetConfigs = value }
private var mArchiverEnabled: Boolean? = null
var archiverEnabled: Boolean
get() { mArchiverEnabled = mArchiverEnabled ?: values.get("archiverEnabled", true); return mArchiverEnabled ?: true }
set(value) { set("archiverEnabled", value); mArchiverEnabled = value }
private var mValidation: String? = null
var validation: String?
get() { mValidation = mValidation ?: values["buildValidation"]; return mValidation }
set(value) { set("buildValidation", value); mValidation = value }
private var mApiInvalidCert: String? = null
var apiInvalidCert: String?
get() { mApiInvalidCert = mApiInvalidCert ?: values["apiInvalidCert"]; return mApiInvalidCert }
set(value) { set("apiInvalidCert", value); mApiInvalidCert = value }
private var mApiAvailabilityCheck: Boolean? = null
var apiAvailabilityCheck: Boolean
get() { mApiAvailabilityCheck = mApiAvailabilityCheck ?: values.get("apiAvailabilityCheck", true); return mApiAvailabilityCheck ?: true }
set(value) { set("apiAvailabilityCheck", value); mApiAvailabilityCheck = value }
private var rawEntries: List<ConfigEntry> = db.configDao().getAllNow()
private val profileConfigs: HashMap<Int, ProfileConfig> = hashMapOf()
init {
rawEntries.toHashMap(-1, values)
}
fun migrate(app: App) { fun migrate(app: App) {
if (dataVersion < DATA_VERSION) if (dataVersion < DATA_VERSION || hash == "")
// migrate old data version OR freshly installed app (or updated from 3.x)
ConfigMigration(app, this) ConfigMigration(app, this)
} }
fun getFor(profileId: Int): ProfileConfig { fun getFor(profileId: Int): ProfileConfig {
return profileConfigs[profileId] ?: ProfileConfig(db, profileId, db.configDao().getAllNow(profileId)).also { return profileConfigs[profileId] ?: ProfileConfig(db, profileId, entries).also {
profileConfigs[profileId] = it profileConfigs[profileId] = it
} }
} }
fun forProfile() = getFor(App.profileId) fun forProfile() = getFor(App.profileId)
fun setProfile(profileId: Int) {
}
override fun set(key: String, value: String?) {
values[key] = value
launch {
db.configDao().add(ConfigEntry(-1, key, value))
}
}
} }

View File

@ -4,13 +4,10 @@
package pl.szczodrzynski.edziennik.config package pl.szczodrzynski.edziennik.config
import pl.szczodrzynski.edziennik.config.utils.get import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.ORDER_BY_DATE_DESC
import pl.szczodrzynski.edziennik.config.utils.set
import pl.szczodrzynski.edziennik.utils.managers.GradesManager
class ConfigGrades(private val config: Config) { @Suppress("RemoveExplicitTypeArguments")
private var mOrderBy: Int? = null class ConfigGrades(base: Config) {
var orderBy: Int
get() { mOrderBy = mOrderBy ?: config.values.get("gradesOrderBy", 0); return mOrderBy ?: GradesManager.ORDER_BY_DATE_DESC } var orderBy by base.config<Int>("gradesOrderBy", ORDER_BY_DATE_DESC)
set(value) { config.set("gradesOrderBy", value); mOrderBy = value }
} }

View File

@ -4,139 +4,53 @@
package pl.szczodrzynski.edziennik.config package pl.szczodrzynski.edziennik.config
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import pl.szczodrzynski.edziennik.BuildConfig import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.config.utils.get
import pl.szczodrzynski.edziennik.config.utils.getIntList
import pl.szczodrzynski.edziennik.config.utils.set
import pl.szczodrzynski.edziennik.config.utils.setMap
import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus
import pl.szczodrzynski.edziennik.ext.HOUR
import pl.szczodrzynski.edziennik.utils.models.Time import pl.szczodrzynski.edziennik.utils.models.Time
class ConfigSync(private val config: Config) { @Suppress("RemoveExplicitTypeArguments")
private val gson = Gson() class ConfigSync(base: Config) {
private var mDontShowAppManagerDialog: Boolean? = null var enabled by base.config<Boolean>("syncEnabled", true)
var dontShowAppManagerDialog: Boolean var interval by base.config<Int>("syncInterval", 1 * HOUR.toInt())
get() { mDontShowAppManagerDialog = mDontShowAppManagerDialog ?: config.values.get("dontShowAppManagerDialog", false); return mDontShowAppManagerDialog ?: false } var onlyWifi by base.config<Boolean>("syncOnlyWifi", false)
set(value) { config.set("dontShowAppManagerDialog", value); mDontShowAppManagerDialog = value }
private var mSyncEnabled: Boolean? = null var dontShowAppManagerDialog by base.config<Boolean>(false)
var enabled: Boolean var lastAppSync by base.config<Long>(0L)
get() { mSyncEnabled = mSyncEnabled ?: config.values.get("syncEnabled", true); return mSyncEnabled ?: true } var notifyAboutUpdates by base.config<Boolean>(true)
set(value) { config.set("syncEnabled", value); mSyncEnabled = value } var webPushEnabled by base.config<Boolean>(true)
private var mWebPushEnabled: Boolean? = null // Quiet Hours
var webPushEnabled: Boolean var quietHoursEnabled by base.config<Boolean>(false)
get() { mWebPushEnabled = mWebPushEnabled ?: config.values.get("webPushEnabled", true); return mWebPushEnabled ?: true } var quietHoursStart by base.config<Time?>(null)
set(value) { config.set("webPushEnabled", value); mWebPushEnabled = value } var quietHoursEnd by base.config<Time?>(null)
var quietDuringLessons by base.config<Boolean>(false)
private var mSyncOnlyWifi: Boolean? = null // FCM Tokens
var onlyWifi: Boolean var tokenApp by base.config<String?>(null)
get() { mSyncOnlyWifi = mSyncOnlyWifi ?: config.values.get("syncOnlyWifi", false); return mSyncOnlyWifi ?: notifyAboutUpdates } var tokenMobidziennik by base.config<String?>(null)
set(value) { config.set("syncOnlyWifi", value); mSyncOnlyWifi = value } var tokenLibrus by base.config<String?>(null)
var tokenVulcan by base.config<String?>(null)
var tokenVulcanHebe by base.config<String?>(null)
private var mSyncInterval: Int? = null var tokenMobidziennikList by base.config<List<Int>> { listOf() }
var interval: Int var tokenLibrusList by base.config<List<Int>> { listOf() }
get() { mSyncInterval = mSyncInterval ?: config.values.get("syncInterval", 60*60); return mSyncInterval ?: 60*60 } var tokenVulcanList by base.config<List<Int>> { listOf() }
set(value) { config.set("syncInterval", value); mSyncInterval = value } var tokenVulcanHebeList by base.config<List<Int>> { listOf() }
private var mNotifyAboutUpdates: Boolean? = null // Register Availability
var notifyAboutUpdates: Boolean private var registerAvailabilityMap by base.config<Map<String, RegisterAvailabilityStatus>>("registerAvailability") { mapOf() }
get() { mNotifyAboutUpdates = mNotifyAboutUpdates ?: config.values.get("notifyAboutUpdates", true); return mNotifyAboutUpdates ?: true } private var registerAvailabilityFlavor by base.config<String?>(null)
set(value) { config.set("notifyAboutUpdates", value); mNotifyAboutUpdates = value }
private var mLastAppSync: Long? = null
var lastAppSync: Long
get() { mLastAppSync = mLastAppSync ?: config.values.get("lastAppSync", 0L); return mLastAppSync ?: 0L }
set(value) { config.set("lastAppSync", value); mLastAppSync = value }
/* ____ _ _ _
/ __ \ (_) | | | |
| | | |_ _ _ ___| |_ | |__ ___ _ _ _ __ ___
| | | | | | | |/ _ \ __| | '_ \ / _ \| | | | '__/ __|
| |__| | |_| | | __/ |_ | | | | (_) | |_| | | \__ \
\___\_\\__,_|_|\___|\__| |_| |_|\___/ \__,_|_| |__*/
private var mQuietHoursEnabled: Boolean? = null
var quietHoursEnabled: Boolean
get() { mQuietHoursEnabled = mQuietHoursEnabled ?: config.values.get("quietHoursEnabled", false); return mQuietHoursEnabled ?: false }
set(value) { config.set("quietHoursEnabled", value); mQuietHoursEnabled = value }
private var mQuietHoursStart: Time? = null
var quietHoursStart: Time?
get() { mQuietHoursStart = mQuietHoursStart ?: config.values.get("quietHoursStart", null as Time?); return mQuietHoursStart }
set(value) { config.set("quietHoursStart", value); mQuietHoursStart = value }
private var mQuietHoursEnd: Time? = null
var quietHoursEnd: Time?
get() { mQuietHoursEnd = mQuietHoursEnd ?: config.values.get("quietHoursEnd", null as Time?); return mQuietHoursEnd }
set(value) { config.set("quietHoursEnd", value); mQuietHoursEnd = value }
private var mQuietDuringLessons: Boolean? = null
var quietDuringLessons: Boolean
get() { mQuietDuringLessons = mQuietDuringLessons ?: config.values.get("quietDuringLessons", false); return mQuietDuringLessons ?: false }
set(value) { config.set("quietDuringLessons", value); mQuietDuringLessons = value }
/* ______ _____ __ __ _______ _
| ____/ ____| \/ | |__ __| | |
| |__ | | | \ / | | | ___ | | _____ _ __ ___
| __|| | | |\/| | | |/ _ \| |/ / _ \ '_ \/ __|
| | | |____| | | | | | (_) | < __/ | | \__ \
|_| \_____|_| |_| |_|\___/|_|\_\___|_| |_|__*/
private var mTokenApp: String? = null
var tokenApp: String?
get() { mTokenApp = mTokenApp ?: config.values.get("tokenApp", null as String?); return mTokenApp }
set(value) { config.set("tokenApp", value); mTokenApp = value }
private var mTokenMobidziennik: String? = null
var tokenMobidziennik: String?
get() { mTokenMobidziennik = mTokenMobidziennik ?: config.values.get("tokenMobidziennik", null as String?); return mTokenMobidziennik }
set(value) { config.set("tokenMobidziennik", value); mTokenMobidziennik = value }
private var mTokenLibrus: String? = null
var tokenLibrus: String?
get() { mTokenLibrus = mTokenLibrus ?: config.values.get("tokenLibrus", null as String?); return mTokenLibrus }
set(value) { config.set("tokenLibrus", value); mTokenLibrus = value }
private var mTokenVulcan: String? = null
var tokenVulcan: String?
get() { mTokenVulcan = mTokenVulcan ?: config.values.get("tokenVulcan", null as String?); return mTokenVulcan }
set(value) { config.set("tokenVulcan", value); mTokenVulcan = value }
private var mTokenVulcanHebe: String? = null
var tokenVulcanHebe: String?
get() { mTokenVulcanHebe = mTokenVulcanHebe ?: config.values.get("tokenVulcanHebe", null as String?); return mTokenVulcanHebe }
set(value) { config.set("tokenVulcanHebe", value); mTokenVulcanHebe = value }
private var mTokenMobidziennikList: List<Int>? = null
var tokenMobidziennikList: List<Int>
get() { mTokenMobidziennikList = mTokenMobidziennikList ?: config.values.getIntList("tokenMobidziennikList", listOf()); return mTokenMobidziennikList ?: listOf() }
set(value) { config.set("tokenMobidziennikList", value); mTokenMobidziennikList = value }
private var mTokenLibrusList: List<Int>? = null
var tokenLibrusList: List<Int>
get() { mTokenLibrusList = mTokenLibrusList ?: config.values.getIntList("tokenLibrusList", listOf()); return mTokenLibrusList ?: listOf() }
set(value) { config.set("tokenLibrusList", value); mTokenLibrusList = value }
private var mTokenVulcanList: List<Int>? = null
var tokenVulcanList: List<Int>
get() { mTokenVulcanList = mTokenVulcanList ?: config.values.getIntList("tokenVulcanList", listOf()); return mTokenVulcanList ?: listOf() }
set(value) { config.set("tokenVulcanList", value); mTokenVulcanList = value }
private var mTokenVulcanHebeList: List<Int>? = null
var tokenVulcanHebeList: List<Int>
get() { mTokenVulcanHebeList = mTokenVulcanHebeList ?: config.values.getIntList("tokenVulcanHebeList", listOf()); return mTokenVulcanHebeList ?: listOf() }
set(value) { config.set("tokenVulcanHebeList", value); mTokenVulcanHebeList = value }
private var mRegisterAvailability: Map<String, RegisterAvailabilityStatus>? = null
var registerAvailability: Map<String, RegisterAvailabilityStatus> var registerAvailability: Map<String, RegisterAvailabilityStatus>
get() { get() {
val flavor = config.values.get("registerAvailabilityFlavor", null as String?) if (BuildConfig.FLAVOR != registerAvailabilityFlavor)
if (BuildConfig.FLAVOR != flavor)
return mapOf() return mapOf()
return registerAvailabilityMap
mRegisterAvailability = mRegisterAvailability ?: config.values.get("registerAvailability", null as String?)?.let { it ->
gson.fromJson(it, object: TypeToken<Map<String, RegisterAvailabilityStatus>>(){}.type)
}
return mRegisterAvailability ?: mapOf()
} }
set(value) { set(value) {
config.setMap("registerAvailability", value) registerAvailabilityMap = value
config.set("registerAvailabilityFlavor", BuildConfig.FLAVOR) registerAvailabilityFlavor = BuildConfig.FLAVOR
mRegisterAvailability = value
} }
} }

View File

@ -4,23 +4,12 @@
package pl.szczodrzynski.edziennik.config package pl.szczodrzynski.edziennik.config
import pl.szczodrzynski.edziennik.config.utils.get
import pl.szczodrzynski.edziennik.config.utils.set
import pl.szczodrzynski.edziennik.utils.models.Time import pl.szczodrzynski.edziennik.utils.models.Time
class ConfigTimetable(private val config: Config) { @Suppress("RemoveExplicitTypeArguments")
private var mBellSyncMultiplier: Int? = null class ConfigTimetable(base: Config) {
var bellSyncMultiplier: Int
get() { mBellSyncMultiplier = mBellSyncMultiplier ?: config.values.get("bellSyncMultiplier", 0); return mBellSyncMultiplier ?: 0 }
set(value) { config.set("bellSyncMultiplier", value); mBellSyncMultiplier = value }
private var mBellSyncDiff: Time? = null var bellSyncMultiplier by base.config<Int>(0)
var bellSyncDiff: Time? var bellSyncDiff by base.config<Time?>(null)
get() { mBellSyncDiff = mBellSyncDiff ?: config.values.get("bellSyncDiff", null as Time?); return mBellSyncDiff } var countInSeconds by base.config<Boolean>(false)
set(value) { config.set("bellSyncDiff", value); mBellSyncDiff = value } }
private var mCountInSeconds: Boolean? = null
var countInSeconds: Boolean
get() { mCountInSeconds = mCountInSeconds ?: config.values.get("countInSeconds", false); return mCountInSeconds ?: false }
set(value) { config.set("countInSeconds", value); mCountInSeconds = value }
}

View File

@ -4,60 +4,32 @@
package pl.szczodrzynski.edziennik.config package pl.szczodrzynski.edziennik.config
import pl.szczodrzynski.edziennik.config.utils.get
import pl.szczodrzynski.edziennik.config.utils.getIntList
import pl.szczodrzynski.edziennik.config.utils.set
import pl.szczodrzynski.edziennik.ext.asNavTargetOrNull
import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget
class ConfigUI(private val config: Config) { @Suppress("RemoveExplicitTypeArguments")
private var mTheme: Int? = null class ConfigUI(base: Config) {
var theme: Int
get() { mTheme = mTheme ?: config.values.get("theme", 1); return mTheme ?: 1 }
set(value) { config.set("theme", value); mTheme = value }
private var mLanguage: String? = null var theme by base.config<Int>(1)
var language: String? var language by base.config<String?>(null)
get() { mLanguage = mLanguage ?: config.values.get("language", null as String?); return mLanguage }
set(value) { config.set("language", value); mLanguage = value }
private var mHeaderBackground: String? = null var appBackground by base.config<String?>("appBg", null)
var headerBackground: String? var headerBackground by base.config<String?>("headerBg", null)
get() { mHeaderBackground = mHeaderBackground ?: config.values.get("headerBg", null as String?); return mHeaderBackground }
set(value) { config.set("headerBg", value); mHeaderBackground = value }
private var mAppBackground: String? = null var miniMenuVisible by base.config<Boolean>(false)
var appBackground: String? var miniMenuButtons by base.config<Set<NavTarget>> {
get() { mAppBackground = mAppBackground ?: config.values.get("appBg", null as String?); return mAppBackground } setOf(
set(value) { config.set("appBg", value); mAppBackground = value } NavTarget.HOME,
NavTarget.TIMETABLE,
NavTarget.AGENDA,
NavTarget.GRADES,
NavTarget.MESSAGES,
NavTarget.HOMEWORK,
NavTarget.SETTINGS
)
}
var openDrawerOnBackPressed by base.config<Boolean>(false)
private var mMiniMenuVisible: Boolean? = null var bottomSheetOpened by base.config<Boolean>(false)
var miniMenuVisible: Boolean var snowfall by base.config<Boolean>(false)
get() { mMiniMenuVisible = mMiniMenuVisible ?: config.values.get("miniMenuVisible", false); return mMiniMenuVisible ?: false } var eggfall by base.config<Boolean>(false)
set(value) { config.set("miniMenuVisible", value); mMiniMenuVisible = value }
private var mMiniMenuButtons: Set<NavTarget>? = null
var miniMenuButtons: Set<NavTarget>
get() { mMiniMenuButtons = mMiniMenuButtons ?: config.values.getIntList("miniMenuButtons", listOf())?.mapNotNull { it.asNavTargetOrNull() }?.toSet(); return mMiniMenuButtons ?: setOf() }
set(value) { config.set("miniMenuButtons", value.map { it.id }); mMiniMenuButtons = value }
private var mOpenDrawerOnBackPressed: Boolean? = null
var openDrawerOnBackPressed: Boolean
get() { mOpenDrawerOnBackPressed = mOpenDrawerOnBackPressed ?: config.values.get("openDrawerOnBackPressed", false); return mOpenDrawerOnBackPressed ?: false }
set(value) { config.set("openDrawerOnBackPressed", value); mOpenDrawerOnBackPressed = value }
private var mSnowfall: Boolean? = null
var snowfall: Boolean
get() { mSnowfall = mSnowfall ?: config.values.get("snowfall", false); return mSnowfall ?: false }
set(value) { config.set("snowfall", value); mSnowfall = value }
private var mEggfall: Boolean? = null
var eggfall: Boolean
get() { mEggfall = mEggfall ?: config.values.get("eggfall", false); return mEggfall ?: false }
set(value) { config.set("eggfall", value); mEggfall = value }
private var mBottomSheetOpened: Boolean? = null
var bottomSheetOpened: Boolean
get() { mBottomSheetOpened = mBottomSheetOpened ?: config.values.get("bottomSheetOpened", false); return mBottomSheetOpened ?: false }
set(value) { config.set("bottomSheetOpened", value); mBottomSheetOpened = value }
} }

View File

@ -0,0 +1,175 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-21.
*/
package pl.szczodrzynski.edziennik.config
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.reflect.TypeToken
import pl.szczodrzynski.edziennik.ext.*
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import java.lang.reflect.ParameterizedType
import java.lang.reflect.WildcardType
import kotlin.reflect.KProperty
private val gson = Gson()
inline fun <reified T> BaseConfig.config(name: String? = null, noinline default: () -> T) = ConfigDelegate(
config = this,
type = T::class.java,
nullable = null is T,
typeToken = object : TypeToken<T>() {},
defaultFunc = default,
defaultValue = null,
fieldName = name,
)
inline fun <reified T> BaseConfig.config(default: T) = ConfigDelegate(
config = this,
type = T::class.java,
nullable = null is T,
typeToken = object : TypeToken<T>() {},
defaultFunc = null,
defaultValue = default,
fieldName = null,
)
inline fun <reified T> BaseConfig.config(name: String? = null, default: T) = ConfigDelegate(
config = this,
type = T::class.java,
nullable = null is T,
typeToken = object : TypeToken<T>() {},
defaultFunc = null,
defaultValue = default,
fieldName = name,
)
@Suppress("UNCHECKED_CAST")
class ConfigDelegate<T>(
private val config: BaseConfig,
private val type: Class<T>,
private val nullable: Boolean,
private val typeToken: TypeToken<T>,
private val defaultFunc: (() -> T)?,
private val defaultValue: T?,
private val fieldName: String?,
) {
private var value: T? = null
private var isInitialized = false
private fun getDefault(): T = when {
defaultFunc != null -> defaultFunc.invoke()
else -> defaultValue as T
}
private fun getGenericType(index: Int = 0): Class<*> {
val parameterizedType = typeToken.type as ParameterizedType
val typeArgument = parameterizedType.actualTypeArguments[index] as WildcardType
return typeArgument.upperBounds[0] as Class<*>
}
operator fun setValue(_thisRef: Any, property: KProperty<*>, newValue: T) {
value = newValue
isInitialized = true
config.set(fieldName ?: property.name, serialize(newValue)?.toString())
}
operator fun getValue(_thisRef: Any, property: KProperty<*>): T {
if (isInitialized)
return value as T
val key = fieldName ?: property.name
if (key !in config.values) {
value = getDefault()
isInitialized = true
return value as T
}
val str = config.values[key]
value = if (str == null && nullable)
null as T
else if (str == null)
getDefault()
else
deserialize(str)
isInitialized = true
return value as T
}
private fun <I> serialize(value: I?, serializeObjects: Boolean = true): Any? {
if (value == null)
return null
return when (value) {
is String -> value
is Date -> value.stringY_m_d
is Time -> value.stringValue
is JsonObject -> value
is JsonArray -> value
// primitives
is Number -> value
is Boolean -> value
// enums, maps & collections
is Enum<*> -> value.toInt()
is Collection<*> -> JsonArray(value.map {
if (it is Number || it is Boolean) it else serialize(it, serializeObjects = false)
})
is Map<*, *> -> gson.toJson(value.mapValues { (_, it) ->
if (it is Number || it is Boolean) it else serialize(it, serializeObjects = false)
})
// objects or else
else -> if (serializeObjects) gson.toJson(value) else value
}
}
private fun <I> deserialize(value: String?, type: Class<*> = this.type): I? {
if (value == null)
return null
@Suppress("TYPE_MISMATCH_WARNING")
return when (type) {
String::class.java -> value
Date::class.java -> Date.fromY_m_d(value)
Time::class.java -> Time.fromHms(value)
JsonObject::class.java -> value.toJsonObject()
JsonArray::class.java -> value.toJsonArray()
// primitives
java.lang.Integer::class.java -> value.toIntOrNull()
java.lang.Boolean::class.java -> value.toBooleanStrictOrNull()
java.lang.Long::class.java -> value.toLongOrNull()
java.lang.Float::class.java -> value.toFloatOrNull()
// enums, maps & collections
else -> when {
Enum::class.java.isAssignableFrom(type) -> value.toIntOrNull()?.toEnum(type) as Enum<*>
Collection::class.java.isAssignableFrom(type) -> {
val array = value.toJsonArray()
val genericType = getGenericType()
val list = array?.map {
val str = if (it.isJsonPrimitive) it.asString else it.toString()
deserialize<Any>(str, genericType)
}
when {
List::class.java.isAssignableFrom(type) -> list
Set::class.java.isAssignableFrom(type) -> list?.toSet()
else -> list?.toTypedArray()
}
}
Map::class.java.isAssignableFrom(type) -> {
val obj = value.toJsonObject()
val genericType = getGenericType(index = 1)
val map = obj?.entrySet()?.associate { (key, it) ->
val str = if (it.isJsonPrimitive) it.asString else it.toString()
key to deserialize<Any>(str, genericType)
}
map
}
// objects or else
else -> gson.fromJson(value, type)
}
} as? I
}
}

View File

@ -4,29 +4,20 @@
package pl.szczodrzynski.edziennik.config package pl.szczodrzynski.edziennik.config
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import pl.szczodrzynski.edziennik.config.db.ConfigEntry import pl.szczodrzynski.edziennik.config.db.ConfigEntry
import pl.szczodrzynski.edziennik.config.utils.ProfileConfigMigration import pl.szczodrzynski.edziennik.config.utils.ProfileConfigMigration
import pl.szczodrzynski.edziennik.config.utils.get
import pl.szczodrzynski.edziennik.config.utils.set
import pl.szczodrzynski.edziennik.config.utils.toHashMap
import pl.szczodrzynski.edziennik.data.db.AppDb import pl.szczodrzynski.edziennik.data.db.AppDb
import kotlin.coroutines.CoroutineContext
class ProfileConfig(val db: AppDb, val profileId: Int, rawEntries: List<ConfigEntry>) : CoroutineScope, AbstractConfig { @Suppress("RemoveExplicitTypeArguments")
class ProfileConfig(
db: AppDb,
profileId: Int,
entries: List<ConfigEntry>?,
) : BaseConfig(db, profileId, entries) {
companion object { companion object {
const val DATA_VERSION = 3 const val DATA_VERSION = 4
} }
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Default
val values: HashMap<String, String?> = hashMapOf()
val grades by lazy { ProfileConfigGrades(this) } val grades by lazy { ProfileConfigGrades(this) }
val ui by lazy { ProfileConfigUI(this) } val ui by lazy { ProfileConfigUI(this) }
val sync by lazy { ProfileConfigSync(this) } val sync by lazy { ProfileConfigSync(this) }
@ -35,26 +26,11 @@ class ProfileConfig(val db: AppDb, val profileId: Int, rawEntries: List<ConfigEn
val timetable by lazy { ConfigTimetable(this) } val timetable by lazy { ConfigTimetable(this) }
val grades by lazy { ConfigGrades(this) }*/ val grades by lazy { ConfigGrades(this) }*/
private var mDataVersion: Int? = null var dataVersion by config<Int>(DATA_VERSION)
var dataVersion: Int var hash by config<String>("")
get() { mDataVersion = mDataVersion ?: values.get("dataVersion", 0); return mDataVersion ?: 0 }
set(value) { set("dataVersion", value); mDataVersion = value }
private var mHash: String? = null
var hash: String
get() { mHash = mHash ?: values.get("hash", ""); return mHash ?: "" }
set(value) { set("hash", value); mHash = value }
init { init {
rawEntries.toHashMap(profileId, values)
if (dataVersion < DATA_VERSION) if (dataVersion < DATA_VERSION)
ProfileConfigMigration(this) ProfileConfigMigration(this)
} }
override fun set(key: String, value: String?) {
values[key] = value
launch {
db.configDao().add(ConfigEntry(profileId, key, value))
}
}
} }

View File

@ -4,27 +4,11 @@
package pl.szczodrzynski.edziennik.config package pl.szczodrzynski.edziennik.config
import pl.szczodrzynski.edziennik.config.utils.get @Suppress("RemoveExplicitTypeArguments")
import pl.szczodrzynski.edziennik.config.utils.set class ProfileConfigAttendance(base: ProfileConfig) {
class ProfileConfigAttendance(private val config: ProfileConfig) { var attendancePageSelection by base.config<Int>(1)
private var mAttendancePageSelection: Int? = null var groupConsecutiveDays by base.config<Boolean>(true)
var attendancePageSelection: Int var showPresenceInMonth by base.config<Boolean>(false)
get() { mAttendancePageSelection = mAttendancePageSelection ?: config.values.get("attendancePageSelection", 1); return mAttendancePageSelection ?: 1 } var useSymbols by base.config<Boolean>(false)
set(value) { config.set("attendancePageSelection", value); mAttendancePageSelection = value }
private var mUseSymbols: Boolean? = null
var useSymbols: Boolean
get() { mUseSymbols = mUseSymbols ?: config.values.get("useSymbols", false); return mUseSymbols ?: false }
set(value) { config.set("useSymbols", value); mUseSymbols = value }
private var mGroupConsecutiveDays: Boolean? = null
var groupConsecutiveDays: Boolean
get() { mGroupConsecutiveDays = mGroupConsecutiveDays ?: config.values.get("groupConsecutiveDays", true); return mGroupConsecutiveDays ?: true }
set(value) { config.set("groupConsecutiveDays", value); mGroupConsecutiveDays = value }
private var mShowPresenceInMonth: Boolean? = null
var showPresenceInMonth: Boolean
get() { mShowPresenceInMonth = mShowPresenceInMonth ?: config.values.get("showPresenceInMonth", false); return mShowPresenceInMonth ?: false }
set(value) { config.set("showPresenceInMonth", value); mShowPresenceInMonth = value }
} }

View File

@ -4,54 +4,19 @@
package pl.szczodrzynski.edziennik.config package pl.szczodrzynski.edziennik.config
import pl.szczodrzynski.edziennik.config.utils.get
import pl.szczodrzynski.edziennik.config.utils.getFloat
import pl.szczodrzynski.edziennik.config.utils.set
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_WEIGHTED import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_WEIGHTED
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_ALL_GRADES import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_ALL_GRADES
class ProfileConfigGrades(private val config: ProfileConfig) { @Suppress("RemoveExplicitTypeArguments")
private var mColorMode: Int? = null class ProfileConfigGrades(base: ProfileConfig) {
var colorMode: Int
get() { mColorMode = mColorMode ?: config.values.get("gradesColorMode", COLOR_MODE_WEIGHTED); return mColorMode ?: COLOR_MODE_WEIGHTED }
set(value) { config.set("gradesColorMode", value); mColorMode = value }
private var mYearAverageMode: Int? = null var averageWithoutWeight by base.config<Boolean>(true)
var yearAverageMode: Int var colorMode by base.config<Int>(COLOR_MODE_WEIGHTED)
get() { mYearAverageMode = mYearAverageMode ?: config.values.get("yearAverageMode", YEAR_ALL_GRADES); return mYearAverageMode ?: YEAR_ALL_GRADES } var dontCountEnabled by base.config<Boolean>(false)
set(value) { config.set("yearAverageMode", value); mYearAverageMode = value } var dontCountGrades by base.config<List<String>> { listOf() }
var hideImproved by base.config<Boolean>(false)
private var mHideImproved: Boolean? = null var hideSticksFromOld by base.config<Boolean>(false)
var hideImproved: Boolean var minusValue by base.config<Float?>(null)
get() { mHideImproved = mHideImproved ?: config.values.get("hideImproved", false); return mHideImproved ?: false } var plusValue by base.config<Float?>(null)
set(value) { config.set("hideImproved", value); mHideImproved = value } var yearAverageMode by base.config<Int>(YEAR_ALL_GRADES)
private var mAverageWithoutWeight: Boolean? = null
var averageWithoutWeight: Boolean
get() { mAverageWithoutWeight = mAverageWithoutWeight ?: config.values.get("averageWithoutWeight", true); return mAverageWithoutWeight ?: true }
set(value) { config.set("averageWithoutWeight", value); mAverageWithoutWeight = value }
private var mPlusValue: Float? = null
var plusValue: Float?
get() { mPlusValue = mPlusValue ?: config.values.getFloat("plusValue"); return mPlusValue }
set(value) { config.set("plusValue", value); mPlusValue = value }
private var mMinusValue: Float? = null
var minusValue: Float?
get() { mMinusValue = mMinusValue ?: config.values.getFloat("minusValue"); return mMinusValue }
set(value) { config.set("minusValue", value); mMinusValue = value }
private var mDontCountEnabled: Boolean? = null
var dontCountEnabled: Boolean
get() { mDontCountEnabled = mDontCountEnabled ?: config.values.get("dontCountEnabled", false); return mDontCountEnabled ?: false }
set(value) { config.set("dontCountEnabled", value); mDontCountEnabled = value }
private var mDontCountGrades: List<String>? = null
var dontCountGrades: List<String>
get() { mDontCountGrades = mDontCountGrades ?: config.values.get("dontCountGrades", listOf()); return mDontCountGrades ?: listOf() }
set(value) { config.set("dontCountGrades", value); mDontCountGrades = value }
private var mHideSticksFromOld: Boolean? = null
var hideSticksFromOld: Boolean
get() { mHideSticksFromOld = mHideSticksFromOld ?: config.values.get("hideSticksFromOld", false); return mHideSticksFromOld ?: false }
set(value) { config.set("hideSticksFromOld", value); mHideSticksFromOld = value }
} }

View File

@ -4,14 +4,14 @@
package pl.szczodrzynski.edziennik.config package pl.szczodrzynski.edziennik.config
import pl.szczodrzynski.edziennik.config.utils.getIntList
import pl.szczodrzynski.edziennik.config.utils.set
import pl.szczodrzynski.edziennik.data.db.enums.NotificationType import pl.szczodrzynski.edziennik.data.db.enums.NotificationType
import pl.szczodrzynski.edziennik.ext.asNotificationTypeOrNull
class ProfileConfigSync(private val config: ProfileConfig) { @Suppress("RemoveExplicitTypeArguments")
private var mNotificationFilter: Set<NotificationType>? = null class ProfileConfigSync(base: ProfileConfig) {
var notificationFilter: Set<NotificationType>
get() { mNotificationFilter = mNotificationFilter ?: config.values.getIntList("notificationFilter", listOf())?.mapNotNull { it.asNotificationTypeOrNull() }?.toSet(); return mNotificationFilter ?: setOf() } var notificationFilter by base.config<Set<NotificationType>> {
set(value) { config.set("notificationFilter", value.map { it.id }); mNotificationFilter = value } NotificationType.values()
.filter { it.enabledByDefault == false }
.toSet()
}
} }

View File

@ -4,89 +4,29 @@
package pl.szczodrzynski.edziennik.config package pl.szczodrzynski.edziennik.config
import pl.szczodrzynski.edziennik.config.utils.get
import pl.szczodrzynski.edziennik.config.utils.set
import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.AGENDA_DEFAULT import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.AGENDA_DEFAULT
import pl.szczodrzynski.edziennik.ui.home.HomeCardModel import pl.szczodrzynski.edziennik.ui.home.HomeCardModel
class ProfileConfigUI(private val config: ProfileConfig) { @Suppress("RemoveExplicitTypeArguments")
private var mAgendaViewType: Int? = null class ProfileConfigUI(base: ProfileConfig) {
var agendaViewType: Int
get() { mAgendaViewType = mAgendaViewType ?: config.values.get("agendaViewType", 0); return mAgendaViewType ?: AGENDA_DEFAULT }
set(value) { config.set("agendaViewType", value); mAgendaViewType = value }
private var mAgendaCompactMode: Boolean? = null var agendaViewType by base.config<Int>(AGENDA_DEFAULT)
var agendaCompactMode: Boolean var agendaCompactMode by base.config<Boolean>(false)
get() { mAgendaCompactMode = mAgendaCompactMode ?: config.values.get("agendaCompactMode", false); return mAgendaCompactMode ?: false } var agendaGroupByType by base.config<Boolean>(false)
set(value) { config.set("agendaCompactMode", value); mAgendaCompactMode = value } var agendaLessonChanges by base.config<Boolean>(true)
var agendaTeacherAbsence by base.config<Boolean>(true)
var agendaElearningMark by base.config<Boolean>(false)
var agendaElearningGroup by base.config<Boolean>(true)
private var mAgendaGroupByType: Boolean? = null var homeCards by base.config<List<HomeCardModel>> { listOf() }
var agendaGroupByType: Boolean
get() { mAgendaGroupByType = mAgendaGroupByType ?: config.values.get("agendaGroupByType", false); return mAgendaGroupByType ?: false }
set(value) { config.set("agendaGroupByType", value); mAgendaGroupByType = value }
private var mAgendaLessonChanges: Boolean? = null var messagesGreetingOnCompose by base.config<Boolean>(true)
var agendaLessonChanges: Boolean var messagesGreetingOnReply by base.config<Boolean>(true)
get() { mAgendaLessonChanges = mAgendaLessonChanges ?: config.values.get("agendaLessonChanges", true); return mAgendaLessonChanges ?: true } var messagesGreetingOnForward by base.config<Boolean>(false)
set(value) { config.set("agendaLessonChanges", value); mAgendaLessonChanges = value } var messagesGreetingText by base.config<String?>(null)
private var mAgendaTeacherAbsence: Boolean? = null var timetableShowAttendance by base.config<Boolean>(true)
var agendaTeacherAbsence: Boolean var timetableShowEvents by base.config<Boolean>(true)
get() { mAgendaTeacherAbsence = mAgendaTeacherAbsence ?: config.values.get("agendaTeacherAbsence", true); return mAgendaTeacherAbsence ?: true } var timetableTrimHourRange by base.config<Boolean>(false)
set(value) { config.set("agendaTeacherAbsence", value); mAgendaTeacherAbsence = value } var timetableColorSubjectName by base.config<Boolean>(false)
private var mAgendaElearningMark: Boolean? = null
var agendaElearningMark: Boolean
get() { mAgendaElearningMark = mAgendaElearningMark ?: config.values.get("agendaElearningMark", false); return mAgendaElearningMark ?: false }
set(value) { config.set("agendaElearningMark", value); mAgendaElearningMark = value }
private var mAgendaElearningGroup: Boolean? = null
var agendaElearningGroup: Boolean
get() { mAgendaElearningGroup = mAgendaElearningGroup ?: config.values.get("agendaElearningGroup", true); return mAgendaElearningGroup ?: true }
set(value) { config.set("agendaElearningGroup", value); mAgendaElearningGroup = value }
private var mHomeCards: List<HomeCardModel>? = null
var homeCards: List<HomeCardModel>
get() { mHomeCards = mHomeCards ?: config.values.get("homeCards", listOf(), HomeCardModel::class.java); return mHomeCards ?: listOf() }
set(value) { config.set("homeCards", value); mHomeCards = value }
private var mMessagesGreetingOnCompose: Boolean? = null
var messagesGreetingOnCompose: Boolean
get() { mMessagesGreetingOnCompose = mMessagesGreetingOnCompose ?: config.values.get("messagesGreetingOnCompose", true); return mMessagesGreetingOnCompose ?: true }
set(value) { config.set("messagesGreetingOnCompose", value); mMessagesGreetingOnCompose = value }
private var mMessagesGreetingOnReply: Boolean? = null
var messagesGreetingOnReply: Boolean
get() { mMessagesGreetingOnReply = mMessagesGreetingOnReply ?: config.values.get("messagesGreetingOnReply", true); return mMessagesGreetingOnReply ?: true }
set(value) { config.set("messagesGreetingOnReply", value); mMessagesGreetingOnReply = value }
private var mMessagesGreetingOnForward: Boolean? = null
var messagesGreetingOnForward: Boolean
get() { mMessagesGreetingOnForward = mMessagesGreetingOnForward ?: config.values.get("messagesGreetingOnForward", false); return mMessagesGreetingOnForward ?: false }
set(value) { config.set("messagesGreetingOnForward", value); mMessagesGreetingOnForward = value }
private var mMessagesGreetingText: String? = null
var messagesGreetingText: String?
get() { mMessagesGreetingText = mMessagesGreetingText ?: config.values["messagesGreetingText"]; return mMessagesGreetingText }
set(value) { config.set("messagesGreetingText", value); mMessagesGreetingText = value }
private var mTimetableShowAttendance: Boolean? = null
var timetableShowAttendance: Boolean
get() { mTimetableShowAttendance = mTimetableShowAttendance ?: config.values.get("timetableShowAttendance", true); return mTimetableShowAttendance ?: true }
set(value) { config.set("timetableShowAttendance", value); mTimetableShowAttendance = value }
private var mTimetableShowEvents: Boolean? = null
var timetableShowEvents: Boolean
get() { mTimetableShowEvents = mTimetableShowEvents ?: config.values.get("timetableShowEvents", true); return mTimetableShowEvents ?: true }
set(value) { config.set("timetableShowEvents", value); mTimetableShowEvents = value }
private var mTimetableTrimHourRange: Boolean? = null
var timetableTrimHourRange: Boolean
get() { mTimetableTrimHourRange = mTimetableTrimHourRange ?: config.values.get("timetableTrimHourRange", false); return mTimetableTrimHourRange ?: false }
set(value) { config.set("timetableTrimHourRange", value); mTimetableTrimHourRange = value }
private var mTimetableColorSubjectName: Boolean? = null
var timetableColorSubjectName: Boolean
get() { mTimetableColorSubjectName = mTimetableColorSubjectName ?: config.values.get("timetableColorSubjectName", false); return mTimetableColorSubjectName ?: false }
set(value) { config.set("timetableColorSubjectName", value); mTimetableColorSubjectName = value }
} }

View File

@ -17,7 +17,7 @@ interface ConfigDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun addAll(list: List<ConfigEntry>) fun addAll(list: List<ConfigEntry>)
@Query("SELECT * FROM config WHERE profileId = -1") @Query("SELECT * FROM config")
fun getAllNow(): List<ConfigEntry> fun getAllNow(): List<ConfigEntry>
@Query("SELECT * FROM config WHERE profileId = :profileId") @Query("SELECT * FROM config WHERE profileId = :profileId")
@ -25,4 +25,4 @@ interface ConfigDao {
@Query("DELETE FROM config WHERE profileId = :profileId") @Query("DELETE FROM config WHERE profileId = :profileId")
fun clear(profileId: Int) fun clear(profileId: Int)
} }

View File

@ -16,9 +16,9 @@ import pl.szczodrzynski.edziennik.utils.models.Time
import kotlin.math.abs import kotlin.math.abs
class AppConfigMigrationV3(p: SharedPreferences, config: Config) { class AppConfigMigrationV3(p: SharedPreferences, config: Config) {
init { config.apply { init {
val s = "app.appConfig" val s = "app.appConfig"
if (dataVersion < 1) { config.apply {
ui.theme = p.getString("$s.appTheme", null)?.toIntOrNull() ?: 1 ui.theme = p.getString("$s.appTheme", null)?.toIntOrNull() ?: 1
sync.enabled = p.getString("$s.registerSyncEnabled", null)?.toBoolean() ?: true sync.enabled = p.getString("$s.registerSyncEnabled", null)?.toBoolean() ?: true
sync.interval = p.getString("$s.registerSyncInterval", null)?.toIntOrNull() ?: 3600 sync.interval = p.getString("$s.registerSyncInterval", null)?.toIntOrNull() ?: 3600
@ -37,9 +37,6 @@ class AppConfigMigrationV3(p: SharedPreferences, config: Config) {
NavTarget.HOMEWORK, NavTarget.HOMEWORK,
NavTarget.SETTINGS NavTarget.SETTINGS
) )
dataVersion = 1
}
if (dataVersion < 2) {
devModePassword = p.getString("$s.devModePassword", null).fix() devModePassword = p.getString("$s.devModePassword", null).fix()
sync.tokenApp = p.getString("$s.fcmToken", null).fix() sync.tokenApp = p.getString("$s.fcmToken", null).fix()
timetable.bellSyncMultiplier = p.getString("$s.bellSyncMultiplier", null)?.toIntOrNull() ?: 0 timetable.bellSyncMultiplier = p.getString("$s.bellSyncMultiplier", null)?.toIntOrNull() ?: 0
@ -86,9 +83,8 @@ class AppConfigMigrationV3(p: SharedPreferences, config: Config) {
LoginType.LIBRUS.id -> sync.tokenLibrus = token LoginType.LIBRUS.id -> sync.tokenLibrus = token
} }
} }
dataVersion = 2
} }
}} }
private fun String?.fix(): String? { private fun String?.fix(): String? {
return this?.replace("\"", "")?.let { if (it == "null") null else it } return this?.replace("\"", "")?.let { if (it == "null") null else it }

View File

@ -1,113 +0,0 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-27.
*/
package pl.szczodrzynski.edziennik.config.utils
import com.google.gson.*
import com.google.gson.reflect.TypeToken
import pl.szczodrzynski.edziennik.config.AbstractConfig
import pl.szczodrzynski.edziennik.config.db.ConfigEntry
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
private val gson = Gson()
fun AbstractConfig.set(key: String, value: Int) {
set(key, value.toString())
}
fun AbstractConfig.set(key: String, value: Boolean) {
set(key, value.toString())
}
fun AbstractConfig.set(key: String, value: Long) {
set(key, value.toString())
}
fun AbstractConfig.set(key: String, value: Float) {
set(key, value.toString())
}
fun AbstractConfig.set(key: String, value: Date?) {
set(key, value?.stringY_m_d)
}
fun AbstractConfig.set(key: String, value: Time?) {
set(key, value?.stringValue)
}
fun AbstractConfig.set(key: String, value: JsonElement?) {
set(key, value?.toString())
}
fun AbstractConfig.set(key: String, value: List<Any>?) {
set(key, value?.let { gson.toJson(it) })
}
fun AbstractConfig.set(key: String, value: Any?) {
set(key, value?.let { gson.toJson(it) })
}
fun AbstractConfig.setStringList(key: String, value: List<String>?) {
set(key, value?.let { gson.toJson(it) })
}
fun AbstractConfig.setIntList(key: String, value: List<Int>?) {
set(key, value?.let { gson.toJson(it) })
}
fun AbstractConfig.setLongList(key: String, value: List<Long>?) {
set(key, value?.let { gson.toJson(it) })
}
fun <K, V> AbstractConfig.setMap(key: String, value: Map<K, V>?) {
set(key, value?.let { gson.toJson(it) })
}
fun HashMap<String, String?>.get(key: String, default: String?): String? {
return this[key] ?: default
}
fun HashMap<String, String?>.get(key: String, default: Boolean): Boolean {
return this[key]?.toBoolean() ?: default
}
fun HashMap<String, String?>.getBooleanOrNull(key: String): Boolean? {
return this[key]?.toBooleanStrictOrNull()
}
fun HashMap<String, String?>.get(key: String, default: Int): Int {
return this[key]?.toIntOrNull() ?: default
}
fun HashMap<String, String?>.get(key: String, default: Long): Long {
return this[key]?.toLongOrNull() ?: default
}
fun HashMap<String, String?>.get(key: String, default: Float): Float {
return this[key]?.toFloatOrNull() ?: default
}
fun HashMap<String, String?>.get(key: String, default: Date?): Date? {
return this[key]?.let { Date.fromY_m_d(it) } ?: default
}
fun HashMap<String, String?>.get(key: String, default: Time?): Time? {
return this[key]?.let { Time.fromHms(it) } ?: default
}
fun HashMap<String, String?>.get(key: String, default: JsonObject?): JsonObject? {
return this[key]?.let { JsonParser().parse(it)?.asJsonObject } ?: default
}
fun HashMap<String, String?>.get(key: String, default: JsonArray?): JsonArray? {
return this[key]?.let { JsonParser().parse(it)?.asJsonArray } ?: default
}
inline fun <reified T> HashMap<String, String?>.get(key: String, default: T?): T? {
return this[key]?.let { Gson().fromJson(it, T::class.java) } ?: default
}
/* !!! cannot use mutable list here - modifying it will not update the DB */
fun <T> HashMap<String, String?>.get(key: String, default: List<T>?, classOfT: Class<T>): List<T>? {
return this[key]?.let { ConfigGsonUtils().deserializeList<T>(gson, it, classOfT) } ?: default
}
fun HashMap<String, String?>.getStringList(key: String, default: List<String>?): List<String>? {
return this[key]?.let { gson.fromJson<List<String>>(it, object: TypeToken<List<String>>(){}.type) } ?: default
}
fun HashMap<String, String?>.getIntList(key: String, default: List<Int>?): List<Int>? {
return this[key]?.let { gson.fromJson<List<Int>>(it, object: TypeToken<List<Int>>(){}.type) } ?: default
}
fun HashMap<String, String?>.getLongList(key: String, default: List<Long>?): List<Long>? {
return this[key]?.let { gson.fromJson<List<Long>>(it, object: TypeToken<List<Long>>(){}.type) } ?: default
}
fun HashMap<String, String?>.getFloat(key: String): Float? {
return this[key]?.toFloatOrNull()
}
fun List<ConfigEntry>.toHashMap(profileId: Int, map: HashMap<String, String?>) {
map.clear()
forEach {
if (it.profileId == profileId)
map[it.key] = it.value
}
}

View File

@ -1,45 +0,0 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-12-2.
*/
package pl.szczodrzynski.edziennik.config.utils
import com.google.gson.Gson
import com.google.gson.JsonParser
import pl.szczodrzynski.edziennik.ext.getInt
import pl.szczodrzynski.edziennik.ui.home.HomeCardModel
import pl.szczodrzynski.edziennik.utils.models.Time
class ConfigGsonUtils {
@Suppress("UNCHECKED_CAST")
fun <T> deserializeList(gson: Gson, str: String?, classOfT: Class<T>): List<T> {
val json = JsonParser.parseString(str)
val list: MutableList<T> = mutableListOf()
if (!json.isJsonArray)
return list
json.asJsonArray.forEach { e ->
when (classOfT) {
String::class.java -> {
list += e.asString as T
}
HomeCardModel::class.java -> {
val o = e.asJsonObject
list += HomeCardModel(
o.getInt("profileId", 0),
o.getInt("cardId", 0)
) as T
}
Time::class.java -> {
val o = e.asJsonObject
list += Time(
o.getInt("hour", 0),
o.getInt("minute", 0),
o.getInt("second", 0)
) as T
}
}
}
return list
}
}

View File

@ -5,13 +5,9 @@
package pl.szczodrzynski.edziennik.config.utils package pl.szczodrzynski.edziennik.config.utils
import android.content.Context import android.content.Context
import androidx.core.content.edit
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.config.Config import pl.szczodrzynski.edziennik.config.Config
import pl.szczodrzynski.edziennik.ext.HOUR
import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.ORDER_BY_DATE_DESC
import pl.szczodrzynski.edziennik.utils.models.Time import pl.szczodrzynski.edziennik.utils.models.Time
import kotlin.math.abs import kotlin.math.abs
@ -23,74 +19,14 @@ class ConfigMigration(app: App, config: Config) {
// migrate appConfig from app version 3.x and lower. // migrate appConfig from app version 3.x and lower.
// Updates dataVersion to level 2. // Updates dataVersion to level 2.
AppConfigMigrationV3(p, config) AppConfigMigrationV3(p, config)
} p.edit {
remove("app.appConfig.appTheme")
if (dataVersion < 2) { }
appVersion = BuildConfig.VERSION_CODE
loginFinished = false
ui.language = null
ui.theme = 1
ui.appBackground = null
ui.headerBackground = null
ui.miniMenuVisible = false
ui.miniMenuButtons = setOf(
NavTarget.HOME,
NavTarget.TIMETABLE,
NavTarget.AGENDA,
NavTarget.GRADES,
NavTarget.MESSAGES,
NavTarget.HOMEWORK,
NavTarget.SETTINGS
)
sync.enabled = true
sync.interval = 1* HOUR.toInt()
sync.notifyAboutUpdates = true
sync.onlyWifi = false
sync.quietHoursEnabled = false
sync.quietHoursStart = null
sync.quietHoursEnd = null
sync.quietDuringLessons = false
sync.tokenApp = null
sync.tokenMobidziennik = null
sync.tokenMobidziennikList = listOf()
sync.tokenLibrus = null
sync.tokenLibrusList = listOf()
sync.tokenVulcan = null
sync.tokenVulcanList = listOf()
timetable.bellSyncMultiplier = 0
timetable.bellSyncDiff = null
timetable.countInSeconds = false
grades.orderBy = ORDER_BY_DATE_DESC
dataVersion = 2
}
if (dataVersion < 3) {
update = null
privacyPolicyAccepted = false
devMode = null
devModePassword = null
appInstalledTime = 0L
appRateSnackbarTime = 0L
dataVersion = 3
}
if (dataVersion < 10) {
ui.openDrawerOnBackPressed = false
ui.snowfall = false
ui.bottomSheetOpened = false
sync.dontShowAppManagerDialog = false
sync.webPushEnabled = true
sync.lastAppSync = 0L
dataVersion = 10
} }
if (dataVersion < 11) { if (dataVersion < 11) {
val startMillis = config.values.get("quietHoursStart", 0L) val startMillis = config.values["quietHoursStart"]?.toLongOrNull() ?: 0L
val endMillis = config.values.get("quietHoursEnd", 0L) val endMillis = config.values["quietHoursEnd"]?.toLongOrNull() ?: 0L
if (startMillis > 0) { if (startMillis > 0) {
try { try {
sync.quietHoursStart = Time.fromMillis(abs(startMillis)) sync.quietHoursStart = Time.fromMillis(abs(startMillis))
@ -107,5 +43,7 @@ class ConfigMigration(app: App, config: Config) {
dataVersion = 11 dataVersion = 11
} }
hash = "invalid"
}} }}
} }

View File

@ -7,6 +7,7 @@ package pl.szczodrzynski.edziennik.config.utils
import pl.szczodrzynski.edziennik.config.ProfileConfig import pl.szczodrzynski.edziennik.config.ProfileConfig
import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.AGENDA_DEFAULT import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.AGENDA_DEFAULT
import pl.szczodrzynski.edziennik.data.db.enums.NotificationType import pl.szczodrzynski.edziennik.data.db.enums.NotificationType
import pl.szczodrzynski.edziennik.data.db.enums.SchoolType
import pl.szczodrzynski.edziennik.ui.home.HomeCard import pl.szczodrzynski.edziennik.ui.home.HomeCard
import pl.szczodrzynski.edziennik.ui.home.HomeCardModel import pl.szczodrzynski.edziennik.ui.home.HomeCardModel
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_WEIGHTED import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_WEIGHTED
@ -15,21 +16,6 @@ import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_AL
class ProfileConfigMigration(config: ProfileConfig) { class ProfileConfigMigration(config: ProfileConfig) {
init { config.apply { init { config.apply {
if (dataVersion < 1) {
grades.colorMode = COLOR_MODE_WEIGHTED
grades.yearAverageMode = YEAR_ALL_GRADES
grades.hideImproved = false
grades.averageWithoutWeight = true
grades.plusValue = null
grades.minusValue = null
grades.dontCountEnabled = false
grades.dontCountGrades = listOf()
ui.agendaViewType = AGENDA_DEFAULT
// no migration for ui.homeCards
dataVersion = 1
}
if (dataVersion < 2) { if (dataVersion < 2) {
sync.notificationFilter = sync.notificationFilter + NotificationType.TEACHER_ABSENCE sync.notificationFilter = sync.notificationFilter + NotificationType.TEACHER_ABSENCE
@ -38,12 +24,24 @@ class ProfileConfigMigration(config: ProfileConfig) {
if (dataVersion < 3) { if (dataVersion < 3) {
if (ui.homeCards.isNotEmpty()) { if (ui.homeCards.isNotEmpty()) {
ui.homeCards = ui.homeCards.toMutableList().also { ui.homeCards = ui.homeCards + HomeCardModel(
it.add(HomeCardModel(config.profileId, HomeCard.CARD_NOTES)) profileId = config.profileId ?: -1,
} cardId = HomeCard.CARD_NOTES,
)
} }
dataVersion = 3 dataVersion = 3
} }
if (dataVersion < 4) {
// switch to new event types (USOS)
dataVersion = 4
val profile = db.profileDao().getByIdNow(profileId ?: -1)
if (profile?.loginStoreType?.schoolType == SchoolType.UNIVERSITY) {
db.eventTypeDao().clear(profileId ?: -1)
db.eventTypeDao().addDefaultTypes(profile)
}
}
}} }}
} }

View File

@ -41,24 +41,18 @@ class UsosApiTerms(
} }
private fun processResponse(json: JsonArray): Boolean { private fun processResponse(json: JsonArray): Boolean {
val dates = mutableSetOf<Date>() val today = Date.getToday()
for (term in json.asJsonObjectList()) { for (term in json.asJsonObjectList()) {
if (!term.getBoolean("is_active", false)) if (!term.getBoolean("is_active", false))
continue continue
val startDate = term.getString("start_date")?.let { Date.fromY_m_d(it) } val startDate = term.getString("start_date")?.let { Date.fromY_m_d(it) } ?: continue
val finishDate = term.getString("finish_date")?.let { Date.fromY_m_d(it) } val finishDate = term.getString("finish_date")?.let { Date.fromY_m_d(it) } ?: continue
if (startDate != null) if (today in startDate..finishDate) {
dates += startDate profile?.studentSchoolYearStart = startDate.year
if (finishDate != null) profile?.dateSemester1Start = startDate
dates += finishDate profile?.dateSemester2Start = finishDate
}
} }
val datesSorted = dates.sorted()
if (datesSorted.size != 3)
return false
profile?.studentSchoolYearStart = datesSorted[0].year
profile?.dateSemester1Start = datesSorted[0]
profile?.dateSemester2Start = datesSorted[1]
profile?.dateYearEnd = datesSorted[2]
return true return true
} }
} }

View File

@ -13,7 +13,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.ERROR_API_INVALID_SIGNATURE import pl.szczodrzynski.edziennik.data.api.ERROR_API_INVALID_SIGNATURE
import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.DateAdapter import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.DateAdapter
@ -128,16 +127,10 @@ class SzkolnyApi(val app: App) : CoroutineScope {
response: Response<ApiResponse<T>>, response: Response<ApiResponse<T>>,
updateDeviceHash: Boolean = false, updateDeviceHash: Boolean = false,
): T { ): T {
app.config.update = response.body()?.update?.let { update -> response.body()?.update?.let { update ->
if (update.versionCode > BuildConfig.VERSION_CODE) { // do not process "null" update, as it might not mean there's no update
if (update.updateMandatory // do not notify; silently check and show the home update card
&& EventBus.getDefault().hasSubscriberForEvent(update::class.java)) { app.updateManager.process(update, notify = false)
EventBus.getDefault().postSticky(update)
}
update
}
else
null
} }
response.body()?.registerAvailability?.let { registerAvailability -> response.body()?.registerAvailability?.let { registerAvailability ->
@ -431,8 +424,8 @@ class SzkolnyApi(val app: App) : CoroutineScope {
} }
@Throws(Exception::class) @Throws(Exception::class)
fun getUpdate(channel: String): List<Update> { fun getUpdate(channel: Update.Type): List<Update> {
val response = api.updates(channel).execute() val response = api.updates(channel.name.lowercase()).execute()
return parseResponse(response) return parseResponse(response)
} }

View File

@ -12,10 +12,11 @@ import pl.szczodrzynski.edziennik.ext.bodyToString
import pl.szczodrzynski.edziennik.ext.currentTimeUnix import pl.szczodrzynski.edziennik.ext.currentTimeUnix
import pl.szczodrzynski.edziennik.ext.hmacSHA1 import pl.szczodrzynski.edziennik.ext.hmacSHA1
import pl.szczodrzynski.edziennik.ext.md5 import pl.szczodrzynski.edziennik.ext.md5
import pl.szczodrzynski.edziennik.ext.takeValue
class SignatureInterceptor(val app: App) : Interceptor { class SignatureInterceptor(val app: App) : Interceptor {
companion object { companion object {
private const val API_KEY = "szkolny_android_42a66f0842fc7da4e37c66732acf705a" const val API_KEY = "szkolny_android_42a66f0842fc7da4e37c66732acf705a"
} }
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
@ -27,7 +28,7 @@ class SignatureInterceptor(val app: App) : Interceptor {
return chain.proceed( return chain.proceed(
request.newBuilder() request.newBuilder()
.header("X-ApiKey", API_KEY) .header("X-ApiKey", app.config.apiKeyCustom?.takeValue() ?: API_KEY)
.header("X-AppVersion", BuildConfig.VERSION_CODE.toString()) .header("X-AppVersion", BuildConfig.VERSION_CODE.toString())
.header("X-Timestamp", timestamp.toString()) .header("X-Timestamp", timestamp.toString())
.header("X-Signature", sign(timestamp, body, url)) .header("X-Signature", sign(timestamp, body, url))

View File

@ -46,6 +46,6 @@ object Signing {
/*fun provideKey(param1: String, param2: Long): ByteArray {*/ /*fun provideKey(param1: String, param2: Long): ByteArray {*/
fun pleaseStopRightNow(param1: String, param2: Long): ByteArray { fun pleaseStopRightNow(param1: String, param2: Long): ByteArray {
return "$param1.MTIzNDU2Nzg5MDoKyw75d3===.$param2".sha256() return "$param1.MTIzNDU2Nzg5MD7hH6ZvtD===.$param2".sha256()
} }
} }

View File

@ -5,12 +5,21 @@
package pl.szczodrzynski.edziennik.data.api.szkolny.response package pl.szczodrzynski.edziennik.data.api.szkolny.response
data class Update( data class Update(
val versionCode: Int, val versionCode: Int,
val versionName: String, val versionName: String,
val releaseDate: String, val releaseDate: String,
val releaseNotes: String?, val releaseNotes: String?,
val releaseType: String, val releaseType: String,
val isOnGooglePlay: Boolean, val isOnGooglePlay: Boolean,
val downloadUrl: String?, val downloadUrl: String?,
val updateMandatory: Boolean val updateMandatory: Boolean,
) ) {
enum class Type {
NIGHTLY,
DEV,
BETA,
RC,
RELEASE,
}
}

View File

@ -3,15 +3,16 @@
*/ */
package pl.szczodrzynski.edziennik.data.db.dao package pl.szczodrzynski.edziennik.data.db.dao
import android.content.Context import android.graphics.Color
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_DEFAULT import pl.szczodrzynski.edziennik.config.AppData
import pl.szczodrzynski.edziennik.data.db.entity.EventType import pl.szczodrzynski.edziennik.data.db.entity.EventType
import pl.szczodrzynski.edziennik.data.db.entity.EventType.Companion.SOURCE_DEFAULT import pl.szczodrzynski.edziennik.data.db.entity.EventType.Companion.SOURCE_DEFAULT
import pl.szczodrzynski.edziennik.data.db.entity.Profile
@Dao @Dao
abstract class EventTypeDao { abstract class EventTypeDao {
@ -36,17 +37,17 @@ abstract class EventTypeDao {
@get:Query("SELECT * FROM eventTypes") @get:Query("SELECT * FROM eventTypes")
abstract val allNow: List<EventType> abstract val allNow: List<EventType>
fun addDefaultTypes(context: Context, profileId: Int): List<EventType> { fun addDefaultTypes(profile: Profile): List<EventType> {
val data = AppData.get(profile.loginStoreType)
var order = 100 var order = 100
val colorMap = EventType.getTypeColorMap() val typeList = data.eventTypes.map {
val typeList = EventType.getTypeNameMap().map { (id, name) ->
EventType( EventType(
profileId = profileId, profileId = profile.id,
id = id, id = it.id.toLong(),
name = context.getString(name), name = it.name,
color = colorMap[id] ?: COLOR_DEFAULT, color = Color.parseColor(it.color),
order = order++, order = order++,
source = SOURCE_DEFAULT source = SOURCE_DEFAULT,
) )
} }
addAll(typeList) addAll(typeList)

View File

@ -7,12 +7,13 @@ package pl.szczodrzynski.edziennik.data.db.enums
enum class LoginType( enum class LoginType(
val id: Int, val id: Int,
val features: Set<FeatureType>, val features: Set<FeatureType>,
val schoolType: SchoolType = SchoolType.STANDARD,
) { ) {
MOBIDZIENNIK(id = 1, features = FEATURES_MOBIDZIENNIK), MOBIDZIENNIK(id = 1, features = FEATURES_MOBIDZIENNIK),
LIBRUS(id = 2, features = FEATURES_LIBRUS), LIBRUS(id = 2, features = FEATURES_LIBRUS),
VULCAN(id = 4, features = FEATURES_VULCAN), VULCAN(id = 4, features = FEATURES_VULCAN),
PODLASIE(id = 6, features = FEATURES_PODLASIE), PODLASIE(id = 6, features = FEATURES_PODLASIE),
USOS(id = 7, features = FEATURES_USOS), USOS(id = 7, features = FEATURES_USOS, schoolType = SchoolType.UNIVERSITY),
DEMO(id = 20, features = setOf()), DEMO(id = 20, features = setOf()),
TEMPLATE(id = 21, features = setOf()), TEMPLATE(id = 21, features = setOf()),

View File

@ -0,0 +1,10 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-21.
*/
package pl.szczodrzynski.edziennik.data.db.enums
enum class SchoolType {
STANDARD,
UNIVERSITY,
}

View File

@ -6,7 +6,11 @@ package pl.szczodrzynski.edziennik.data.firebase
import com.google.gson.JsonParser import com.google.gson.JsonParser
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.api.events.FeedbackMessageEvent import pl.szczodrzynski.edziennik.data.api.events.FeedbackMessageEvent
@ -14,14 +18,18 @@ import pl.szczodrzynski.edziennik.data.api.events.RegisterAvailabilityEvent
import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
import pl.szczodrzynski.edziennik.data.api.task.PostNotifications import pl.szczodrzynski.edziennik.data.api.task.PostNotifications
import pl.szczodrzynski.edziennik.data.db.entity.* import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.data.db.entity.FeedbackMessage
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.Note
import pl.szczodrzynski.edziennik.data.db.entity.Notification
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.enums.MetadataType import pl.szczodrzynski.edziennik.data.db.enums.MetadataType
import pl.szczodrzynski.edziennik.data.db.enums.NotificationType import pl.szczodrzynski.edziennik.data.db.enums.NotificationType
import pl.szczodrzynski.edziennik.ext.getInt import pl.szczodrzynski.edziennik.ext.getInt
import pl.szczodrzynski.edziennik.ext.getLong import pl.szczodrzynski.edziennik.ext.getLong
import pl.szczodrzynski.edziennik.ext.getString import pl.szczodrzynski.edziennik.ext.getString
import pl.szczodrzynski.edziennik.ext.resolveString import pl.szczodrzynski.edziennik.ext.resolveString
import pl.szczodrzynski.edziennik.sync.UpdateWorker
import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget
import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time import pl.szczodrzynski.edziennik.utils.models.Time
@ -64,7 +72,10 @@ class SzkolnyAppFirebase(val app: App, val profiles: List<Profile>, val message:
message.data.getString("title") ?: "", message.data.getString("title") ?: "",
message.data.getString("message") ?: "" message.data.getString("message") ?: ""
) )
"appUpdate" -> launch { UpdateWorker.runNow(app, app.gson.fromJson(message.data.getString("update"), Update::class.java)) } "appUpdate" -> {
val update = app.gson.fromJson(message.data.getString("update"), Update::class.java)
app.updateManager.process(update, notify = true)
}
"feedbackMessage" -> launch { "feedbackMessage" -> launch {
val message = app.gson.fromJson(message.data.getString("message"), FeedbackMessage::class.java) ?: return@launch val message = app.gson.fromJson(message.data.getString("message"), FeedbackMessage::class.java) ?: return@launch
feedbackMessage(message) feedbackMessage(message)

View File

@ -8,6 +8,8 @@ import android.util.LongSparseArray
import androidx.core.util.forEach import androidx.core.util.forEach
import com.google.android.material.datepicker.CalendarConstraints import com.google.android.material.datepicker.CalendarConstraints
import com.google.gson.JsonElement import com.google.gson.JsonElement
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.config.AppData
import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.entity.Team import pl.szczodrzynski.edziennik.data.db.entity.Team
@ -48,3 +50,6 @@ fun Profile.getSchoolYearConstrains(): CalendarConstraints {
fun Profile.hasFeature(featureType: FeatureType) = featureType in this.loginStoreType.features fun Profile.hasFeature(featureType: FeatureType) = featureType in this.loginStoreType.features
fun Profile.hasUIFeature(featureType: FeatureType) = featureType.isUIAlwaysAvailable || hasFeature(featureType) fun Profile.hasUIFeature(featureType: FeatureType) = featureType.isUIAlwaysAvailable || hasFeature(featureType)
fun Profile.getAppData() =
if (App.profileId == this.id) App.data else AppData.get(this.loginStoreType)

View File

@ -50,13 +50,13 @@ inline fun <reified E : Enum<E>> Int.toEnum() = when (E::class.java) {
} as E } as E
fun <E : Enum<E>> Int.toEnum(type: Class<*>) = when (type) { fun <E : Enum<E>> Int.toEnum(type: Class<*>) = when (type) {
// enums commented out are not really used in Bundles // this is used for Config so all enums are here
FeatureType::class.java -> this.asFeatureType() FeatureType::class.java -> this.asFeatureType()
// LoginMethod::class.java -> this.asLoginMethod() LoginMethod::class.java -> this.asLoginMethod()
LoginMode::class.java -> this.asLoginMode() LoginMode::class.java -> this.asLoginMode()
LoginType::class.java -> this.asLoginType() LoginType::class.java -> this.asLoginType()
// MetadataType::class.java -> this.asMetadataType() MetadataType::class.java -> this.asMetadataType()
// NotificationType::class.java -> this.asNotificationType() NotificationType::class.java -> this.asNotificationType()
NavTarget::class.java -> this.asNavTarget() NavTarget::class.java -> this.asNavTarget()
else -> throw IllegalArgumentException("Unknown type $type") else -> throw IllegalArgumentException("Unknown type $type")
} as E } as E

View File

@ -46,6 +46,7 @@ inline fun <reified E : Enum<E>> JsonObject?.getEnum(key: String) = this?.getInt
fun JsonObject.putEnum(key: String, value: Enum<*>) = addProperty(key, value.toInt()) fun JsonObject.putEnum(key: String, value: Enum<*>) = addProperty(key, value.toInt())
fun String.toJsonObject(): JsonObject? = try { JsonParser.parseString(this).asJsonObject } catch (ignore: Exception) { null } fun String.toJsonObject(): JsonObject? = try { JsonParser.parseString(this).asJsonObject } catch (ignore: Exception) { null }
fun String.toJsonArray(): JsonArray? = try { JsonParser.parseString(this).asJsonArray } catch (ignore: Exception) { null }
operator fun JsonObject.set(key: String, value: JsonElement) = this.add(key, value) operator fun JsonObject.set(key: String, value: JsonElement) = this.add(key, value)
operator fun JsonObject.set(key: String, value: Boolean) = this.addProperty(key, value) operator fun JsonObject.set(key: String, value: Boolean) = this.addProperty(key, value)
@ -86,7 +87,9 @@ fun JsonObject.toBundle(): Bundle {
} }
} }
fun JsonArray(vararg properties: Any?): JsonArray { fun JsonArray(vararg properties: Any?) = JsonArray(properties.toList())
fun JsonArray(properties: Collection<Any?>): JsonArray {
return JsonArray().apply { return JsonArray().apply {
for (property in properties) { for (property in properties) {
when (property) { when (property) {
@ -112,3 +115,20 @@ operator fun JsonArray.plusAssign(o: String) = this.add(o)
operator fun JsonArray.plusAssign(o: Char) = this.add(o) operator fun JsonArray.plusAssign(o: Char) = this.add(o)
operator fun JsonArray.plusAssign(o: Number) = this.add(o) operator fun JsonArray.plusAssign(o: Number) = this.add(o)
operator fun JsonArray.plusAssign(o: Boolean) = this.add(o) operator fun JsonArray.plusAssign(o: Boolean) = this.add(o)
fun JsonObject.mergeWith(other: JsonObject): JsonObject {
for ((key, value) in other.entrySet()) {
when (value) {
is JsonObject -> when {
this.has(key) -> this.getJsonObject(key)?.mergeWith(value)
else -> this.add(key, value)
}
is JsonArray -> when {
this.has(key) -> this.getJsonArray(key)?.addAll(value)
else -> this.add(key, value)
}
else -> this.add(key, value)
}
}
return this
}

View File

@ -72,3 +72,10 @@ fun pendingIntentFlag(): Int {
return PendingIntent.FLAG_IMMUTABLE return PendingIntent.FLAG_IMMUTABLE
return 0 return 0
} }
fun Int?.takeValue() = if (this == -1) null else this
fun Int?.takePositive() = if (this == -1 || this == 0) null else this
fun String?.takeValue() = if (this.isNullOrBlank()) null else this
fun Any?.ignore() = Unit

View File

@ -0,0 +1,9 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-22.
*/
package pl.szczodrzynski.edziennik.sync
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
class UpdateStateEvent(val running: Boolean, val update: Update?, val downloadId: Long)

View File

@ -5,25 +5,14 @@
package pl.szczodrzynski.edziennik.sync package pl.szczodrzynski.edziennik.sync
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.work.* import androidx.work.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
import pl.szczodrzynski.edziennik.ext.DAY import pl.szczodrzynski.edziennik.ext.DAY
import pl.szczodrzynski.edziennik.ext.concat
import pl.szczodrzynski.edziennik.ext.formatDate import pl.szczodrzynski.edziennik.ext.formatDate
import pl.szczodrzynski.edziennik.ext.pendingIntentFlag
import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.html.BetterHtml
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -76,63 +65,6 @@ class UpdateWorker(val context: Context, val params: WorkerParameters) : Worker(
Utils.d(TAG, "Cancelling work by tag $TAG") Utils.d(TAG, "Cancelling work by tag $TAG")
WorkManager.getInstance(app).cancelAllWorkByTag(TAG) WorkManager.getInstance(app).cancelAllWorkByTag(TAG)
} }
suspend fun runNow(app: App, overrideUpdate: Update? = null) {
try {
val update = overrideUpdate
?: run {
withContext(Dispatchers.Default) {
SzkolnyApi(app).runCatching({
getUpdate("beta")
}, {
Toast.makeText(app, app.getString(R.string.notification_cant_check_update), Toast.LENGTH_SHORT).show()
})
} ?: return@run null
if (app.config.update == null
|| app.config.update?.versionCode ?: BuildConfig.VERSION_CODE <= BuildConfig.VERSION_CODE) {
app.config.update = null
Toast.makeText(app, app.getString(R.string.notification_no_update), Toast.LENGTH_SHORT).show()
return@run null
}
app.config.update
} ?: return
if (update.versionCode <= BuildConfig.VERSION_CODE) {
app.config.update = null
return
}
if (EventBus.getDefault().hasSubscriberForEvent(update::class.java)) {
if (!update.updateMandatory) // mandatory updates are posted by the SzkolnyApi
EventBus.getDefault().postSticky(update)
return
}
val notificationIntent = Intent(app, UpdateDownloaderService::class.java)
val pendingIntent = PendingIntent.getService(app, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT or pendingIntentFlag())
val notification = NotificationCompat.Builder(app, app.notificationChannelsManager.updates.key)
.setContentTitle(app.getString(R.string.notification_updates_title))
.setContentText(app.getString(R.string.notification_updates_text, update.versionName))
.setTicker(app.getString(R.string.notification_updates_summary))
.setSmallIcon(R.drawable.ic_notification)
.setStyle(NotificationCompat.BigTextStyle()
.bigText(listOf(
app.getString(R.string.notification_updates_text, update.versionName),
update.releaseNotes?.let { BetterHtml.fromHtml(context = null, it) }
).concat("\n")))
.setColor(0xff2196f3.toInt())
.setLights(0xFF00FFFF.toInt(), 2000, 2000)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setGroup(app.notificationChannelsManager.updates.key)
.setContentIntent(pendingIntent)
.setAutoCancel(false)
.build()
(app.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).notify(app.notificationChannelsManager.updates.id, notification)
} catch (ignore: Exception) { }
}
} }
private val job = Job() private val job = Job()
@ -146,22 +78,13 @@ class UpdateWorker(val context: Context, val params: WorkerParameters) : Worker(
return Result.success() return Result.success()
} }
launch { val channel = if (App.devMode)
runNow(app) Update.Type.BETA
} else
Update.Type.RELEASE
app.updateManager.checkNowSync(channel, notify = true)
rescheduleNext(this.context) rescheduleNext(this.context)
return Result.success() return Result.success()
} }
class JavaWrapper(app: App) : CoroutineScope {
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
init {
launch {
runNow(app)
}
}
}
} }

View File

@ -22,7 +22,6 @@ import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.EventType import pl.szczodrzynski.edziennik.data.db.entity.EventType
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.enums.MetadataType import pl.szczodrzynski.edziennik.data.db.enums.MetadataType
import pl.szczodrzynski.edziennik.databinding.FragmentAgendaCalendarBinding import pl.szczodrzynski.edziennik.databinding.FragmentAgendaCalendarBinding
@ -33,7 +32,6 @@ import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem
import java.util.*
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
class AgendaFragment : Fragment(), CoroutineScope { class AgendaFragment : Fragment(), CoroutineScope {
@ -144,7 +142,7 @@ class AgendaFragment : Fragment(), CoroutineScope {
} }
val defaultEventTypes = EventType.getTypeColorMap().keys val defaultEventTypes = EventType.getTypeColorMap().keys
if (!eventTypes.containsAll(defaultEventTypes)) { if (!eventTypes.containsAll(defaultEventTypes)) {
app.db.eventTypeDao().addDefaultTypes(activity, app.profileId) app.db.eventTypeDao().addDefaultTypes(app.profile)
} }
} }
} }

View File

@ -72,7 +72,7 @@ public class AnnouncementsFragment extends Fragment {
if (app.getProfile().getLoginStoreType() == LoginType.LIBRUS) { if (app.getProfile().getLoginStoreType() == LoginType.LIBRUS) {
EdziennikTask.Companion.announcementsRead(App.Companion.getProfileId()).enqueue(requireContext()); EdziennikTask.Companion.announcementsRead(App.Companion.getProfileId()).enqueue(requireContext());
} else { } else {
AsyncTask.execute(() -> App.db.metadataDao().setAllSeen(App.Companion.getProfileId(), MetadataType.ANNOUNCEMENT, true)); AsyncTask.execute(() -> App.Companion.getDb().metadataDao().setAllSeen(App.Companion.getProfileId(), MetadataType.ANNOUNCEMENT, true));
Toast.makeText(activity, R.string.main_menu_mark_as_read_success, Toast.LENGTH_SHORT).show(); Toast.makeText(activity, R.string.main_menu_mark_as_read_success, Toast.LENGTH_SHORT).show();
} }
}) })
@ -103,7 +103,7 @@ public class AnnouncementsFragment extends Fragment {
} }
}); });
app.db.announcementDao().getAll(App.Companion.getProfileId()).observe(getViewLifecycleOwner(), announcements -> { app.getDb().announcementDao().getAll(App.Companion.getProfileId()).observe(getViewLifecycleOwner(), announcements -> {
if (app == null || activity == null || b == null || !isAdded()) if (app == null || activity == null || b == null || !isAdded())
return; return;
@ -174,7 +174,7 @@ public class AnnouncementsFragment extends Fragment {
b.text.setText(announcement.getTeacherName() +"\n\n"+ (announcement.getStartDate() != null ? announcement.getStartDate().getFormattedString() : "-") + (announcement.getEndDate() != null ? " do " + announcement.getEndDate().getFormattedString() : "")+"\n\n" +announcement.getText()); b.text.setText(announcement.getTeacherName() +"\n\n"+ (announcement.getStartDate() != null ? announcement.getStartDate().getFormattedString() : "-") + (announcement.getEndDate() != null ? " do " + announcement.getEndDate().getFormattedString() : "")+"\n\n" +announcement.getText());
if (!announcement.getSeen() && app.getProfile().getLoginStoreType() != LoginType.LIBRUS) { if (!announcement.getSeen() && app.getProfile().getLoginStoreType() != LoginType.LIBRUS) {
announcement.setSeen(true); announcement.setSeen(true);
AsyncTask.execute(() -> App.db.metadataDao().setSeen(App.Companion.getProfileId(), announcement, true)); AsyncTask.execute(() -> App.Companion.getDb().metadataDao().setSeen(App.Companion.getProfileId(), announcement, true));
if (recyclerView.getAdapter() != null) if (recyclerView.getAdapter() != null)
recyclerView.getAdapter().notifyDataSetChanged(); recyclerView.getAdapter().notifyDataSetChanged();
} }

View File

@ -72,7 +72,7 @@ public class BehaviourFragment extends Fragment {
.withIcon(CommunityMaterial.Icon.cmd_eye_check_outline) .withIcon(CommunityMaterial.Icon.cmd_eye_check_outline)
.withOnClickListener(v3 -> { .withOnClickListener(v3 -> {
activity.getBottomSheet().close(); activity.getBottomSheet().close();
AsyncTask.execute(() -> App.db.metadataDao().setAllSeen(App.Companion.getProfileId(), MetadataType.NOTICE, true)); AsyncTask.execute(() -> App.Companion.getDb().metadataDao().setAllSeen(App.Companion.getProfileId(), MetadataType.NOTICE, true));
Toast.makeText(activity, R.string.main_menu_mark_as_read_success, Toast.LENGTH_SHORT).show(); Toast.makeText(activity, R.string.main_menu_mark_as_read_success, Toast.LENGTH_SHORT).show();
}) })
); );
@ -111,7 +111,7 @@ public class BehaviourFragment extends Fragment {
} }
}); });
app.db.noticeDao().getAll(App.Companion.getProfileId()).observe(getViewLifecycleOwner(), notices -> { app.getDb().noticeDao().getAll(App.Companion.getProfileId()).observe(getViewLifecycleOwner(), notices -> {
if (app == null || activity == null || b == null || !isAdded()) if (app == null || activity == null || b == null || !isAdded())
return; return;

View File

@ -19,7 +19,6 @@ import eu.szkolny.font.SzkolnyFont
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.Notice import pl.szczodrzynski.edziennik.data.db.entity.Notice
import pl.szczodrzynski.edziennik.data.db.enums.LoginType
import pl.szczodrzynski.edziennik.data.db.full.NoticeFull import pl.szczodrzynski.edziennik.data.db.full.NoticeFull
import pl.szczodrzynski.edziennik.utils.BetterLink import pl.szczodrzynski.edziennik.utils.BetterLink
import pl.szczodrzynski.edziennik.utils.Utils.bs import pl.szczodrzynski.edziennik.utils.Utils.bs
@ -40,7 +39,7 @@ class NoticesAdapter//getting the context and product list with constructor
val notice = noticeList[position] val notice = noticeList[position]
if (app.profile.loginStoreType == LoginType.MOBIDZIENNIK && false) { if (app.data.uiConfig.enableNoticePoints && false) {
holder.noticesItemReason.text = bs(null, notice.category, "\n") + notice.text holder.noticesItemReason.text = bs(null, notice.category, "\n") + notice.text
holder.noticesItemTeacherName.text = app.getString(R.string.notices_points_format, notice.teacherName, if (notice.points ?: 0f > 0) "+" + notice.points else notice.points) holder.noticesItemTeacherName.text = app.getString(R.string.notices_points_format, notice.teacherName, if (notice.points ?: 0f > 0) "+" + notice.points else notice.points)
} else { } else {

View File

@ -11,6 +11,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged
import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SimpleSQLiteQuery
import com.chuckerteam.chucker.api.Chucker import com.chuckerteam.chucker.api.Chucker
import com.chuckerteam.chucker.api.Chucker.SCREEN_HTTP import com.chuckerteam.chucker.api.Chucker.SCREEN_HTTP
@ -21,6 +22,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.config.Config import pl.szczodrzynski.edziennik.config.Config
import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.SignatureInterceptor
import pl.szczodrzynski.edziennik.databinding.LabFragmentBinding import pl.szczodrzynski.edziennik.databinding.LabFragmentBinding
import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.ext.*
import pl.szczodrzynski.edziennik.ui.base.lazypager.LazyFragment import pl.szczodrzynski.edziennik.ui.base.lazypager.LazyFragment
@ -141,6 +143,16 @@ class LabPageFragment : LazyFragment(), CoroutineScope {
app.config.apiInvalidCert = null app.config.apiInvalidCert = null
} }
b.apiKey.setText(app.config.apiKeyCustom ?: SignatureInterceptor.API_KEY)
b.apiKey.doAfterTextChanged {
it?.toString()?.let { key ->
if (key == SignatureInterceptor.API_KEY)
app.config.apiKeyCustom = null
else
app.config.apiKeyCustom = key.takeValue()?.trim()
}
}
b.rebuildConfig.onClick { b.rebuildConfig.onClick {
App.config = Config(App.db) App.config = Config(App.db)
} }

View File

@ -43,11 +43,11 @@ class UpdateAvailableDialog(
override suspend fun onShow() = Unit override suspend fun onShow() = Unit
override suspend fun onPositiveClick(): Boolean { override suspend fun onPositiveClick(): Boolean {
if (update == null) if (update == null || update.isOnGooglePlay)
Utils.openGooglePlay(activity) Utils.openGooglePlay(activity)
else else
activity.startService(Intent(app, UpdateDownloaderService::class.java)) activity.startService(Intent(app, UpdateDownloaderService::class.java))
return NO_DISMISS return DISMISS
} }
override suspend fun onBeforeShow(): Boolean { override suspend fun onBeforeShow(): Boolean {

View File

@ -0,0 +1,87 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-22.
*/
package pl.szczodrzynski.edziennik.ui.dialogs.sync
import android.app.DownloadManager
import android.database.CursorIndexOutOfBoundsException
import android.view.LayoutInflater
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.getSystemService
import kotlinx.coroutines.Job
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
import pl.szczodrzynski.edziennik.databinding.UpdateProgressDialogBinding
import pl.szczodrzynski.edziennik.ext.getInt
import pl.szczodrzynski.edziennik.ext.startCoroutineTimer
import pl.szczodrzynski.edziennik.sync.UpdateStateEvent
import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog
import pl.szczodrzynski.edziennik.utils.Utils
class UpdateProgressDialog(
activity: AppCompatActivity,
private val update: Update,
private val downloadId: Long,
onShowListener: ((tag: String) -> Unit)? = null,
onDismissListener: ((tag: String) -> Unit)? = null,
) : BindingDialog<UpdateProgressDialogBinding>(activity, onShowListener, onDismissListener) {
override val TAG = "UpdateProgressDialog"
override fun getTitleRes() = R.string.notification_downloading_update
override fun inflate(layoutInflater: LayoutInflater) =
UpdateProgressDialogBinding.inflate(layoutInflater)
override fun isCancelable() = false
override fun getNegativeButtonText() = R.string.cancel
private var timerJob: Job? = null
override suspend fun onShow() {
EventBus.getDefault().register(this)
b.update = update
b.progress.progress = 0
val downloadManager = app.getSystemService<DownloadManager>() ?: return
val query = DownloadManager.Query().setFilterById(downloadId)
timerJob?.cancel()
timerJob = startCoroutineTimer(repeatMillis = 100L) {
try {
val cursor = downloadManager.query(query)
cursor.moveToFirst()
val progress = cursor.getInt(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
?.toFloat() ?: return@startCoroutineTimer
b.downloadedSize.text = Utils.readableFileSize(progress.toLong())
val total = cursor.getInt(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)
?.toFloat() ?: return@startCoroutineTimer
b.totalSize.text = Utils.readableFileSize(total.toLong())
b.progress.progress = (progress / total * 100.0f).toInt()
} catch (_: CursorIndexOutOfBoundsException) {}
}
}
override fun onDismiss() {
EventBus.getDefault().unregister(this)
timerJob?.cancel()
}
override suspend fun onNegativeClick(): Boolean {
val downloadManager = app.getSystemService<DownloadManager>() ?: return NO_DISMISS
downloadManager.remove(downloadId)
return DISMISS
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onUpdateStateEvent(event: UpdateStateEvent) {
if (event.downloadId != downloadId)
return
EventBus.getDefault().removeStickyEvent(event)
if (!event.running)
dismiss()
}
}

View File

@ -26,7 +26,6 @@ import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.enums.FeatureType import pl.szczodrzynski.edziennik.data.db.enums.FeatureType
import pl.szczodrzynski.edziennik.data.db.enums.LoginType
import pl.szczodrzynski.edziennik.databinding.FragmentHomeBinding import pl.szczodrzynski.edziennik.databinding.FragmentHomeBinding
import pl.szczodrzynski.edziennik.ext.hasUIFeature import pl.szczodrzynski.edziennik.ext.hasUIFeature
import pl.szczodrzynski.edziennik.ext.onClick import pl.szczodrzynski.edziennik.ext.onClick
@ -127,7 +126,7 @@ class HomeFragment : Fragment(), CoroutineScope {
.withOnClickListener(OnClickListener { .withOnClickListener(OnClickListener {
activity.bottomSheet.close() activity.bottomSheet.close()
launch { withContext(Dispatchers.Default) { launch { withContext(Dispatchers.Default) {
if (app.profile.loginStoreType != LoginType.LIBRUS) { if (!app.data.uiConfig.enableMarkAsReadAnnouncements) {
app.db.metadataDao().setAllSeenExceptMessagesAndAnnouncements(App.profileId, true) app.db.metadataDao().setAllSeenExceptMessagesAndAnnouncements(App.profileId, true)
} else { } else {
app.db.metadataDao().setAllSeenExceptMessages(App.profileId, true) app.db.metadataDao().setAllSeenExceptMessages(App.profileId, true)

View File

@ -15,7 +15,10 @@ import coil.load
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.databinding.CardHomeAvailabilityBinding import pl.szczodrzynski.edziennik.databinding.CardHomeAvailabilityBinding
import pl.szczodrzynski.edziennik.ext.Intent import pl.szczodrzynski.edziennik.ext.Intent
@ -28,6 +31,7 @@ import pl.szczodrzynski.edziennik.ui.dialogs.sync.UpdateAvailableDialog
import pl.szczodrzynski.edziennik.ui.home.HomeCard import pl.szczodrzynski.edziennik.ui.home.HomeCard
import pl.szczodrzynski.edziennik.ui.home.HomeCardAdapter import pl.szczodrzynski.edziennik.ui.home.HomeCardAdapter
import pl.szczodrzynski.edziennik.ui.home.HomeFragment import pl.szczodrzynski.edziennik.ui.home.HomeFragment
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.html.BetterHtml import pl.szczodrzynski.edziennik.utils.html.BetterHtml
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -79,7 +83,7 @@ class HomeAvailabilityCard(
else if (update != null && update.versionCode > BuildConfig.VERSION_CODE) { else if (update != null && update.versionCode > BuildConfig.VERSION_CODE) {
b.homeAvailabilityTitle.setText(R.string.home_availability_title) b.homeAvailabilityTitle.setText(R.string.home_availability_title)
b.homeAvailabilityText.setText(R.string.home_availability_text, update.versionName) b.homeAvailabilityText.setText(R.string.home_availability_text, update.versionName)
b.homeAvailabilityUpdate.isVisible = true b.homeAvailabilityUpdate.isVisible = !app.buildManager.isPlayRelease
b.homeAvailabilityIcon.setImageResource(R.drawable.ic_update) b.homeAvailabilityIcon.setImageResource(R.drawable.ic_update)
onInfoClick = { onInfoClick = {
UpdateAvailableDialog(activity, update).show() UpdateAvailableDialog(activity, update).show()
@ -92,7 +96,10 @@ class HomeAvailabilityCard(
b.homeAvailabilityUpdate.onClick { b.homeAvailabilityUpdate.onClick {
if (update == null) if (update == null)
return@onClick return@onClick
activity.startService(Intent(app, UpdateDownloaderService::class.java)) if (update.isOnGooglePlay)
Utils.openGooglePlay(activity)
else
activity.startService(Intent(app, UpdateDownloaderService::class.java))
} }
b.homeAvailabilityInfo.onClick(onInfoClick) b.homeAvailabilityInfo.onClick(onInfoClick)

View File

@ -11,14 +11,13 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.Navigation import androidx.navigation.Navigation
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.config.AppData
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.events.ApiTaskAllFinishedEvent import pl.szczodrzynski.edziennik.data.api.events.ApiTaskAllFinishedEvent
import pl.szczodrzynski.edziennik.data.api.events.ApiTaskErrorEvent import pl.szczodrzynski.edziennik.data.api.events.ApiTaskErrorEvent
@ -29,6 +28,7 @@ import pl.szczodrzynski.edziennik.databinding.LoginSyncFragmentBinding
import pl.szczodrzynski.edziennik.ext.Bundle import pl.szczodrzynski.edziennik.ext.Bundle
import pl.szczodrzynski.edziennik.ext.asBoldSpannable import pl.szczodrzynski.edziennik.ext.asBoldSpannable
import pl.szczodrzynski.edziennik.ext.concat import pl.szczodrzynski.edziennik.ext.concat
import pl.szczodrzynski.edziennik.ext.ignore
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -56,7 +56,7 @@ class LoginSyncFragment : Fragment(), CoroutineScope {
return b.root return b.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) = launch {
EventBus.getDefault().removeStickyEvent(ApiTaskAllFinishedEvent::class.java) EventBus.getDefault().removeStickyEvent(ApiTaskAllFinishedEvent::class.java)
EventBus.getDefault().removeStickyEvent(ApiTaskErrorEvent::class.java) EventBus.getDefault().removeStickyEvent(ApiTaskErrorEvent::class.java)
@ -64,26 +64,34 @@ class LoginSyncFragment : Fragment(), CoroutineScope {
val profiles = activity.profiles.filter { it.isSelected }.map { it.profile } val profiles = activity.profiles.filter { it.isSelected }.map { it.profile }
val loginStores = activity.loginStores.filter { store -> profiles.any { it.loginStoreId == store.id } } val loginStores = activity.loginStores.filter { store -> profiles.any { it.loginStoreId == store.id } }
val registrationAllowed = arguments?.getBoolean("registrationAllowed") ?: false withContext(Dispatchers.IO) {
profiles.forEach { val registrationAllowed = arguments?.getBoolean("registrationAllowed") ?: false
it.registration = if (registrationAllowed) profiles.forEach {
Profile.REGISTRATION_ENABLED it.registration = if (registrationAllowed)
else Profile.REGISTRATION_ENABLED
Profile.REGISTRATION_DISABLED else
Profile.REGISTRATION_DISABLED
app.db.eventTypeDao().addDefaultTypes(activity, it.id) val data = AppData.get(it.loginStoreType)
val config = app.config.getFor(it.id)
for ((key, value) in data.configOverrides) {
config.set(key, value)
}
app.db.eventTypeDao().addDefaultTypes(it)
}
app.db.profileDao().addAll(profiles)
app.db.loginStoreDao().addAll(loginStores)
} }
app.db.profileDao().addAll(profiles)
app.db.loginStoreDao().addAll(loginStores)
finishArguments = Bundle( finishArguments = Bundle(
"firstProfileId" to profiles.firstOrNull()?.id "firstProfileId" to profiles.firstOrNull()?.id
) )
val profileIds = profiles.map { it.id }.toSet() val profileIds = profiles.map { it.id }.toSet()
EdziennikTask.syncProfileList(profileIds).enqueue(activity) EdziennikTask.syncProfileList(profileIds).enqueue(activity)
} }.ignore()
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
fun onSyncStartedEvent(event: ApiTaskStartedEvent) { fun onSyncStartedEvent(event: ApiTaskStartedEvent) {

View File

@ -30,7 +30,6 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.events.MessageSentEvent import pl.szczodrzynski.edziennik.data.api.events.MessageSentEvent
import pl.szczodrzynski.edziennik.data.api.events.RecipientListGetEvent import pl.szczodrzynski.edziennik.data.api.events.RecipientListGetEvent
import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.enums.LoginType import pl.szczodrzynski.edziennik.data.db.enums.LoginType
@ -76,8 +75,6 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
private lateinit var stylingConfig: StylingConfig private lateinit var stylingConfig: StylingConfig
private lateinit var uiConfig: UIConfig private lateinit var uiConfig: UIConfig
private val enableTextStyling
get() = app.profile.loginStoreType != LoginType.LIBRUS
private var changedRecipients = false private var changedRecipients = false
private var changedSubject = false private var changedSubject = false
private var changedBody = false private var changedBody = false
@ -154,14 +151,14 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
} }
private fun getMessageBody(): String { private fun getMessageBody(): String {
return if (enableTextStyling) return if (app.data.messagesConfig.textStyling)
textStylingManager.getHtmlText(stylingConfig) textStylingManager.getHtmlText(stylingConfig)
else else
b.text.text?.toString() ?: "" b.text.text?.toString() ?: ""
} }
private fun getRecipientList() { private fun getRecipientList() {
if (System.currentTimeMillis() - app.profile.lastReceiversSync > 1 * DAY * 1000 && app.profile.loginStoreType != LoginType.VULCAN) { if (app.data.messagesConfig.syncRecipientList && System.currentTimeMillis() - app.profile.lastReceiversSync > 1 * DAY * 1000) {
activity.snackbar("Pobieranie listy odbiorców...") activity.snackbar("Pobieranie listy odbiorców...")
EdziennikTask.recipientListGet(App.profileId).enqueue(activity) EdziennikTask.recipientListGet(App.profileId).enqueue(activity)
} }
@ -262,8 +259,8 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
}, },
) )
b.fontStyle.root.isVisible = enableTextStyling b.fontStyle.root.isVisible = app.data.messagesConfig.textStyling
if (enableTextStyling) { if (app.data.messagesConfig.textStyling) {
textStylingManager.attach(stylingConfig) textStylingManager.attach(stylingConfig)
b.fontStyle.styles.addOnButtonCheckedListener { _, _, _ -> b.fontStyle.styles.addOnButtonCheckedListener { _, _, _ ->
changedBody = true changedBody = true
@ -374,7 +371,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
else -> b.text.requestFocus() else -> b.text.requestFocus()
} }
if (!enableTextStyling) if (!app.data.messagesConfig.textStyling)
b.text.setText(b.text.text?.toString()) b.text.setText(b.text.text?.toString())
b.text.setSelection(0) b.text.setSelection(0)
(b.root as? ScrollView)?.smoothScrollTo(0, 0) (b.root as? ScrollView)?.smoothScrollTo(0, 0)

View File

@ -188,7 +188,7 @@ class MessageFragment : Fragment(), CoroutineScope {
} }
} }
if (app.profile.loginStoreType == LoginType.VULCAN) { if (app.data.messagesConfig.needsReadStatus) {
// vulcan: change message status or download attachments // vulcan: change message status or download attachments
if ((message.isReceived || message.isDeleted) && !message.seen || message.attachmentIds == null) { if ((message.isReceived || message.isDeleted) && !message.seen || message.attachmentIds == null) {
EdziennikTask.messageGet(App.profileId, message).enqueue(activity) EdziennikTask.messageGet(App.profileId, message).enqueue(activity)

View File

@ -18,8 +18,8 @@ import kotlinx.coroutines.launch
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.BuildConfig import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
import pl.szczodrzynski.edziennik.ext.after import pl.szczodrzynski.edziennik.ext.after
import pl.szczodrzynski.edziennik.sync.UpdateWorker
import pl.szczodrzynski.edziennik.ui.dialogs.ChangelogDialog import pl.szczodrzynski.edziennik.ui.dialogs.ChangelogDialog
import pl.szczodrzynski.edziennik.ui.settings.SettingsCard import pl.szczodrzynski.edziennik.ui.settings.SettingsCard
import pl.szczodrzynski.edziennik.ui.settings.SettingsLicenseActivity import pl.szczodrzynski.edziennik.ui.settings.SettingsLicenseActivity
@ -113,7 +113,17 @@ class SettingsAboutCard(util: SettingsUtil) : SettingsCard(util), CoroutineScope
icon = CommunityMaterial.Icon3.cmd_update icon = CommunityMaterial.Icon3.cmd_update
) { ) {
launch { launch {
UpdateWorker.runNow(app) val channel = if (App.devMode)
Update.Type.BETA
else
Update.Type.RC
val result = app.updateManager.checkNow(channel, notify = false)
val update = result.getOrNull()
// the dialog is shown by MainActivity (EventBus)
when {
result.isFailure -> Toast.makeText(app, app.getString(R.string.notification_cant_check_update), Toast.LENGTH_SHORT).show()
update == null -> Toast.makeText(app, app.getString(R.string.notification_no_update), Toast.LENGTH_SHORT).show()
}
} }
} }
)), )),

View File

@ -83,7 +83,7 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
startHour = startHour, startHour = startHour,
endHour = endHour, endHour = endHour,
dividerHeight = 1.dp, dividerHeight = 1.dp,
halfHourHeight = if (app.profile.loginStoreType == LoginType.USOS) 45.dp else 60.dp, halfHourHeight = app.data.uiConfig.lessonHeight.dp,
hourDividerColor = R.attr.hourDividerColor.resolveAttr(context), hourDividerColor = R.attr.hourDividerColor.resolveAttr(context),
halfHourDividerColor = R.attr.halfHourDividerColor.resolveAttr(context), halfHourDividerColor = R.attr.halfHourDividerColor.resolveAttr(context),
hourLabelWidth = 40.dp, hourLabelWidth = 40.dp,

View File

@ -37,7 +37,8 @@ class EventTypeDropdown : TextInputDropDown {
var types = db.eventTypeDao().getAllNow(profileId) var types = db.eventTypeDao().getAllNow(profileId)
if (types.none { it.id in -1L..10L }) { if (types.none { it.id in -1L..10L }) {
types = db.eventTypeDao().addDefaultTypes(context, profileId) val profile = db.profileDao().getByIdNow(profileId) ?: return@withContext listOf()
types = db.eventTypeDao().addDefaultTypes(profile)
} }
list += types.map { list += types.map {

View File

@ -85,7 +85,7 @@ public class WidgetConfigActivity extends Activity {
opacity = 0.8f; opacity = 0.8f;
AsyncTask.execute(() -> { AsyncTask.execute(() -> {
profileList = App.db.profileDao().getAllNow(); profileList = App.Companion.getDb().profileDao().getAllNow();
profileList = filterOutArchived(profileList); profileList = filterOutArchived(profileList);
if (widgetType == WIDGET_NOTIFICATIONS) if (widgetType == WIDGET_NOTIFICATIONS)

View File

@ -25,7 +25,9 @@ import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp import com.mikepenz.iconics.utils.sizeDp
import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.db.entity.Lesson import pl.szczodrzynski.edziennik.data.db.entity.Lesson
import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_NO_LESSONS import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_NO_LESSONS
@ -393,7 +395,7 @@ class WidgetTimetableProvider : AppWidgetProvider() {
headerIntent.putExtra("profileId", it) headerIntent.putExtra("profileId", it)
} }
displayingDate?.let { displayingDate?.let {
headerIntent.putExtra("timetableDate", it.value) headerIntent.putExtra("timetableDate", it.stringY_m_d)
} }
} }
headerIntent.putExtras("fragmentId" to NavTarget.TIMETABLE) headerIntent.putExtras("fragmentId" to NavTarget.TIMETABLE)

View File

@ -9,11 +9,29 @@ import android.text.TextUtils
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.Request import okhttp3.Request
import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.Signing import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.Signing
import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
import pl.szczodrzynski.edziennik.ext.Intent
import pl.szczodrzynski.edziennik.ext.asBoldSpannable
import pl.szczodrzynski.edziennik.ext.asColoredSpannable
import pl.szczodrzynski.edziennik.ext.concat
import pl.szczodrzynski.edziennik.ext.getJsonObject
import pl.szczodrzynski.edziennik.ext.getString
import pl.szczodrzynski.edziennik.ext.isNotNullNorBlank
import pl.szczodrzynski.edziennik.ext.join
import pl.szczodrzynski.edziennik.ext.md5
import pl.szczodrzynski.edziennik.ext.resolveAttr
import pl.szczodrzynski.edziennik.ext.resolveColor
import pl.szczodrzynski.edziennik.ext.toJsonObject
import pl.szczodrzynski.edziennik.ui.base.BuildInvalidActivity import pl.szczodrzynski.edziennik.ui.base.BuildInvalidActivity
import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.Utils.d import pl.szczodrzynski.edziennik.utils.Utils.d
@ -75,6 +93,14 @@ class BuildManager(val app: App) : CoroutineScope {
else -> null else -> null
} }
val releaseType = when {
isNightly || isDaily -> Update.Type.NIGHTLY
BuildConfig.VERSION_BASE.endsWith("-dev") -> Update.Type.DEV
BuildConfig.VERSION_BASE.contains("-beta.") -> Update.Type.BETA
BuildConfig.VERSION_BASE.contains("-rc.") -> Update.Type.RC
else -> Update.Type.RELEASE
}
fun showVersionDialog(activity: AppCompatActivity) { fun showVersionDialog(activity: AppCompatActivity) {
val yes = activity.getString(R.string.yes) val yes = activity.getString(R.string.yes)
val no = activity.getString(R.string.no) val no = activity.getString(R.string.no)

View File

@ -0,0 +1,110 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-22.
*/
package pl.szczodrzynski.edziennik.utils.managers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
import pl.szczodrzynski.edziennik.data.api.task.PostNotifications
import pl.szczodrzynski.edziennik.data.db.entity.Notification
import pl.szczodrzynski.edziennik.data.db.enums.NotificationType
import pl.szczodrzynski.edziennik.ext.concat
import pl.szczodrzynski.edziennik.ext.resolveString
import pl.szczodrzynski.edziennik.utils.html.BetterHtml
import kotlin.coroutines.CoroutineContext
class UpdateManager(val app: App) : CoroutineScope {
companion object {
private const val TAG = "UpdateManager"
}
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Default
/**
* Check for updates on the specified [maxChannel].
* If the running build is of "more-unstable" type,
* that channel is used instead.
*
* Optionally, post a notification if [notify] is true.
*
* @return [Result] containing a newer update, or null if not available
*/
suspend fun checkNow(
maxChannel: Update.Type,
notify: Boolean,
): Result<Update?> = withContext(Dispatchers.IO) {
return@withContext checkNowSync(maxChannel, notify)
}
/**
* Check for updates on the specified [maxChannel].
* If the running build is of "more-unstable" type,
* that channel is used instead.
*
* Optionally, post a notification if [notify] is true.
*
* @return [Result] containing a newer update, or null if not available
*/
fun checkNowSync(
maxChannel: Update.Type,
notify: Boolean,
): Result<Update?> {
val channel = minOf(app.buildManager.releaseType, maxChannel)
val update = app.api.runCatching({
getUpdate(channel).firstOrNull()
}, {
return Result.failure(it)
})
return Result.success(process(update, notify))
}
/**
* Process the update: check if the version is newer, and optionally
* post a notification.
*
* @return [update] if it's a newer version, null otherwise
*/
fun process(update: Update?, notify: Boolean): Update? {
if (update == null || update.versionCode <= BuildConfig.VERSION_CODE) {
app.config.update = null
return null
}
app.config.update = update
if (EventBus.getDefault().hasSubscriberForEvent(update::class.java)) {
EventBus.getDefault().postSticky(update)
return update
}
if (notify)
notify(update)
return update
}
fun notify(update: Update) {
val bigText = listOf(
app.getString(R.string.notification_updates_text, update.versionName),
update.releaseNotes?.let { BetterHtml.fromHtml(context = null, it) },
)
val notification = Notification(
id = System.currentTimeMillis(),
title = R.string.notification_updates_title.resolveString(app),
text = bigText.concat("\n").toString(),
type = NotificationType.UPDATE,
profileId = null,
profileName = R.string.notification_updates_title.resolveString(app),
).addExtra("action", "updateRequest")
app.db.notificationDao().add(notification)
PostNotifications(app, listOf(notification))
}
}

View File

@ -133,6 +133,18 @@
android:text="Reset API signature" android:text="Reset API signature"
android:textAllCaps="false" /> android:textAllCaps="false" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<pl.szczodrzynski.edziennik.utils.TextInputKeyboardEdit
android:id="@+id/apiKey"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="API Key" />
</com.google.android.material.textfield.TextInputLayout>
<Button <Button
android:id="@+id/disableDebug" android:id="@+id/disableDebug"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2022-10-22.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="pl.szczodrzynski.edziennik.BuildConfig" />
<variable
name="update"
type="pl.szczodrzynski.edziennik.data.api.szkolny.response.Update" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="16dp"
android:paddingTop="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/app_name"
android:textAppearance="@style/NavView.TextView.Medium" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{BuildConfig.VERSION_BASE}"
android:textAppearance="@style/NavView.TextView.Medium"
tools:text="4.13-rc.1" />
<com.mikepenz.iconics.view.IconicsImageView
android:layout_width="24dp"
android:layout_height="24dp"
app:iiv_color="?android:textColorPrimary"
app:iiv_icon="cmd-chevron-right"
app:iiv_size="24dp"
tools:background="@drawable/ic_arrow_right" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{update.versionName}"
android:textAppearance="@style/NavView.TextView.Medium"
tools:text="4.13-rc.1" />
</LinearLayout>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
tools:progress="33" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="64dp"
android:orientation="horizontal">
<TextView
android:id="@+id/downloadedSize"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
tools:text="2.5 MiB" />
<TextView
android:id="@+id/totalSize"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="11.4 MiB" />
</LinearLayout>
</LinearLayout>
</layout>

View File

@ -0,0 +1,178 @@
{
"base": {
"configOverrides": {},
"messagesConfig": {
"subjectLength": null,
"bodyLength": null,
"textStyling": true,
"syncRecipientList": true,
"htmlMode": "ORIGINAL",
"needsReadStatus": false
},
"uiConfig": {
"lessonHeight": 60,
"enableMarkAsReadAnnouncements": true,
"enableNoticePoints": false
},
"eventTypes": [
{
"id": -5,
"color": "#f57f17",
"name": "lekcja online"
},
{
"id": -1,
"color": "#795548",
"name": "zadanie domowe"
},
{
"id": 0,
"color": "#ffc107",
"name": "inny"
}
]
},
"standard": {
"eventTypes": [
{
"id": 1,
"color": "#f44336",
"name": "sprawdzian"
},
{
"id": 2,
"color": "#76ff03",
"name": "kartkówka"
},
{
"id": 3,
"color": "#4050b5",
"name": "wypracowanie"
},
{
"id": 4,
"color": "#673ab7",
"name": "projekt"
},
{
"id": 5,
"color": "#90caf9",
"name": "zebranie z rodzicami"
},
{
"id": 6,
"color": "#4caf50",
"name": "wycieczka"
},
{
"id": 7,
"color": "#ffeb3b",
"name": "lektura"
},
{
"id": 8,
"color": "#388e3c",
"name": "wydarzenie klasowe"
},
{
"id": 9,
"color": "#039be5",
"name": "informacja"
}
]
},
"university": {
"configOverrides": {
"timetableTrimHourRange": true,
"timetableColorSubjectName": true
},
"uiConfig": {
"lessonHeight": 45
},
"eventTypes": [
{
"id": 1,
"color": "#f44336",
"name": "egzamin"
},
{
"id": 2,
"color": "#e91e63",
"name": "kolokwium"
},
{
"id": 3,
"color": "#76ff03",
"name": "kartkówka"
},
{
"id": 4,
"color": "#ffeb3b",
"name": "wejściówka"
},
{
"id": 5,
"color": "#90caf9",
"name": "zaliczenie"
},
{
"id": 6,
"color": "#4050b5",
"name": "zadanie"
},
{
"id": 7,
"color": "#673ab7",
"name": "projekt"
},
{
"id": 8,
"color": "#388e3c",
"name": "zajęcia"
},
{
"id": 9,
"color": "#4caf50",
"name": "zajęcia dodatkowe"
},
{
"id": 10,
"color": "#039be5",
"name": "informacja"
}
]
},
"mobidziennik": {
"messagesConfig": {
"subjectLength": 100,
"htmlMode": "COMPATIBLE"
},
"uiConfig": {
"enableNoticePoints": true
}
},
"librus": {
"messagesConfig": {
"subjectLength": 150,
"bodyLength": 20000,
"textStyling": false
},
"uiConfig": {
"enableMarkAsReadAnnouncements": false
}
},
"vulcan": {
"messagesConfig": {
"subjectLength": 200,
"syncRecipientList": false,
"needsReadStatus": true
}
},
"idziennik": {
"messagesConfig": {
"subjectLength": 180,
"bodyLength": 1983,
"textStyling": false
}
}
}

View File

@ -1548,4 +1548,5 @@
<string name="login_mode_usos_oauth_guide">TODO</string> <string name="login_mode_usos_oauth_guide">TODO</string>
<string name="notification_user_action_required_oauth_usos">USOS - wymagane logowanie z użyciem przeglądarki</string> <string name="notification_user_action_required_oauth_usos">USOS - wymagane logowanie z użyciem przeglądarki</string>
<string name="oauth_dialog_title">Zaloguj się</string> <string name="oauth_dialog_title">Zaloguj się</string>
<string name="app_cannot_load_data">Nie można załadować danych aplikacji</string>
</resources> </resources>

View File

@ -15,6 +15,8 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.content.getSystemService
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
@ -49,6 +51,8 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
return return
val app = context.applicationContext as App val app = context.applicationContext as App
EventBus.getDefault().postSticky(UpdateStateEvent(running = false, update = null, downloadId))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !app.permissionChecker.canRequestApkInstall()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !app.permissionChecker.canRequestApkInstall()) {
app.permissionChecker.requestApkInstall() app.permissionChecker.requestApkInstall()
return return
@ -79,11 +83,14 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
val app = application as App val app = application as App
val update = App.config.update ?: return val update = App.config.update ?: return
if (tryUpdateWithGooglePlay(update)) if (tryUpdateWithGooglePlay(update)) {
stopSelf()
return return
}
if (update.downloadUrl == null) { if (update.downloadUrl == null) {
Toast.makeText(app, "Nie można pobrać tej aktualizacji. Pobierz ręcznie z Google Play.", Toast.LENGTH_LONG).show() Toast.makeText(app, "Nie można pobrać tej aktualizacji. Pobierz ręcznie z Google Play.", Toast.LENGTH_LONG).show()
stopSelf()
return return
} }
@ -92,7 +99,7 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
return return
} }
(app.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).cancel(app.notificationChannelsManager.updates.id) app.getSystemService<NotificationManager>()?.cancel(app.notificationChannelsManager.updates.id)
val dir: File? = app.getExternalFilesDir(null) val dir: File? = app.getExternalFilesDir(null)
if (dir?.isDirectory == true) { if (dir?.isDirectory == true) {
@ -117,5 +124,6 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
} }
Toast.makeText(app, "Pobieranie aktualizacji Szkolny.eu ${update.versionName}", Toast.LENGTH_LONG).show() Toast.makeText(app, "Pobieranie aktualizacji Szkolny.eu ${update.versionName}", Toast.LENGTH_LONG).show()
downloadId = downloadManager.enqueue(request) downloadId = downloadManager.enqueue(request)
EventBus.getDefault().postSticky(UpdateStateEvent(running = true, update, downloadId))
} }
} }

View File

@ -5,8 +5,8 @@ buildscript {
kotlin_version = '1.6.10' kotlin_version = '1.6.10'
release = [ release = [
versionName: "4.13-beta.3", versionName: "4.13-rc.3",
versionCode: 4130003 versionCode: 4130030
] ]
setup = [ setup = [