diff --git a/.github/workflows/build-release-aab-play.yml b/.github/workflows/build-release-aab-play.yml index 95fec5cb..eb6923bb 100644 --- a/.github/workflows/build-release-aab-play.yml +++ b/.github/workflows/build-release-aab-play.yml @@ -113,10 +113,11 @@ jobs: with: serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }} packageName: pl.szczodrzynski.edziennik - releaseFile: ${{ needs.sign.outputs.signedReleaseFile }} + releaseFiles: ${{ needs.sign.outputs.signedReleaseFile }} releaseName: ${{ steps.changelog.outputs.appVersionName }} track: ${{ secrets.PLAY_RELEASE_TRACK }} whatsNewDirectory: ${{ steps.changelog.outputs.changelogDir }} + status: completed - name: Upload workflow artifact uses: actions/upload-artifact@v2 diff --git a/app/build.gradle b/app/build.gradle index c6e8ef86..0ceae1a1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -156,6 +156,7 @@ dependencies { implementation "androidx.navigation:navigation-fragment-ktx:2.5.2" implementation "androidx.recyclerview:recyclerview:1.2.1" implementation "androidx.room:room-runtime:2.4.3" + implementation "androidx.room:room-ktx:2.4.3" implementation "androidx.work:work-runtime-ktx:2.7.1" kapt "androidx.room:room-compiler:2.4.3" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2c24b426..da531480 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -160,7 +160,11 @@ + android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar" /> + diff --git a/app/src/main/assets/pl-changelog.html b/app/src/main/assets/pl-changelog.html index e49ccaa1..af4aaca4 100644 --- a/app/src/main/assets/pl-changelog.html +++ b/app/src/main/assets/pl-changelog.html @@ -1,9 +1,10 @@ -

Wersja 4.13.2, 2022-11-28

+

Wersja 4.13.6, 2023-03-24

    -
  • Poprawiono synchronizację w Mobidzienniku bez ustawionego adresu e-mail.
  • -
  • Poprawiono błąd synchronizacji w Vulcanie.
  • +
  • Naprawiono pobieranie załączników na Androidzie 13 i nowszym.
  • +
  • Dodano opcję odświeżenia planu lekcji na wybrany tydzień.
  • +
  • Usunięto błędy logowania. @BxOxSxS


Dzięki za korzystanie ze Szkolnego!
-© [Kuba Szczodrzyński](@kuba2k2) 2022 +© [Kuba Szczodrzyński](@kuba2k2) 2023 diff --git a/app/src/main/cpp/szkolny-signing.cpp b/app/src/main/cpp/szkolny-signing.cpp index 582bc8b3..9ae8f745 100644 --- a/app/src/main/cpp/szkolny-signing.cpp +++ b/app/src/main/cpp/szkolny-signing.cpp @@ -9,7 +9,7 @@ /*secret password - removed for source code publication*/ static toys AES_IV[16] = { - 0x8c, 0xad, 0x9c, 0x3e, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; + 0x6d, 0xa5, 0x32, 0xe6, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat); diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/App.kt b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt index 30225982..74e23d88 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/App.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt @@ -235,6 +235,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { } Signing.getCert(this) + Utils.initializeStorageDir(this) launch { withContext(Dispatchers.Default) { @@ -422,6 +423,12 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { try { App.data = AppData.get(profile.loginStoreType) d("App", "Loaded AppData: ${App.data}") + // apply newly-added config overrides, if not changed by the user yet + for ((key, value) in App.data.configOverrides) { + val config = App.profile.config + if (!config.has(key)) + config.set(key, value) + } } catch (e: Exception) { Log.e("App", "Cannot load AppData", e) Toast.makeText(this, R.string.app_cannot_load_data, Toast.LENGTH_LONG).show() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt index ce39bc77..7965d17a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt @@ -322,7 +322,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope { // IT'S WINTER MY DUDES val today = Date.getToday() - if ((today.month % 11 == 1) && app.config.ui.snowfall) { + if ((today.month / 3 % 4 == 0) && app.config.ui.snowfall) { b.rootFrame.addView(layoutInflater.inflate(R.layout.snowfall, b.rootFrame, false)) } else if (app.config.ui.eggfall && BigNightUtil().isDataWielkanocyNearDzisiaj()) { val eggfall = layoutInflater.inflate( diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/AppData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/AppData.kt index 6f72b82e..a0fa7c27 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/AppData.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/AppData.kt @@ -59,10 +59,11 @@ data class AppData( val lessonHeight: Int, val enableMarkAsReadAnnouncements: Boolean, val enableNoticePoints: Boolean, + val eventManualShowSubjectDropdown: Boolean, ) data class EventType( - val id: Int, + val id: Long, val color: String, val name: String, ) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/BaseConfig.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/BaseConfig.kt index 69d232c5..76d95b0b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/BaseConfig.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/BaseConfig.kt @@ -43,4 +43,6 @@ abstract class BaseConfig( db.configDao().add(ConfigEntry(profileId ?: -1, key, value)) } } + + fun has(key: String) = values.containsKey(key) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfig.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfig.kt index 4df2ef47..c36b6fae 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfig.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfig.kt @@ -15,7 +15,7 @@ class ProfileConfig( entries: List?, ) : BaseConfig(db, profileId, entries) { companion object { - const val DATA_VERSION = 4 + const val DATA_VERSION = 5 } val grades by lazy { ProfileConfigGrades(this) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigUI.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigUI.kt index 4bfaf047..35951082 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigUI.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigUI.kt @@ -15,6 +15,7 @@ class ProfileConfigUI(base: ProfileConfig) { var agendaGroupByType by base.config(false) var agendaLessonChanges by base.config(true) var agendaTeacherAbsence by base.config(true) + var agendaSubjectImportant by base.config(false) var agendaElearningMark by base.config(false) var agendaElearningGroup by base.config(true) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ProfileConfigMigration.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ProfileConfigMigration.kt index c5b56690..ab915b1d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ProfileConfigMigration.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ProfileConfigMigration.kt @@ -16,6 +16,8 @@ import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_AL class ProfileConfigMigration(config: ProfileConfig) { init { config.apply { + val profile = db.profileDao().getByIdNow(profileId ?: -1) + if (dataVersion < 2) { sync.notificationFilter = sync.notificationFilter + NotificationType.TEACHER_ABSENCE @@ -37,11 +39,23 @@ class ProfileConfigMigration(config: ProfileConfig) { // 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) } } + + if (dataVersion < 5) { + // update USOS event types and the appropriate events (2022-12-25) + dataVersion = 5 + + if (profile?.loginStoreType?.schoolType == SchoolType.UNIVERSITY) { + db.eventTypeDao().getAllWithDefaults(profile) + // wejściówka (4) -> kartkówka (3) + db.eventDao().getRawNow("UPDATE events SET eventType = 3 WHERE profileId = $profileId AND eventType = 4;") + // zadanie (6) -> zadanie domowe (-1) + db.eventDao().getRawNow("UPDATE events SET eventType = -1 WHERE profileId = $profileId AND eventType = 6;") + } + } }} } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt index c29b8698..31fabaea 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt @@ -26,9 +26,10 @@ val LIBRUS_USER_AGENT = "${SYSTEM_USER_AGENT}LibrusMobileApp" const val SYNERGIA_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Gecko/20100101 Firefox/62.0" const val LIBRUS_CLIENT_ID = "VaItV6oRutdo8fnjJwysnTjVlvaswf52ZqmXsJGP" const val LIBRUS_REDIRECT_URL = "app://librus" -const val LIBRUS_AUTHORIZE_URL = "https://portal.librus.pl/oauth2/authorize?client_id=$LIBRUS_CLIENT_ID&redirect_uri=$LIBRUS_REDIRECT_URL&response_type=code" -const val LIBRUS_LOGIN_URL = "https://portal.librus.pl/rodzina/login/action" +const val LIBRUS_AUTHORIZE_URL = "https://portal.librus.pl/konto-librus/redirect/dru" +const val LIBRUS_LOGIN_URL = "https://portal.librus.pl/konto-librus/login/action" const val LIBRUS_TOKEN_URL = "https://portal.librus.pl/oauth2/access_token" +const val LIBRUS_HEADER = "pl.librus.synergiaDru2" const val LIBRUS_ACCOUNT_URL = "/v3/SynergiaAccounts/fresh/" // + login const val LIBRUS_ACCOUNTS_URL = "/v3/SynergiaAccounts" @@ -59,9 +60,6 @@ const val LIBRUS_SANDBOX_URL = "https://sandbox.librus.pl/index.php?action=" const val LIBRUS_SYNERGIA_HOMEWORK_ATTACHMENT_URL = "https://synergia.librus.pl/homework/downloadFile" const val LIBRUS_SYNERGIA_MESSAGES_ATTACHMENT_URL = "https://synergia.librus.pl/wiadomosci/pobierz_zalacznik" -const val LIBRUS_PORTAL_RECAPTCHA_KEY = "6Lf48moUAAAAAB9ClhdvHr46gRWR" -const val LIBRUS_PORTAL_RECAPTCHA_REFERER = "https://portal.librus.pl/rodzina/login" - val MOBIDZIENNIK_USER_AGENT = SYSTEM_USER_AGENT diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Regexes.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Regexes.kt index f3d5a297..70388abf 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Regexes.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Regexes.kt @@ -24,6 +24,25 @@ object Regexes { """^\[META:([A-z0-9-&=]+)]""".toRegex() } + val HTML_INPUT_HIDDEN by lazy { + """""".toRegex() + } + val HTML_INPUT_NAME by lazy { + """name="(.+?)"""".toRegex() + } + val HTML_INPUT_VALUE by lazy { + """value="(.+?)"""".toRegex() + } + val HTML_CSRF_TOKEN by lazy { + """name="csrf-token" content="([A-z0-9=+/\-_]+?)"""".toRegex() + } + val HTML_FORM_ACTION by lazy { + """
Unit) { private const val TAG = "LoginLibrusPortal" } + // loop failsafe + private var loginPerformed = false + init { run { if (data.loginStore.mode != LoginMode.LIBRUS_EMAIL) { data.error(ApiError(TAG, ERROR_INVALID_LOGIN_MODE)) @@ -33,6 +36,7 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) { data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING)) return@run } + loginPerformed = false // succeed having a non-expired access token and a refresh token if (data.isPortalLoginValid()) { @@ -58,18 +62,23 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) { } }} - private fun authorize(url: String?) { + private fun authorize(url: String, referer: String? = null) { d(TAG, "Request: Librus/Login/Portal - $url") Request.builder() .url(url) .userAgent(LIBRUS_USER_AGENT) + .also { + if (referer != null) + it.addHeader("Referer", referer) + } + .addHeader("X-Requested-With", LIBRUS_HEADER) .withClient(data.app.httpLazy) .callback(object : TextCallbackHandler() { override fun onSuccess(text: String, response: Response) { val location = response.headers().get("Location") if (location != null) { - val authMatcher = Pattern.compile("$LIBRUS_REDIRECT_URL\\?code=([A-z0-9]+?)$", Pattern.DOTALL or Pattern.MULTILINE).matcher(location) + val authMatcher = Pattern.compile("$LIBRUS_REDIRECT_URL\\?code=([^&?]+)", Pattern.DOTALL or Pattern.MULTILINE).matcher(location) when { authMatcher.find() -> { accessToken(authMatcher.group(1), null) @@ -83,16 +92,31 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) { authorize(location) } } - } else { - val csrfMatcher = Pattern.compile("name=\"csrf-token\" content=\"([A-z0-9=+/\\-_]+?)\"", Pattern.DOTALL).matcher(text) - if (csrfMatcher.find()) { - login(csrfMatcher.group(1) ?: "") - } else { - data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_CSRF_MISSING) - .withResponse(response) - .withApiResponse(text)) + return + } + + if (checkError(text, response)) + return + + var loginUrl = if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL + val csrfToken = Regexes.HTML_CSRF_TOKEN.find(text)?.get(1) ?: "" + + for (match in Regexes.HTML_FORM_ACTION.findAll(text)) { + val form = match.value.lowercase() + if ("login" in form && "post" in form) { + loginUrl = match[1] } } + + val params = mutableMapOf() + for (match in Regexes.HTML_INPUT_HIDDEN.findAll(text)) { + val input = match.value + val name = Regexes.HTML_INPUT_NAME.find(input)?.get(1) ?: continue + val value = Regexes.HTML_INPUT_VALUE.find(input)?.get(1) ?: continue + params[name] = value + } + + login(url = loginUrl, referer = url, csrfToken, params) } override fun onFailure(response: Response, throwable: Throwable) { @@ -105,8 +129,54 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) { .enqueue() } - private fun login(csrfToken: String) { - d(TAG, "Request: Librus/Login/Portal - ${if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL}") + private fun checkError(text: String, response: Response): Boolean { + when { + text.contains("librus_account_settings_main") -> return false + text.contains("Sesja logowania wygasła") -> ERROR_LOGIN_LIBRUS_PORTAL_CSRF_EXPIRED + text.contains("Upewnij się, że nie") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN + text.contains("Podany adres e-mail jest nieprawidłowy.") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN + else -> null // no error for now + }?.let { errorCode -> + data.error(ApiError(TAG, errorCode) + .withApiResponse(text) + .withResponse(response)) + return true + } + + if ("robotem" in text || "g-recaptcha" in text || "captchaValidate" in text) { + val siteKey = Regexes.HTML_RECAPTCHA_KEY.find(text)?.get(1) + if (siteKey == null) { + data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR) + .withApiResponse(text) + .withResponse(response)) + return true + } + data.requireUserAction( + type = UserActionRequiredEvent.Type.RECAPTCHA, + params = Bundle( + "siteKey" to siteKey, + "referer" to response.request().url().toString(), + "userAgent" to LIBRUS_USER_AGENT, + ), + errorText = R.string.notification_user_action_required_captcha_librus, + ) + return true + } + return false + } + + private fun login( + url: String, + referer: String, + csrfToken: String?, + params: Map, + ) { + if (loginPerformed) { + data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR)) + return + } + + d(TAG, "Request: Librus/Login/Portal - $url") val recaptchaCode = data.arguments?.getString("recaptchaCode") ?: data.loginStore.getLoginData("recaptchaCode", null) val recaptchaTime = data.arguments?.getLong("recaptchaTime") ?: data.loginStore.getLoginData("recaptchaTime", 0L) @@ -116,67 +186,46 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) { Request.builder() .url(if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL) .userAgent(LIBRUS_USER_AGENT) + .addHeader("X-Requested-With", LIBRUS_HEADER) + .addHeader("Referer", referer) + .withClient(data.app.httpLazy) .addParameter("email", data.portalEmail) .addParameter("password", data.portalPassword) .also { if (recaptchaCode != null && System.currentTimeMillis() - recaptchaTime < 2*60*1000 /* 2 minutes */) it.addParameter("g-recaptcha-response", recaptchaCode) + if (csrfToken != null) + it.addHeader("X-CSRF-TOKEN", csrfToken) + for ((key, value) in params) { + it.addParameter(key, value) + } } - .addHeader("X-CSRF-TOKEN", csrfToken) - .allowErrorCode(HTTP_BAD_REQUEST) - .allowErrorCode(HTTP_FORBIDDEN) - .contentType(MediaTypeUtils.APPLICATION_JSON) + .contentType(MediaTypeUtils.APPLICATION_FORM) .post() - .callback(object : JsonCallbackHandler() { - override fun onSuccess(json: JsonObject?, response: Response) { + .callback(object : TextCallbackHandler() { + override fun onSuccess(text: String?, response: Response) { + loginPerformed = true val location = response.headers()?.get("Location") if (location == "$LIBRUS_REDIRECT_URL?command=close") { data.error(ApiError(TAG, ERROR_LIBRUS_PORTAL_MAINTENANCE) - .withApiResponse(json) + .withApiResponse(text) .withResponse(response)) return } - - if (json == null) { - if (response.parserErrorBody?.contains("wciąż nieaktywne") == true) { - data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_NOT_ACTIVATED) - .withResponse(response)) - return - } + if (text == null) { data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY) .withResponse(response)) return } - val error = if (response.code() == 200) null else - json.getJsonArray("errors")?.getString(0) - ?: json.getJsonObject("errors")?.entrySet()?.firstOrNull()?.value?.asString - if (error?.contains("robotem") == true || json.getBoolean("captchaRequired") == true) { - data.requireUserAction( - type = UserActionRequiredEvent.Type.RECAPTCHA, - params = Bundle( - "siteKey" to LIBRUS_PORTAL_RECAPTCHA_KEY, - "referer" to LIBRUS_PORTAL_RECAPTCHA_REFERER, - ), - errorText = R.string.notification_user_action_required_captcha_librus, - ) - return - } - - error?.let { code -> - when { - code.contains("Sesja logowania wygasła") -> ERROR_LOGIN_LIBRUS_PORTAL_CSRF_EXPIRED - code.contains("Upewnij się, że nie") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN - code.contains("Podany adres e-mail jest nieprawidłowy.") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN - else -> ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR - }.let { errorCode -> - data.error(ApiError(TAG, errorCode) - .withApiResponse(json) - .withResponse(response)) - return - } - } - authorize(json.getString("redirect", LIBRUS_AUTHORIZE_URL)) + authorize( + url = location + ?: if (data.fakeLogin) + FAKE_LIBRUS_AUTHORIZE + else + LIBRUS_AUTHORIZE_URL, + referer = referer, + ) } override fun onFailure(response: Response, throwable: Throwable) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/SignatureInterceptor.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/SignatureInterceptor.kt index 710bcb74..275a1909 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/SignatureInterceptor.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/SignatureInterceptor.kt @@ -29,11 +29,12 @@ class SignatureInterceptor(val app: App) : Interceptor { return chain.proceed( request.newBuilder() .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)) .header("X-AppBuild", BuildConfig.BUILD_TYPE) .header("X-AppFlavor", BuildConfig.FLAVOR) + .header("X-AppVersion", BuildConfig.VERSION_CODE.toString()) + .header("X-DeviceId", app.deviceId) + .header("X-Signature", sign(timestamp, body, url)) + .header("X-Timestamp", timestamp.toString()) .build()) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt index cea7a064..f520ac62 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt @@ -46,6 +46,6 @@ object Signing { /*fun provideKey(param1: String, param2: Long): ByteArray {*/ fun pleaseStopRightNow(param1: String, param2: Long): ByteArray { - return "$param1.MTIzNDU2Nzg5MDLUEjNGMN===.$param2".sha256() + return "$param1.MTIzNDU2Nzg5MD01uMP7oW===.$param2".sha256() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventTypeDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventTypeDao.kt index c6c9ee44..2f3f5c94 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventTypeDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventTypeDao.kt @@ -25,6 +25,9 @@ abstract class EventTypeDao { @Query("DELETE FROM eventTypes WHERE profileId = :profileId") abstract fun clear(profileId: Int) + @Query("DELETE FROM eventTypes WHERE profileId = :profileId AND eventTypeSource = :source") + abstract fun clearBySource(profileId: Int, source: Int) + @Query("SELECT * FROM eventTypes WHERE profileId = :profileId AND eventType = :typeId") abstract fun getByIdNow(profileId: Int, typeId: Long): EventType? @@ -43,7 +46,7 @@ abstract class EventTypeDao { val typeList = data.eventTypes.map { EventType( profileId = profile.id, - id = it.id.toLong(), + id = it.id, name = it.name, color = Color.parseColor(it.color), order = order++, @@ -53,4 +56,21 @@ abstract class EventTypeDao { addAll(typeList) return typeList } + + fun getAllWithDefaults(profile: Profile): List { + val eventTypes = getAllNow(profile.id) + + val defaultIdsExpected = AppData.get(profile.loginStoreType).eventTypes + .map { it.id } + val defaultIdsFound = eventTypes.filter { it.source == SOURCE_DEFAULT } + .sortedBy { it.order } + .map { it.id } + + if (defaultIdsExpected == defaultIdsFound) + return eventTypes + + clearBySource(profile.id, SOURCE_DEFAULT) + addDefaultTypes(profile) + return eventTypes + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/ProfileDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/ProfileDao.kt index ef5a3655..f83a8e76 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/ProfileDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/ProfileDao.kt @@ -28,6 +28,9 @@ interface ProfileDao { @Query("SELECT * FROM profiles WHERE profileId = :profileId") fun getByIdNow(profileId: Int): Profile? + @Query("SELECT * FROM profiles WHERE profileId = :profileId") + suspend fun getByIdSuspend(profileId: Int): Profile? + @get:Query("SELECT * FROM profiles WHERE profileId >= 0 ORDER BY profileId") val all: LiveData> diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EventType.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EventType.kt index 7753b38a..00ba4c04 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EventType.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EventType.kt @@ -5,31 +5,6 @@ package pl.szczodrzynski.edziennik.data.db.entity import androidx.room.ColumnInfo import androidx.room.Entity -import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_CLASS_EVENT -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_DEFAULT -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_ELEARNING -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_ESSAY -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_EXAM -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_EXCURSION -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_HOMEWORK -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_INFORMATION -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_PROJECT -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_PT_MEETING -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_READING -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_SHORT_QUIZ -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_CLASS_EVENT -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_DEFAULT -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_ELEARNING -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_ESSAY -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_EXAM -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_EXCURSION -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_HOMEWORK -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_INFORMATION -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_PROJECT -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_PT_MEETING -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_READING -import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_SHORT_QUIZ @Entity( tableName = "eventTypes", @@ -55,35 +30,5 @@ class EventType( const val SOURCE_REGISTER = 1 const val SOURCE_CUSTOM = 2 const val SOURCE_SHARED = 3 - - fun getTypeColorMap() = mapOf( - TYPE_ELEARNING to COLOR_ELEARNING, - TYPE_HOMEWORK to COLOR_HOMEWORK, - TYPE_DEFAULT to COLOR_DEFAULT, - TYPE_EXAM to COLOR_EXAM, - TYPE_SHORT_QUIZ to COLOR_SHORT_QUIZ, - TYPE_ESSAY to COLOR_ESSAY, - TYPE_PROJECT to COLOR_PROJECT, - TYPE_PT_MEETING to COLOR_PT_MEETING, - TYPE_EXCURSION to COLOR_EXCURSION, - TYPE_READING to COLOR_READING, - TYPE_CLASS_EVENT to COLOR_CLASS_EVENT, - TYPE_INFORMATION to COLOR_INFORMATION - ) - - fun getTypeNameMap() = mapOf( - TYPE_ELEARNING to R.string.event_type_elearning, - TYPE_HOMEWORK to R.string.event_type_homework, - TYPE_DEFAULT to R.string.event_other, - TYPE_EXAM to R.string.event_exam, - TYPE_SHORT_QUIZ to R.string.event_short_quiz, - TYPE_ESSAY to R.string.event_essay, - TYPE_PROJECT to R.string.event_project, - TYPE_PT_MEETING to R.string.event_pt_meeting, - TYPE_EXCURSION to R.string.event_excursion, - TYPE_READING to R.string.event_reading, - TYPE_CLASS_EVENT to R.string.event_class_event, - TYPE_INFORMATION to R.string.event_information - ) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/JsonExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/JsonExtensions.kt index b91399a6..fb261e0e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ext/JsonExtensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/JsonExtensions.kt @@ -73,7 +73,8 @@ fun JsonObject(vararg properties: Pair): JsonObject { is Number -> addProperty(key, value) is Boolean -> addProperty(key, value) is Enum<*> -> addProperty(key, value.toInt()) - else -> add(key, property.toJsonElement()) + null -> add(key, null) + else -> add(key, value.toJsonElement()) } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/MiscExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/MiscExtensions.kt index e53950f9..cd2d8a1f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ext/MiscExtensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/MiscExtensions.kt @@ -73,6 +73,12 @@ fun pendingIntentFlag(): Int { return 0 } +fun pendingIntentMutable(): Int { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + return PendingIntent.FLAG_MUTABLE + return 0 +} + fun Int?.takeValue() = if (this == -1) null else this fun Int?.takePositive() = if (this == -1 || this == 0) null else this diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt index adb6fe55..d4149320 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt @@ -15,6 +15,7 @@ import android.text.style.CharacterStyle import android.text.style.ForegroundColorSpan import android.text.style.StrikethroughSpan import android.text.style.StyleSpan +import android.text.style.UnderlineSpan import androidx.annotation.PluralsRes import androidx.annotation.StringRes import com.mikepenz.materialdrawer.holder.StringHolder @@ -160,6 +161,11 @@ fun CharSequence?.asBoldSpannable(): Spannable { spannable.setSpan(StyleSpan(Typeface.BOLD), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) return spannable } +fun CharSequence?.asUnderlineSpannable(): Spannable { + val spannable = SpannableString(this) + spannable.setSpan(UnderlineSpan(), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + return spannable +} fun CharSequence.asSpannable( vararg spans: CharacterStyle, substring: CharSequence? = null, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/ViewExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/ViewExtensions.kt index 3a0468ef..ddf49975 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ext/ViewExtensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/ViewExtensions.kt @@ -8,6 +8,7 @@ import android.content.Context import android.content.res.Resources import android.graphics.Rect import android.view.View +import android.view.ViewGroup import android.view.WindowManager import android.widget.* import androidx.annotation.StringRes @@ -161,3 +162,12 @@ val SwipeRefreshLayout.onScrollListener: RecyclerView.OnScrollListener } } +fun View.removeFromParent() { + (parent as? ViewGroup)?.removeView(this) +} + +fun View.appendView(child: View) { + val parent = parent as? ViewGroup ?: return + val index = parent.indexOfChild(this) + parent.addView(child, index + 1) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookie.kt b/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookie.kt index 75b60309..ecd513b0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookie.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookie.kt @@ -5,8 +5,20 @@ package pl.szczodrzynski.edziennik.network.cookie import okhttp3.Cookie +import okhttp3.HttpUrl class DumbCookie(var cookie: Cookie) { + companion object { + fun deserialize(key: String, value: String): DumbCookie? { + val (domain, _) = key.split('|', limit = 2) + val url = HttpUrl.Builder() + .scheme("https") + .host(domain) + .build() + val cookie = Cookie.parse(url, value) ?: return null + return DumbCookie(cookie) + } + } constructor(domain: String, name: String, value: String, expiresAt: Long? = null) : this( Cookie.Builder() @@ -21,7 +33,10 @@ class DumbCookie(var cookie: Cookie) { cookie = Cookie.Builder() .name(cookie.name()) .value(cookie.value()) - .expiresAt(cookie.expiresAt()) + .also { + if (cookie.persistent()) + it.expiresAt(cookie.expiresAt()) + } .domain(cookie.domain()) .build() } @@ -45,4 +60,7 @@ class DumbCookie(var cookie: Cookie) { hash = 31 * hash + cookie.domain().hashCode() return hash } + + fun serializeKey() = cookie.domain() + "|" + cookie.name() + fun serialize() = serializeKey() to cookie.toString() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookieJar.kt b/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookieJar.kt index 39cec6eb..945e16ea 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookieJar.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookieJar.kt @@ -5,6 +5,7 @@ package pl.szczodrzynski.edziennik.network.cookie import android.content.Context +import androidx.core.content.edit import okhttp3.Cookie import okhttp3.CookieJar import okhttp3.HttpUrl @@ -26,22 +27,48 @@ class DumbCookieJar( ) : CookieJar { private val prefs = context.getSharedPreferences("cookies", Context.MODE_PRIVATE) - val sessionCookies = mutableSetOf() - private val savedCookies = mutableSetOf() + private val sessionCookies = mutableSetOf() + + init { + val toRemove = mutableListOf() + prefs.all.forEach { (key, value) -> + if (value !is String) + return@forEach + val dc = DumbCookie.deserialize(key, value) ?: return@forEach + if (dc.cookie.expiresAt() > System.currentTimeMillis()) + sessionCookies.add(dc) + else + toRemove.add(key) + } + prefs.edit { + for (key in toRemove) { + remove(key) + } + } + } + private fun save(dc: DumbCookie) { sessionCookies.remove(dc) sessionCookies.add(dc) if (dc.cookie.persistent() || persistAll) { - savedCookies.remove(dc) - savedCookies.add(dc) + prefs.edit { + val (key, value) = dc.serialize() + putString(key, value) + } } } private fun delete(vararg toRemove: DumbCookie) { - sessionCookies.removeAll(toRemove) - savedCookies.removeAll(toRemove) + sessionCookies.removeAll(toRemove.toSet()) + prefs.edit { + for (dc in toRemove) { + val key = dc.serializeKey() + if (prefs.contains(key)) + remove(key) + } + } } - override fun saveFromResponse(url: HttpUrl?, cookies: List) { + override fun saveFromResponse(url: HttpUrl, cookies: MutableList) { for (cookie in cookies) { val dc = DumbCookie(cookie) save(dc) @@ -54,6 +81,10 @@ class DumbCookieJar( }.map { it.cookie } } + fun getAllDomains(): List { + return sessionCookies.map { it.cookie } + } + fun get(domain: String, name: String): String? { return sessionCookies.firstOrNull { it.domainMatches(domain) && it.cookie.name() == name @@ -84,7 +115,7 @@ class DumbCookieJar( fun getAll(domain: String): Map { return sessionCookies.filter { it.domainMatches(domain) - }.map { it.cookie.name() to it.cookie.value() }.toMap() + }.associate { it.cookie.name() to it.cookie.value() } } fun remove(domain: String, name: String) { @@ -100,4 +131,11 @@ class DumbCookieJar( } delete(*toRemove.toTypedArray()) } + + fun clearAllDomains() { + sessionCookies.clear() + prefs.edit { + clear() + } + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/AgendaFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/AgendaFragment.kt index 52c9f345..cbdbce7b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/AgendaFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/AgendaFragment.kt @@ -142,13 +142,7 @@ class AgendaFragment : Fragment(), CoroutineScope { private suspend fun checkEventTypes() { withContext(Dispatchers.Default) { - val eventTypes = app.db.eventTypeDao().getAllNow(app.profileId).map { - it.id - } - val defaultEventTypes = EventType.getTypeColorMap().keys - if (!eventTypes.containsAll(defaultEventTypes)) { - app.db.eventTypeDao().addDefaultTypes(app.profile) - } + app.db.eventTypeDao().getAllWithDefaults(app.profile) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/event/AgendaEventRenderer.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/event/AgendaEventRenderer.kt index 31eb6730..a1b9ead4 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/event/AgendaEventRenderer.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/event/AgendaEventRenderer.kt @@ -11,6 +11,7 @@ import android.widget.TextView import androidx.core.view.isVisible import com.github.tibolte.agendacalendarview.render.EventRenderer import com.mikepenz.iconics.view.IconicsTextView +import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.databinding.AgendaWrappedEventBinding import pl.szczodrzynski.edziennik.databinding.AgendaWrappedEventCompactBinding @@ -53,16 +54,24 @@ class AgendaEventRenderer( else event.time!!.stringHM + val agendaSubjectImportant = App.profile.config.ui.agendaSubjectImportant val eventSubtitle = listOfNotNull( timeText, - event.subjectLongName, + event.subjectLongName.takeIf { !agendaSubjectImportant }, + event.typeName.takeIf { agendaSubjectImportant }, event.teacherName, event.teamName ).join(", ") card.foreground.setTintColor(event.eventColor) card.background.setTintColor(event.eventColor) - manager.setEventTopic(title, event, doneIconColor = textColor) + manager.setEventTopic( + title = title, + event = event, + doneIconColor = textColor, + showType = !agendaSubjectImportant, + showSubject = agendaSubjectImportant, + ) title.setTextColor(textColor) subtitle?.text = eventSubtitle subtitle?.setTextColor(textColor) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/base/enums/NavTarget.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/base/enums/NavTarget.kt index a62ecf7c..749c1343 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/base/enums/NavTarget.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/base/enums/NavTarget.kt @@ -217,6 +217,7 @@ enum class NavTarget( location = NavTargetLocation.BOTTOM_SHEET, nameRes = R.string.menu_debug, icon = CommunityMaterial.Icon.cmd_android_debug_bridge, + devModeOnly = true, ), GRADES_EDITOR( id = 501, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaDialog.kt index cfecde91..56d3976a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaDialog.kt @@ -25,6 +25,7 @@ class RecaptchaDialog( private val autoRetry: Boolean = true, private val onSuccess: (recaptchaCode: String) -> Unit, private val onFailure: (() -> Unit)? = null, + private val onServerError: (() -> Unit)? = null, onShowListener: ((tag: String) -> Unit)? = null, onDismissListener: ((tag: String) -> Unit)? = null, ) : BindingDialog(activity, onShowListener, onDismissListener) { @@ -44,7 +45,11 @@ class RecaptchaDialog( override suspend fun onBeforeShow(): Boolean { val (title, text, bitmap) = withContext(Dispatchers.Default) { - val html = loadCaptchaHtml() ?: return@withContext null + val html = loadCaptchaHtml() + if (html == null) { + onServerError?.invoke() + return@withContext null + } return@withContext loadCaptchaData(html) } ?: run { onFailure?.invoke() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaPromptDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaPromptDialog.kt index a927347d..ca876ddb 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaPromptDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaPromptDialog.kt @@ -19,6 +19,7 @@ class RecaptchaPromptDialog( private val referer: String, private val onSuccess: (recaptchaCode: String) -> Unit, private val onCancel: (() -> Unit)?, + private val onServerError: (() -> Unit)? = null, onShowListener: ((tag: String) -> Unit)? = null, onDismissListener: ((tag: String) -> Unit)? = null, ) : BindingDialog(activity, onShowListener, onDismissListener) { @@ -62,7 +63,8 @@ class RecaptchaPromptDialog( b.checkbox.background = checkboxBackground b.checkbox.foreground = checkboxForeground b.progress.visibility = View.GONE - } + }, + onServerError = onServerError, ).show() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/LabPageFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/LabPageFragment.kt index 5aae3c11..2e40eefc 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/LabPageFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/LabPageFragment.kt @@ -23,6 +23,7 @@ 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.data.db.entity.EventType.Companion.SOURCE_DEFAULT import pl.szczodrzynski.edziennik.databinding.LabFragmentBinding import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.ui.base.lazypager.LazyFragment @@ -65,6 +66,7 @@ class LabPageFragment : LazyFragment(), CoroutineScope { b.clearEndpointTimers.isVisible = false b.rodo.isVisible = false b.removeHomework.isVisible = false + b.resetEventTypes.isVisible = false b.unarchive.isVisible = false b.profile.isVisible = false } @@ -100,6 +102,11 @@ class LabPageFragment : LazyFragment(), CoroutineScope { app.db.eventDao().getRawNow("UPDATE events SET homeworkBody = NULL WHERE profileId = ${App.profileId}") } + b.resetEventTypes.onClick { + app.db.eventTypeDao().clearBySource(App.profileId, SOURCE_DEFAULT) + app.db.eventTypeDao().getAllWithDefaults(App.profile) + } + b.chucker.isChecked = App.enableChucker b.chucker.onChange { _, isChecked -> app.config.enableChucker = isChecked @@ -172,28 +179,39 @@ class LabPageFragment : LazyFragment(), CoroutineScope { return@setOnChangeListener true } + b.clearCookies.onClick { + app.cookieJar.clearAllDomains() + } + val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity) startCoroutineTimer(500L, 300L) { - val text = app.cookieJar.sessionCookies - .map { it.cookie } - .sortedBy { it.domain() } - .groupBy { it.domain() } - .map { - listOf( - it.key.asBoldSpannable(), - ":\n", - it.value - .sortedBy { it.name() } - .map { - listOf( - " ", - it.name(), - "=", - it.value().decode().take(40).asItalicSpannable().asColoredSpannable(colorSecondary) - ).concat("") - }.concat("\n") - ).concat("") - }.concat("\n\n") + val text = app.cookieJar.getAllDomains() + .sortedBy { it.domain() } + .groupBy { it.domain() } + .map { pair -> + listOf( + pair.key.asBoldSpannable(), + ":\n", + pair.value + .sortedBy { it.name() } + .map { cookie -> + listOf( + " ", + if (cookie.persistent()) + cookie.name() + .asUnderlineSpannable() + else + cookie.name(), + "=", + cookie.value() + .decode() + .take(40) + .asItalicSpannable() + .asColoredSpannable(colorSecondary), + ).concat("") + }.concat("\n") + ).concat("") + }.concat("\n\n") b.cookies.text = text } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventDetailsDialog.kt index efd8f700..30701f9c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventDetailsDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventDetailsDialog.kt @@ -113,9 +113,20 @@ class EventDetailsDialog( b.typeColor.background?.setTintColor(event.eventColor) - b.details = mutableListOf( + val agendaSubjectImportant = event.subjectLongName != null + && App.config[event.profileId].ui.agendaSubjectImportant + + b.name = if (agendaSubjectImportant) + event.subjectLongName + else + event.typeName + + b.details = listOfNotNull( + if (agendaSubjectImportant) + event.typeName + else event.subjectLongName, - event.teamName?.asColoredSpannable(colorSecondary) + event.teamName?.asColoredSpannable(colorSecondary) ).concat(bullet) b.addedBy.setText( diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventListAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventListAdapter.kt index f991eeef..5c2f4eef 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventListAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventListAdapter.kt @@ -24,6 +24,7 @@ class EventListAdapter( val showDate: Boolean = false, val showColor: Boolean = true, val showType: Boolean = true, + val showTypeColor: Boolean = showType, val showTime: Boolean = true, val showSubject: Boolean = true, val markAsSeen: Boolean = true, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventManualDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventManualDialog.kt index 1ba200a8..49553e9a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventManualDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventManualDialog.kt @@ -19,7 +19,9 @@ import kotlinx.coroutines.withContext import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode +import pl.szczodrzynski.edziennik.App 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.events.ApiTaskAllFinishedEvent import pl.szczodrzynski.edziennik.data.api.events.ApiTaskErrorEvent @@ -35,9 +37,11 @@ import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.LessonFull import pl.szczodrzynski.edziennik.databinding.DialogEventManualV2Binding import pl.szczodrzynski.edziennik.ext.JsonObject +import pl.szczodrzynski.edziennik.ext.appendView import pl.szczodrzynski.edziennik.ext.getStudentData import pl.szczodrzynski.edziennik.ext.onChange import pl.szczodrzynski.edziennik.ext.onClick +import pl.szczodrzynski.edziennik.ext.removeFromParent import pl.szczodrzynski.edziennik.ext.setText import pl.szczodrzynski.edziennik.ext.setTintColor import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog @@ -117,6 +121,15 @@ class EventManualDialog( } override suspend fun onShow() { + val data = withContext(Dispatchers.IO) { + val profile = app.db.profileDao().getByIdSuspend(profileId) ?: return@withContext null + AppData.get(profile.loginStoreType) + } + if (data?.uiConfig?.eventManualShowSubjectDropdown == true) { + b.subjectDropdownLayout.removeFromParent() + b.timeDropdownLayout.appendView(b.subjectDropdownLayout) + } + b.showMore.onClick { // TODO iconics is broken it.apply { refreshDrawableState() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventViewHolder.kt index bee5e305..f22be345 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventViewHolder.kt @@ -113,7 +113,7 @@ class EventViewHolder( b.attachmentIcon.isVisible = item.hasAttachments b.typeColor.background?.setTintColor(item.eventColor) - b.typeColor.isVisible = adapter.showType && adapter.showColor + b.typeColor.isVisible = adapter.showTypeColor b.editButton.isVisible = !adapter.simpleMode && item.addedManually diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/CounterActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/CounterActivity.kt index c63311e1..b1f10d3b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/CounterActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/CounterActivity.kt @@ -4,8 +4,10 @@ package pl.szczodrzynski.edziennik.ui.home +import android.graphics.BitmapFactory import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import com.jetradarmobile.snowfall.SnowfallView import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp @@ -20,6 +22,7 @@ import pl.szczodrzynski.edziennik.ext.startCoroutineTimer import pl.szczodrzynski.edziennik.ext.timeLeft import pl.szczodrzynski.edziennik.ext.timeTill import pl.szczodrzynski.edziennik.ui.dialogs.BellSyncTimeChooseDialog +import pl.szczodrzynski.edziennik.utils.BigNightUtil import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time import kotlin.coroutines.CoroutineContext @@ -61,6 +64,7 @@ class CounterActivity : AppCompatActivity(), CoroutineScope { it.type != Lesson.TYPE_SHIFTED_SOURCE }) } + lessonList.onEach { it.filterNotes() } } b.bellSync.setImageDrawable( @@ -81,6 +85,27 @@ class CounterActivity : AppCompatActivity(), CoroutineScope { counterJob = startCoroutineTimer(repeatMillis = 500) { update() } + + // IT'S WINTER MY DUDES + val today = Date.getToday() + if ((today.month / 3 % 4 == 0) && app.config.ui.snowfall) { + b.rootFrame.addView(layoutInflater.inflate(R.layout.snowfall, b.rootFrame, false)) + } else if (app.config.ui.eggfall && BigNightUtil().isDataWielkanocyNearDzisiaj()) { + val eggfall = layoutInflater.inflate( + R.layout.eggfall, + b.rootFrame, + false + ) as SnowfallView + eggfall.setSnowflakeBitmaps(listOf( + BitmapFactory.decodeResource(resources, R.drawable.egg1), + BitmapFactory.decodeResource(resources, R.drawable.egg2), + BitmapFactory.decodeResource(resources, R.drawable.egg3), + BitmapFactory.decodeResource(resources, R.drawable.egg4), + BitmapFactory.decodeResource(resources, R.drawable.egg5), + BitmapFactory.decodeResource(resources, R.drawable.egg6) + )) + b.rootFrame.addView(eggfall) + } }} private fun update() { @@ -101,13 +126,15 @@ class CounterActivity : AppCompatActivity(), CoroutineScope { when { actual != null -> { - b.lessonName.text = actual.displaySubjectName + b.lessonName.text = actual.getNoteSubstituteText(showNotes = true) + ?: actual.displaySubjectName val left = actual.displayEndTime!! - now b.timeLeft.text = timeLeft(left.toInt(), "\n", countInSeconds) } next != null -> { - b.lessonName.text = next.displaySubjectName + b.lessonName.text = next.getNoteSubstituteText(showNotes = true) + ?: next.displaySubjectName val till = next.displayStartTime!! - now b.timeLeft.text = timeTill(till.toInt(), "\n", countInSeconds) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeEventsCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeEventsCard.kt index db26d8e9..acf99b2c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeEventsCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeEventsCard.kt @@ -63,9 +63,10 @@ class HomeEventsCard( simpleMode = true, showWeekDay = true, showDate = true, - showType = true, + showType = !profile.config.ui.agendaSubjectImportant, + showTypeColor = true, showTime = false, - showSubject = false, + showSubject = profile.config.ui.agendaSubjectImportant, markAsSeen = false, onEventClick = { EventDetailsDialog( diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeTimetableCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeTimetableCard.kt index f5523eec..39959121 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeTimetableCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeTimetableCard.kt @@ -232,6 +232,7 @@ class HomeTimetableCard( } lessons = lessons.filter { it.type != Lesson.TYPE_NO_LESSONS } + lessons.onEach { it.filterNotes() } b.timetableLayout.visibility = View.VISIBLE b.noTimetableLayout.visibility = View.GONE @@ -344,6 +345,7 @@ class HomeTimetableCard( private val LessonFull?.subjectSpannable: CharSequence get() = if (this == null) "?" else when { + hasReplacingNotes() -> getNoteSubstituteText(showNotes = true) ?: "?" isCancelled -> displaySubjectName?.asStrikethroughSpannable() ?: "?" isChange -> displaySubjectName?.asItalicSpannable() ?: "?" else -> displaySubjectName ?: "?" diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaActivity.kt new file mode 100644 index 00000000..2e58da07 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaActivity.kt @@ -0,0 +1,132 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2023-3-24. + */ + +package pl.szczodrzynski.edziennik.ui.login.recaptcha + +import android.annotation.SuppressLint +import android.graphics.Color +import android.os.Build +import android.os.Bundle +import android.util.Base64 +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.webkit.JavascriptInterface +import android.webkit.WebView +import android.widget.FrameLayout +import androidx.appcompat.app.AppCompatActivity +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.SYSTEM_USER_AGENT +import pl.szczodrzynski.edziennik.utils.Themes +import java.nio.charset.Charset + +class RecaptchaActivity : AppCompatActivity() { + companion object { + private const val TAG = "RecaptchaActivity" + + private const val CODE = """ + PCFET0NUWVBFIGh0bWw+PGh0bWw+PGhlYWQ+PHNjcmlwdCBzcmM9Imh0dHBzOi8vd3d3Lmdvb2ds + ZS5jb20vcmVjYXB0Y2hhL2FwaS5qcz9vbmxvYWQ9cmVhZHkmcmVuZGVyPWV4cGxpY2l0Ij48L3Nj + cmlwdD48L2hlYWQ+PGJvZHk+PGJyPjxkaXYgaWQ9ImdyIiBzdHlsZT0icG9zaXRpb246YWJzb2x1 + dGU7dG9wOjUwJTt0cmFuc2Zvcm06dHJhbnNsYXRlKDAsLTUwJSk7Ij48L2Rpdj48YnI+PHNjcmlw + dD5mdW5jdGlvbiByZWFkeSgpe2dyZWNhcHRjaGEucmVuZGVyKCJnciIse3NpdGVrZXk6IlNJVEVL + RVkiLHRoZW1lOiJUSEVNRSIsY2FsbGJhY2s6ZnVuY3Rpb24oZSl7d2luZG93LmlmLmNhbGxiYWNr + KGUpO30sImV4cGlyZWQtY2FsbGJhY2siOndpbmRvdy5pZi5leHBpcmVkQ2FsbGJhY2ssImVycm9y + LWNhbGxiYWNrIjp3aW5kb3cuaWYuZXJyb3JDYWxsYmFja30pO308L3NjcmlwdD48L2JvZHk+PC9o + dG1sPg== + """ + } + + private var isSuccessful = false + private lateinit var jsInterface: CaptchaCallbackInterface + + interface CaptchaCallbackInterface { + @JavascriptInterface + fun callback(recaptchaResponse: String) + + @JavascriptInterface + fun expiredCallback() + + @JavascriptInterface + fun errorCallback() + } + + @SuppressLint("AddJavascriptInterface", "SetJavaScriptEnabled") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setTitle(R.string.recaptcha_dialog_title) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + WebView.setWebContentsDebuggingEnabled(true) + } + + val siteKey = intent.getStringExtra("siteKey") ?: return + val referer = intent.getStringExtra("referer") ?: return + val userAgent = intent.getStringExtra("userAgent") ?: SYSTEM_USER_AGENT + + val htmlContent = Base64.decode(CODE, Base64.DEFAULT) + .toString(Charset.defaultCharset()) + .replace("THEME", if (Themes.isDark) "dark" else "light") + .replace("SITEKEY", siteKey) + + jsInterface = object : CaptchaCallbackInterface { + @JavascriptInterface + override fun callback(recaptchaResponse: String) { + isSuccessful = true + EventBus.getDefault().post( + RecaptchaResult( + isError = false, + code = recaptchaResponse, + ) + ) + finish() + } + + @JavascriptInterface + override fun expiredCallback() { + isSuccessful = false + } + + @JavascriptInterface + override fun errorCallback() { + isSuccessful = false + EventBus.getDefault().post( + RecaptchaResult( + isError = true, + code = null, + ) + ) + finish() + } + } + + val webView = WebView(this).apply { + setBackgroundColor(Color.TRANSPARENT) + settings.javaScriptEnabled = true + settings.userAgentString = userAgent + addJavascriptInterface(jsInterface, "if") + loadDataWithBaseURL( + referer, + htmlContent, + "text/html", + "UTF-8", + null, + ) + // setLayerType(WebView.LAYER_TYPE_SOFTWARE, null) + layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + } + setContentView(webView) + } + + override fun onDestroy() { + super.onDestroy() + if (!isSuccessful) + EventBus.getDefault().post( + RecaptchaResult( + isError = false, + code = null, + ) + ) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaResult.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaResult.kt new file mode 100644 index 00000000..ae5fae19 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaResult.kt @@ -0,0 +1,10 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2023-3-24. + */ + +package pl.szczodrzynski.edziennik.ui.login.recaptcha + +data class RecaptchaResult( + val isError: Boolean, + val code: String?, +) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/GenerateBlockTimetableDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/GenerateBlockTimetableDialog.kt index 701ed9ed..66401d31 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/GenerateBlockTimetableDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/GenerateBlockTimetableDialog.kt @@ -109,12 +109,10 @@ class GenerateBlockTimetableDialog( .show() dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.onClick { - app.permissionManager.requestStoragePermission(activity, permissionMessage = R.string.permissions_generate_timetable) { - when (b.weekSelectionRadioGroup.checkedRadioButtonId) { - R.id.withChangesCurrentWeekRadio -> generateBlockTimetable(weekCurrentStart, weekCurrentEnd) - R.id.withChangesNextWeekRadio -> generateBlockTimetable(weekNextStart, weekNextEnd) - R.id.forSelectedWeekRadio -> selectDate() - } + when (b.weekSelectionRadioGroup.checkedRadioButtonId) { + R.id.withChangesCurrentWeekRadio -> generateBlockTimetable(weekCurrentStart, weekCurrentEnd) + R.id.withChangesNextWeekRadio -> generateBlockTimetable(weekNextStart, weekNextEnd) + R.id.forSelectedWeekRadio -> selectDate() } } }} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableDayFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableDayFragment.kt index b65f4319..d2016b29 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableDayFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableDayFragment.kt @@ -40,6 +40,7 @@ import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.LessonFull import pl.szczodrzynski.edziennik.databinding.TimetableDayFragmentBinding import pl.szczodrzynski.edziennik.databinding.TimetableLessonBinding +import pl.szczodrzynski.edziennik.databinding.TimetableNoLessonsBinding import pl.szczodrzynski.edziennik.databinding.TimetableNoTimetableBinding import pl.szczodrzynski.edziennik.ext.Intent import pl.szczodrzynski.edziennik.ext.JsonObject @@ -63,6 +64,7 @@ import pl.szczodrzynski.edziennik.utils.Colors import pl.szczodrzynski.edziennik.utils.managers.NoteManager import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time +import pl.szczodrzynski.edziennik.utils.models.Week import pl.szczodrzynski.edziennik.utils.mutableLazy import kotlin.coroutines.CoroutineContext import kotlin.math.max @@ -182,6 +184,20 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope { b.root.removeAllViews() b.root.addView(view) viewsRemoved = true + + val b = TimetableNoLessonsBinding.bind(view) + val weekStart = date.weekStart.stringY_m_d + b.noLessonsSync.onClick { + it.isEnabled = false + EdziennikTask.syncProfile( + profileId = App.profileId, + featureTypes = setOf(FeatureType.TIMETABLE), + arguments = JsonObject( + "weekStart" to weekStart + ) + ).enqueue(activity) + } + b.noLessonsSync.isVisible = date.weekDay !in Week.SATURDAY..Week.SUNDAY } return } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableFragment.kt index b9e6c5f7..2e037526 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableFragment.kt @@ -27,8 +27,11 @@ import kotlinx.coroutines.launch 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.db.enums.FeatureType import pl.szczodrzynski.edziennik.data.db.enums.MetadataType import pl.szczodrzynski.edziennik.databinding.FragmentTimetableV2Binding +import pl.szczodrzynski.edziennik.ext.JsonObject import pl.szczodrzynski.edziennik.ext.getSchoolYearConstrains import pl.szczodrzynski.edziennik.ext.getStudentData import pl.szczodrzynski.edziennik.ui.dialogs.settings.TimetableConfigDialog @@ -178,6 +181,21 @@ class TimetableFragment : Fragment(), CoroutineScope { b.tabLayout.setCurrentItem(items.indexOfFirst { it.value == selectedDate?.value ?: today }, false) activity.navView.bottomSheet.prependItems( + BottomSheetPrimaryItem(true) + .withTitle(R.string.menu_timetable_sync) + .withIcon(CommunityMaterial.Icon.cmd_calendar_sync_outline) + .withOnClickListener { + activity.bottomSheet.close() + val date = pageSelection ?: Date.getToday() + val weekStart = date.weekStart.stringY_m_d + EdziennikTask.syncProfile( + profileId = App.profileId, + featureTypes = setOf(FeatureType.TIMETABLE), + arguments = JsonObject( + "weekStart" to weekStart + ) + ).enqueue(activity) + }, BottomSheetPrimaryItem(true) .withTitle(R.string.timetable_select_day) .withIcon(SzkolnyFont.Icon.szf_calendar_today_outline) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/AttachmentsView.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/AttachmentsView.kt index 7e2bc5af..81240d62 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/AttachmentsView.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/AttachmentsView.kt @@ -5,6 +5,7 @@ package pl.szczodrzynski.edziennik.ui.views import android.content.Context +import android.os.Build import android.os.Bundle import android.util.AttributeSet import androidx.appcompat.app.AppCompatActivity @@ -50,6 +51,10 @@ class AttachmentsView @JvmOverloads constructor( val attachmentSizes = arguments.getLongArray("attachmentSizes") val adapter = AttachmentAdapter(context, onAttachmentClick = { item -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + downloadAttachment(item) + return@AttachmentAdapter + } app.permissionManager.requestStoragePermission(activity, R.string.permissions_attachment) { downloadAttachment(item) } @@ -57,6 +62,10 @@ class AttachmentsView @JvmOverloads constructor( val popupMenu = PopupMenu(chip.context, chip) popupMenu.menu.add(0, 1, 0, R.string.messages_attachment_download_again) popupMenu.setOnMenuItemClickListener { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + downloadAttachment(item) + return@setOnMenuItemClickListener true + } app.permissionManager.requestStoragePermission(activity, R.string.permissions_attachment) { downloadAttachment(item, forceDownload = true) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/EventTypeDropdown.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/EventTypeDropdown.kt index d6a8d1cd..48d327fc 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/EventTypeDropdown.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/EventTypeDropdown.kt @@ -33,13 +33,8 @@ class EventTypeDropdown : TextInputDropDown { suspend fun loadItems() { val types = withContext(Dispatchers.Default) { val list = mutableListOf() - - var types = db.eventTypeDao().getAllNow(profileId) - - if (types.none { it.id in -1L..10L }) { - val profile = db.profileDao().getByIdNow(profileId) ?: return@withContext listOf() - types = db.eventTypeDao().addDefaultTypes(profile) - } + val types = db.eventTypeDao().getAllNow(profileId) + .sortedBy { it.order } list += types.map { Item(it.id, it.name, tag = it, icon = IconicsDrawable(context).apply { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsProvider.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsProvider.kt index b4af3f68..a082c3da 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsProvider.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsProvider.kt @@ -23,6 +23,7 @@ import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.ext.Bundle import pl.szczodrzynski.edziennik.ext.getJsonObject import pl.szczodrzynski.edziennik.ext.pendingIntentFlag +import pl.szczodrzynski.edziennik.ext.pendingIntentMutable import pl.szczodrzynski.edziennik.ext.putExtras import pl.szczodrzynski.edziennik.receivers.SzkolnyReceiver import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget @@ -50,7 +51,7 @@ class WidgetNotificationsProvider : AppWidgetProvider() { val syncIntent = SzkolnyReceiver.getIntent(context, Bundle( "task" to "SyncRequest" )) - val syncPendingIntent = PendingIntent.getBroadcast(context, 0, syncIntent, pendingIntentFlag()) + val syncPendingIntent = PendingIntent.getBroadcast(context, 0, syncIntent, pendingIntentMutable()) views.setOnClickPendingIntent(R.id.widgetNotificationsSync, syncPendingIntent) views.setImageViewBitmap( @@ -71,13 +72,13 @@ class WidgetNotificationsProvider : AppWidgetProvider() { val itemIntent = Intent(context, MainActivity::class.java) itemIntent.action = Intent.ACTION_MAIN - val itemPendingIntent = PendingIntent.getActivity(context, 0, itemIntent, pendingIntentFlag()) + val itemPendingIntent = PendingIntent.getActivity(context, appWidgetId, itemIntent, pendingIntentMutable()) views.setPendingIntentTemplate(R.id.widgetNotificationsListView, itemPendingIntent) val headerIntent = Intent(context, MainActivity::class.java) headerIntent.action = Intent.ACTION_MAIN headerIntent.putExtras("fragmentId" to NavTarget.NOTIFICATIONS) - val headerPendingIntent = PendingIntent.getActivity(context, 0, headerIntent, pendingIntentFlag()) + val headerPendingIntent = PendingIntent.getActivity(context, appWidgetId, headerIntent, pendingIntentMutable()) views.setOnClickPendingIntent(R.id.widgetNotificationsHeader, headerPendingIntent) appWidgetManager.updateAppWidget(appWidgetId, views) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/timetable/WidgetTimetableProvider.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/timetable/WidgetTimetableProvider.kt index 2ac510f5..a0be777c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/timetable/WidgetTimetableProvider.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/timetable/WidgetTimetableProvider.kt @@ -34,6 +34,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_NO_LESSON import pl.szczodrzynski.edziennik.ext.filterOutArchived import pl.szczodrzynski.edziennik.ext.getJsonObject import pl.szczodrzynski.edziennik.ext.pendingIntentFlag +import pl.szczodrzynski.edziennik.ext.pendingIntentMutable import pl.szczodrzynski.edziennik.ext.putExtras import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget import pl.szczodrzynski.edziennik.ui.widgets.LessonDialogActivity @@ -119,7 +120,7 @@ class WidgetTimetableProvider : AppWidgetProvider() { 0, refreshIntent, PendingIntent.FLAG_UPDATE_CURRENT or pendingIntentFlag()) views.setOnClickPendingIntent(R.id.widgetTimetableRefresh, refreshPendingIntent) - views.setOnClickPendingIntent(R.id.widgetTimetableSync, getPendingSelfIntent(context, ACTION_SYNC_DATA)) + views.setViewVisibility(R.id.widgetTimetableSync, View.GONE) views.setImageViewBitmap( R.id.widgetTimetableRefresh, @@ -129,14 +130,6 @@ class WidgetTimetableProvider : AppWidgetProvider() { }.toBitmap() ) - views.setImageViewBitmap( - R.id.widgetTimetableSync, - IconicsDrawable(context, CommunityMaterial.Icon.cmd_download_outline).apply { - colorInt = Color.WHITE - sizeDp = if (config.bigStyle) 28 else 20 - }.toBitmap() - ) - prepareAppWidget(app, appWidgetId, views, config, bellSyncDiffMillis) appWidgetManager.updateAppWidget(appWidgetId, views) @@ -337,8 +330,11 @@ class WidgetTimetableProvider : AppWidgetProvider() { scrollPos = pos + 1 } + // remove notes from other profiles + lesson.filterNotes() // set the subject and classroom name - model.subjectName = lesson.displaySubjectName + model.subjectName = lesson.getNoteSubstituteText(showNotes = true) + ?: lesson.displaySubjectName model.classroomName = lesson.displayClassroom // set the bell sync to calculate progress in ListProvider @@ -399,7 +395,7 @@ class WidgetTimetableProvider : AppWidgetProvider() { } } headerIntent.putExtras("fragmentId" to NavTarget.TIMETABLE) - val headerPendingIntent = PendingIntent.getActivity(app, appWidgetId, headerIntent, pendingIntentFlag()) + val headerPendingIntent = PendingIntent.getActivity(app, appWidgetId, headerIntent, pendingIntentMutable()) views.setOnClickPendingIntent(R.id.widgetTimetableHeader, headerPendingIntent) timetables!!.put(appWidgetId, models) @@ -413,7 +409,7 @@ class WidgetTimetableProvider : AppWidgetProvider() { // create an intent used to display the lesson details dialog val itemIntent = Intent(app, LessonDialogActivity::class.java) itemIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK/* or Intent.FLAG_ACTIVITY_CLEAR_TASK*/) - val itemPendingIntent = PendingIntent.getActivity(app, appWidgetId, itemIntent, PendingIntent.FLAG_MUTABLE) + val itemPendingIntent = PendingIntent.getActivity(app, appWidgetId, itemIntent, pendingIntentMutable()) views.setPendingIntentTemplate(R.id.widgetTimetableListView, itemPendingIntent) if (!unified) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/Utils.java b/app/src/main/java/pl/szczodrzynski/edziennik/utils/Utils.java index d4ffae05..f039952d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/Utils.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/Utils.java @@ -774,14 +774,21 @@ public class Utils { private static File storageDir = null; public static File getStorageDir() { - if (storageDir != null) - return storageDir; - storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); - storageDir = new File(storageDir, "Szkolny.eu"); - storageDir.mkdirs(); return storageDir; } + public static void initializeStorageDir(Context context) { + if (storageDir != null) + return; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + storageDir = context.getExternalFilesDir(null); + } else { + storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + storageDir = new File(storageDir, "Szkolny.eu"); + } + storageDir.mkdirs(); + } + public static void writeStringToFile(File file, String data) throws IOException { OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream(file)); outputStreamWriter.write(data); diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/EventManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/EventManager.kt index cedf3122..38a8fee3 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/EventManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/EventManager.kt @@ -48,6 +48,7 @@ class EventManager(val app: App) : CoroutineScope { title: TextView, event: EventFull, showType: Boolean = true, + showSubject: Boolean = false, showNotes: Boolean = true, doneIconColor: Int? = null ) { @@ -60,6 +61,7 @@ class EventManager(val app: App) : CoroutineScope { 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, + if (showSubject) event.subjectLongName?.plus(" - ") else null, topicSpan, ).concat() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/NoteManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/NoteManager.kt index 866555bd..50ac84cd 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/NoteManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/NoteManager.kt @@ -171,7 +171,7 @@ class NoteManager(private val app: App) { activity = activity, simpleMode = true, showDate = true, - showColor = false, + showTypeColor = false, showTime = false, markAsSeen = false, showNotes = false, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt index 3e8761c8..9ca4c233 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/UserActionManager.kt @@ -22,6 +22,8 @@ import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.ui.captcha.RecaptchaPromptDialog import pl.szczodrzynski.edziennik.ui.login.oauth.OAuthLoginActivity import pl.szczodrzynski.edziennik.ui.login.oauth.OAuthLoginResult +import pl.szczodrzynski.edziennik.ui.login.recaptcha.RecaptchaActivity +import pl.szczodrzynski.edziennik.ui.login.recaptcha.RecaptchaResult import pl.szczodrzynski.edziennik.utils.Utils.d class UserActionManager(val app: App) { @@ -107,10 +109,45 @@ class UserActionManager(val app: App) { )) }, onCancel = callback.onCancel, + onServerError = { + executeRecaptchaActivity(activity, event, callback) + }, ).show() return true } + private fun executeRecaptchaActivity( + activity: AppCompatActivity, + event: UserActionRequiredEvent, + callback: UserActionCallback, + ): Boolean { + event.params.getString("siteKey") ?: return false + event.params.getString("referer") ?: return false + + var listener: Any? = null + listener = object { + @Subscribe(threadMode = ThreadMode.MAIN) + fun onRecaptchaResult(result: RecaptchaResult) { + EventBus.getDefault().unregister(listener) + when { + result.isError -> callback.onFailure?.invoke() + result.code != null -> { + finishAction(activity, event, callback, Bundle( + "recaptchaCode" to result.code, + "recaptchaTime" to System.currentTimeMillis(), + )) + } + else -> callback.onCancel?.invoke() + } + } + } + EventBus.getDefault().register(listener) + + val intent = Intent(activity, RecaptchaActivity::class.java).putExtras(event.params) + activity.startActivity(intent) + return true + } + private fun executeOauth( activity: AppCompatActivity, event: UserActionRequiredEvent, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/ItemWidgetTimetableModel.java b/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/ItemWidgetTimetableModel.java index 05c41e3b..964bee93 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/ItemWidgetTimetableModel.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/ItemWidgetTimetableModel.java @@ -19,7 +19,7 @@ public class ItemWidgetTimetableModel { public Time endTime; public boolean lessonPassed; public boolean lessonCurrent; - public String subjectName = ""; + public CharSequence subjectName = ""; public String classroomName = ""; public boolean lessonChange = false; public boolean lessonChangeNoClassroom = false; diff --git a/app/src/main/res/layout/activity_counter.xml b/app/src/main/res/layout/activity_counter.xml index 9c7f832d..77b1cde2 100644 --- a/app/src/main/res/layout/activity_counter.xml +++ b/app/src/main/res/layout/activity_counter.xml @@ -1,6 +1,9 @@ - + - + diff --git a/app/src/main/res/layout/dialog_config_agenda.xml b/app/src/main/res/layout/dialog_config_agenda.xml index 3258f656..effe23fe 100644 --- a/app/src/main/res/layout/dialog_config_agenda.xml +++ b/app/src/main/res/layout/dialog_config_agenda.xml @@ -50,6 +50,14 @@ android:minHeight="32dp" android:text="@string/agenda_config_teacher_absence" /> + + + @@ -55,7 +56,7 @@ +