Compare commits

..

14 Commits

Author SHA1 Message Date
eeb3fc4621 [4.13-rc.4] Update build.gradle, signing and changelog. 2022-10-24 23:33:54 +02:00
41693a9fc8 [App] Respect user setting before notifying about updates. 2022-10-24 23:33:02 +02:00
d3599b8c89 [UI] Fix DateDropdown next week Friday not visible. 2022-10-24 23:32:21 +02:00
ffd81f8b82 [UI] Add setting to share events/notes by default. 2022-10-24 23:32:09 +02:00
2c34924052 [UI] Mark Firebase-received events as manual. Update legend icons. 2022-10-24 22:41:15 +02:00
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
41 changed files with 657 additions and 197 deletions

View File

@ -1,6 +1,7 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="ALLOW_TRAILING_COMMA" value="true" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>

View File

@ -208,7 +208,7 @@ dependencies {
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.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.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

View File

@ -22,6 +22,7 @@
-keep class android.support.v7.widget.** { *; }
-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.full.EventFull { *; }
-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.notifications.WidgetNotificationsProvider
-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.MenuPopupHelper { showPopup(int, int, boolean, boolean); }

View File

@ -1,10 +1,13 @@
<h3>Wersja 4.13-rc.1, 2022-10-22</h3>
<h3>Wersja 4.13-rc.4, 2022-10-24</h3>
<ul>
<li>Poprawione powiadomienia na Androidzie 13. @santoni0</li>
<li>Możliwość dostosowania wyświetlania planu lekcji.</li>
<li>Opcja kolorowania bloków w planie lekcji.</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>Możliwość dostosowania wyświetlania planu lekcji.</li>
<li>Opcja ustawienia nowych wydarzeń domyślnie jako udostępnione.</li>
<li>Bardziej czytelna legenda rodzaju udostępnionego wydarzenia.</li>
<li>Poprawione opcje filtrowania powiadomień i wyboru przycisków menu bocznego.</li>
<li>Ulepszony system pobierania aktualizacji aplikacji.</li>
</ul>
<br>
<br>

View File

@ -9,7 +9,7 @@
/*secret password - removed for source code publication*/
static toys AES_IV[16] = {
0xb7, 0x04, 0xed, 0xb5, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
0xfa, 0x74, 0xdd, 0xa5, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat);

View File

@ -28,7 +28,11 @@ import com.google.gson.Gson
import com.hypertrack.hyperlog.HyperLog
import com.mikepenz.iconics.Iconics
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 okhttp3.OkHttpClient
import org.greenrobot.eventbus.EventBus
@ -55,7 +59,19 @@ 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.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 kotlin.coroutines.CoroutineContext
import kotlin.system.exitProcess
@ -80,18 +96,19 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
}
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 buildManager by lazy { BuildManager(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 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
get() = App.db
@ -190,12 +207,6 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
Iconics.init(applicationContext)
Iconics.respectFontBoundsDefault = true
if (devMode) {
HyperLog.initialize(this)
HyperLog.setLogLevel(Log.VERBOSE)
HyperLog.setLogFormat(DebugLogFormat(this))
}
// initialize companion object values
AppData.read(this)
App.db = AppDb(this)
@ -204,6 +215,12 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
devMode = config.devMode ?: debugMode
enableChucker = config.enableChucker ?: devMode
if (devMode) {
HyperLog.initialize(this)
HyperLog.setLogLevel(Log.VERBOSE)
HyperLog.setLogFormat(DebugLogFormat(this))
}
if (!profileLoadById(config.lastProfileId)) {
val success = db.profileDao().firstId?.let { profileLoadById(it) }
if (success != true)
@ -445,6 +462,8 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
}
fun profileSave() = profileSave(profile)
fun profileSave(profile: Profile) {
if (profile.id == profileId)
App.profile = profile
launch(Dispatchers.Default) {
App.db.profileDao().add(profile)
}

View File

@ -46,6 +46,7 @@ import pl.szczodrzynski.edziennik.databinding.ActivitySzkolnyBinding
import pl.szczodrzynski.edziennik.ext.*
import pl.szczodrzynski.edziennik.sync.AppManagerDetectedEvent
import pl.szczodrzynski.edziennik.sync.SyncWorker
import pl.szczodrzynski.edziennik.sync.UpdateStateEvent
import pl.szczodrzynski.edziennik.sync.UpdateWorker
import pl.szczodrzynski.edziennik.ui.base.MainSnackbar
import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget
@ -56,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.SyncViewListDialog
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.ErrorSnackbar
import pl.szczodrzynski.edziennik.ui.event.EventManualDialog
@ -536,6 +538,14 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
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)
fun onRegisterAvailabilityEvent(event: RegisterAvailabilityEvent) {
EventBus.getDefault().removeStickyEvent(event)
@ -699,6 +709,10 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
if (extras?.containsKey("action") == true) {
val handled = when (extras.getString("action")) {
"updateRequest" -> {
UpdateAvailableDialog(this, app.config.update).show()
true
}
"serverMessage" -> {
ServerMessageDialog(
this,

View File

@ -24,7 +24,7 @@ class Config(db: AppDb) : BaseConfig(db) {
val timetable by lazy { ConfigTimetable(this) }
val grades by lazy { ConfigGrades(this) }
var dataVersion by config<Int>(0)
var dataVersion by config<Int>(DATA_VERSION)
var hash by config<String>("")
var lastProfileId by config<Int>(0)
@ -39,6 +39,7 @@ class Config(db: AppDb) : BaseConfig(db) {
var apiAvailabilityCheck by config<Boolean>(true)
var apiInvalidCert by config<String?>(null)
var apiKeyCustom by config<String?>(null)
var appInstalledTime by config<Long>(0L)
var appRateSnackbarTime by config<Long>(0L)
var appVersion by config<Int>(BuildConfig.VERSION_CODE)
@ -49,7 +50,8 @@ class Config(db: AppDb) : BaseConfig(db) {
var widgetConfigs by config<JsonObject> { JsonObject() }
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)
}

View File

@ -26,9 +26,11 @@ class ProfileConfig(
val timetable by lazy { ConfigTimetable(this) }
val grades by lazy { ConfigGrades(this) }*/
var dataVersion by config<Int>(0)
var dataVersion by config<Int>(DATA_VERSION)
var hash by config<String>("")
var shareByDefault by config<Boolean>(false)
init {
if (dataVersion < DATA_VERSION)
ProfileConfigMigration(this)

View File

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

View File

@ -5,12 +5,9 @@
package pl.szczodrzynski.edziennik.config.utils
import android.content.Context
import androidx.core.content.edit
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.BuildConfig
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 kotlin.math.abs
@ -22,6 +19,9 @@ class ConfigMigration(app: App, config: Config) {
// migrate appConfig from app version 3.x and lower.
// Updates dataVersion to level 2.
AppConfigMigrationV3(p, config)
p.edit {
remove("app.appConfig.appTheme")
}
}
if (dataVersion < 11) {
@ -43,5 +43,7 @@ class ConfigMigration(app: App, config: Config) {
dataVersion = 11
}
hash = "invalid"
}}
}

View File

@ -41,24 +41,18 @@ class UsosApiTerms(
}
private fun processResponse(json: JsonArray): Boolean {
val dates = mutableSetOf<Date>()
val today = Date.getToday()
for (term in json.asJsonObjectList()) {
if (!term.getBoolean("is_active", false))
continue
val startDate = term.getString("start_date")?.let { Date.fromY_m_d(it) }
val finishDate = term.getString("finish_date")?.let { Date.fromY_m_d(it) }
if (startDate != null)
dates += startDate
if (finishDate != null)
dates += finishDate
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) } ?: continue
if (today in startDate..finishDate) {
profile?.studentSchoolYearStart = startDate.year
profile?.dateSemester1Start = startDate
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
}
}

View File

@ -13,7 +13,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.ERROR_API_INVALID_SIGNATURE
import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.DateAdapter
@ -128,16 +127,10 @@ class SzkolnyApi(val app: App) : CoroutineScope {
response: Response<ApiResponse<T>>,
updateDeviceHash: Boolean = false,
): T {
app.config.update = response.body()?.update?.let { update ->
if (update.versionCode > BuildConfig.VERSION_CODE) {
if (update.updateMandatory
&& EventBus.getDefault().hasSubscriberForEvent(update::class.java)) {
EventBus.getDefault().postSticky(update)
}
update
}
else
null
response.body()?.update?.let { update ->
// do not process "null" update, as it might not mean there's no update
// do not notify; silently check and show the home update card
app.updateManager.process(update, notify = false)
}
response.body()?.registerAvailability?.let { registerAvailability ->
@ -265,12 +258,10 @@ class SzkolnyApi(val app: App) : CoroutineScope {
seen = profile.empty
notified = profile.empty
if (profile.userCode == event.sharedBy) {
sharedBy = "self"
addedManually = true
} else {
sharedBy = eventSharedBy
}
sharedBy = if (profile.userCode == event.sharedBy)
"self"
else
eventSharedBy
}
}
}
@ -431,8 +422,8 @@ class SzkolnyApi(val app: App) : CoroutineScope {
}
@Throws(Exception::class)
fun getUpdate(channel: String): List<Update> {
val response = api.updates(channel).execute()
fun getUpdate(channel: Update.Type): List<Update> {
val response = api.updates(channel.name.lowercase()).execute()
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.hmacSHA1
import pl.szczodrzynski.edziennik.ext.md5
import pl.szczodrzynski.edziennik.ext.takeValue
class SignatureInterceptor(val app: App) : Interceptor {
companion object {
private const val API_KEY = "szkolny_android_42a66f0842fc7da4e37c66732acf705a"
const val API_KEY = "szkolny_android_42a66f0842fc7da4e37c66732acf705a"
}
override fun intercept(chain: Interceptor.Chain): Response {
@ -27,7 +28,7 @@ class SignatureInterceptor(val app: App) : Interceptor {
return chain.proceed(
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-Timestamp", timestamp.toString())
.header("X-Signature", sign(timestamp, body, url))

View File

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

View File

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

View File

@ -73,13 +73,22 @@ open class Event(
const val COLOR_INFORMATION = 0xff039be5.toInt()
}
/**
* Added manually - added by self, shared by self, or shared by someone else.
*/
@ColumnInfo(name = "eventAddedManually")
var addedManually: Boolean = false
get() = field || sharedBy == "self"
get() = field || isShared
/**
* Shared by - user code who shared the event. Null if not shared.
* "Self" if shared by this app user.
*/
@ColumnInfo(name = "eventSharedBy")
var sharedBy: String? = null
@ColumnInfo(name = "eventSharedByName")
var sharedByName: String? = null
@ColumnInfo(name = "eventBlacklisted")
var blacklisted: Boolean = false
@ColumnInfo(name = "eventIsDone")
@ -104,6 +113,27 @@ open class Event(
var attachmentIds: MutableList<Long>? = null
var attachmentNames: MutableList<String>? = null
val isHomework
get() = type == TYPE_HOMEWORK
/**
* Whether the event is shared by anyone. Note that this implies [addedManually].
*/
val isShared
get() = sharedBy != null
/**
* Whether the event is shared by "self" (this app user).
*/
val isSharedSent
get() = sharedBy == "self"
/**
* Whether the event is shared by someone else from the class group.
*/
val isSharedReceived
get() = sharedBy != null && sharedBy != "self"
/**
* Add an attachment
* @param id attachment ID
@ -134,9 +164,6 @@ open class Event(
it.timeInMillis += 45 * MINUTE * 1000
}
val isHomework
get() = type == TYPE_HOMEWORK
@Ignore
fun withMetadata(metadata: Metadata) = EventFull(this, metadata)
}

View File

@ -6,7 +6,11 @@ package pl.szczodrzynski.edziennik.data.firebase
import com.google.gson.JsonParser
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 pl.szczodrzynski.edziennik.App
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.Update
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.NotificationType
import pl.szczodrzynski.edziennik.ext.getInt
import pl.szczodrzynski.edziennik.ext.getLong
import pl.szczodrzynski.edziennik.ext.getString
import pl.szczodrzynski.edziennik.ext.resolveString
import pl.szczodrzynski.edziennik.sync.UpdateWorker
import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget
import pl.szczodrzynski.edziennik.utils.models.Date
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("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 {
val message = app.gson.fromJson(message.data.getString("message"), FeedbackMessage::class.java) ?: return@launch
feedbackMessage(message)
@ -149,11 +160,11 @@ class SzkolnyAppFirebase(val app: App, val profiles: List<Profile>, val message:
if (event.color == -1)
event.color = null
event.addedManually = true
event.sharedBy = json.getString("sharedBy")
event.sharedByName = json.getString("sharedByName")
if (profile.userCode == event.sharedBy) {
event.sharedBy = "self"
event.addedManually = true
}
val metadata = Metadata(

View File

@ -50,13 +50,13 @@ inline fun <reified E : Enum<E>> Int.toEnum() = when (E::class.java) {
} as E
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()
// LoginMethod::class.java -> this.asLoginMethod()
LoginMethod::class.java -> this.asLoginMethod()
LoginMode::class.java -> this.asLoginMode()
LoginType::class.java -> this.asLoginType()
// MetadataType::class.java -> this.asMetadataType()
// NotificationType::class.java -> this.asNotificationType()
MetadataType::class.java -> this.asMetadataType()
NotificationType::class.java -> this.asNotificationType()
NavTarget::class.java -> this.asNavTarget()
else -> throw IllegalArgumentException("Unknown type $type")
} as E

View File

@ -76,4 +76,6 @@ fun pendingIntentFlag(): Int {
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
import android.annotation.SuppressLint
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.work.*
import kotlinx.coroutines.*
import org.greenrobot.eventbus.EventBus
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.ext.DAY
import pl.szczodrzynski.edziennik.ext.concat
import pl.szczodrzynski.edziennik.ext.formatDate
import pl.szczodrzynski.edziennik.ext.pendingIntentFlag
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.html.BetterHtml
import java.util.concurrent.TimeUnit
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")
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()
@ -146,22 +78,13 @@ class UpdateWorker(val context: Context, val params: WorkerParameters) : Worker(
return Result.success()
}
launch {
runNow(app)
}
val channel = if (App.devMode)
Update.Type.BETA
else
Update.Type.RELEASE
app.updateManager.checkNowSync(channel, notify = true)
rescheduleNext(this.context)
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

@ -11,6 +11,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged
import androidx.sqlite.db.SimpleSQLiteQuery
import com.chuckerteam.chucker.api.Chucker
import com.chuckerteam.chucker.api.Chucker.SCREEN_HTTP
@ -21,6 +22,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import pl.szczodrzynski.edziennik.*
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.ext.*
import pl.szczodrzynski.edziennik.ui.base.lazypager.LazyFragment
@ -141,6 +143,16 @@ class LabPageFragment : LazyFragment(), CoroutineScope {
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 {
App.config = Config(App.db)
}

View File

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

@ -80,6 +80,8 @@ class EventManualDialog(
SzkolnyApi(app)
}
private val profileConfig by lazy { app.config.forProfile() }
private var enqueuedWeekDialog: AlertDialog? = null
private var enqueuedWeekStart = Date.getToday()
@ -107,9 +109,6 @@ class EventManualDialog(
}
override suspend fun onShow() {
b.shareSwitch.isChecked = editingShared
b.shareSwitch.isEnabled = !editingShared || (editingShared && editingOwn)
b.showMore.onClick { // TODO iconics is broken
it.apply {
refreshDrawableState()
@ -137,6 +136,13 @@ class EventManualDialog(
}
loadLists()
val shareByDefault = profileConfig.shareByDefault
&& profile.enableSharedEvents
&& profile.registration == Profile.REGISTRATION_ENABLED
b.shareSwitch.isChecked = editingShared || editingEvent == null && shareByDefault
b.shareSwitch.isEnabled = !editingShared || editingOwn
}
private fun updateShareText(checked: Boolean = b.shareSwitch.isChecked) {
@ -411,6 +417,20 @@ class EventManualDialog(
return
}
if (share && !profile.enableSharedEvents) {
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.event_sharing)
.setMessage(R.string.settings_register_shared_events_dialog_enabled_text)
.setPositiveButton(R.string.ok) { _, _ ->
profile.enableSharedEvents = true
app.profileSave(profile)
saveEvent()
}
.setNegativeButton(R.string.cancel, null)
.show()
return
}
b.dateDropdown.error = null
b.teamDropdown.error = null
b.typeDropdown.error = null

View File

@ -15,7 +15,10 @@ import coil.load
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
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.databinding.CardHomeAvailabilityBinding
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.HomeCardAdapter
import pl.szczodrzynski.edziennik.ui.home.HomeFragment
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.html.BetterHtml
import kotlin.coroutines.CoroutineContext
@ -79,7 +83,7 @@ class HomeAvailabilityCard(
else if (update != null && update.versionCode > BuildConfig.VERSION_CODE) {
b.homeAvailabilityTitle.setText(R.string.home_availability_title)
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)
onInfoClick = {
UpdateAvailableDialog(activity, update).show()
@ -92,7 +96,10 @@ class HomeAvailabilityCard(
b.homeAvailabilityUpdate.onClick {
if (update == null)
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)

View File

@ -13,6 +13,7 @@ import com.mikepenz.iconics.typeface.library.community.material.CommunityMateria
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.Note
@ -59,6 +60,8 @@ class NoteEditorDialog(
private val textStylingManager
get() = app.textStylingManager
private val profileConfig by lazy { app.config.forProfile() }
private var progressDialog: AlertDialog? = null
override suspend fun onPositiveClick(): Boolean {
@ -76,6 +79,23 @@ class NoteEditorDialog(
return NO_DISMISS
}
if (note.isShared && !profile.enableSharedEvents) {
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.event_sharing)
.setMessage(R.string.settings_register_shared_events_dialog_enabled_text)
.setPositiveButton(R.string.ok) { _, _ ->
profile.enableSharedEvents = true
app.profileSave(profile)
launch {
if (onPositiveClick())
dismiss()
}
}
.setNegativeButton(R.string.cancel, null)
.show()
return NO_DISMISS
}
if (note.isShared || editingNote?.isShared == true) {
progressDialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.please_wait)
@ -127,8 +147,15 @@ class NoteEditorDialog(
topicStylingConfig = StylingConfigBase(editText = b.topic, htmlMode = HtmlMode.SIMPLE)
bodyStylingConfig = StylingConfigBase(editText = b.body, htmlMode = HtmlMode.SIMPLE)
val profile = withContext(Dispatchers.IO) {
app.db.profileDao().getByIdNow(profileId)
}
b.ownerType = owner?.getNoteType() ?: Note.OwnerType.NONE
b.editingNote = editingNote
b.shareByDefault = profileConfig.shareByDefault
&& profile?.enableSharedEvents == true
&& profile.registration == Profile.REGISTRATION_ENABLED
b.color.clear().append(Note.Color.values().map { color ->
TextInputDropDown.Item(

View File

@ -18,8 +18,8 @@ import kotlinx.coroutines.launch
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.ext.after
import pl.szczodrzynski.edziennik.sync.UpdateWorker
import pl.szczodrzynski.edziennik.ui.dialogs.ChangelogDialog
import pl.szczodrzynski.edziennik.ui.settings.SettingsCard
import pl.szczodrzynski.edziennik.ui.settings.SettingsLicenseActivity
@ -113,7 +113,17 @@ class SettingsAboutCard(util: SettingsUtil) : SettingsCard(util), CoroutineScope
icon = CommunityMaterial.Icon3.cmd_update
) {
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

@ -39,7 +39,7 @@ class SettingsRegisterCard(util: SettingsUtil) : SettingsCard(util) {
subText = R.string.settings_register_shared_events_subtext,
icon = CommunityMaterial.Icon3.cmd_share_outline,
value = app.profile.enableSharedEvents
) { _, value ->
) { item, value ->
app.profile.enableSharedEvents = value
app.profileSave()
MaterialAlertDialogBuilder(activity)
@ -52,6 +52,23 @@ class SettingsRegisterCard(util: SettingsUtil) : SettingsCard(util) {
)
.setPositiveButton(R.string.ok, null)
.show()
if (value) {
card.items.after(item, sharedEventsDefaultItem)
} else {
card.items.remove(sharedEventsDefaultItem)
}
util.refresh()
}
}
private val sharedEventsDefaultItem by lazy {
util.createPropertyItem(
text = R.string.settings_register_share_by_default_text,
subText = R.string.settings_register_share_by_default_subtext,
icon = CommunityMaterial.Icon3.cmd_toggle_switch_outline,
value = configProfile.shareByDefault
) { _, value ->
configProfile.shareByDefault = value
}
}
@ -112,8 +129,11 @@ class SettingsRegisterCard(util: SettingsUtil) : SettingsCard(util) {
item.isChecked = enabled
if (value) {
card.items.after(item, sharedEventsItem)
if (app.profile.enableSharedEvents)
card.items.after(item, sharedEventsDefaultItem)
} else {
card.items.remove(sharedEventsItem)
card.items.remove(sharedEventsDefaultItem)
}
util.refresh()
})
@ -127,6 +147,11 @@ class SettingsRegisterCard(util: SettingsUtil) : SettingsCard(util) {
if (app.profile.canShare)
sharedEventsItem
else
null,
if (app.profile.enableSharedEvents)
sharedEventsDefaultItem
else
null
)

View File

@ -112,7 +112,7 @@ class DateDropdown : TextInputDropDown {
date.stepForward(0, 0, -weekDay + 7)
weekDay = 0
// ALL SCHOOL DAYS OF THE NEXT WEEK
while (weekDay < 4) {
while (weekDay < 5) {
dates += Item(
date.value.toLong(),
context.getString(R.string.dialog_event_manual_date_next_week, Week.getFullDayName(weekDay), date.formattedString),

View File

@ -9,11 +9,29 @@ import android.text.TextUtils
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
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 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.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.utils.Utils
import pl.szczodrzynski.edziennik.utils.Utils.d
@ -75,6 +93,14 @@ class BuildManager(val app: App) : CoroutineScope {
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) {
val yes = activity.getString(R.string.yes)
val no = activity.getString(R.string.no)

View File

@ -55,7 +55,8 @@ class EventManager(val app: App) : CoroutineScope {
val hasReplacingNotes = event.hasReplacingNotes()
title.text = listOfNotNull(
if (event.addedManually) "{cmd-clipboard-edit-outline} " else null,
if (event.addedManually && !event.isSharedReceived) "{cmd-calendar-edit} " else null,
if (event.isSharedReceived) "{cmd-share-variant} " else null,
if (event.hasNotes() && hasReplacingNotes && showNotes) "{cmd-swap-horizontal} " else null,
if (event.hasNotes() && !hasReplacingNotes && showNotes) "{cmd-playlist-edit} " else null,
if (showType) "${event.typeName ?: "wydarzenie"} - " else null,
@ -77,6 +78,8 @@ class EventManager(val app: App) : CoroutineScope {
fun setLegendText(legend: IconicsTextView, event: EventFull, showNotes: Boolean = true) {
legend.text = listOfNotNull(
if (event.addedManually) R.string.legend_event_added_manually else null,
if (event.isSharedSent) R.string.legend_event_shared_sent else null,
if (event.isSharedReceived) R.string.legend_event_shared_received else null,
if (event.isDone) R.string.legend_event_is_done else null,
if (showNotes) NoteManager.getLegendText(event) else null,
).map { legend.context.getString(it) }.join("\n")

View File

@ -0,0 +1,112 @@
/*
* 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) {
if (!app.config.sync.notifyAboutUpdates)
return
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: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
android:id="@+id/disableDebug"
android:layout_width="match_parent"

View File

@ -18,6 +18,10 @@
<variable
name="editingNote"
type="Note" />
<variable
name="shareByDefault"
type="boolean" />
</data>
<androidx.core.widget.NestedScrollView
@ -89,7 +93,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:checked="@{editingNote.sharedBy != null}"
android:checked="@{editingNote.sharedBy != null || editingNote == null &amp;&amp; shareByDefault}"
android:isVisible="@{ownerType.shareable}"
android:minHeight="32dp"
android:text="@string/dialog_event_manual_share_enabled" />

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

@ -1,5 +1,6 @@
{
"base": {
"configOverrides": {},
"messagesConfig": {
"subjectLength": null,
"bodyLength": null,
@ -82,7 +83,9 @@
},
"university": {
"configOverrides": {
"timetableColorSubjectName": true
"shareByDefault": true,
"timetableColorSubjectName": true,
"timetableTrimHourRange": true
},
"uiConfig": {
"lessonHeight": 45

View File

@ -1437,7 +1437,7 @@
<string name="agenda_config_elearning_mark">Ustaw wydarzenia jako lekcje on-line</string>
<string name="agenda_config_elearning_type">Wybierz rodzaj wydarzeń</string>
<string name="agenda_config_elearning_group">Grupuj lekcje on-line na liście</string>
<string name="legend_event_added_manually">{cmd-clipboard-edit-outline} wydarzenie dodane ręcznie</string>
<string name="legend_event_added_manually">{cmd-calendar-edit} wydarzenie dodane ręcznie</string>
<string name="legend_event_is_done">{cmd-check} oznaczono jako wykonane</string>
<string name="agenda_config_not_available_yet">Funkcja jeszcze nie jest dostępna.</string>
<string name="messages_config_compose">Tworzenie wiadomości</string>
@ -1549,4 +1549,8 @@
<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="app_cannot_load_data">Nie można załadować danych aplikacji</string>
<string name="legend_event_shared_received">{cmd-share-variant} udostępnione w klasie</string>
<string name="legend_event_shared_sent">{cmd-share-variant} udostępnione przez Ciebie</string>
<string name="settings_register_share_by_default_text">Domyślnie udostępniaj wydarzenia</string>
<string name="settings_register_share_by_default_subtext">Ustaw tworzone wydarzenia domyślnie jako udostępnione</string>
</resources>

View File

@ -15,6 +15,8 @@ import android.net.Uri
import android.os.Build
import android.widget.Toast
import androidx.core.content.FileProvider
import androidx.core.content.getSystemService
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
@ -49,6 +51,8 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
return
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()) {
app.permissionChecker.requestApkInstall()
return
@ -79,11 +83,14 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
val app = application as App
val update = App.config.update ?: return
if (tryUpdateWithGooglePlay(update))
if (tryUpdateWithGooglePlay(update)) {
stopSelf()
return
}
if (update.downloadUrl == null) {
Toast.makeText(app, "Nie można pobrać tej aktualizacji. Pobierz ręcznie z Google Play.", Toast.LENGTH_LONG).show()
stopSelf()
return
}
@ -92,7 +99,7 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
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)
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()
downloadId = downloadManager.enqueue(request)
EventBus.getDefault().postSticky(UpdateStateEvent(running = true, update, downloadId))
}
}

View File

@ -5,8 +5,8 @@ buildscript {
kotlin_version = '1.6.10'
release = [
versionName: "4.13-rc.1",
versionCode: 4130010
versionName: "4.13-rc.4",
versionCode: 4130040
]
setup = [