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 dc0296b2..af4aaca4 100644 --- a/app/src/main/assets/pl-changelog.html +++ b/app/src/main/assets/pl-changelog.html @@ -1,13 +1,10 @@ -

Wersja 4.13.4, 2022-12-26

+

Wersja 4.13.6, 2023-03-24

    -
  • USOS: zaktualizowano rodzaje wydarzeń. Wybór przedmiotu jest teraz zawsze widoczny.
  • -
  • Opcja wyświetlania nazwy przedmiotu w miejscu rodzaju wydarzenia.
  • -
  • Notatki zastępujące treść lekcji są teraz wyświetlane wszędzie.
  • -
  • Na ekranie odliczania czasu lekcji również pada śnieg.
  • -
  • 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 fdf05a94..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] = { - 0x4b, 0x43, 0x7e, 0xa2, 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 cf688b03..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) { 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 1fe8b48b..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.MTIzNDU2Nzg5MD4BikzMWC===.$param2".sha256() + return "$param1.MTIzNDU2Nzg5MD01uMP7oW===.$param2".sha256() } } 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/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/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 792811eb..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 @@ -179,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/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/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 10df27c3..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) @@ -402,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) @@ -416,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/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/res/layout/lab_fragment.xml b/app/src/main/res/layout/lab_fragment.xml index e41a66f2..af55e6f0 100644 --- a/app/src/main/res/layout/lab_fragment.xml +++ b/app/src/main/res/layout/lab_fragment.xml @@ -107,6 +107,14 @@ android:text="Reset event types" android:textAllCaps="false" /> +