mirror of
https://github.com/szkolny-eu/szkolny-android.git
synced 2024-11-09 19:50:26 -06:00
Merge branch 'develop'
This commit is contained in:
commit
53c813f014
@ -160,7 +160,11 @@
|
||||
<activity android:name=".ui.login.oauth.OAuthLoginActivity"
|
||||
android:configChanges="orientation|keyboardHidden"
|
||||
android:exported="false"
|
||||
android:theme="@style/AppTheme.Light" />
|
||||
android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar" />
|
||||
<activity android:name=".ui.login.recaptcha.RecaptchaActivity"
|
||||
android:configChanges="orientation|keyboardHidden"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar" />
|
||||
<activity android:name=".ui.base.BuildInvalidActivity" android:exported="false" />
|
||||
<activity android:name=".ui.settings.contributors.ContributorsActivity" android:exported="false" />
|
||||
|
||||
|
@ -1,13 +1,10 @@
|
||||
<h3>Wersja 4.13.4, 2022-12-26</h3>
|
||||
<h3>Wersja 4.13.6, 2023-03-24</h3>
|
||||
<ul>
|
||||
<li>USOS: zaktualizowano rodzaje wydarzeń. Wybór przedmiotu jest teraz zawsze widoczny.</li>
|
||||
<li>Opcja wyświetlania nazwy przedmiotu w miejscu rodzaju wydarzenia.</li>
|
||||
<li>Notatki zastępujące treść lekcji są teraz wyświetlane wszędzie.</li>
|
||||
<li>Na ekranie odliczania czasu lekcji również pada śnieg.</li>
|
||||
<li>Poprawiono synchronizację w Mobidzienniku bez ustawionego adresu e-mail.</li>
|
||||
<li>Poprawiono błąd synchronizacji w Vulcanie.</li>
|
||||
<li>Naprawiono pobieranie załączników na Androidzie 13 i nowszym.</li>
|
||||
<li>Dodano opcję odświeżenia planu lekcji na wybrany tydzień.</li>
|
||||
<li>Usunięto błędy logowania. @BxOxSxS</li>
|
||||
</ul>
|
||||
<br>
|
||||
<br>
|
||||
Dzięki za korzystanie ze Szkolnego!<br>
|
||||
<i>© [Kuba Szczodrzyński](@kuba2k2) 2022</i>
|
||||
<i>© [Kuba Szczodrzyński](@kuba2k2) 2023</i>
|
||||
|
@ -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);
|
||||
|
||||
|
@ -235,6 +235,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
|
||||
}
|
||||
|
||||
Signing.getCert(this)
|
||||
Utils.initializeStorageDir(this)
|
||||
|
||||
launch {
|
||||
withContext(Dispatchers.Default) {
|
||||
|
@ -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
|
||||
|
||||
|
@ -24,6 +24,25 @@ object Regexes {
|
||||
"""^\[META:([A-z0-9-&=]+)]""".toRegex()
|
||||
}
|
||||
|
||||
val HTML_INPUT_HIDDEN by lazy {
|
||||
"""<input .*?type="hidden".+?>""".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 {
|
||||
"""<form .*?action="(.+?)"""".toRegex()
|
||||
}
|
||||
val HTML_RECAPTCHA_KEY by lazy {
|
||||
"""data-sitekey="(.+?)"""".toRegex()
|
||||
}
|
||||
|
||||
|
||||
|
||||
val MOBIDZIENNIK_GRADES_SUBJECT_NAME by lazy {
|
||||
|
@ -24,6 +24,9 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> 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<String, String>()
|
||||
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<String, String>,
|
||||
) {
|
||||
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)
|
||||
.withResponse(response))
|
||||
return
|
||||
}
|
||||
|
||||
if (json == null) {
|
||||
if (response.parserErrorBody?.contains("wciąż nieaktywne") == true) {
|
||||
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_NOT_ACTIVATED)
|
||||
.withApiResponse(text)
|
||||
.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,
|
||||
authorize(
|
||||
url = location
|
||||
?: if (data.fakeLogin)
|
||||
FAKE_LIBRUS_AUTHORIZE
|
||||
else
|
||||
LIBRUS_AUTHORIZE_URL,
|
||||
referer = referer,
|
||||
)
|
||||
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))
|
||||
}
|
||||
|
||||
override fun onFailure(response: Response, throwable: Throwable) {
|
||||
|
@ -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())
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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<DumbCookie>()
|
||||
private val savedCookies = mutableSetOf<DumbCookie>()
|
||||
private val sessionCookies = mutableSetOf<DumbCookie>()
|
||||
|
||||
init {
|
||||
val toRemove = mutableListOf<String>()
|
||||
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<Cookie>) {
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: MutableList<Cookie>) {
|
||||
for (cookie in cookies) {
|
||||
val dc = DumbCookie(cookie)
|
||||
save(dc)
|
||||
@ -54,6 +81,10 @@ class DumbCookieJar(
|
||||
}.map { it.cookie }
|
||||
}
|
||||
|
||||
fun getAllDomains(): List<Cookie> {
|
||||
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<String, String> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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<RecaptchaDialogBinding>(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()
|
||||
|
@ -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<RecaptchaViewBinding>(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()
|
||||
}
|
||||
}
|
||||
|
@ -179,24 +179,35 @@ 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 }
|
||||
val text = app.cookieJar.getAllDomains()
|
||||
.sortedBy { it.domain() }
|
||||
.groupBy { it.domain() }
|
||||
.map {
|
||||
.map { pair ->
|
||||
listOf(
|
||||
it.key.asBoldSpannable(),
|
||||
pair.key.asBoldSpannable(),
|
||||
":\n",
|
||||
it.value
|
||||
pair.value
|
||||
.sortedBy { it.name() }
|
||||
.map {
|
||||
.map { cookie ->
|
||||
listOf(
|
||||
" ",
|
||||
it.name(),
|
||||
if (cookie.persistent())
|
||||
cookie.name()
|
||||
.asUnderlineSpannable()
|
||||
else
|
||||
cookie.name(),
|
||||
"=",
|
||||
it.value().decode().take(40).asItalicSpannable().asColoredSpannable(colorSecondary)
|
||||
cookie.value()
|
||||
.decode()
|
||||
.take(40)
|
||||
.asItalicSpannable()
|
||||
.asColoredSpannable(colorSecondary),
|
||||
).concat("")
|
||||
}.concat("\n")
|
||||
).concat("")
|
||||
|
@ -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,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
@ -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?,
|
||||
)
|
@ -109,14 +109,12 @@ 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
private fun selectDate() {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -774,12 +774,19 @@ public class Utils {
|
||||
|
||||
private static File storageDir = null;
|
||||
public static File getStorageDir() {
|
||||
if (storageDir != null)
|
||||
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();
|
||||
return storageDir;
|
||||
}
|
||||
|
||||
public static void writeStringToFile(File file, String data) throws IOException {
|
||||
|
@ -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,
|
||||
|
@ -107,6 +107,14 @@
|
||||
android:text="Reset event types"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/clearCookies"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="Clear all cookies"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/cookies"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -2,8 +2,16 @@
|
||||
~ Copyright (c) Kuba Szczodrzyński 2019-11-15.
|
||||
-->
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/noTimetableLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
@ -12,3 +20,11 @@
|
||||
android:text="@string/timetable_no_lessons_title"
|
||||
android:textSize="24sp"
|
||||
app:drawableTopCompat="@drawable/ic_timetable" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/noLessonsSync"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/refresh" />
|
||||
</LinearLayout>
|
||||
|
@ -847,7 +847,7 @@
|
||||
<string name="settings_about_licenses_text">Open-Source-Lizenzen</string>
|
||||
<string name="settings_about_privacy_policy_text">Datenschutzrichtlinie</string>
|
||||
<string name="settings_card_register_title">E-Klassenbuch</string>
|
||||
<string name="settings_about_title_subtext">© Kuba Szczodrzyński, September 2018 - 2022</string>
|
||||
<string name="settings_about_title_subtext">© Kuba Szczodrzyński, September 2018 - 2023</string>
|
||||
<string name="settings_about_update_subtext">Klicken Sie hier, um nach Aktualisierungen zu suchen</string>
|
||||
<string name="settings_about_update_text">Aktualisierung</string>
|
||||
<string name="settings_about_version_text">Version</string>
|
||||
|
@ -849,7 +849,7 @@
|
||||
<string name="settings_about_licenses_text">Open-source licenses</string>
|
||||
<string name="settings_about_privacy_policy_text">Privacy policy</string>
|
||||
<string name="settings_card_register_title">E-register</string>
|
||||
<string name="settings_about_title_subtext">© Kuba Szczodrzyński, September 2018 – 2022</string>
|
||||
<string name="settings_about_title_subtext">© Kuba Szczodrzyński, September 2018 – 2023</string>
|
||||
<string name="settings_about_update_subtext">Click to check for updates</string>
|
||||
<string name="settings_about_update_text">Update</string>
|
||||
<string name="settings_about_version_text">Version</string>
|
||||
@ -1353,7 +1353,6 @@
|
||||
<string name="home_archive_close_no_target_text">Child %s does not have a profile on this account in the current school year. Probably this profile has been deleted or the student no longer attends this class.\n\nTo go to the current profile, select a student from the list or log in to their account with the Add student button.</string>
|
||||
<string name="build_invalid_no_remote_repo">A reference to a remote repository was not found. Make sure you are using the official repository fork and verify your Gradle configuration.</string>
|
||||
<string name="login_mode_mobidziennik_web_guide">"Enter the data you use to log in to the MobiDziennik website. As the server address, you can enter the address of the website where you have MobiDziennik. "</string>
|
||||
<string name="permissions_generate_timetable">In order to be able to save the generated timetable, you must grant access rights to the device\'s memory.\n\nClick OK to grant permissions.</string>
|
||||
<string name="login_summary_account_child">(Child)</string>
|
||||
<string name="login_summary_account_parent">(Parent)</string>
|
||||
<string name="menu_teachers">Teachers</string>
|
||||
|
@ -916,7 +916,7 @@
|
||||
<string name="settings_about_licenses_text">Licencje open-source</string>
|
||||
<string name="settings_about_privacy_policy_text">Polityka prywatności</string>
|
||||
<string name="settings_card_register_title">E-dziennik</string>
|
||||
<string name="settings_about_title_subtext">© Kuba Szczodrzyński, wrzesień 2018 - 2022</string>
|
||||
<string name="settings_about_title_subtext">© Kuba Szczodrzyński, wrzesień 2018 - 2023</string>
|
||||
<string name="settings_about_update_subtext">Kliknij, aby sprawdzić aktualizacje</string>
|
||||
<string name="settings_about_update_text">Aktualizacja</string>
|
||||
<string name="settings_about_version_text">Wersja</string>
|
||||
@ -1241,7 +1241,7 @@
|
||||
<string name="timetable_not_public_hint">Skontaktuj się z wychowawcą w celu udostępnienia planu lekcji.</string>
|
||||
<string name="timetable_not_public_text">Plan lekcji nie został opublikowany przez szkołę.</string>
|
||||
<string name="timetable_not_public_title">Brak planu lekcji</string>
|
||||
<string name="timetable_select_day">Wybierz dzień</string>
|
||||
<string name="timetable_select_day">Przejdź do daty</string>
|
||||
<string name="timetable_syncing_text">Pobieranie planu lekcji na wybrany tydzień…</string>
|
||||
<string name="timetable_today">Dzisiaj</string>
|
||||
<string name="title_all">Synchronizuj wszystko</string>
|
||||
@ -1416,7 +1416,6 @@
|
||||
<string name="build_dialog_open_repo">Sprawdź kod</string>
|
||||
<string name="error_no_api_access">Brak dostępu do API</string>
|
||||
<string name="build_date">Data kompilacji</string>
|
||||
<string name="permissions_generate_timetable">Aby móc zapisać wygenerowany plan lekcji musisz przyznać uprawnienia dostępu do pamięci urządzenia.\n\nKliknij OK, aby przyznać uprawnienia.</string>
|
||||
<string name="privacy_policy_dialog_html"><![CDATA[Korzystając z aplikacji potwierdzasz <a href="https://szkolny.eu/privacy-policy">przeczytanie Polityki prywatności</a> i akceptujesz jej postanowienia.<br /><br />Autorzy aplikacji nie biorą odpowiedzialności za korzystanie z aplikacji Szkolny.eu.]]></string>
|
||||
<string name="login_chooser_version_format">Szkolny.eu v%s\n%s</string>
|
||||
<string name="menu_agenda_config">Ustawienia terminarza</string>
|
||||
@ -1544,6 +1543,7 @@
|
||||
<string name="login_mode_usos_oauth_guide">TODO</string>
|
||||
<string name="notification_user_action_required_oauth_usos">USOS - wymagane logowanie z użyciem przeglądarki</string>
|
||||
<string name="oauth_dialog_title">Zaloguj się</string>
|
||||
<string name="recaptcha_dialog_title">reCAPTCHA</string>
|
||||
<string name="app_cannot_load_data">Nie można załadować danych aplikacji</string>
|
||||
<string name="legend_event_shared_received">{cmd-share-variant} udostępnione w klasie</string>
|
||||
<string name="legend_event_shared_sent">{cmd-share-variant} udostępnione przez Ciebie</string>
|
||||
@ -1552,4 +1552,5 @@
|
||||
<string name="settings_registration_section">Rejestracja</string>
|
||||
<string name="home_timetable_all_lessons">Wszystkie lekcje:</string>
|
||||
<string name="agenda_config_subject_important">Wyświetl nazwę przedmiotu zamiast rodzaju</string>
|
||||
<string name="menu_timetable_sync">Odśwież wybrany tydzień</string>
|
||||
</resources>
|
||||
|
@ -5,8 +5,8 @@ buildscript {
|
||||
kotlin_version = '1.6.10'
|
||||
|
||||
release = [
|
||||
versionName: "4.13.4",
|
||||
versionCode: 4130499
|
||||
versionName: "4.13.6",
|
||||
versionCode: 4130699
|
||||
]
|
||||
|
||||
setup = [
|
||||
|
Loading…
Reference in New Issue
Block a user