1
0

Compare commits

...

24 Commits
2.3.0 ... 2.3.4

Author SHA1 Message Date
1fe464a289 Merge branch 'release/2.3.4' 2024-01-14 17:33:14 +01:00
497acf9d68 Version 2.3.4 2024-01-14 17:32:41 +01:00
976eb5a772 Fix cancelling dashboard jobs (#2395) 2024-01-14 16:45:30 +01:00
9ececeb4e9 New Crowdin updates (#2394) 2024-01-14 16:41:57 +01:00
096fe359e7 Make some improvements in captcha dialog (#2393)
* Add improvements retrying after captcha solved

* Add showAuthDialog from BaseActivity instead of displaying this dialog manually

* Add getCookieStore() with removeAll impl in WebkitCookieManagerProxy

* Add debounce to captcha dialog showing logic

* Add refresh button to captcha dialog

* Destroy webview along with captcha dialog

* Add clear webkit cookies button to debug menu

* Add captcha error message

* Update captcha verified message
2024-01-14 13:09:04 +00:00
a98e8398fd Add webview to obtain cloudflare captcha cookies for okhttp (#2392) 2024-01-12 18:34:43 +01:00
d8c4926a97 Merge branch 'release/2.3.3' into develop 2024-01-09 21:46:10 +01:00
17c139b559 Merge branch 'release/2.3.3' 2024-01-09 21:46:04 +01:00
ddbcc7a04c Version 2.3.3 2024-01-09 21:45:59 +01:00
9f9eb60280 Merge branch 'release/2.3.2' into develop 2024-01-09 19:31:29 +01:00
f893170dec Merge branch 'release/2.3.2' 2024-01-09 19:31:22 +01:00
cff08d6322 Version 2.3.2 2024-01-09 19:27:03 +01:00
9dee7f01f6 Avoid deleting luckynumber when SDK returns null (#2391) 2024-01-09 19:07:46 +01:00
8324a9cac3 Use emptyCookieJarInterceptor in SDK configuration (#2390) 2024-01-09 19:00:37 +01:00
5316e3e1bf Bump mockk from 1.13.8 to 1.13.9 (#2389) 2024-01-08 15:32:30 +00:00
81e80181f2 New Crowdin updates (#2388) 2024-01-08 16:32:06 +01:00
6ee38e9259 Add clearing all data and key entry when decryption failed (#2386) 2024-01-06 00:01:33 +01:00
40df80371c Use forked slf4j-timber to fix logging problems with slf4j v2 (#2387) 2024-01-05 16:03:50 +01:00
a3596c35b8 Update AGP and Gradle (#2385) 2024-01-04 09:33:51 +01:00
66b7ea4cb4 Merge branch 'release/2.3.1' into develop 2024-01-03 16:02:39 +01:00
770749e158 Merge branch 'release/2.3.1' 2024-01-03 16:02:32 +01:00
0aa83b020e Bump sdk to 2.3.3 2024-01-03 16:01:30 +01:00
4d1218d1d3 Version 2.3.1 2024-01-03 14:53:16 +01:00
0ea6cbc8ed Merge branch 'release/2.3.0' into develop 2024-01-02 01:51:58 +01:00
49 changed files with 701 additions and 179 deletions

View File

@ -27,8 +27,8 @@ android {
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21
targetSdkVersion 34
versionCode 140
versionName "2.3.0"
versionCode 144
versionName "2.3.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "app_name", "Wulkanowy"
@ -162,8 +162,8 @@ play {
defaultToAppBundles = false
track = 'production'
releaseStatus = ReleaseStatus.IN_PROGRESS
userFraction = 0.15d
updatePriority = 3
userFraction = 0.99d
updatePriority = 1
enabled.set(false)
}
@ -188,12 +188,12 @@ ext {
android_hilt = "1.1.0"
room = "2.6.1"
chucker = "4.0.0"
mockk = "1.13.8"
mockk = "1.13.9"
coroutines = "1.7.3"
}
dependencies {
implementation 'io.github.wulkanowy:sdk:2.3.1'
implementation 'io.github.wulkanowy:sdk:2.3.6'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
@ -238,9 +238,10 @@ dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0"
implementation "com.squareup.okhttp3:logging-interceptor:4.12.0"
implementation "com.squareup.okhttp3:okhttp-urlconnection:4.12.0"
implementation "com.jakewharton.timber:timber:5.0.1"
implementation "at.favre.lib:slf4j-timber:1.0.1"
implementation 'com.github.Faierbel:slf4j-timber:2.0'
implementation 'com.github.bastienpaulfr:Treessence:1.1.2'
implementation "com.mikepenz:aboutlibraries-core:$about_libraries"
implementation 'io.coil-kt:coil:2.5.0'

View File

@ -21,6 +21,7 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.RemoteConfigHelper
import io.github.wulkanowy.utils.WebkitCookieManagerProxy
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
@ -43,6 +44,7 @@ internal class DataModule {
buildTag = android.os.Build.MODEL
userAgentTemplate = remoteConfig.userAgentTemplate
setSimpleHttpLogger { Timber.d(it) }
setAdditionalCookieManager(WebkitCookieManagerProxy())
// for debug only
addInterceptor(chuckerInterceptor, network = true)

View File

@ -1,6 +1,16 @@
package io.github.wulkanowy.data
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
@ -131,7 +141,7 @@ inline fun <ResultType, RequestType> networkBoundResource(
query().map { Resource.Success(filterResult(it)) }
} catch (throwable: Throwable) {
onFetchFailed(throwable)
query().map { Resource.Error(throwable) }
flowOf(Resource.Error(throwable))
}
} else {
query().map { Resource.Success(filterResult(it)) }
@ -165,7 +175,7 @@ inline fun <ResultType, RequestType, T> networkBoundResource(
query().map { Resource.Success(mapResult(it)) }
} catch (throwable: Throwable) {
onFetchFailed(throwable)
query().map { Resource.Error(throwable) }
flowOf(Resource.Error(throwable))
}
} else {
query().map { Resource.Success(mapResult(it)) }

View File

@ -1,11 +1,16 @@
package io.github.wulkanowy.data.db.dao
import androidx.room.*
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentName
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import javax.inject.Singleton
@Singleton
@ -47,6 +52,9 @@ abstract class StudentDao {
@Query("UPDATE Students SET is_current = 0")
abstract suspend fun resetCurrent()
@Query("DELETE FROM Students WHERE email = :email AND user_name = :userName")
abstract suspend fun deleteByEmailAndUserName(email: String, userName: String)
@Transaction
open suspend fun switchCurrent(id: Long) {
resetCurrent()

View File

@ -35,12 +35,15 @@ class LuckyNumberRepository @Inject constructor(
fetch = {
sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student)
},
saveFetchResult = { old, new ->
if (new != old) {
old?.let { luckyNumberDb.deleteAll(listOfNotNull(it)) }
luckyNumberDb.insertAll(listOfNotNull((new?.apply {
if (notify) isNotified = false
})))
saveFetchResult = { oldLuckyNumber, newLuckyNumber ->
newLuckyNumber ?: return@networkBoundResource
if (newLuckyNumber != oldLuckyNumber) {
val updatedLuckNumberList =
listOf(newLuckyNumber.apply { if (notify) isNotified = false })
oldLuckyNumber?.let { luckyNumberDb.deleteAll(listOfNotNull(it)) }
luckyNumberDb.insertAll(updatedLuckNumberList)
}
}
)

View File

@ -1,8 +1,6 @@
package io.github.wulkanowy.data.repositories
import android.content.Context
import androidx.room.withTransaction
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao
@ -17,20 +15,19 @@ import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.DispatchersProvider
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.security.decrypt
import io.github.wulkanowy.utils.security.encrypt
import io.github.wulkanowy.utils.security.Scrambler
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class StudentRepository @Inject constructor(
@ApplicationContext private val context: Context,
private val dispatchers: DispatchersProvider,
private val studentDb: StudentDao,
private val semesterDb: SemesterDao,
private val sdk: Sdk,
private val appDatabase: AppDatabase
private val appDatabase: AppDatabase,
private val scrambler: Scrambler,
) {
suspend fun isCurrentStudentSet() = studentDb.loadCurrent()?.isCurrent ?: false
@ -68,7 +65,7 @@ class StudentRepository @Inject constructor(
student = student.apply {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
student.password = withContext(dispatchers.io) {
decrypt(student.password)
scrambler.decrypt(student.password)
}
}
},
@ -86,7 +83,7 @@ class StudentRepository @Inject constructor(
}.apply {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
student.password = withContext(dispatchers.io) {
decrypt(student.password)
scrambler.decrypt(student.password)
}
}
}
@ -96,7 +93,7 @@ class StudentRepository @Inject constructor(
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
student.password = withContext(dispatchers.io) {
decrypt(student.password)
scrambler.decrypt(student.password)
}
}
return student
@ -107,7 +104,7 @@ class StudentRepository @Inject constructor(
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
student.password = withContext(dispatchers.io) {
decrypt(student.password)
scrambler.decrypt(student.password)
}
}
return student
@ -120,7 +117,7 @@ class StudentRepository @Inject constructor(
it.apply {
if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.HEBE) {
password = withContext(dispatchers.io) {
encrypt(password, context)
scrambler.encrypt(password)
}
}
}
@ -166,4 +163,15 @@ class StudentRepository @Inject constructor(
studentDb.update(studentName)
}
suspend fun deleteStudentsAssociatedWithAccount(student: Student) {
studentDb.deleteByEmailAndUserName(student.email, student.userName)
}
suspend fun clearAll() {
withContext(dispatchers.io) {
scrambler.clearKeyPair()
appDatabase.clearAllTables()
}
}
}

View File

@ -65,8 +65,6 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
range = lesson.start..lesson.end,
requestCode = getRequestCode(lesson.start, studentId)
)
Timber.d("TimetableNotification canceled: type 1 & 2, subject: ${lesson.subject}, start: ${lesson.start}, student: $studentId")
}
}
}

View File

@ -11,6 +11,7 @@ import com.google.android.material.snackbar.Snackbar
import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.modules.auth.AuthDialog
import io.github.wulkanowy.ui.modules.captcha.CaptchaDialog
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.utils.FragmentLifecycleLogger
import io.github.wulkanowy.utils.getThemeAttrColor
@ -68,11 +69,24 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
} else Toast.makeText(this, text, Toast.LENGTH_LONG).show()
}
override fun showExpiredDialog() {
override fun showExpiredCredentialsDialog() {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.main_expired_credentials_title)
.setMessage(R.string.main_expired_credentials_description)
.setPositiveButton(R.string.main_log_in) { _, _ -> presenter.onConfirmExpiredCredentialsSelected() }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
}
override fun onCaptchaVerificationRequired(url: String?) {
CaptchaDialog.newInstance(url).show(supportFragmentManager, "captcha_dialog")
}
override fun showDecryptionFailedDialog() {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.main_session_expired)
.setMessage(R.string.main_session_relogin)
.setPositiveButton(R.string.main_log_in) { _, _ -> presenter.onExpiredLoginSelected() }
.setPositiveButton(R.string.main_log_in) { _, _ -> presenter.onConfirmDecryptionFailedSelected() }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
}

View File

@ -8,7 +8,6 @@ import android.widget.Toast
import androidx.fragment.app.DialogFragment
import androidx.viewbinding.ViewBinding
import com.google.android.material.elevation.SurfaceColors
import io.github.wulkanowy.ui.modules.auth.AuthDialog
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.lifecycleAwareVariable
import javax.inject.Inject
@ -28,8 +27,16 @@ abstract class BaseDialogFragment<VB : ViewBinding> : DialogFragment(), BaseView
Toast.makeText(context, text, Toast.LENGTH_LONG).show()
}
override fun showExpiredDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredDialog()
override fun showExpiredCredentialsDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog()
}
override fun onCaptchaVerificationRequired(url: String?) {
(activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url)
}
override fun showDecryptionFailedDialog() {
(activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog()
}
override fun openClearLoginView() {
@ -41,7 +48,7 @@ abstract class BaseDialogFragment<VB : ViewBinding> : DialogFragment(), BaseView
}
override fun showAuthDialog() {
AuthDialog.newInstance().show(childFragmentManager, "auth_dialog")
(activity as? BaseActivity<*, *>)?.showAuthDialog()
}
override fun showErrorDetailsDialog(error: Throwable) {

View File

@ -7,7 +7,6 @@ import androidx.viewbinding.ViewBinding
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.modules.auth.AuthDialog
import io.github.wulkanowy.utils.lifecycleAwareVariable
abstract class BaseFragment<VB : ViewBinding>(@LayoutRes layoutId: Int) : Fragment(layoutId),
@ -39,12 +38,20 @@ abstract class BaseFragment<VB : ViewBinding>(@LayoutRes layoutId: Int) : Fragme
}
}
override fun showExpiredDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredDialog()
override fun showExpiredCredentialsDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog()
}
override fun onCaptchaVerificationRequired(url: String?) {
(activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url)
}
override fun showDecryptionFailedDialog() {
(activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog()
}
override fun showAuthDialog() {
AuthDialog.newInstance().show(childFragmentManager, "auth_dialog")
(activity as? BaseActivity<*, *>)?.showAuthDialog()
}
override fun openClearLoginView() {

View File

@ -28,20 +28,38 @@ open class BasePresenter<T : BaseView>(
this.view = view
errorHandler.apply {
showErrorMessage = view::showError
onSessionExpired = view::showExpiredDialog
onExpiredCredentials = view::showExpiredCredentialsDialog
onCaptchaVerificationRequired = view::onCaptchaVerificationRequired
onDecryptionFailed = view::showDecryptionFailedDialog
onNoCurrentStudent = view::openClearLoginView
onPasswordChangeRequired = view::showChangePasswordSnackbar
onAuthorizationRequired = view::showAuthDialog
}
}
fun onExpiredLoginSelected() {
Timber.i("Attempt to switch the student after the session expires")
fun onConfirmDecryptionFailedSelected() {
Timber.i("Attempt to clear all data")
presenterScope.launch {
runCatching { studentRepository.clearAll() }
.onFailure {
Timber.i("Clear data result: An exception occurred")
errorHandler.dispatch(it)
}
.onSuccess {
Timber.i("Clear data result: Open login view")
view?.openClearLoginView()
}
}
}
fun onConfirmExpiredCredentialsSelected() {
Timber.i("Attempt to delete students associated with the account and switch to new student")
presenterScope.launch {
runCatching {
val student = studentRepository.getCurrentStudent(false)
studentRepository.logoutStudent(student)
studentRepository.deleteStudentsAssociatedWithAccount(student)
val students = studentRepository.getSavedStudents(false)
if (students.isNotEmpty()) {
@ -50,11 +68,11 @@ open class BasePresenter<T : BaseView>(
}
}
.onFailure {
Timber.i("Switch student result: An exception occurred")
Timber.i("Delete students result: An exception occurred")
errorHandler.dispatch(it)
}
.onSuccess {
Timber.i("Switch student result: Open login view")
Timber.i("Delete students result: Open login view")
view?.openClearLoginView()
}
}

View File

@ -6,7 +6,11 @@ interface BaseView {
fun showMessage(text: String)
fun showExpiredDialog()
fun showExpiredCredentialsDialog()
fun onCaptchaVerificationRequired(url: String?)
fun showDecryptionFailedDialog()
fun showAuthDialog()

View File

@ -4,6 +4,7 @@ import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.sdk.scrapper.exception.AuthorizationRequiredException
import io.github.wulkanowy.sdk.scrapper.exception.CloudflareVerificationException
import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException
import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException
import io.github.wulkanowy.utils.getErrorString
@ -15,7 +16,9 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
var showErrorMessage: (String, Throwable) -> Unit = { _, _ -> }
var onSessionExpired: () -> Unit = {}
var onExpiredCredentials: () -> Unit = {}
var onDecryptionFailed: () -> Unit = {}
var onNoCurrentStudent: () -> Unit = {}
@ -23,6 +26,8 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
var onAuthorizationRequired: () -> Unit = {}
var onCaptchaVerificationRequired: (url: String?) -> Unit = {}
fun dispatch(error: Throwable) {
Timber.e(error, "An exception occurred while the Wulkanowy was running")
proceed(error)
@ -32,15 +37,18 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
showErrorMessage(context.resources.getErrorString(error), error)
when (error) {
is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl)
is ScramblerException, is BadCredentialsException -> onSessionExpired()
is ScramblerException -> onDecryptionFailed()
is BadCredentialsException -> onExpiredCredentials()
is NoCurrentStudentException -> onNoCurrentStudent()
is AuthorizationRequiredException -> onAuthorizationRequired()
is CloudflareVerificationException -> onCaptchaVerificationRequired(error.originalUrl)
}
}
open fun clear() {
showErrorMessage = { _, _ -> }
onSessionExpired = {}
onExpiredCredentials = {}
onDecryptionFailed = {}
onNoCurrentStudent = {}
onPasswordChangeRequired = {}
onAuthorizationRequired = {}

View File

@ -0,0 +1,86 @@
package io.github.wulkanowy.ui.modules.captcha
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.core.os.bundleOf
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.DialogCaptchaBinding
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.ui.base.BaseDialogFragment
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class CaptchaDialog : BaseDialogFragment<DialogCaptchaBinding>() {
@Inject
lateinit var sdk: Sdk
private var webView: WebView? = null
companion object {
const val CAPTCHA_SUCCESS = "captcha_success"
private const val CAPTCHA_URL = "captcha_url"
private const val CAPTCHA_CHECK_JS = "document.getElementById('challenge-running') == null"
fun newInstance(url: String?): CaptchaDialog {
return CaptchaDialog().apply {
arguments = bundleOf(CAPTCHA_URL to url)
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = DialogCaptchaBinding.inflate(inflater).apply { binding = this }.root
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
isCancelable = false
binding.captchaRefresh.setOnClickListener {
binding.captchaWebview.loadUrl(arguments?.getString(CAPTCHA_URL).orEmpty())
}
binding.captchaClose.setOnClickListener { dismiss() }
with(binding.captchaWebview) {
webView = this
with(settings) {
javaScriptEnabled = true
userAgentString = sdk.userAgent
}
webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
view?.evaluateJavascript(CAPTCHA_CHECK_JS) {
if (it == "true") {
onChallengeAccepted()
}
}
}
}
loadUrl(arguments?.getString(CAPTCHA_URL).orEmpty())
}
}
private fun onChallengeAccepted() {
runCatching { parentFragmentManager.setFragmentResult(CAPTCHA_SUCCESS, bundleOf()) }
.onFailure { Timber.e(it) }
showMessage(getString(R.string.captcha_verified_message))
dismissAllowingStateLoss()
}
override fun onDestroy() {
webView?.destroy()
super.onDestroy()
}
}

View File

@ -18,6 +18,7 @@ import io.github.wulkanowy.databinding.FragmentDashboardBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsFragment
import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment
import io.github.wulkanowy.ui.modules.captcha.CaptchaDialog.Companion.CAPTCHA_SUCCESS
import io.github.wulkanowy.ui.modules.conference.ConferenceFragment
import io.github.wulkanowy.ui.modules.dashboard.adapters.DashboardAdapter
import io.github.wulkanowy.ui.modules.exam.ExamFragment
@ -30,7 +31,13 @@ import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.notificationscenter.NotificationsCenterFragment
import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
import io.github.wulkanowy.utils.*
import io.github.wulkanowy.utils.capitalise
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getErrorString
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.openInternetBrowser
import io.github.wulkanowy.utils.toFormattedString
import timber.log.Timber
import java.time.LocalDate
import javax.inject.Inject
@ -57,6 +64,9 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
return ((recyclerWidth - margin) / resources.displayMetrics.density).toInt()
}
override val isViewEmpty
get() = dashboardAdapter.itemCount == 0
companion object {
fun newInstance() = DashboardFragment()
@ -72,6 +82,13 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
super.onViewCreated(view, savedInstanceState)
binding = FragmentDashboardBinding.bind(view)
presenter.onAttachView(this)
initializeCaptchaResultObserver()
}
private fun initializeCaptchaResultObserver() {
childFragmentManager.setFragmentResultListener(CAPTCHA_SUCCESS, this) { _, _ ->
presenter.onRetryAfterCaptcha()
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {

View File

@ -239,6 +239,14 @@ class DashboardPresenter @Inject constructor(
loadData(selectedDashboardTiles, forceRefresh = true)
}
fun onRetryAfterCaptcha() {
view?.run {
showErrorView(false)
showProgress(true)
}
loadData(selectedDashboardTiles, forceRefresh = true)
}
fun onViewReselected() {
Timber.i("Dashboard view is reselected")
view?.run {
@ -316,7 +324,7 @@ class DashboardPresenter @Inject constructor(
) { luckyNumberResource, messageResource, attendanceResource ->
val resList = listOf(luckyNumberResource, messageResource, attendanceResource)
DashboardItem.HorizontalGroup(
resList to DashboardItem.HorizontalGroup(
isLoading = resList.any { it is Resource.Loading },
error = resList.map { it.errorOrNull }.let { errors ->
if (errors.all { it != null }) {
@ -341,9 +349,9 @@ class DashboardPresenter @Inject constructor(
)
})
}
.filterNot { it.isLoading && forceRefresh }
.filterNot { (_, it) -> it.isLoading && forceRefresh }
.distinctUntilChanged()
.onEach {
.onEach { (_, it) ->
updateData(it, forceRefresh)
if (it.isLoading) {
@ -361,7 +369,7 @@ class DashboardPresenter @Inject constructor(
)
errorHandler.dispatch(it)
}
.launch("horizontal_group ${if (forceRefresh) "-forceRefresh" else ""}")
.launchWithUniqueRefreshJob("horizontal_group", forceRefresh)
}
private fun loadGrades(student: Student, forceRefresh: Boolean) {
@ -854,6 +862,28 @@ class DashboardPresenter @Inject constructor(
onEach {
if (it is Resource.Success) {
cancelJobs(jobName)
} else if (it is Resource.Error) {
cancelJobs(jobName)
}
}.launch(jobName)
} else {
launch(jobName)
}
}
@JvmName("launchWithUniqueRefreshJobHorizontalGroup")
private fun Flow<Pair<List<Resource<*>>, *>>.launchWithUniqueRefreshJob(
name: String,
forceRefresh: Boolean
) {
val jobName = if (forceRefresh) "$name-forceRefresh" else name
if (forceRefresh) {
onEach { (resources, _) ->
if (resources.all { it is Resource.Success<*> }) {
cancelJobs(jobName)
} else if (resources.any { it is Resource.Error<*> }) {
cancelJobs(jobName)
}
}.launch(jobName)
} else {

View File

@ -6,6 +6,8 @@ interface DashboardView : BaseView {
val tileWidth: Int
val isViewEmpty: Boolean
fun initView()
fun updateData(data: List<DashboardItem>)
@ -27,6 +29,5 @@ interface DashboardView : BaseView {
fun popViewToRoot()
fun openNotificationsCenterView()
fun openInternetBrowser(url: String)
}

View File

@ -2,6 +2,7 @@ package io.github.wulkanowy.ui.modules.debug
import android.os.Bundle
import android.view.View
import android.webkit.CookieManager
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
@ -58,6 +59,10 @@ class DebugFragment : BaseFragment<FragmentDebugBinding>(R.layout.fragment_debug
(activity as? MainActivity)?.pushView(NotificationDebugFragment.newInstance())
}
override fun clearWebkitCookies() {
CookieManager.getInstance().removeAllCookies(null)
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()

View File

@ -15,6 +15,7 @@ class DebugPresenter @Inject constructor(
val items = listOf(
DebugItem(R.string.logviewer_title),
DebugItem(R.string.notification_debug_title),
DebugItem(R.string.debug_cookies_clear),
)
override fun onAttachView(view: DebugView) {
@ -31,6 +32,7 @@ class DebugPresenter @Inject constructor(
when (item.title) {
R.string.logviewer_title -> view?.openLogViewer()
R.string.notification_debug_title -> view?.openNotificationsDebug()
R.string.debug_cookies_clear -> view?.clearWebkitCookies()
else -> Timber.d("Unknown debug item: $item")
}
}

View File

@ -11,4 +11,6 @@ interface DebugView : BaseView {
fun openLogViewer()
fun openNotificationsDebug()
fun clearWebkitCookies()
}

View File

@ -7,6 +7,7 @@ import android.view.View.GONE
import android.view.View.VISIBLE
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.setFragmentResultListener
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.AdminMessage
@ -14,6 +15,7 @@ import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentLoginFormBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.captcha.CaptchaDialog
import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.login.LoginData
@ -72,6 +74,13 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
super.onViewCreated(view, savedInstanceState)
binding = FragmentLoginFormBinding.bind(view)
presenter.onAttachView(this)
initializeCaptchaResultObserver()
}
private fun initializeCaptchaResultObserver() {
setFragmentResultListener(CaptchaDialog.CAPTCHA_SUCCESS) { _, _ ->
presenter.onRetryAfterCaptcha()
}
}
override fun initView() {

View File

@ -152,6 +152,10 @@ class LoginFormPresenter @Inject constructor(
)
}
fun onRetryAfterCaptcha() {
onSignInClick()
}
fun onSignInClick() {
val loginData = getLoginData()

View File

@ -16,6 +16,7 @@ import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -30,6 +31,8 @@ import io.github.wulkanowy.databinding.ActivityMainBinding
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog
import io.github.wulkanowy.ui.modules.auth.AuthDialog
import io.github.wulkanowy.ui.modules.captcha.CaptchaDialog
import io.github.wulkanowy.ui.modules.settings.appearance.menuorder.AppMenuItem
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.AppInfo
@ -40,10 +43,17 @@ import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.nickOrName
import io.github.wulkanowy.utils.safelyPopFragments
import io.github.wulkanowy.utils.setOnViewChangeListener
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import timber.log.Timber
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
@AndroidEntryPoint
class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainView,
@ -73,6 +83,8 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
private val navController =
FragNavController(supportFragmentManager, R.id.main_fragment_container)
private val captchaVerificationEvent = MutableSharedFlow<String?>()
companion object {
private const val EXTRA_START_DESTINATION = "start_destination_json"
@ -144,6 +156,7 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
initializeToolbar()
initializeBottomNavigation(startMenuIndex, rootAppMenuItems)
initializeNavController(startMenuIndex, rootUpdatedDestinations)
initializeCaptchaVerificationEvent()
}
private fun initializeNavController(
@ -323,6 +336,27 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
.show()
}
@OptIn(FlowPreview::class)
private fun initializeCaptchaVerificationEvent() {
captchaVerificationEvent
.debounce(1.seconds)
.onEach { url ->
Timber.d("Showing captcha dialog for: $url")
showDialogFragment(CaptchaDialog.newInstance(url))
}
.launchIn(lifecycleScope)
}
override fun onCaptchaVerificationRequired(url: String?) {
lifecycleScope.launch {
captchaVerificationEvent.emit(url)
}
}
override fun showAuthDialog() {
showDialogFragment(AuthDialog.newInstance())
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
navController.onSaveInstanceState(outState)

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.settings
import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.modules.main.MainView
import timber.log.Timber
@ -24,7 +25,11 @@ class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView, Settin
override fun showMessage(text: String) {}
override fun showExpiredDialog() {}
override fun showExpiredCredentialsDialog() {}
override fun onCaptchaVerificationRequired(url: String?) = Unit
override fun showDecryptionFailedDialog() {}
override fun openClearLoginView() {}

View File

@ -8,7 +8,6 @@ import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.base.ErrorDialog
import io.github.wulkanowy.ui.modules.auth.AuthDialog
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.AppInfo
import javax.inject.Inject
@ -47,8 +46,16 @@ class AdvancedFragment : PreferenceFragmentCompat(),
(activity as? BaseActivity<*, *>)?.showMessage(text)
}
override fun showExpiredDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredDialog()
override fun showExpiredCredentialsDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog()
}
override fun onCaptchaVerificationRequired(url: String?) {
(activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url)
}
override fun showDecryptionFailedDialog() {
(activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog()
}
override fun showChangePasswordSnackbar(redirectUrl: String) {
@ -64,7 +71,7 @@ class AdvancedFragment : PreferenceFragmentCompat(),
}
override fun showAuthDialog() {
AuthDialog.newInstance().show(childFragmentManager, "auth_dialog")
(activity as? BaseActivity<*, *>)?.showAuthDialog()
}
override fun onResume() {

View File

@ -9,7 +9,6 @@ import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.base.ErrorDialog
import io.github.wulkanowy.ui.modules.auth.AuthDialog
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.AppInfo
import javax.inject.Inject
@ -63,8 +62,16 @@ class AppearanceFragment : PreferenceFragmentCompat(),
(activity as? BaseActivity<*, *>)?.showMessage(text)
}
override fun showExpiredDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredDialog()
override fun showExpiredCredentialsDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog()
}
override fun onCaptchaVerificationRequired(url: String?) {
(activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url)
}
override fun showDecryptionFailedDialog() {
(activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog()
}
override fun showChangePasswordSnackbar(redirectUrl: String) {
@ -80,7 +87,7 @@ class AppearanceFragment : PreferenceFragmentCompat(),
}
override fun showAuthDialog() {
AuthDialog.newInstance().show(childFragmentManager, "auth_dialog")
(activity as? BaseActivity<*, *>)?.showAuthDialog()
}
override fun onResume() {

View File

@ -21,7 +21,6 @@ import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.base.ErrorDialog
import io.github.wulkanowy.ui.modules.auth.AuthDialog
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.openInternetBrowser
@ -133,8 +132,16 @@ class NotificationsFragment : PreferenceFragmentCompat(),
(activity as? BaseActivity<*, *>)?.showMessage(text)
}
override fun showExpiredDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredDialog()
override fun showExpiredCredentialsDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog()
}
override fun onCaptchaVerificationRequired(url: String?) {
(activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url)
}
override fun showDecryptionFailedDialog() {
(activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog()
}
override fun showChangePasswordSnackbar(redirectUrl: String) {
@ -150,7 +157,7 @@ class NotificationsFragment : PreferenceFragmentCompat(),
}
override fun showAuthDialog() {
AuthDialog.newInstance().show(childFragmentManager, "auth_dialog")
(activity as? BaseActivity<*, *>)?.showAuthDialog()
}
override fun showFixSyncDialog() {

View File

@ -10,7 +10,6 @@ import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.base.ErrorDialog
import io.github.wulkanowy.ui.modules.auth.AuthDialog
import io.github.wulkanowy.ui.modules.main.MainView
import javax.inject.Inject
@ -84,8 +83,16 @@ class SyncFragment : PreferenceFragmentCompat(),
}
}
override fun showExpiredDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredDialog()
override fun showExpiredCredentialsDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog()
}
override fun onCaptchaVerificationRequired(url: String?) {
(activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url)
}
override fun showDecryptionFailedDialog() {
(activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog()
}
override fun showChangePasswordSnackbar(redirectUrl: String) {
@ -101,7 +108,7 @@ class SyncFragment : PreferenceFragmentCompat(),
}
override fun showAuthDialog() {
AuthDialog.newInstance().show(childFragmentManager, "auth_dialog")
(activity as? BaseActivity<*, *>)?.showAuthDialog()
}
override fun onResume() {

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.utils
import android.content.res.Resources
import io.github.wulkanowy.R
import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException
import io.github.wulkanowy.sdk.scrapper.exception.CloudflareVerificationException
import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException
import io.github.wulkanowy.sdk.scrapper.exception.ScrapperException
import io.github.wulkanowy.sdk.scrapper.exception.ServiceUnavailableException
@ -34,6 +35,7 @@ fun Resources.getErrorString(error: Throwable): String = when (error) {
is FeatureNotAvailableException -> R.string.error_feature_not_available
is VulcanException -> R.string.error_unknown_uonet
is ScrapperException -> R.string.error_unknown_app
is CloudflareVerificationException -> R.string.error_cloudflare_captcha
is SSLHandshakeException -> when {
error.isCausedByCertificateNotValidNow() -> R.string.error_invalid_device_datetime
else -> R.string.error_timeout

View File

@ -11,6 +11,7 @@ fun Sdk.init(student: Student): Sdk {
schoolSymbol = student.schoolSymbol
studentId = student.studentId
classId = student.classId
emptyCookieJarInterceptor = true
if (Sdk.Mode.valueOf(student.loginMode) == Sdk.Mode.HEBE) {
mobileBaseUrl = student.mobileBaseUrl

View File

@ -0,0 +1,58 @@
package io.github.wulkanowy.utils
import java.net.CookiePolicy
import java.net.CookieStore
import java.net.HttpCookie
import java.net.URI
import android.webkit.CookieManager as WebkitCookieManager
import java.net.CookieManager as JavaCookieManager
class WebkitCookieManagerProxy : JavaCookieManager(null, CookiePolicy.ACCEPT_ALL) {
private val webkitCookieManager: WebkitCookieManager = WebkitCookieManager.getInstance()
override fun put(uri: URI?, responseHeaders: Map<String?, List<String?>>?) {
if (uri == null || responseHeaders == null) return
val url = uri.toString()
for (headerKey in responseHeaders.keys) {
if (headerKey == null || !(
headerKey.equals("Set-Cookie2", ignoreCase = true) ||
headerKey.equals("Set-Cookie", ignoreCase = true)
)
) continue
// process each of the headers
for (headerValue in responseHeaders[headerKey].orEmpty()) {
webkitCookieManager.setCookie(url, headerValue)
}
}
}
override operator fun get(
uri: URI?,
requestHeaders: Map<String?, List<String?>?>?
): Map<String, List<String>> {
require(!(uri == null || requestHeaders == null)) { "Argument is null" }
val res = mutableMapOf<String, List<String>>()
val cookie = webkitCookieManager.getCookie(uri.toString())
if (cookie != null) res["Cookie"] = listOf(cookie)
return res
}
override fun getCookieStore(): CookieStore {
val cookies = super.getCookieStore()
return object : CookieStore {
override fun add(uri: URI?, cookie: HttpCookie?) = cookies.add(uri, cookie)
override fun get(uri: URI?): List<HttpCookie> = cookies.get(uri)
override fun getCookies(): List<HttpCookie> = cookies.cookies
override fun getURIs(): List<URI> = cookies.urIs
override fun remove(uri: URI?, cookie: HttpCookie?): Boolean =
cookies.remove(uri, cookie)
override fun removeAll(): Boolean {
webkitCookieManager.removeAllCookies(null)
return true
}
}
}
}

View File

@ -16,6 +16,7 @@ import android.util.Base64.DEFAULT
import android.util.Base64.decode
import android.util.Base64.encode
import android.util.Base64.encodeToString
import dagger.hilt.android.qualifiers.ApplicationContext
import timber.log.Timber
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
@ -33,108 +34,124 @@ import javax.crypto.CipherInputStream
import javax.crypto.CipherOutputStream
import javax.crypto.spec.OAEPParameterSpec
import javax.crypto.spec.PSource.PSpecified
import javax.inject.Inject
import javax.inject.Singleton
import javax.security.auth.x500.X500Principal
private const val KEYSTORE_NAME = "AndroidKeyStore"
@Singleton
class Scrambler @Inject constructor(
@ApplicationContext private val context: Context,
) {
private val keyCharset = Charset.forName("UTF-8")
private const val KEY_ALIAS = "wulkanowy_password"
private val isKeyPairExists: Boolean
get() = keyStore.getKey(KEY_ALIAS, null) != null
private val KEY_CHARSET = Charset.forName("UTF-8")
private val keyStore: KeyStore
get() = KeyStore.getInstance(KEYSTORE_NAME).apply { load(null) }
private val isKeyPairExists: Boolean
get() = keyStore.getKey(KEY_ALIAS, null) != null
private val cipher: Cipher
get() {
return if (SDK_INT >= M) Cipher.getInstance(
"RSA/ECB/OAEPWithSHA-256AndMGF1Padding",
"AndroidKeyStoreBCWorkaround"
)
else Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL")
}
private val keyStore: KeyStore
get() = KeyStore.getInstance(KEYSTORE_NAME).apply { load(null) }
fun encrypt(plainText: String): String {
if (plainText.isEmpty()) throw ScramblerException("Text to be encrypted is empty")
private val cipher: Cipher
get() {
return if (SDK_INT >= M) Cipher.getInstance(
"RSA/ECB/OAEPWithSHA-256AndMGF1Padding",
"AndroidKeyStoreBCWorkaround"
)
else Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL")
return try {
if (!isKeyPairExists) generateKeyPair()
cipher.let {
if (SDK_INT >= M) {
OAEPParameterSpec("SHA-256", "MGF1", SHA1, PSpecified.DEFAULT).let { spec ->
it.init(ENCRYPT_MODE, keyStore.getCertificate(KEY_ALIAS).publicKey, spec)
}
} else it.init(ENCRYPT_MODE, keyStore.getCertificate(KEY_ALIAS).publicKey)
ByteArrayOutputStream().let { output ->
CipherOutputStream(output, it).apply {
write(plainText.toByteArray(keyCharset))
close()
}
encodeToString(output.toByteArray(), DEFAULT)
}
}
} catch (exception: Exception) {
Timber.e(exception, "An error occurred while encrypting text")
String(encode(plainText.toByteArray(keyCharset), DEFAULT), keyCharset)
}
}
fun encrypt(plainText: String, context: Context): String {
if (plainText.isEmpty()) throw ScramblerException("Text to be encrypted is empty")
fun decrypt(cipherText: String): String {
if (cipherText.isEmpty()) throw ScramblerException("Text to be encrypted is empty")
return try {
if (!isKeyPairExists) generateKeyPair(context)
return try {
if (!isKeyPairExists) throw ScramblerException("KeyPair doesn't exist")
cipher.let {
if (SDK_INT >= M) {
OAEPParameterSpec("SHA-256", "MGF1", SHA1, PSpecified.DEFAULT).let { spec ->
it.init(ENCRYPT_MODE, keyStore.getCertificate(KEY_ALIAS).publicKey, spec)
cipher.let {
if (SDK_INT >= M) {
OAEPParameterSpec("SHA-256", "MGF1", SHA1, PSpecified.DEFAULT).let { spec ->
it.init(DECRYPT_MODE, keyStore.getKey(KEY_ALIAS, null), spec)
}
} else it.init(DECRYPT_MODE, keyStore.getKey(KEY_ALIAS, null))
CipherInputStream(
ByteArrayInputStream(decode(cipherText, DEFAULT)),
it
).let { input ->
val values = ArrayList<Byte>()
var nextByte: Int
while (run { nextByte = input.read(); nextByte } != -1) {
values.add(nextByte.toByte())
}
val bytes = ByteArray(values.size)
for (i in bytes.indices) {
bytes[i] = values[i]
}
String(bytes, 0, bytes.size, keyCharset)
}
} else it.init(ENCRYPT_MODE, keyStore.getCertificate(KEY_ALIAS).publicKey)
}
} catch (e: Exception) {
throw ScramblerException("An error occurred while decrypting text", e)
}
}
ByteArrayOutputStream().let { output ->
CipherOutputStream(output, it).apply {
write(plainText.toByteArray(KEY_CHARSET))
close()
}
encodeToString(output.toByteArray(), DEFAULT)
private fun generateKeyPair() {
(if (SDK_INT >= M) {
KeyGenParameterSpec.Builder(KEY_ALIAS, PURPOSE_DECRYPT or PURPOSE_ENCRYPT)
.setDigests(DIGEST_SHA256, DIGEST_SHA512)
.setEncryptionPaddings(ENCRYPTION_PADDING_RSA_OAEP)
.setCertificateSerialNumber(BigInteger.TEN)
.setCertificateSubject(X500Principal("CN=Wulkanowy"))
.build()
} else {
KeyPairGeneratorSpec.Builder(context)
.setAlias(KEY_ALIAS)
.setSubject(X500Principal("CN=Wulkanowy"))
.setSerialNumber(BigInteger.TEN)
.setStartDate(Calendar.getInstance().time)
.setEndDate(Calendar.getInstance().apply { add(YEAR, 99) }.time)
.build()
}).let {
KeyPairGenerator.getInstance("RSA", KEYSTORE_NAME).apply {
initialize(it)
genKeyPair()
}
}
} catch (exception: Exception) {
Timber.e(exception, "An error occurred while encrypting text")
String(encode(plainText.toByteArray(KEY_CHARSET), DEFAULT), KEY_CHARSET)
Timber.i("A new KeyPair has been generated")
}
fun clearKeyPair() {
keyStore.deleteEntry(KEY_ALIAS)
Timber.i("KeyPair has been cleared")
}
private companion object {
private const val KEYSTORE_NAME = "AndroidKeyStore"
private const val KEY_ALIAS = "wulkanowy_password"
}
}
fun decrypt(cipherText: String): String {
if (cipherText.isEmpty()) throw ScramblerException("Text to be encrypted is empty")
return try {
if (!isKeyPairExists) throw ScramblerException("KeyPair doesn't exist")
cipher.let {
if (SDK_INT >= M) {
OAEPParameterSpec("SHA-256", "MGF1", SHA1, PSpecified.DEFAULT).let { spec ->
it.init(DECRYPT_MODE, keyStore.getKey(KEY_ALIAS, null), spec)
}
} else it.init(DECRYPT_MODE, keyStore.getKey(KEY_ALIAS, null))
CipherInputStream(ByteArrayInputStream(decode(cipherText, DEFAULT)), it).let { input ->
val values = ArrayList<Byte>()
var nextByte: Int
while (run { nextByte = input.read(); nextByte } != -1) {
values.add(nextByte.toByte())
}
val bytes = ByteArray(values.size)
for (i in bytes.indices) {
bytes[i] = values[i]
}
String(bytes, 0, bytes.size, KEY_CHARSET)
}
}
} catch (e: Exception) {
throw ScramblerException("An error occurred while decrypting text", e)
}
}
private fun generateKeyPair(context: Context) {
(if (SDK_INT >= M) {
KeyGenParameterSpec.Builder(KEY_ALIAS, PURPOSE_DECRYPT or PURPOSE_ENCRYPT)
.setDigests(DIGEST_SHA256, DIGEST_SHA512)
.setEncryptionPaddings(ENCRYPTION_PADDING_RSA_OAEP)
.setCertificateSerialNumber(BigInteger.TEN)
.setCertificateSubject(X500Principal("CN=Wulkanowy"))
.build()
} else {
KeyPairGeneratorSpec.Builder(context)
.setAlias(KEY_ALIAS)
.setSubject(X500Principal("CN=Wulkanowy"))
.setSerialNumber(BigInteger.TEN)
.setStartDate(Calendar.getInstance().time)
.setEndDate(Calendar.getInstance().apply { add(YEAR, 99) }.time)
.build()
}).let {
KeyPairGenerator.getInstance("RSA", KEYSTORE_NAME).apply {
initialize(it)
genKeyPair()
}
}
Timber.i("A new KeyPair has been generated")
}

View File

@ -1,6 +1,6 @@
Wersja 2.3.0
Wersja 2.3.4
poprawiliśmy kilka usterek przy odświeżaniu danych (ale pewnie nie wszystkie)
zaktualizowaliśmy sposób pytania o zgodę na personalizowane reklamy
dodaliśmy obsługę captchy, co umożliwi używanie apki np. na odmianie ResMan Rzeszów
naprawiliśmy wyświetlanie frekwencji w szkołach używających eduOne (piszcie, jeśli nadal nie działa)
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases

View File

@ -0,0 +1,51 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minWidth="350dp"
tools:context=".ui.modules.captcha.CaptchaDialog">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:gravity="center_vertical"
android:text="@string/captcha_dialog_title"
app:layout_constraintBottom_toBottomOf="@id/captcha_close"
app:layout_constraintEnd_toStartOf="@id/captcha_refresh"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/captcha_refresh"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:contentDescription="@string/logviewer_refresh"
app:icon="@drawable/ic_refresh"
app:iconTint="?colorOnSurface"
app:layout_constraintEnd_toStartOf="@id/captcha_close"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/captcha_close"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:contentDescription="@string/all_close"
app:icon="@drawable/ic_all_close_circle"
app:iconTint="?colorOnSurface"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<WebView
android:id="@+id/captcha_webview"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/captcha_close" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -13,6 +13,7 @@
<string name="logviewer_title">Prohlížeč protokolů</string>
<string name="debug_title">Ladění</string>
<string name="notification_debug_title">Ladění oznámení</string>
<string name="debug_cookies_clear">Vymazat soubory cookie webview</string>
<string name="contributors_title">Tvůrci</string>
<string name="license_title">Licence</string>
<string name="message_title">Zprávy</string>
@ -96,6 +97,8 @@
<string name="main_log_in">Přihlásit se</string>
<string name="main_session_expired">Relace vypršela</string>
<string name="main_session_relogin">Relace vypršela. Přihlaste se prosím znovu</string>
<string name="main_expired_credentials_description">Heslo k vašemu účtu bylo změněno. Musíte se znovu přihlásit do Wulkanového</string>
<string name="main_expired_credentials_title">Heslo bylo změněno</string>
<string name="main_support_title">Podpora aplikace</string>
<string name="main_support_description">Líbí se Vám tato aplikace? Podpořte její vývoj tím, že povolíte neinvazivní reklamy, které můžete kdykoliv vypnout</string>
<string name="main_support_positive">Zapnout reklamy</string>
@ -760,7 +763,7 @@
<string name="pref_ads_support_category_name">Podpora</string>
<string name="pref_ads_privacy_policy">Ochrana osobních údajů</string>
<string name="pref_ads_agreements">Souhlasy</string>
<string name="pref_ads_consent">Show consent to data processing</string>
<string name="pref_ads_consent">Zobrazit souhlas se zpracováním údajů</string>
<string name="pref_ads_show_in_app">Zobrazit reklamy v aplikaci</string>
<string name="pref_ads_support">Podívejte se na jednu reklamu pro podporu projektu</string>
<string name="pref_ads_privacy_title">Souhlas se zpracováním dat</string>
@ -831,6 +834,9 @@
<string name="auth_title">Autorizace</string>
<string name="auth_description">Pro provoz aplikace potřebujeme potvrdit vaši identitu. Zadejte PESEL žáka &lt;b&gt;%1$s&lt;/b&gt; v níže uvedeném poli</string>
<string name="auth_button_skip">Zatím přeskočit</string>
<!--Captcha-->
<string name="captcha_dialog_title">Probíhá ověřování. Počkejte…</string>
<string name="captcha_verified_message">Úspěšně ověřeno</string>
<!--Errors-->
<string name="error_no_internet">Žádné internetové připojení</string>
<string name="error_invalid_device_datetime">Vyskytla se chyba. Zkontrolujte hodiny svého zařízení</string>
@ -840,6 +846,7 @@
<string name="error_service_unavailable">Probíhá údržba deníku UONET+. Zkuste to později znovu</string>
<string name="error_unknown_uonet">Neznámá chyba deniku UONET+. Prosím zkuste to znovu později</string>
<string name="error_unknown_app">Neznámá chyba aplikace. Prosím zkuste to znovu později</string>
<string name="error_cloudflare_captcha">Vyžadováno ověření Captcha</string>
<string name="error_unknown">Vyskytla se neočekávaná chyba</string>
<string name="error_feature_disabled">Funkce je deaktivována přes vaší školou</string>
<string name="error_feature_not_available">Funkce není k dispozici. Přihlaste se v jiném režimu než Mobile API</string>

View File

@ -13,6 +13,7 @@
<string name="logviewer_title">Log viewer</string>
<string name="debug_title">Debug</string>
<string name="notification_debug_title">Notification debug</string>
<string name="debug_cookies_clear">Clear webview cookies</string>
<string name="contributors_title">Contributors</string>
<string name="license_title">Licenses</string>
<string name="message_title">Messages</string>
@ -96,6 +97,8 @@
<string name="main_log_in">Log in</string>
<string name="main_session_expired">Session expired</string>
<string name="main_session_relogin">Session expired, log in again</string>
<string name="main_expired_credentials_description">Your account password has been changed. You need to log in to Wulkanowy again</string>
<string name="main_expired_credentials_title">Password changed</string>
<string name="main_support_title">Application support</string>
<string name="main_support_description">Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time</string>
<string name="main_support_positive">Enable ads</string>
@ -741,6 +744,9 @@
<string name="auth_title">Authorization</string>
<string name="auth_description">To operate the application, we need to confirm your identity. Please enter the student\'s PESEL &lt;b&gt;%1$s&lt;/b&gt; in the field below</string>
<string name="auth_button_skip">Skip for now</string>
<!--Captcha-->
<string name="captcha_dialog_title">Verification is in progress. Wait…</string>
<string name="captcha_verified_message">Verified successfully</string>
<!--Errors-->
<string name="error_no_internet">No internet connection</string>
<string name="error_invalid_device_datetime">An error occurred. Check your device clock</string>
@ -750,6 +756,7 @@
<string name="error_service_unavailable">Maintenance underway UONET + register. Try again later</string>
<string name="error_unknown_uonet">Unknown UONET + register error. Try again later</string>
<string name="error_unknown_app">Unknown application error. Please try again later</string>
<string name="error_cloudflare_captcha">Captcha verification required</string>
<string name="error_unknown">An unexpected error occurred</string>
<string name="error_feature_disabled">Feature disabled by your school</string>
<string name="error_feature_not_available">Feature not available. Login in a mode other than Mobile API</string>

View File

@ -13,6 +13,7 @@
<string name="logviewer_title">Log Viewer</string>
<string name="debug_title">Debuggen</string>
<string name="notification_debug_title">Benachrichtigungen debuggen</string>
<string name="debug_cookies_clear">Clear webview cookies</string>
<string name="contributors_title">Mitarbeiter</string>
<string name="license_title">Lizenzen</string>
<string name="message_title">Nachrichten</string>
@ -96,6 +97,8 @@
<string name="main_log_in">Anmelden</string>
<string name="main_session_expired">Die Sitzung ist abgelaufen</string>
<string name="main_session_relogin">Die Sitzung ist abgelaufen, bitte loggen Sie sich erneut ein</string>
<string name="main_expired_credentials_description">Your account password has been changed. You need to log in to Wulkanowy again</string>
<string name="main_expired_credentials_title">Password changed</string>
<string name="main_support_title">Anwendungsunterstützung</string>
<string name="main_support_description">Gefällt Ihnen diese App? Unterstützen Sie ihre Entwicklung, indem Sie nicht-invasive Werbung aktivieren, die Sie jederzeit deaktivieren können</string>
<string name="main_support_positive">Werbung aktivieren</string>
@ -741,6 +744,9 @@
<string name="auth_title">Authorization</string>
<string name="auth_description">To operate the application, we need to confirm your identity. Please enter the student\'s PESEL &lt;b&gt;%1$s&lt;/b&gt; in the field below</string>
<string name="auth_button_skip">Skip for now</string>
<!--Captcha-->
<string name="captcha_dialog_title">Verification is in progress. Wait…</string>
<string name="captcha_verified_message">Verified successfully</string>
<!--Errors-->
<string name="error_no_internet">Keine Internetverbindung</string>
<string name="error_invalid_device_datetime">Es ist ein Fehler aufgetreten. Überprüfen Sie Ihre Geräteuhr</string>
@ -750,6 +756,7 @@
<string name="error_service_unavailable">Wartung im Gange UONET + Klassenbuch. Versuchen Sie es später noch einmal</string>
<string name="error_unknown_uonet">Unbekannter UONET + Registerfehler. Versuchen Sie es später erneut</string>
<string name="error_unknown_app">Unbekannter Anwendungsfehler. Bitte versuchen Sie es später noch einmal</string>
<string name="error_cloudflare_captcha">Captcha verification required</string>
<string name="error_unknown">Ein unerwarteter Fehler ist aufgetreten</string>
<string name="error_feature_disabled">Funktion, die von Ihrer Schule deaktiviert wurde</string>
<string name="error_feature_not_available">Feature in diesem Modus nicht verfügbar</string>

View File

@ -13,6 +13,7 @@
<string name="logviewer_title">Log viewer</string>
<string name="debug_title">Debug</string>
<string name="notification_debug_title">Notification debug</string>
<string name="debug_cookies_clear">Clear webview cookies</string>
<string name="contributors_title">Contributors</string>
<string name="license_title">Licenses</string>
<string name="message_title">Messages</string>
@ -96,6 +97,8 @@
<string name="main_log_in">Log in</string>
<string name="main_session_expired">Session expired</string>
<string name="main_session_relogin">Session expired, log in again</string>
<string name="main_expired_credentials_description">Your account password has been changed. You need to log in to Wulkanowy again</string>
<string name="main_expired_credentials_title">Password changed</string>
<string name="main_support_title">Application support</string>
<string name="main_support_description">Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time</string>
<string name="main_support_positive">Enable ads</string>
@ -741,6 +744,9 @@
<string name="auth_title">Authorization</string>
<string name="auth_description">To operate the application, we need to confirm your identity. Please enter the student\'s PESEL &lt;b&gt;%1$s&lt;/b&gt; in the field below</string>
<string name="auth_button_skip">Skip for now</string>
<!--Captcha-->
<string name="captcha_dialog_title">Verification is in progress. Wait…</string>
<string name="captcha_verified_message">Verified successfully</string>
<!--Errors-->
<string name="error_no_internet">No internet connection</string>
<string name="error_invalid_device_datetime">An error occurred. Check your device clock</string>
@ -750,6 +756,7 @@
<string name="error_service_unavailable">Maintenance underway UONET + register. Try again later</string>
<string name="error_unknown_uonet">Unknown UONET + register error. Try again later</string>
<string name="error_unknown_app">Unknown application error. Please try again later</string>
<string name="error_cloudflare_captcha">Captcha verification required</string>
<string name="error_unknown">An unexpected error occurred</string>
<string name="error_feature_disabled">Feature disabled by your school</string>
<string name="error_feature_not_available">Feature not available. Login in a mode other than Mobile API</string>

View File

@ -13,6 +13,7 @@
<string name="logviewer_title">Log viewer</string>
<string name="debug_title">Debug</string>
<string name="notification_debug_title">Notification debug</string>
<string name="debug_cookies_clear">Clear webview cookies</string>
<string name="contributors_title">Contributors</string>
<string name="license_title">Licenses</string>
<string name="message_title">Messages</string>
@ -96,6 +97,8 @@
<string name="main_log_in">Log in</string>
<string name="main_session_expired">Session expired</string>
<string name="main_session_relogin">Session expired, log in again</string>
<string name="main_expired_credentials_description">Your account password has been changed. You need to log in to Wulkanowy again</string>
<string name="main_expired_credentials_title">Password changed</string>
<string name="main_support_title">Application support</string>
<string name="main_support_description">Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time</string>
<string name="main_support_positive">Enable ads</string>
@ -741,6 +744,9 @@
<string name="auth_title">Authorization</string>
<string name="auth_description">To operate the application, we need to confirm your identity. Please enter the student\'s PESEL &lt;b&gt;%1$s&lt;/b&gt; in the field below</string>
<string name="auth_button_skip">Skip for now</string>
<!--Captcha-->
<string name="captcha_dialog_title">Verification is in progress. Wait…</string>
<string name="captcha_verified_message">Verified successfully</string>
<!--Errors-->
<string name="error_no_internet">No internet connection</string>
<string name="error_invalid_device_datetime">An error occurred. Check your device clock</string>
@ -750,6 +756,7 @@
<string name="error_service_unavailable">Maintenance underway UONET + register. Try again later</string>
<string name="error_unknown_uonet">Unknown UONET + register error. Try again later</string>
<string name="error_unknown_app">Unknown application error. Please try again later</string>
<string name="error_cloudflare_captcha">Captcha verification required</string>
<string name="error_unknown">An unexpected error occurred</string>
<string name="error_feature_disabled">Feature disabled by your school</string>
<string name="error_feature_not_available">Feature not available. Login in a mode other than Mobile API</string>

View File

@ -13,6 +13,7 @@
<string name="logviewer_title">Przeglądarka logów</string>
<string name="debug_title">Debugowanie</string>
<string name="notification_debug_title">Debugowanie powiadomień</string>
<string name="debug_cookies_clear">Wyczyść ciasteczka webview</string>
<string name="contributors_title">Twórcy</string>
<string name="license_title">Licencje</string>
<string name="message_title">Wiadomości</string>
@ -96,6 +97,8 @@
<string name="main_log_in">Zaloguj się</string>
<string name="main_session_expired">Sesja wygasła</string>
<string name="main_session_relogin">Sesja wygasła, zaloguj się ponownie</string>
<string name="main_expired_credentials_description">Hasło do Twojego konta zostało zmienione. Musisz zalogować się ponownie do Wulkanowego</string>
<string name="main_expired_credentials_title">Hasło zostało zmienione</string>
<string name="main_support_title">Wparcie aplikacji</string>
<string name="main_support_description">Podoba Ci się ta aplikacja? Wspieraj jej rozwój poprzez włączenie nieinwazyjnych reklam, które możesz wyłączyć w dowolnym momencie</string>
<string name="main_support_positive">Włącz reklamy</string>
@ -831,6 +834,9 @@
<string name="auth_title">Autoryzacja</string>
<string name="auth_description">Rodzicu, musimy mieć pewność, że Twój adres e-mail został powiązany z prawidłowym kontem ucznia. W celu autoryzacji konta podaj numer PESEL ucznia &lt;b&gt;%1$s&lt;/b&gt; w polu poniżej</string>
<string name="auth_button_skip">Na razie pomiń</string>
<!--Captcha-->
<string name="captcha_dialog_title">Trwa weryfikacja. Czekaj…</string>
<string name="captcha_verified_message">Pomyślnie zweryfikowano</string>
<!--Errors-->
<string name="error_no_internet">Brak połączenia z internetem</string>
<string name="error_invalid_device_datetime">Wystąpił błąd. Sprawdź poprawność daty w urządzeniu</string>
@ -840,6 +846,7 @@
<string name="error_service_unavailable">Trwa przerwa techniczna dziennika UONET+. Spróbuj ponownie później</string>
<string name="error_unknown_uonet">Nieznany błąd dziennika UONET+. Spróbuj ponownie później</string>
<string name="error_unknown_app">Nieznany błąd aplikacji. Spróbuj ponownie później</string>
<string name="error_cloudflare_captcha">Wymagana weryfikacja captcha</string>
<string name="error_unknown">Wystąpił nieoczekiwany błąd</string>
<string name="error_feature_disabled">Funkcja wyłączona przez szkołę</string>
<string name="error_feature_not_available">Funkcja niedostępna. Zaloguj się w trybie innym niż Mobilne API</string>

View File

@ -13,6 +13,7 @@
<string name="logviewer_title">Просмотр журнала</string>
<string name="debug_title">Отладка</string>
<string name="notification_debug_title">Отладка уведомлений</string>
<string name="debug_cookies_clear">Clear webview cookies</string>
<string name="contributors_title">Разработчики</string>
<string name="license_title">Лицензии</string>
<string name="message_title">Сообщения</string>
@ -96,6 +97,8 @@
<string name="main_log_in">Войти</string>
<string name="main_session_expired">Сеанс истёк</string>
<string name="main_session_relogin">Сеанс истёк, авторизуйтесь снова</string>
<string name="main_expired_credentials_description">Your account password has been changed. You need to log in to Wulkanowy again</string>
<string name="main_expired_credentials_title">Password changed</string>
<string name="main_support_title">Поддержка приложения</string>
<string name="main_support_description">Вам нравится это приложение? Поддержите его разработку, включив неинвазивную рекламу, которую можно отключить в любое время</string>
<string name="main_support_positive">Включить рекламу</string>
@ -831,6 +834,9 @@
<string name="auth_title">Авторизация</string>
<string name="auth_description">Для работы приложения нам необходимо подтвердить вашу личность. Введите PESEL учащегося &lt;b&gt;%1$s&lt;/b&gt; в поле ниже</string>
<string name="auth_button_skip">Пропустить сейчас</string>
<!--Captcha-->
<string name="captcha_dialog_title">Verification is in progress. Wait…</string>
<string name="captcha_verified_message">Verified successfully</string>
<!--Errors-->
<string name="error_no_internet">Интернет-соединение отсутствует</string>
<string name="error_invalid_device_datetime">Произошла ошибка. Проверьте время на вашем устройстве</string>
@ -840,6 +846,7 @@
<string name="error_service_unavailable">UONET+ проводит техническое обслуживание, повторите попытку позже</string>
<string name="error_unknown_uonet">Неизвестная ошибка дневника UONET+, повторите попытку позже</string>
<string name="error_unknown_app">Неизвестная ошибка приложения, повторите попытку позже</string>
<string name="error_cloudflare_captcha">Captcha verification required</string>
<string name="error_unknown">Произошла непредвиденная ошибка</string>
<string name="error_feature_disabled">Функция отключена вашей школой</string>
<string name="error_feature_not_available">Функция недоступна в режиме Mobile API. Воспользуйтесь другим режимом</string>

View File

@ -13,6 +13,7 @@
<string name="logviewer_title">Prehliadač protokolov</string>
<string name="debug_title">Ladenie</string>
<string name="notification_debug_title">Ladenie oznámení</string>
<string name="debug_cookies_clear">Vymazať súbory cookie webview</string>
<string name="contributors_title">Tvorcovia</string>
<string name="license_title">Licencie</string>
<string name="message_title">Správy</string>
@ -96,6 +97,8 @@
<string name="main_log_in">Prihlásiť sa</string>
<string name="main_session_expired">Relácia vypršala</string>
<string name="main_session_relogin">Relácia vypršala. Prihláste sa prosím znovu</string>
<string name="main_expired_credentials_description">Heslo k vášmu účtu bolo zmenené. Musíte sa znovu prihlásiť do Wulkanového</string>
<string name="main_expired_credentials_title">Heslo bolo zmenené</string>
<string name="main_support_title">Podpora aplikácie</string>
<string name="main_support_description">Páči sa Vám táto aplikácia? Podporte jej vývoj tým, že povolíte neinvazívne reklamy, ktoré môžete kedykoľvek vypnúť</string>
<string name="main_support_positive">Zapnúť reklamy</string>
@ -760,7 +763,7 @@
<string name="pref_ads_support_category_name">Podpora</string>
<string name="pref_ads_privacy_policy">Ochrana osobných údajov</string>
<string name="pref_ads_agreements">Súhlasy</string>
<string name="pref_ads_consent">Show consent to data processing</string>
<string name="pref_ads_consent">Zobraziť súhlas so spracovaním údajov</string>
<string name="pref_ads_show_in_app">Zobraziť reklamy v aplikácii</string>
<string name="pref_ads_support">Pozrite sa na jednu reklamu pre podporu projektu</string>
<string name="pref_ads_privacy_title">Súhlas so spracovaním dát</string>
@ -831,6 +834,9 @@
<string name="auth_title">Autorizácia</string>
<string name="auth_description">Na prevádzku aplikácie potrebujeme potvrdiť vašu identitu. Zadajte PESEL žiaka &lt;b&gt;%1$s&lt;/b&gt; v nižšie uvedenom poli</string>
<string name="auth_button_skip">Zatiaľ preskočiť</string>
<!--Captcha-->
<string name="captcha_dialog_title">Overovanie prebieha. Počkajte…</string>
<string name="captcha_verified_message">Úspešne overené</string>
<!--Errors-->
<string name="error_no_internet">Žiadne internetové pripojenie</string>
<string name="error_invalid_device_datetime">Vyskytla sa chyba. Skontrolujte hodiny svojho zariadenia</string>
@ -840,6 +846,7 @@
<string name="error_service_unavailable">Prebieha údržba denníka UONET+. Skúste to neskôr znova</string>
<string name="error_unknown_uonet">Neznáma chyba dennika UONET+. Prosím skúste to znova neskôr</string>
<string name="error_unknown_app">Neznáma chyba aplikácie. Prosím skúste to znova neskôr</string>
<string name="error_cloudflare_captcha">Vyžaduje sa overenie Captcha</string>
<string name="error_unknown">Vyskytla sa neočakávaná chyba</string>
<string name="error_feature_disabled">Funkcia je deaktivovaná cez vašou školou</string>
<string name="error_feature_not_available">Funkcia nie je k dispozícii. Prihláste sa v inom režime než Mobile API</string>

View File

@ -13,6 +13,7 @@
<string name="logviewer_title">Переглядач логів</string>
<string name="debug_title">Відладка</string>
<string name="notification_debug_title">Відладка сповіщень</string>
<string name="debug_cookies_clear">Очистити кукі веб - перегляду</string>
<string name="contributors_title">Розробники</string>
<string name="license_title">Ліцензії</string>
<string name="message_title">Листи</string>
@ -96,6 +97,8 @@
<string name="main_log_in">Увійти</string>
<string name="main_session_expired">Минув термін дії сесії</string>
<string name="main_session_relogin">Минув термін дії сесії, авторизуйтеся знову</string>
<string name="main_expired_credentials_description">Your account password has been changed. You need to log in to Wulkanowy again</string>
<string name="main_expired_credentials_title">Password changed</string>
<string name="main_support_title">Підтримка додатку</string>
<string name="main_support_description">Вам подобається цей додаток? Підтримайте його розвиток, увімкнувши неінвазивну рекламу, яку ви можете відключити в будь-який час</string>
<string name="main_support_positive">Увімкнути рекламу</string>
@ -831,6 +834,9 @@
<string name="auth_title">Авторизувати</string>
<string name="auth_description">Для роботи програми нам потрібно підтвердити вашу особу. Будь ласка, введіть число PESEL &lt;b&gt;%1$s&lt;/b&gt; студента в поле нижче</string>
<string name="auth_button_skip">Поки що пропустити</string>
<!--Captcha-->
<string name="captcha_dialog_title">Верифікація в процесі. Чекайте…</string>
<string name="captcha_verified_message">Верифікація завершена</string>
<!--Errors-->
<string name="error_no_internet">Немає з\'єднання з інтернетом</string>
<string name="error_invalid_device_datetime">Сталася помилка. Перевірте годинник пристрою</string>
@ -840,6 +846,7 @@
<string name="error_service_unavailable">UONET+ проводить технічне осблуговування, спробуйте пізніше</string>
<string name="error_unknown_uonet">Невідома помилка щоденника UONET+, спробуйте пізніше</string>
<string name="error_unknown_app">Невідома помилка програми, спробуйте пізніше</string>
<string name="error_cloudflare_captcha">Необхідна перевірка Captcha</string>
<string name="error_unknown">Відбулася несподівана помилка</string>
<string name="error_feature_disabled">Функція вимкнена вашою школою</string>
<string name="error_feature_not_available">Функція недоступна в режимі Mobile API. Увійдіть в інший режим</string>

View File

@ -14,6 +14,7 @@
<string name="logviewer_title">Log viewer</string>
<string name="debug_title">Debug</string>
<string name="notification_debug_title">Notification debug</string>
<string name="debug_cookies_clear">Clear webview cookies</string>
<string name="contributors_title">Contributors</string>
<string name="license_title">Licenses</string>
<string name="message_title">Messages</string>
@ -107,6 +108,8 @@
<string name="main_log_in">Log in</string>
<string name="main_session_expired">Session expired</string>
<string name="main_session_relogin">Session expired, log in again</string>
<string name="main_expired_credentials_description">Your account password has been changed. You need to log in to Wulkanowy again</string>
<string name="main_expired_credentials_title">Password changed</string>
<string name="main_support_title">Application support</string>
<string name="main_support_description">Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time</string>
<string name="main_support_positive">Enable ads</string>
@ -831,6 +834,11 @@
<string name="auth_button_skip">Skip for now</string>
<!--Captcha-->
<string name="captcha_dialog_title">Verification is in progress. Wait…</string>
<string name="captcha_verified_message">Verified successfully</string>
<!--Errors-->
<string name="error_no_internet">No internet connection</string>
<string name="error_invalid_device_datetime">An error occurred. Check your device clock</string>
@ -840,6 +848,7 @@
<string name="error_service_unavailable">Maintenance underway UONET + register. Try again later</string>
<string name="error_unknown_uonet">Unknown UONET + register error. Try again later</string>
<string name="error_unknown_app">Unknown application error. Please try again later</string>
<string name="error_cloudflare_captcha">Captcha verification required</string>
<string name="error_unknown">An unexpected error occurred</string>
<string name="error_feature_disabled">Feature disabled by your school</string>
<string name="error_feature_not_available">Feature not available. Login in a mode other than Mobile API</string>

View File

@ -101,8 +101,16 @@ class AdsFragment : PreferenceFragmentCompat(), MainView.TitledView, AdsView {
(activity as? BaseActivity<*, *>)?.showMessage(text)
}
override fun showExpiredDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredDialog()
override fun showExpiredCredentialsDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog()
}
override fun onCaptchaVerificationRequired(url: String?) {
(activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url)
}
override fun showDecryptionFailedDialog() {
(activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog()
}
override fun showChangePasswordSnackbar(redirectUrl: String) {

View File

@ -14,7 +14,7 @@ buildscript {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$kotlin_version-1.0.16"
classpath 'com.android.tools.build:gradle:8.2.0'
classpath 'com.android.tools.build:gradle:8.2.1'
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
classpath 'com.google.gms:google-services:4.4.0'
classpath 'com.huawei.agconnect:agcp:1.9.1.303'
@ -29,6 +29,7 @@ buildscript {
allprojects {
repositories {
mavenLocal()
mavenCentral()
google()
maven { url "https://jitpack.io" }

Binary file not shown.

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

17
gradlew vendored
View File

@ -83,7 +83,8 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@ -201,11 +202,11 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \