1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2024-09-20 03:39:08 -05:00

Merge branch 'release/2.3.2'

This commit is contained in:
Mikołaj Pich 2024-01-09 19:31:22 +01:00
commit f893170dec
34 changed files with 279 additions and 154 deletions

View File

@ -27,8 +27,8 @@ android {
testApplicationId "io.github.tests.wulkanowy" testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 34 targetSdkVersion 34
versionCode 141 versionCode 142
versionName "2.3.1" versionName "2.3.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "app_name", "Wulkanowy" resValue "string", "app_name", "Wulkanowy"
@ -188,12 +188,12 @@ ext {
android_hilt = "1.1.0" android_hilt = "1.1.0"
room = "2.6.1" room = "2.6.1"
chucker = "4.0.0" chucker = "4.0.0"
mockk = "1.13.8" mockk = "1.13.9"
coroutines = "1.7.3" coroutines = "1.7.3"
} }
dependencies { dependencies {
implementation 'com.github.wulkanowy:sdk:2.3.3' implementation 'io.github.wulkanowy:sdk:2.3.4'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
@ -240,7 +240,7 @@ dependencies {
implementation "com.squareup.okhttp3:logging-interceptor:4.12.0" implementation "com.squareup.okhttp3:logging-interceptor:4.12.0"
implementation "com.jakewharton.timber:timber:5.0.1" 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.github.bastienpaulfr:Treessence:1.1.2'
implementation "com.mikepenz:aboutlibraries-core:$about_libraries" implementation "com.mikepenz:aboutlibraries-core:$about_libraries"
implementation 'io.coil-kt:coil:2.5.0' implementation 'io.coil-kt:coil:2.5.0'

View File

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

View File

@ -35,12 +35,15 @@ class LuckyNumberRepository @Inject constructor(
fetch = { fetch = {
sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student) sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student)
}, },
saveFetchResult = { old, new -> saveFetchResult = { oldLuckyNumber, newLuckyNumber ->
if (new != old) { newLuckyNumber ?: return@networkBoundResource
old?.let { luckyNumberDb.deleteAll(listOfNotNull(it)) }
luckyNumberDb.insertAll(listOfNotNull((new?.apply { if (newLuckyNumber != oldLuckyNumber) {
if (notify) isNotified = false 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 package io.github.wulkanowy.data.repositories
import android.content.Context
import androidx.room.withTransaction import androidx.room.withTransaction
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.dao.SemesterDao import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao 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.sdk.Sdk
import io.github.wulkanowy.utils.DispatchersProvider import io.github.wulkanowy.utils.DispatchersProvider
import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.security.decrypt import io.github.wulkanowy.utils.security.Scrambler
import io.github.wulkanowy.utils.security.encrypt
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class StudentRepository @Inject constructor( class StudentRepository @Inject constructor(
@ApplicationContext private val context: Context,
private val dispatchers: DispatchersProvider, private val dispatchers: DispatchersProvider,
private val studentDb: StudentDao, private val studentDb: StudentDao,
private val semesterDb: SemesterDao, private val semesterDb: SemesterDao,
private val sdk: Sdk, private val sdk: Sdk,
private val appDatabase: AppDatabase private val appDatabase: AppDatabase,
private val scrambler: Scrambler,
) { ) {
suspend fun isCurrentStudentSet() = studentDb.loadCurrent()?.isCurrent ?: false suspend fun isCurrentStudentSet() = studentDb.loadCurrent()?.isCurrent ?: false
@ -68,7 +65,7 @@ class StudentRepository @Inject constructor(
student = student.apply { student = student.apply {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
student.password = withContext(dispatchers.io) { student.password = withContext(dispatchers.io) {
decrypt(student.password) scrambler.decrypt(student.password)
} }
} }
}, },
@ -86,7 +83,7 @@ class StudentRepository @Inject constructor(
}.apply { }.apply {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
student.password = withContext(dispatchers.io) { 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) { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
student.password = withContext(dispatchers.io) { student.password = withContext(dispatchers.io) {
decrypt(student.password) scrambler.decrypt(student.password)
} }
} }
return student return student
@ -107,7 +104,7 @@ class StudentRepository @Inject constructor(
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
student.password = withContext(dispatchers.io) { student.password = withContext(dispatchers.io) {
decrypt(student.password) scrambler.decrypt(student.password)
} }
} }
return student return student
@ -120,7 +117,7 @@ class StudentRepository @Inject constructor(
it.apply { it.apply {
if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.HEBE) { if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.HEBE) {
password = withContext(dispatchers.io) { password = withContext(dispatchers.io) {
encrypt(password, context) scrambler.encrypt(password)
} }
} }
} }
@ -166,4 +163,15 @@ class StudentRepository @Inject constructor(
studentDb.update(studentName) 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

@ -68,11 +68,20 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
} else Toast.makeText(this, text, Toast.LENGTH_LONG).show() } 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 showDecryptionFailedDialog() {
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setTitle(R.string.main_session_expired) .setTitle(R.string.main_session_expired)
.setMessage(R.string.main_session_relogin) .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) { _, _ -> } .setNegativeButton(android.R.string.cancel) { _, _ -> }
.show() .show()
} }

View File

@ -28,8 +28,12 @@ abstract class BaseDialogFragment<VB : ViewBinding> : DialogFragment(), BaseView
Toast.makeText(context, text, Toast.LENGTH_LONG).show() Toast.makeText(context, text, Toast.LENGTH_LONG).show()
} }
override fun showExpiredDialog() { override fun showExpiredCredentialsDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredDialog() (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog()
}
override fun showDecryptionFailedDialog() {
(activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog()
} }
override fun openClearLoginView() { override fun openClearLoginView() {

View File

@ -39,8 +39,12 @@ abstract class BaseFragment<VB : ViewBinding>(@LayoutRes layoutId: Int) : Fragme
} }
} }
override fun showExpiredDialog() { override fun showExpiredCredentialsDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredDialog() (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog()
}
override fun showDecryptionFailedDialog() {
(activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog()
} }
override fun showAuthDialog() { override fun showAuthDialog() {

View File

@ -28,20 +28,37 @@ open class BasePresenter<T : BaseView>(
this.view = view this.view = view
errorHandler.apply { errorHandler.apply {
showErrorMessage = view::showError showErrorMessage = view::showError
onSessionExpired = view::showExpiredDialog onExpiredCredentials = view::showExpiredCredentialsDialog
onDecryptionFailed = view::showDecryptionFailedDialog
onNoCurrentStudent = view::openClearLoginView onNoCurrentStudent = view::openClearLoginView
onPasswordChangeRequired = view::showChangePasswordSnackbar onPasswordChangeRequired = view::showChangePasswordSnackbar
onAuthorizationRequired = view::showAuthDialog onAuthorizationRequired = view::showAuthDialog
} }
} }
fun onExpiredLoginSelected() { fun onConfirmDecryptionFailedSelected() {
Timber.i("Attempt to switch the student after the session expires") 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 { presenterScope.launch {
runCatching { runCatching {
val student = studentRepository.getCurrentStudent(false) val student = studentRepository.getCurrentStudent(false)
studentRepository.logoutStudent(student) studentRepository.deleteStudentsAssociatedWithAccount(student)
val students = studentRepository.getSavedStudents(false) val students = studentRepository.getSavedStudents(false)
if (students.isNotEmpty()) { if (students.isNotEmpty()) {
@ -50,11 +67,11 @@ open class BasePresenter<T : BaseView>(
} }
} }
.onFailure { .onFailure {
Timber.i("Switch student result: An exception occurred") Timber.i("Delete students result: An exception occurred")
errorHandler.dispatch(it) errorHandler.dispatch(it)
} }
.onSuccess { .onSuccess {
Timber.i("Switch student result: Open login view") Timber.i("Delete students result: Open login view")
view?.openClearLoginView() view?.openClearLoginView()
} }
} }

View File

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

View File

@ -15,7 +15,9 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
var showErrorMessage: (String, Throwable) -> Unit = { _, _ -> } var showErrorMessage: (String, Throwable) -> Unit = { _, _ -> }
var onSessionExpired: () -> Unit = {} var onExpiredCredentials: () -> Unit = {}
var onDecryptionFailed: () -> Unit = {}
var onNoCurrentStudent: () -> Unit = {} var onNoCurrentStudent: () -> Unit = {}
@ -32,7 +34,8 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
showErrorMessage(context.resources.getErrorString(error), error) showErrorMessage(context.resources.getErrorString(error), error)
when (error) { when (error) {
is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl) is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl)
is ScramblerException, is BadCredentialsException -> onSessionExpired() is ScramblerException -> onDecryptionFailed()
is BadCredentialsException -> onExpiredCredentials()
is NoCurrentStudentException -> onNoCurrentStudent() is NoCurrentStudentException -> onNoCurrentStudent()
is AuthorizationRequiredException -> onAuthorizationRequired() is AuthorizationRequiredException -> onAuthorizationRequired()
} }
@ -40,7 +43,8 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
open fun clear() { open fun clear() {
showErrorMessage = { _, _ -> } showErrorMessage = { _, _ -> }
onSessionExpired = {} onExpiredCredentials = {}
onDecryptionFailed = {}
onNoCurrentStudent = {} onNoCurrentStudent = {}
onPasswordChangeRequired = {} onPasswordChangeRequired = {}
onAuthorizationRequired = {} onAuthorizationRequired = {}

View File

@ -30,7 +30,12 @@ import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.notificationscenter.NotificationsCenterFragment import io.github.wulkanowy.ui.modules.notificationscenter.NotificationsCenterFragment
import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment 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 java.time.LocalDate import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject

View File

@ -24,7 +24,9 @@ class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView, Settin
override fun showMessage(text: String) {} override fun showMessage(text: String) {}
override fun showExpiredDialog() {} override fun showExpiredCredentialsDialog() {}
override fun showDecryptionFailedDialog() {}
override fun openClearLoginView() {} override fun openClearLoginView() {}

View File

@ -47,8 +47,12 @@ class AdvancedFragment : PreferenceFragmentCompat(),
(activity as? BaseActivity<*, *>)?.showMessage(text) (activity as? BaseActivity<*, *>)?.showMessage(text)
} }
override fun showExpiredDialog() { override fun showExpiredCredentialsDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredDialog() (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog()
}
override fun showDecryptionFailedDialog() {
(activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog()
} }
override fun showChangePasswordSnackbar(redirectUrl: String) { override fun showChangePasswordSnackbar(redirectUrl: String) {

View File

@ -63,8 +63,12 @@ class AppearanceFragment : PreferenceFragmentCompat(),
(activity as? BaseActivity<*, *>)?.showMessage(text) (activity as? BaseActivity<*, *>)?.showMessage(text)
} }
override fun showExpiredDialog() { override fun showExpiredCredentialsDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredDialog() (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog()
}
override fun showDecryptionFailedDialog() {
(activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog()
} }
override fun showChangePasswordSnackbar(redirectUrl: String) { override fun showChangePasswordSnackbar(redirectUrl: String) {

View File

@ -133,8 +133,12 @@ class NotificationsFragment : PreferenceFragmentCompat(),
(activity as? BaseActivity<*, *>)?.showMessage(text) (activity as? BaseActivity<*, *>)?.showMessage(text)
} }
override fun showExpiredDialog() { override fun showExpiredCredentialsDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredDialog() (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog()
}
override fun showDecryptionFailedDialog() {
(activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog()
} }
override fun showChangePasswordSnackbar(redirectUrl: String) { override fun showChangePasswordSnackbar(redirectUrl: String) {

View File

@ -84,8 +84,12 @@ class SyncFragment : PreferenceFragmentCompat(),
} }
} }
override fun showExpiredDialog() { override fun showExpiredCredentialsDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredDialog() (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog()
}
override fun showDecryptionFailedDialog() {
(activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog()
} }
override fun showChangePasswordSnackbar(redirectUrl: String) { override fun showChangePasswordSnackbar(redirectUrl: String) {

View File

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

View File

@ -16,6 +16,7 @@ import android.util.Base64.DEFAULT
import android.util.Base64.decode import android.util.Base64.decode
import android.util.Base64.encode import android.util.Base64.encode
import android.util.Base64.encodeToString import android.util.Base64.encodeToString
import dagger.hilt.android.qualifiers.ApplicationContext
import timber.log.Timber import timber.log.Timber
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
@ -33,21 +34,23 @@ import javax.crypto.CipherInputStream
import javax.crypto.CipherOutputStream import javax.crypto.CipherOutputStream
import javax.crypto.spec.OAEPParameterSpec import javax.crypto.spec.OAEPParameterSpec
import javax.crypto.spec.PSource.PSpecified import javax.crypto.spec.PSource.PSpecified
import javax.inject.Inject
import javax.inject.Singleton
import javax.security.auth.x500.X500Principal 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
private val KEY_CHARSET = Charset.forName("UTF-8")
private val isKeyPairExists: Boolean
get() = keyStore.getKey(KEY_ALIAS, null) != null get() = keyStore.getKey(KEY_ALIAS, null) != null
private val keyStore: KeyStore private val keyStore: KeyStore
get() = KeyStore.getInstance(KEYSTORE_NAME).apply { load(null) } get() = KeyStore.getInstance(KEYSTORE_NAME).apply { load(null) }
private val cipher: Cipher private val cipher: Cipher
get() { get() {
return if (SDK_INT >= M) Cipher.getInstance( return if (SDK_INT >= M) Cipher.getInstance(
"RSA/ECB/OAEPWithSHA-256AndMGF1Padding", "RSA/ECB/OAEPWithSHA-256AndMGF1Padding",
@ -56,11 +59,11 @@ private val cipher: Cipher
else Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL") else Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL")
} }
fun encrypt(plainText: String, context: Context): String { fun encrypt(plainText: String): String {
if (plainText.isEmpty()) throw ScramblerException("Text to be encrypted is empty") if (plainText.isEmpty()) throw ScramblerException("Text to be encrypted is empty")
return try { return try {
if (!isKeyPairExists) generateKeyPair(context) if (!isKeyPairExists) generateKeyPair()
cipher.let { cipher.let {
if (SDK_INT >= M) { if (SDK_INT >= M) {
@ -71,7 +74,7 @@ fun encrypt(plainText: String, context: Context): String {
ByteArrayOutputStream().let { output -> ByteArrayOutputStream().let { output ->
CipherOutputStream(output, it).apply { CipherOutputStream(output, it).apply {
write(plainText.toByteArray(KEY_CHARSET)) write(plainText.toByteArray(keyCharset))
close() close()
} }
encodeToString(output.toByteArray(), DEFAULT) encodeToString(output.toByteArray(), DEFAULT)
@ -79,11 +82,11 @@ fun encrypt(plainText: String, context: Context): String {
} }
} catch (exception: Exception) { } catch (exception: Exception) {
Timber.e(exception, "An error occurred while encrypting text") Timber.e(exception, "An error occurred while encrypting text")
String(encode(plainText.toByteArray(KEY_CHARSET), DEFAULT), KEY_CHARSET) String(encode(plainText.toByteArray(keyCharset), DEFAULT), keyCharset)
}
} }
}
fun decrypt(cipherText: String): String { fun decrypt(cipherText: String): String {
if (cipherText.isEmpty()) throw ScramblerException("Text to be encrypted is empty") if (cipherText.isEmpty()) throw ScramblerException("Text to be encrypted is empty")
return try { return try {
@ -96,7 +99,10 @@ fun decrypt(cipherText: String): String {
} }
} else it.init(DECRYPT_MODE, keyStore.getKey(KEY_ALIAS, null)) } else it.init(DECRYPT_MODE, keyStore.getKey(KEY_ALIAS, null))
CipherInputStream(ByteArrayInputStream(decode(cipherText, DEFAULT)), it).let { input -> CipherInputStream(
ByteArrayInputStream(decode(cipherText, DEFAULT)),
it
).let { input ->
val values = ArrayList<Byte>() val values = ArrayList<Byte>()
var nextByte: Int var nextByte: Int
while (run { nextByte = input.read(); nextByte } != -1) { while (run { nextByte = input.read(); nextByte } != -1) {
@ -106,15 +112,15 @@ fun decrypt(cipherText: String): String {
for (i in bytes.indices) { for (i in bytes.indices) {
bytes[i] = values[i] bytes[i] = values[i]
} }
String(bytes, 0, bytes.size, KEY_CHARSET) String(bytes, 0, bytes.size, keyCharset)
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
throw ScramblerException("An error occurred while decrypting text", e) throw ScramblerException("An error occurred while decrypting text", e)
} }
} }
private fun generateKeyPair(context: Context) { private fun generateKeyPair() {
(if (SDK_INT >= M) { (if (SDK_INT >= M) {
KeyGenParameterSpec.Builder(KEY_ALIAS, PURPOSE_DECRYPT or PURPOSE_ENCRYPT) KeyGenParameterSpec.Builder(KEY_ALIAS, PURPOSE_DECRYPT or PURPOSE_ENCRYPT)
.setDigests(DIGEST_SHA256, DIGEST_SHA512) .setDigests(DIGEST_SHA256, DIGEST_SHA512)
@ -137,4 +143,15 @@ private fun generateKeyPair(context: Context) {
} }
} }
Timber.i("A new KeyPair has been generated") 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"
}
} }

View File

@ -1,5 +1,5 @@
Wersja 2.3.1 Wersja 2.3.2
— poprawiliśmy kilka usterek przy odświeżaniu danych (ale pewnie nie wszystkie) — poprawiliśmy kolejne usterki przy odświeżaniu danych (teraz to powinno działać już dużo lepiej)
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases

View File

@ -96,6 +96,8 @@
<string name="main_log_in">Přihlásit se</string> <string name="main_log_in">Přihlásit se</string>
<string name="main_session_expired">Relace vypršela</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_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_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_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> <string name="main_support_positive">Zapnout reklamy</string>
@ -760,7 +762,7 @@
<string name="pref_ads_support_category_name">Podpora</string> <string name="pref_ads_support_category_name">Podpora</string>
<string name="pref_ads_privacy_policy">Ochrana osobních údajů</string> <string name="pref_ads_privacy_policy">Ochrana osobních údajů</string>
<string name="pref_ads_agreements">Souhlasy</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_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_support">Podívejte se na jednu reklamu pro podporu projektu</string>
<string name="pref_ads_privacy_title">Souhlas se zpracováním dat</string> <string name="pref_ads_privacy_title">Souhlas se zpracováním dat</string>

View File

@ -96,6 +96,8 @@
<string name="main_log_in">Log in</string> <string name="main_log_in">Log in</string>
<string name="main_session_expired">Session expired</string> <string name="main_session_expired">Session expired</string>
<string name="main_session_relogin">Session expired, log in again</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_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_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> <string name="main_support_positive">Enable ads</string>

View File

@ -96,6 +96,8 @@
<string name="main_log_in">Anmelden</string> <string name="main_log_in">Anmelden</string>
<string name="main_session_expired">Die Sitzung ist abgelaufen</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_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_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_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> <string name="main_support_positive">Werbung aktivieren</string>

View File

@ -96,6 +96,8 @@
<string name="main_log_in">Log in</string> <string name="main_log_in">Log in</string>
<string name="main_session_expired">Session expired</string> <string name="main_session_expired">Session expired</string>
<string name="main_session_relogin">Session expired, log in again</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_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_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> <string name="main_support_positive">Enable ads</string>

View File

@ -96,6 +96,8 @@
<string name="main_log_in">Log in</string> <string name="main_log_in">Log in</string>
<string name="main_session_expired">Session expired</string> <string name="main_session_expired">Session expired</string>
<string name="main_session_relogin">Session expired, log in again</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_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_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> <string name="main_support_positive">Enable ads</string>

View File

@ -96,6 +96,8 @@
<string name="main_log_in">Zaloguj się</string> <string name="main_log_in">Zaloguj się</string>
<string name="main_session_expired">Sesja wygasła</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_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_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_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> <string name="main_support_positive">Włącz reklamy</string>

View File

@ -96,6 +96,8 @@
<string name="main_log_in">Войти</string> <string name="main_log_in">Войти</string>
<string name="main_session_expired">Сеанс истёк</string> <string name="main_session_expired">Сеанс истёк</string>
<string name="main_session_relogin">Сеанс истёк, авторизуйтесь снова</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_title">Поддержка приложения</string>
<string name="main_support_description">Вам нравится это приложение? Поддержите его разработку, включив неинвазивную рекламу, которую можно отключить в любое время</string> <string name="main_support_description">Вам нравится это приложение? Поддержите его разработку, включив неинвазивную рекламу, которую можно отключить в любое время</string>
<string name="main_support_positive">Включить рекламу</string> <string name="main_support_positive">Включить рекламу</string>

View File

@ -96,6 +96,8 @@
<string name="main_log_in">Prihlásiť sa</string> <string name="main_log_in">Prihlásiť sa</string>
<string name="main_session_expired">Relácia vypršala</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_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_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_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> <string name="main_support_positive">Zapnúť reklamy</string>
@ -760,7 +762,7 @@
<string name="pref_ads_support_category_name">Podpora</string> <string name="pref_ads_support_category_name">Podpora</string>
<string name="pref_ads_privacy_policy">Ochrana osobných údajov</string> <string name="pref_ads_privacy_policy">Ochrana osobných údajov</string>
<string name="pref_ads_agreements">Súhlasy</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_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_support">Pozrite sa na jednu reklamu pre podporu projektu</string>
<string name="pref_ads_privacy_title">Súhlas so spracovaním dát</string> <string name="pref_ads_privacy_title">Súhlas so spracovaním dát</string>

View File

@ -96,6 +96,8 @@
<string name="main_log_in">Увійти</string> <string name="main_log_in">Увійти</string>
<string name="main_session_expired">Минув термін дії сесії</string> <string name="main_session_expired">Минув термін дії сесії</string>
<string name="main_session_relogin">Минув термін дії сесії, авторизуйтеся знову</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_title">Підтримка додатку</string>
<string name="main_support_description">Вам подобається цей додаток? Підтримайте його розвиток, увімкнувши неінвазивну рекламу, яку ви можете відключити в будь-який час</string> <string name="main_support_description">Вам подобається цей додаток? Підтримайте його розвиток, увімкнувши неінвазивну рекламу, яку ви можете відключити в будь-який час</string>
<string name="main_support_positive">Увімкнути рекламу</string> <string name="main_support_positive">Увімкнути рекламу</string>

View File

@ -107,6 +107,8 @@
<string name="main_log_in">Log in</string> <string name="main_log_in">Log in</string>
<string name="main_session_expired">Session expired</string> <string name="main_session_expired">Session expired</string>
<string name="main_session_relogin">Session expired, log in again</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_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_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> <string name="main_support_positive">Enable ads</string>

View File

@ -101,8 +101,12 @@ class AdsFragment : PreferenceFragmentCompat(), MainView.TitledView, AdsView {
(activity as? BaseActivity<*, *>)?.showMessage(text) (activity as? BaseActivity<*, *>)?.showMessage(text)
} }
override fun showExpiredDialog() { override fun showExpiredCredentialsDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredDialog() (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog()
}
override fun showDecryptionFailedDialog() {
(activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog()
} }
override fun showChangePasswordSnackbar(redirectUrl: String) { 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-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$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.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.dagger:hilt-android-gradle-plugin:$hilt_version"
classpath 'com.google.gms:google-services:4.4.0' classpath 'com.google.gms:google-services:4.4.0'
classpath 'com.huawei.agconnect:agcp:1.9.1.303' classpath 'com.huawei.agconnect:agcp:1.9.1.303'

Binary file not shown.

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

17
gradlew vendored
View File

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