1
0

Compare commits

...

59 Commits
2.2.6 ... 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
eb31f9578f Merge branch 'release/2.3.0' 2024-01-02 01:51:52 +01:00
f69d50d2c1 Version 2.3.0 2024-01-02 01:51:09 +01:00
8a424ee6a4 New Crowdin updates (#2381) 2024-01-02 01:30:31 +01:00
7dfa48bbe3 Add User Messaging Platform SDK for ads agreements (#2375) 2024-01-01 21:19:00 +01:00
d811cdb919 Bump com.huawei.agconnect:agcp from 1.9.1.302 to 1.9.1.303 (#2377) 2024-01-01 17:39:08 +00:00
e2f2e21081 Bump com.huawei.agconnect:agconnect-crash from 1.9.1.302 to 1.9.1.303 (#2379) 2024-01-01 17:38:29 +00:00
c812310497 Bump about_libraries from 10.9.2 to 10.10.0 (#2380) 2024-01-01 18:36:02 +01:00
7f6475cf35 Merge branch 'release/2.2.7' into develop 2023-12-27 22:21:18 +01:00
a2a7d2ebb2 Merge branch 'release/2.2.7' 2023-12-27 22:21:13 +01:00
a5bc45c5da Version 2.2.7 2023-12-27 22:21:07 +01:00
5646befbd7 Revert "Bump com.google.android.material:material from 1.10.0 to 1.11.0 (#2368)" (#2376)
This reverts commit 7f4539fd27.
2023-12-27 21:52:04 +01:00
75f496b5d2 Bump kotlin_version from 1.9.21 to 1.9.22 (#2371) 2023-12-27 16:11:58 +00:00
23d989d22a Bump hilt_version from 2.49 to 2.50 (#2372) 2023-12-24 07:01:40 +00:00
9e013f7cd9 Bump ru.cian:huawei-publish-gradle-plugin from 1.4.0 to 1.4.2 (#2370) 2023-12-24 07:00:42 +00:00
c63a7c03f1 Bump com.huawei.agconnect:agcp from 1.9.1.301 to 1.9.1.302 (#2373) 2023-12-24 06:54:28 +00:00
5ceee84f0e Bump com.huawei.agconnect:agconnect-crash from 1.9.1.301 to 1.9.1.302 (#2374) 2023-12-24 06:54:09 +00:00
7f4539fd27 Bump com.google.android.material:material from 1.10.0 to 1.11.0 (#2368) 2023-12-22 13:48:13 +00:00
71ebf1260b Bump com.android.tools.build:gradle from 8.1.4 to 8.2.0 (#2361) 2023-12-15 19:44:32 +00:00
784ee58384 Bump work_manager from 2.8.1 to 2.9.0 (#2363) 2023-12-15 16:21:08 +00:00
eceef3f582 Bump com.google.firebase:firebase-bom from 32.6.0 to 32.7.0 (#2366) 2023-12-15 15:52:54 +00:00
0d950fbd86 Bump com.google.android.gms:play-services-ads from 22.5.0 to 22.6.0 (#2367) 2023-12-15 15:52:35 +00:00
003d63b516 Bump room from 2.6.0 to 2.6.1 (#2360) 2023-12-07 22:44:50 +00:00
b4c0440a8e Bump hilt_version from 2.48.1 to 2.49 (#2362) 2023-12-07 22:44:37 +00:00
137c305295 Bump org.jetbrains.kotlinx:kotlinx-serialization-json (#2365) 2023-12-07 22:44:23 +00:00
2c40c221c3 Bump kotlin_version from 1.9.10 to 1.9.21 (#2357) 2023-11-28 22:32:32 +00:00
e82ac78d4a Bump androidx.fragment:fragment-ktx from 1.6.1 to 1.6.2 (#2348) 2023-11-28 22:32:18 +00:00
59d46ce956 Bump com.google.android.gms:play-services-ads from 22.4.0 to 22.5.0 (#2346) 2023-11-28 22:21:19 +00:00
17caa8ecbd Bump com.android.tools:desugar_jdk_libs from 2.0.3 to 2.0.4 (#2345) 2023-11-28 22:21:00 +00:00
e9540b4012 Bump androidx.activity:activity-ktx from 1.8.0 to 1.8.1 (#2354) 2023-11-28 22:20:41 +00:00
643ad60455 Bump com.google.firebase:firebase-bom from 32.5.0 to 32.6.0 (#2355) 2023-11-28 22:20:18 +00:00
01f892ce5c Bump com.android.tools.build:gradle from 8.1.2 to 8.1.4 (#2356) 2023-11-28 22:19:53 +00:00
f61b6a5e78 Bump com.google.android.play:integrity from 1.2.0 to 1.3.0 (#2351) 2023-11-28 22:10:32 +00:00
650cf7484e Bump android_hilt from 1.0.0 to 1.1.0 (#2343) 2023-11-28 22:10:11 +00:00
037cbb0b19 Bump about_libraries from 10.9.1 to 10.9.2 (#2344)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2023-11-28 23:08:13 +01:00
e49835e89e Merge branch 'release/2.2.6' into develop 2023-11-06 11:22:25 +01:00
65 changed files with 959 additions and 583 deletions

10
.idea/migrations.xml generated Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

View File

@ -27,8 +27,8 @@ android {
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21
targetSdkVersion 34
versionCode 138
versionName "2.2.6"
versionCode 144
versionName "2.3.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "app_name", "Wulkanowy"
@ -113,6 +113,7 @@ android {
buildFeatures {
viewBinding true
buildConfig true
}
bundle {
@ -162,7 +163,7 @@ play {
track = 'production'
releaseStatus = ReleaseStatus.IN_PROGRESS
userFraction = 0.99d
updatePriority = 5
updatePriority = 1
enabled.set(false)
}
@ -183,28 +184,28 @@ huaweiPublish {
}
ext {
work_manager = "2.8.1"
android_hilt = "1.0.0"
room = "2.6.0"
chucker = "3.5.2"
mockk = "1.13.8"
work_manager = "2.9.0"
android_hilt = "1.1.0"
room = "2.6.1"
chucker = "4.0.0"
mockk = "1.13.9"
coroutines = "1.7.3"
}
dependencies {
implementation 'io.github.wulkanowy:sdk:2.2.6'
implementation 'io.github.wulkanowy:sdk:2.3.6'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.core:core-splashscreen:1.0.1'
implementation "androidx.activity:activity-ktx:1.8.0"
implementation "androidx.activity:activity-ktx:1.8.2"
implementation "androidx.appcompat:appcompat:1.6.1"
implementation "androidx.fragment:fragment-ktx:1.6.1"
implementation "androidx.annotation:annotation:1.7.0"
implementation "androidx.fragment:fragment-ktx:1.6.2"
implementation "androidx.annotation:annotation:1.7.1"
implementation "androidx.preference:preference-ktx:1.2.1"
implementation "androidx.recyclerview:recyclerview:1.3.2"
@ -217,7 +218,7 @@ dependencies {
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0"
implementation 'com.github.lopspower:CircularImageView:4.3.0'
implementation "androidx.work:work-runtime-ktx:$work_manager"
implementation "androidx.work:work-runtime:$work_manager"
playImplementation "androidx.work:work-gcm:$work_manager"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.2"
@ -237,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'
@ -248,22 +250,24 @@ dependencies {
implementation 'com.fredporciuncula:flow-preferences:1.9.1'
implementation 'org.apache.commons:commons-text:1.11.0'
playImplementation platform('com.google.firebase:firebase-bom:32.5.0')
playImplementation 'com.google.firebase:firebase-analytics-ktx'
playImplementation 'com.google.firebase:firebase-messaging:'
playImplementation platform('com.google.firebase:firebase-bom:32.7.0')
playImplementation 'com.google.firebase:firebase-analytics'
playImplementation 'com.google.firebase:firebase-messaging'
playImplementation 'com.google.firebase:firebase-crashlytics:'
playImplementation 'com.google.firebase:firebase-config-ktx'
playImplementation 'com.google.android.gms:play-services-ads:22.4.0'
playImplementation "com.google.android.play:integrity:1.2.0"
playImplementation 'com.google.firebase:firebase-config'
playImplementation 'com.google.android.gms:play-services-ads:22.6.0'
playImplementation "com.google.android.play:integrity:1.3.0"
playImplementation 'com.google.android.play:app-update-ktx:2.1.0'
playImplementation 'com.google.android.play:review-ktx:2.0.1'
playImplementation "com.google.android.ump:user-messaging-platform:2.1.0"
hmsImplementation 'com.huawei.hms:hianalytics:6.12.0.300'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.1.301'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.1.303'
releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker"
releaseImplementation "com.github.chuckerteam.chucker:library-no-op:$chucker"
debugImplementation "com.github.ChuckerTeam.Chucker:library:$chucker"
debugImplementation "com.github.chuckerteam.chucker:library:$chucker"
debugImplementation 'com.github.amitshekhariitbhu.Android-Debug-Database:debug-db:1.0.6'
debugImplementation 'com.github.haroldadmin:WhatTheStack:1.0.0-alpha04'

View File

@ -1,7 +1,7 @@
apply plugin: "jacoco"
jacoco {
toolVersion "0.8.10"
toolVersion "0.8.11"
reportsDirectory.set(file("$buildDir/reports"))
}

View File

@ -5,6 +5,7 @@ import android.view.View
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Inject
@Suppress("unused")
@ -13,9 +14,11 @@ class AdsHelper @Inject constructor(
private val preferencesRepository: PreferencesRepository
) {
val isMobileAdsSdkInitialized = MutableStateFlow(false)
val canShowAd = false
fun initialize() {
preferencesRepository.isAdsEnabled = false
preferencesRepository.isAgreeToProcessData = false
preferencesRepository.selectedDashboardTiles -= DashboardItem.Tile.ADS
}

View File

@ -5,6 +5,7 @@ import android.view.View
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Inject
@Suppress("unused")
@ -12,10 +13,11 @@ class AdsHelper @Inject constructor(
@ApplicationContext private val context: Context,
private val preferencesRepository: PreferencesRepository
) {
val isMobileAdsSdkInitialized = MutableStateFlow(false)
val canShowAd = false
fun initialize() {
preferencesRepository.isAdsEnabled = false
preferencesRepository.isAgreeToProcessData = false
preferencesRepository.selectedDashboardTiles -= DashboardItem.Tile.ADS
}

View File

@ -1,7 +1,9 @@
package io.github.wulkanowy
import android.app.Application
import android.util.Log.*
import android.util.Log.DEBUG
import android.util.Log.INFO
import android.util.Log.VERBOSE
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import com.yariksoffice.lingver.Lingver
@ -9,16 +11,19 @@ import dagger.hilt.android.HiltAndroidApp
import fr.bipi.treessence.file.FileLoggerTree
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.ui.base.ThemeManager
import io.github.wulkanowy.utils.*
import io.github.wulkanowy.utils.ActivityLifecycleLogger
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.CrashLogExceptionTree
import io.github.wulkanowy.utils.CrashLogTree
import io.github.wulkanowy.utils.DebugLogTree
import io.github.wulkanowy.utils.RemoteConfigHelper
import timber.log.Timber
import javax.inject.Inject
@HiltAndroidApp
class WulkanowyApp : Application(), Configuration.Provider {
@Inject
lateinit var workerFactory: HiltWorkerFactory
@Inject
lateinit var themeManager: ThemeManager
@ -32,16 +37,21 @@ class WulkanowyApp : Application(), Configuration.Provider {
lateinit var analyticsHelper: AnalyticsHelper
@Inject
lateinit var adsHelper: AdsHelper
lateinit var remoteConfigHelper: RemoteConfigHelper
@Inject
lateinit var remoteConfigHelper: RemoteConfigHelper
lateinit var workerFactory: HiltWorkerFactory
override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
.setMinimumLoggingLevel(if (appInfo.isDebug) VERBOSE else INFO)
.build()
override fun onCreate() {
super.onCreate()
initializeAppLanguage()
themeManager.applyDefaultTheme()
adsHelper.initialize()
remoteConfigHelper.initialize()
initLogging()
}
@ -74,9 +84,4 @@ class WulkanowyApp : Application(), Configuration.Provider {
analyticsHelper.logEvent("language", "startup" to preferencesRepository.appLanguage)
}
}
override fun getWorkManagerConfiguration() = Configuration.Builder()
.setWorkerFactory(workerFactory)
.setMinimumLoggingLevel(if (appInfo.isDebug) VERBOSE else INFO)
.build()
}

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

@ -9,7 +9,12 @@ import com.fredporciuncula.flow.preferences.Preference
import com.fredporciuncula.flow.preferences.Serializer
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.enums.*
import io.github.wulkanowy.data.enums.AppTheme
import io.github.wulkanowy.data.enums.GradeColorTheme
import io.github.wulkanowy.data.enums.GradeExpandMode
import io.github.wulkanowy.data.enums.GradeSortingMode
import io.github.wulkanowy.data.enums.TimetableGapsMode
import io.github.wulkanowy.data.enums.TimetableMode
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode
import io.github.wulkanowy.ui.modules.settings.appearance.menuorder.AppMenuItem
@ -18,7 +23,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.time.Instant
import java.util.*
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
@ -303,19 +308,6 @@ class PreferencesRepository @Inject constructor(
get() = sharedPref.getBoolean(PREF_KEY_APP_SUPPORT_SHOWN, false)
set(value) = sharedPref.edit { putBoolean(PREF_KEY_APP_SUPPORT_SHOWN, value) }
var isAgreeToProcessData: Boolean
get() = getBoolean(
R.string.pref_key_ads_consent_data_processing,
R.bool.pref_default_ads_consent_data_processing
)
set(value) = sharedPref.edit {
putBoolean(context.getString(R.string.pref_key_ads_consent_data_processing), value)
}
var isPersonalizedAdsEnabled: Boolean
get() = sharedPref.getBoolean(PREF_KEY_PERSONALIZED_ADS_ENABLED, false)
set(value) = sharedPref.edit { putBoolean(PREF_KEY_PERSONALIZED_ADS_ENABLED, value) }
val isAdsEnabledFlow = flowSharedPref.getBoolean(
context.getString(R.string.pref_key_ads_enabled),
context.resources.getBoolean(R.bool.pref_default_ads_enabled)
@ -398,7 +390,6 @@ class PreferencesRepository @Inject constructor(
private const val PREF_KEY_IN_APP_REVIEW_DATE = "in_app_review_date"
private const val PREF_KEY_IN_APP_REVIEW_DONE = "in_app_review_done"
private const val PREF_KEY_APP_SUPPORT_SHOWN = "app_support_shown"
private const val PREF_KEY_PERSONALIZED_ADS_ENABLED = "personalized_ads_enabled"
private const val PREF_KEY_ADMIN_DISMISSED_MESSAGE_IDS = "admin_message_dismissed_ids"
}
}

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

@ -1,19 +1,46 @@
package io.github.wulkanowy.ui.modules.dashboard
import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.dataOrNull
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.data.repositories.*
import io.github.wulkanowy.data.errorOrNull
import io.github.wulkanowy.data.flatResourceFlow
import io.github.wulkanowy.data.mapResourceData
import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository
import io.github.wulkanowy.data.repositories.ConferenceRepository
import io.github.wulkanowy.data.repositories.ExamRepository
import io.github.wulkanowy.data.repositories.GradeRepository
import io.github.wulkanowy.data.repositories.HomeworkRepository
import io.github.wulkanowy.data.repositories.LuckyNumberRepository
import io.github.wulkanowy.data.repositories.MessageRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SchoolAnnouncementRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AdsHelper
import io.github.wulkanowy.utils.calculatePercentage
import io.github.wulkanowy.utils.nextOrSameSchoolDay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
import java.time.Instant
@ -48,6 +75,11 @@ class DashboardPresenter @Inject constructor(
private val firstLoadedItemList = mutableListOf<DashboardItem.Type>()
private val selectedDashboardTiles
get() = preferencesRepository.selectedDashboardTiles
.filterNot { it == DashboardItem.Tile.ADS && !adsHelper.canShowAd }
.toSet()
private lateinit var lastError: Throwable
override fun onAttachView(view: DashboardView) {
@ -59,10 +91,19 @@ class DashboardPresenter @Inject constructor(
showContent(false)
}
val selectedDashboardTilesFlow = preferencesRepository.selectedDashboardTilesFlow
.map { selectedDashboardTiles }
val isAdsEnabledFlow = preferencesRepository.isAdsEnabledFlow
.filter { (adsHelper.canShowAd && it) || !it }
.map { selectedDashboardTiles }
val isMobileAdsSdkInitializedFlow = adsHelper.isMobileAdsSdkInitialized
.filter { it }
.map { selectedDashboardTiles }
merge(
preferencesRepository.selectedDashboardTilesFlow,
preferencesRepository.isAdsEnabledFlow
.map { preferencesRepository.selectedDashboardTiles }
selectedDashboardTilesFlow,
isAdsEnabledFlow,
isMobileAdsSdkInitializedFlow
)
.onEach { loadData(tilesToLoad = it) }
.launch("dashboard_pref")
@ -71,7 +112,7 @@ class DashboardPresenter @Inject constructor(
fun onAdminMessageDismissed(adminMessage: AdminMessage) {
preferencesRepository.dismissedAdminMessageIds += adminMessage.id
loadData(preferencesRepository.selectedDashboardTiles)
loadData(selectedDashboardTiles)
}
fun onDragAndDropEnd(list: List<DashboardItem>) {
@ -187,7 +228,7 @@ class DashboardPresenter @Inject constructor(
fun onSwipeRefresh() {
Timber.i("Force refreshing the dashboard")
loadData(preferencesRepository.selectedDashboardTiles, forceRefresh = true)
loadData(selectedDashboardTiles, forceRefresh = true)
}
fun onRetry() {
@ -195,7 +236,15 @@ class DashboardPresenter @Inject constructor(
showErrorView(false)
showProgress(true)
}
loadData(preferencesRepository.selectedDashboardTiles, forceRefresh = true)
loadData(selectedDashboardTiles, forceRefresh = true)
}
fun onRetryAfterCaptcha() {
view?.run {
showErrorView(false)
showProgress(true)
}
loadData(selectedDashboardTiles, forceRefresh = true)
}
fun onViewReselected() {
@ -216,7 +265,7 @@ class DashboardPresenter @Inject constructor(
}
fun onDashboardTileSettingsSelected(): Boolean {
view?.showDashboardTileSettings(preferencesRepository.selectedDashboardTiles.toList())
view?.showDashboardTileSettings(selectedDashboardTiles.toList())
return true
}
@ -232,7 +281,7 @@ class DashboardPresenter @Inject constructor(
private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) {
flow {
val selectedTiles = preferencesRepository.selectedDashboardTiles
val selectedTiles = selectedDashboardTiles
val flowSuccess = flowOf(Resource.Success(null))
val luckyNumberFlow = luckyNumberRepository.getLuckyNumber(student, forceRefresh)
@ -275,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 }) {
@ -300,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) {
@ -320,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) {
@ -813,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

@ -9,9 +9,14 @@ import android.view.MenuItem
import android.view.ViewGroup.MarginLayoutParams
import androidx.activity.OnBackPressedCallback
import androidx.activity.addCallback
import androidx.core.view.*
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
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
@ -23,16 +28,32 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.databinding.ActivityMainBinding
import io.github.wulkanowy.databinding.DialogAdsConsentBinding
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.*
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.InAppReviewHelper
import io.github.wulkanowy.utils.InAppUpdateHelper
import io.github.wulkanowy.utils.createNameInitialsDrawable
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,
@ -62,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"
@ -133,6 +156,7 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
initializeToolbar()
initializeBottomNavigation(startMenuIndex, rootAppMenuItems)
initializeNavController(startMenuIndex, rootUpdatedDestinations)
initializeCaptchaVerificationEvent()
}
private fun initializeNavController(
@ -312,38 +336,25 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
.show()
}
override fun showPrivacyPolicyDialog() {
val dialogAdsConsentBinding = DialogAdsConsentBinding.inflate(layoutInflater)
val dialog = MaterialAlertDialogBuilder(this)
.setTitle(R.string.pref_ads_consent_title)
.setMessage(R.string.pref_ads_consent_description)
.setView(dialogAdsConsentBinding.root)
.show()
dialogAdsConsentBinding.adsConsentOver.setOnCheckedChangeListener { _, isChecked ->
dialogAdsConsentBinding.adsConsentPersonalised.isEnabled = isChecked
}
dialogAdsConsentBinding.adsConsentPersonalised.setOnClickListener {
presenter.onPrivacyAgree(true)
dialog.dismiss()
}
dialogAdsConsentBinding.adsConsentNonPersonalised.setOnClickListener {
presenter.onPrivacyAgree(false)
dialog.dismiss()
}
dialogAdsConsentBinding.adsConsentPrivacy.setOnClickListener { presenter.onPrivacySelected() }
dialogAdsConsentBinding.adsConsentCancel.setOnClickListener { dialog.cancel() }
@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 openPrivacyPolicy() {
openInternetBrowser(
"https://wulkanowy.github.io/polityka-prywatnosci.html",
::showMessage
)
override fun onCaptchaVerificationRequired(url: String?) {
lifecycleScope.launch {
captchaVerificationEvent.emit(url)
}
}
override fun showAuthDialog() {
showDialogFragment(AuthDialog.newInstance())
}
override fun onSaveInstanceState(outState: Bundle) {

View File

@ -19,7 +19,6 @@ import io.github.wulkanowy.utils.AdsHelper
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.AppInfo
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import timber.log.Timber
import java.time.Duration
@ -52,6 +51,7 @@ class MainPresenter @Inject constructor(
destinationType in rootDestinationTypeList -> {
rootDestinationTypeList.indexOf(destinationType)
}
else -> 4
}
@ -110,6 +110,7 @@ class MainPresenter @Inject constructor(
is AccountView,
is StudentInfoView,
is AccountDetailsView -> false
else -> true
}
@ -148,20 +149,8 @@ class MainPresenter @Inject constructor(
}
fun onEnableAdsSelected() {
view?.showPrivacyPolicyDialog()
}
fun onPrivacyAgree(isPersonalizedAds: Boolean) {
preferencesRepository.isAgreeToProcessData = true
preferencesRepository.isPersonalizedAdsEnabled = isPersonalizedAds
adsHelper.initialize()
preferencesRepository.isAdsEnabled = true
}
fun onPrivacySelected() {
view?.openPrivacyPolicy()
adsHelper.initialize()
}
private fun checkInAppReview() {
@ -189,8 +178,8 @@ class MainPresenter @Inject constructor(
.getOrElse { return@launch }
if (Instant.now().minus(Duration.ofDays(28)).isAfter(student.registrationDate)) {
view?.showAppSupport()
preferencesRepository.isAppSupportShown = true
view?.showAppSupport()
}
}
}

View File

@ -46,10 +46,6 @@ interface MainView : BaseView {
fun showAppSupport()
fun showPrivacyPolicyDialog()
fun openPrivacyPolicy()
fun openMoreDestination(destination: Destination)
interface MainChildView {

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,5 +1,6 @@
Wersja 2.2.6
Wersja 2.3.4
naprawiliśmy logowanie do aplikacji (tym razem musi się udać) na odmianie standardowej i podobnych
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

@ -1,79 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/ads_consent_privacy"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_marginHorizontal="24dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:padding="0dp"
android:text="@string/pref_ads_privacy_policy"
android:textAllCaps="false"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/ads_consent_over"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="17dp"
android:layout_marginTop="8dp"
android:text="@string/pref_ads_over_18_years_old"
android:textColor="?android:textColorSecondary"
android:textSize="14sp"
app:layout_constraintTop_toBottomOf="@id/ads_consent_privacy" />
<com.google.android.material.button.MaterialButton
android:id="@+id/ads_consent_personalised"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginTop="24dp"
android:enabled="false"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:text="@string/pref_ads_option_personalized"
app:layout_constraintTop_toBottomOf="@id/ads_consent_over" />
<com.google.android.material.button.MaterialButton
android:id="@+id/ads_consent_non_personalised"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginTop="24dp"
android:layout_marginBottom="24dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:text="@string/pref_ads_option_non_personalized"
app:layout_constraintBottom_toTopOf="@id/ads_consent_cancel"
app:layout_constraintTop_toBottomOf="@id/ads_consent_personalised"
app:layout_constraintVertical_bias="0" />
<com.google.android.material.button.MaterialButton
android:id="@+id/ads_consent_cancel"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginTop="24dp"
android:layout_marginBottom="24dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:text="@android:string/cancel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0" />
</androidx.constraintlayout.widget.ConstraintLayout>

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">Souhlas se zpracováním údajů souvisejících s reklamami</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>
@ -769,13 +772,6 @@
<string name="pref_ads_privacy_link">Ochrana osobních údajů</string>
<string name="pref_ads_loading">Reklama se načítá</string>
<string name="pref_ads_once_per_visit">Děkujeme za vaši podporu, vraťte se později pro více reklam</string>
<string name="pref_ads_consent_title">Můžeme použít Vaše data k zobrazení reklam?</string>
<string name="pref_ads_consent_description">Volbu můžete kdykoliv změnit v nastavení aplikace. Můžeme použít Vaše data k zobrazení reklam šitých pro vás nebo pomocí méně vašich dat zobrazovat nepřizpůsobené reklamy. Podrobnosti naleznete v našich Zásadách ochrany osobních údajů</string>
<string name="pref_ads_summary_personalized">Přizpůsobené reklamy</string>
<string name="pref_ads_summary_non_personalized">Nepřizpůsobené reklamy</string>
<string name="pref_ads_over_18_years_old">Je mi více než 18 let</string>
<string name="pref_ads_option_personalized">Ano, přizpůsobené reklamy</string>
<string name="pref_ads_option_non_personalized">Ano, nepřizpůsobené reklamy</string>
<string name="pref_settings_advanced_title">Pokročilé</string>
<string name="pref_settings_appearance_title">Vzhled a chování</string>
<string name="pref_settings_notifications_title">Oznámení</string>
@ -838,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>
@ -847,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>
@ -670,7 +673,7 @@
<string name="pref_ads_support_category_name">Support</string>
<string name="pref_ads_privacy_policy">Privacy Policy</string>
<string name="pref_ads_agreements">Agreements</string>
<string name="pref_ads_consent">Consent to processing of data related to ads</string>
<string name="pref_ads_consent">Show consent to data processing</string>
<string name="pref_ads_show_in_app">Show ads in app</string>
<string name="pref_ads_support">Watch single ad to support project</string>
<string name="pref_ads_privacy_title">Consent to data processing</string>
@ -679,13 +682,6 @@
<string name="pref_ads_privacy_link">Privacy policy</string>
<string name="pref_ads_loading">Ad is loading</string>
<string name="pref_ads_once_per_visit">Thank you for your support, come back later for more ads</string>
<string name="pref_ads_consent_title">Can we use your data to display ads?</string>
<string name="pref_ads_consent_description">You can change your choice anytime in the app settings. We may use your data to display ads tailored to you or, using less of your data, display non-personalized ads. Please see our Privacy Policy for details</string>
<string name="pref_ads_summary_personalized">Personalized ads</string>
<string name="pref_ads_summary_non_personalized">Non-personalized ads</string>
<string name="pref_ads_over_18_years_old">I am over 18 years old</string>
<string name="pref_ads_option_personalized">Yes, personalized ads</string>
<string name="pref_ads_option_non_personalized">Yes, non-personalized ads</string>
<string name="pref_settings_advanced_title">Advanced</string>
<string name="pref_settings_appearance_title">Appearance &amp; Behavior</string>
<string name="pref_settings_notifications_title">Notifications</string>
@ -748,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>
@ -757,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>
@ -670,7 +673,7 @@
<string name="pref_ads_support_category_name">Unterstützung</string>
<string name="pref_ads_privacy_policy">Datenschutz-Bestimmungen</string>
<string name="pref_ads_agreements">Vereinbarungen</string>
<string name="pref_ads_consent">Zustimmung zur Verarbeitung von Daten im Zusammenhang mit Anzeigen</string>
<string name="pref_ads_consent">Show consent to data processing</string>
<string name="pref_ads_show_in_app">Anzeigen in der App anzeigen</string>
<string name="pref_ads_support">Einzelanzeige ansehen, um Projekt zu unterstützen</string>
<string name="pref_ads_privacy_title">Einwilligung in die Datenverarbeitung</string>
@ -679,13 +682,6 @@
<string name="pref_ads_privacy_link">Datenschutzerklärung</string>
<string name="pref_ads_loading">Anzeige wird geladen</string>
<string name="pref_ads_once_per_visit">Vielen Dank für Ihre Unterstützung, kommen Sie später wieder für weitere Anzeigen</string>
<string name="pref_ads_consent_title">Können wir Ihre Daten zur Anzeige von Werbung verwenden?</string>
<string name="pref_ads_consent_description">Sie können Ihre Wahl jederzeit in den App-Einstellungen ändern. Wir verwenden Ihre Daten, um auf Sie zugeschnittene Anzeigen anzuzeigen oder unter Verwendung weniger Ihrer Daten nicht personalisierte Werbung anzuzeigen. Bitte lesen Sie unsere Datenschutzerklärung für Details</string>
<string name="pref_ads_summary_personalized">Personalisierte Werbung</string>
<string name="pref_ads_summary_non_personalized">keine personalisierte Werbung</string>
<string name="pref_ads_over_18_years_old">Ich bin über 18 Jahre alt</string>
<string name="pref_ads_option_personalized">Ja, personalisierte Werbung</string>
<string name="pref_ads_option_non_personalized">Ja, nicht personalisierte Werbung</string>
<string name="pref_settings_advanced_title">Erweitert</string>
<string name="pref_settings_appearance_title">Aussehen &amp; Verhalten</string>
<string name="pref_settings_notifications_title">Benachrichtigungen</string>
@ -748,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>
@ -757,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>
@ -670,7 +673,7 @@
<string name="pref_ads_support_category_name">Support</string>
<string name="pref_ads_privacy_policy">Privacy Policy</string>
<string name="pref_ads_agreements">Agreements</string>
<string name="pref_ads_consent">Consent to processing of data related to ads</string>
<string name="pref_ads_consent">Show consent to data processing</string>
<string name="pref_ads_show_in_app">Show ads in app</string>
<string name="pref_ads_support">Watch single ad to support project</string>
<string name="pref_ads_privacy_title">Consent to data processing</string>
@ -679,13 +682,6 @@
<string name="pref_ads_privacy_link">Privacy policy</string>
<string name="pref_ads_loading">Ad is loading</string>
<string name="pref_ads_once_per_visit">Thank you for your support, come back later for more ads</string>
<string name="pref_ads_consent_title">Can we use your data to display ads?</string>
<string name="pref_ads_consent_description">You can change your choice anytime in the app settings. We may use your data to display ads tailored to you or, using less of your data, display non-personalized ads. Please see our Privacy Policy for details</string>
<string name="pref_ads_summary_personalized">Personalized ads</string>
<string name="pref_ads_summary_non_personalized">Non-personalized ads</string>
<string name="pref_ads_over_18_years_old">I am over 18 years old</string>
<string name="pref_ads_option_personalized">Yes, personalized ads</string>
<string name="pref_ads_option_non_personalized">Yes, non-personalized ads</string>
<string name="pref_settings_advanced_title">Advanced</string>
<string name="pref_settings_appearance_title">Appearance &amp; Behavior</string>
<string name="pref_settings_notifications_title">Notifications</string>
@ -748,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>
@ -757,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>
@ -670,7 +673,7 @@
<string name="pref_ads_support_category_name">Support</string>
<string name="pref_ads_privacy_policy">Privacy Policy</string>
<string name="pref_ads_agreements">Agreements</string>
<string name="pref_ads_consent">Consent to processing of data related to ads</string>
<string name="pref_ads_consent">Show consent to data processing</string>
<string name="pref_ads_show_in_app">Show ads in app</string>
<string name="pref_ads_support">Watch single ad to support project</string>
<string name="pref_ads_privacy_title">Consent to data processing</string>
@ -679,13 +682,6 @@
<string name="pref_ads_privacy_link">Privacy policy</string>
<string name="pref_ads_loading">Ad is loading</string>
<string name="pref_ads_once_per_visit">Thank you for your support, come back later for more ads</string>
<string name="pref_ads_consent_title">Can we use your data to display ads?</string>
<string name="pref_ads_consent_description">You can change your choice anytime in the app settings. We may use your data to display ads tailored to you or, using less of your data, display non-personalized ads. Please see our Privacy Policy for details</string>
<string name="pref_ads_summary_personalized">Personalized ads</string>
<string name="pref_ads_summary_non_personalized">Non-personalized ads</string>
<string name="pref_ads_over_18_years_old">I am over 18 years old</string>
<string name="pref_ads_option_personalized">Yes, personalized ads</string>
<string name="pref_ads_option_non_personalized">Yes, non-personalized ads</string>
<string name="pref_settings_advanced_title">Advanced</string>
<string name="pref_settings_appearance_title">Appearance &amp; Behavior</string>
<string name="pref_settings_notifications_title">Notifications</string>
@ -748,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>
@ -757,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>
@ -760,7 +763,7 @@
<string name="pref_ads_support_category_name">Wsparcie</string>
<string name="pref_ads_privacy_policy">Polityka prywatności</string>
<string name="pref_ads_agreements">Zgody</string>
<string name="pref_ads_consent">Zgoda na przetwarzanie danych związanych z reklamami</string>
<string name="pref_ads_consent">Pokaż zgodę na przetwarzanie danych</string>
<string name="pref_ads_show_in_app">Pokazuj reklamy w aplikacji</string>
<string name="pref_ads_support">Obejrzyj pojedynczą reklamę, aby wesprzeć projekt</string>
<string name="pref_ads_privacy_title">Zgoda na przetwarzanie danych</string>
@ -769,13 +772,6 @@
<string name="pref_ads_privacy_link">Polityka prywatności</string>
<string name="pref_ads_loading">Ładowanie reklamy</string>
<string name="pref_ads_once_per_visit">Dziękujemy za wsparcie, wróć później po więcej reklam</string>
<string name="pref_ads_consent_title">Czy możemy używać Twoich danych do wyświetlania reklam?</string>
<string name="pref_ads_consent_description">Możesz zmienić swój wybór w dowolnym momencie w ustawieniach aplikacji. Możemy wykorzystać Twoje dane do wyświetlania reklam dostosowanych do Ciebie lub, przy użyciu mniejszej ilości danych, wyświetlić niepersonalizowane reklamy. Zobacz naszą Politykę Prywatności, aby uzyskać więcej informacji</string>
<string name="pref_ads_summary_personalized">Spersonalizowane reklamy</string>
<string name="pref_ads_summary_non_personalized">Niespersonalizowane reklamy</string>
<string name="pref_ads_over_18_years_old">Mam ukończone 18 lat</string>
<string name="pref_ads_option_personalized">Tak, spersonalizowane reklamy</string>
<string name="pref_ads_option_non_personalized">Tak, niespersonalizowane reklamy</string>
<string name="pref_settings_advanced_title">Zaawansowane</string>
<string name="pref_settings_appearance_title">Wygląd i zachowanie</string>
<string name="pref_settings_notifications_title">Powiadomienia</string>
@ -838,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>
@ -847,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>
@ -760,7 +763,7 @@
<string name="pref_ads_support_category_name">Поддержка</string>
<string name="pref_ads_privacy_policy">Политика приватности</string>
<string name="pref_ads_agreements">Соглашения</string>
<string name="pref_ads_consent">Согласие на обработку данных, связанных с объявлениями</string>
<string name="pref_ads_consent">Show consent to data processing</string>
<string name="pref_ads_show_in_app">Показать рекламу в приложении</string>
<string name="pref_ads_support">Посмотреть рекламу для поддержки проекта</string>
<string name="pref_ads_privacy_title">Согласие на обработку данных</string>
@ -769,13 +772,6 @@
<string name="pref_ads_privacy_link">Политика конфиденциальности</string>
<string name="pref_ads_loading">Реклама загружается</string>
<string name="pref_ads_once_per_visit">Спасибо за вашу поддержку, возвращайтесь позже для дополнительной рекламы</string>
<string name="pref_ads_consent_title">Можем ли мы использовать ваши данные для показа рекламы?</string>
<string name="pref_ads_consent_description">Вы можете изменить свой выбор в любое время в настройках приложения. Мы можем использовать ваши данные для показа объявлений в соответствии с вашими пожеланиями или, используя меньше данных, отображать неперсональную рекламу. Пожалуйста, ознакомьтесь с нашей политикой конфиденциальности для подробностей</string>
<string name="pref_ads_summary_personalized">Персонализированная реклама</string>
<string name="pref_ads_summary_non_personalized">Неперсонализированная реклама</string>
<string name="pref_ads_over_18_years_old">Я старше 18 лет</string>
<string name="pref_ads_option_personalized">Да, персонализировать рекламу</string>
<string name="pref_ads_option_non_personalized">Да, не персонализировать рекламу</string>
<string name="pref_settings_advanced_title">Расширенные</string>
<string name="pref_settings_appearance_title">Внешний вид и поведение</string>
<string name="pref_settings_notifications_title">Уведомления</string>
@ -838,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>
@ -847,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">Súhlas so spracovaním údajov súvisiacich s reklamami</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>
@ -769,13 +772,6 @@
<string name="pref_ads_privacy_link">Ochrana osobných údajov</string>
<string name="pref_ads_loading">Reklama sa načítava</string>
<string name="pref_ads_once_per_visit">Ďakujeme za vašu podporu, vráťte sa neskôr pre viac reklám</string>
<string name="pref_ads_consent_title">Môžeme použiť Vaše údaje na zobrazenie reklám?</string>
<string name="pref_ads_consent_description">Voľbu môžete kedykoľvek zmeniť v nastavení aplikácie. Môžeme použiť vaše údaje na zobrazenie reklám šitých pre vás alebo pomocou menej vašich dát zobrazovať neprispôsobené reklamy. Podrobnosti nájdete v našich Zásadách ochrany osobných údajov</string>
<string name="pref_ads_summary_personalized">Prispôsobené reklamy</string>
<string name="pref_ads_summary_non_personalized">Neprispôsobené reklamy</string>
<string name="pref_ads_over_18_years_old">Mám viac ako 18 rokov</string>
<string name="pref_ads_option_personalized">Áno, prispôsobené reklamy</string>
<string name="pref_ads_option_non_personalized">Áno, neprispôsobené reklamy</string>
<string name="pref_settings_advanced_title">Pokročilé</string>
<string name="pref_settings_appearance_title">Vzhľad a správanie</string>
<string name="pref_settings_notifications_title">Oznámenia</string>
@ -838,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>
@ -847,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>
@ -760,7 +763,7 @@
<string name="pref_ads_support_category_name">Підтримка</string>
<string name="pref_ads_privacy_policy">Політика конфіденційності</string>
<string name="pref_ads_agreements">Угоди</string>
<string name="pref_ads_consent">Згода на обробку даних, пов\'язаних з рекламою</string>
<string name="pref_ads_consent">Показати згоду на обробку даних</string>
<string name="pref_ads_show_in_app">Показувати рекламу в додатку</string>
<string name="pref_ads_support">Подивіться одну рекламу для підтримки проєкту</string>
<string name="pref_ads_privacy_title">Згода в обробці даних</string>
@ -769,13 +772,6 @@
<string name="pref_ads_privacy_link">Політика конфіденційності</string>
<string name="pref_ads_loading">Реклама завантажується</string>
<string name="pref_ads_once_per_visit">Дякуємо за вашу підтримку, повертайтеся пізніше для більшої кількості реклам</string>
<string name="pref_ads_consent_title">Чи можемо ми використовувати ваші дані для висвітлювання реклами?</string>
<string name="pref_ads_consent_description">Ви можете змінити свій вибір в будь-який час в налаштуваннях додатку. Ми можемо використовувати ваші дані для висвітлювання реклами, адаптованої до вас або, використовуючи менше ваших даних, висвітлювати неперсоналізовану рекламу. Перегляньте нашу Політику конфіденційності для подробиць</string>
<string name="pref_ads_summary_personalized">Персоналізована реклама</string>
<string name="pref_ads_summary_non_personalized">Неперсоналізована реклама</string>
<string name="pref_ads_over_18_years_old">Мені більше 18 років</string>
<string name="pref_ads_option_personalized">Так, персоналізована реклама</string>
<string name="pref_ads_option_non_personalized">Так, неперсоналізована реклама</string>
<string name="pref_settings_advanced_title">Додатково</string>
<string name="pref_settings_appearance_title">Вигляд та поведінка</string>
<string name="pref_settings_notifications_title">Сповіщення</string>
@ -838,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>
@ -847,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

@ -37,8 +37,7 @@
<string name="pref_key_ads_single_support">single_ad_support</string>
<string name="pref_key_ads_enabled">ads_enabled</string>
<string name="pref_key_ads_privacy_policy">ads_privacy_policy</string>
<string name="pref_key_ads_consent_data_processing">ads_consent_data_processing</string>
<string name="pref_key_ads_over_eighteen">ads_over_eighteen</string>
<string name="pref_key_ads_ump_agreements">ads_ump_agreements</string>
<string name="pref_key_incognito_moge">incognito_mode</string>
<string name="pref_key_menu_order">appearance_menu_order</string>
</resources>

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>
@ -749,7 +752,7 @@
<string name="pref_ads_support_category_name">Support</string>
<string name="pref_ads_privacy_policy">Privacy Policy</string>
<string name="pref_ads_agreements">Agreements</string>
<string name="pref_ads_consent">Consent to processing of data related to ads</string>
<string name="pref_ads_consent">Show consent to data processing</string>
<string name="pref_ads_show_in_app">Show ads in app</string>
<string name="pref_ads_support">Watch single ad to support project</string>
<string name="pref_ads_privacy_title">Consent to data processing</string>
@ -758,13 +761,6 @@
<string name="pref_ads_privacy_link">Privacy policy</string>
<string name="pref_ads_loading">Ad is loading</string>
<string name="pref_ads_once_per_visit">Thank you for your support, come back later for more ads</string>
<string name="pref_ads_consent_title">Can we use your data to display ads?</string>
<string name="pref_ads_consent_description">You can change your choice anytime in the app settings. We may use your data to display ads tailored to you or, using less of your data, display non-personalized ads. Please see our Privacy Policy for details</string>
<string name="pref_ads_summary_personalized">Personalized ads</string>
<string name="pref_ads_summary_non_personalized">Non-personalized ads</string>
<string name="pref_ads_over_18_years_old">I am over 18 years old</string>
<string name="pref_ads_option_personalized">Yes, personalized ads</string>
<string name="pref_ads_option_non_personalized">Yes, non-personalized ads</string>
<string name="pref_settings_advanced_title">Advanced</string>
<string name="pref_settings_appearance_title">Appearance &amp; Behavior</string>
<string name="pref_settings_notifications_title">Notifications</string>
@ -838,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>
@ -847,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

@ -2,19 +2,17 @@ package io.github.wulkanowy.ui.modules.settings.ads
import android.os.Bundle
import android.view.View
import androidx.preference.CheckBoxPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAd
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.DialogAdsConsentBinding
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.AdsHelper
import io.github.wulkanowy.utils.openInternetBrowser
import javax.inject.Inject
@ -24,6 +22,9 @@ class AdsFragment : PreferenceFragmentCompat(), MainView.TitledView, AdsView {
@Inject
lateinit var presenter: AdsPresenter
@Inject
lateinit var adsHelper: AdsHelper
override val titleStringId = R.string.pref_settings_ads_title
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@ -46,11 +47,18 @@ class AdsFragment : PreferenceFragmentCompat(), MainView.TitledView, AdsView {
true
}
findPreference<CheckBoxPreference>(getString(R.string.pref_key_ads_consent_data_processing))
?.setOnPreferenceChangeListener { _, newValue ->
presenter.onConsentSelected(newValue as Boolean)
true
}
findPreference<Preference>(getString(R.string.pref_key_ads_ump_agreements))?.setOnPreferenceClickListener {
presenter.onUmpAgreementsSelected()
true
}
findPreference<Preference>(getString(R.string.pref_key_ads_single_support))
?.isEnabled = adsHelper.canShowAd
findPreference<SwitchPreferenceCompat>(getString(R.string.pref_key_ads_enabled))?.setOnPreferenceChangeListener { _, newValue ->
presenter.onAdsEnabledSelected(newValue as Boolean)
true
}
}
override fun showAd(ad: RewardedInterstitialAd) {
@ -59,48 +67,6 @@ class AdsFragment : PreferenceFragmentCompat(), MainView.TitledView, AdsView {
}
}
override fun showPrivacyPolicyDialog() {
val dialogAdsConsentBinding = DialogAdsConsentBinding.inflate(layoutInflater)
val dialog = MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.pref_ads_consent_title)
.setMessage(R.string.pref_ads_consent_description)
.setView(dialogAdsConsentBinding.root)
.setOnCancelListener { presenter.onPrivacyDialogCanceled() }
.show()
dialogAdsConsentBinding.adsConsentOver.setOnCheckedChangeListener { _, isChecked ->
dialogAdsConsentBinding.adsConsentPersonalised.isEnabled = isChecked
}
dialogAdsConsentBinding.adsConsentPersonalised.setOnClickListener {
presenter.onPersonalizedAgree()
dialog.dismiss()
}
dialogAdsConsentBinding.adsConsentNonPersonalised.setOnClickListener {
presenter.onNonPersonalizedAgree()
dialog.dismiss()
}
dialogAdsConsentBinding.adsConsentPrivacy.setOnClickListener { presenter.onPrivacySelected() }
dialogAdsConsentBinding.adsConsentCancel.setOnClickListener { dialog.cancel() }
}
override fun showProcessingDataSummary(isPersonalized: Boolean?) {
val summaryText = isPersonalized?.let {
getString(if (it) R.string.pref_ads_summary_personalized else R.string.pref_ads_summary_non_personalized)
}
findPreference<CheckBoxPreference>(getString(R.string.pref_key_ads_consent_data_processing))
?.summary = summaryText
}
override fun setCheckedProcessingData(checked: Boolean) {
findPreference<CheckBoxPreference>(getString(R.string.pref_key_ads_consent_data_processing))
?.isChecked = checked
}
override fun setCheckedAdsEnabled(checked: Boolean) {
findPreference<SwitchPreferenceCompat>(getString(R.string.pref_key_ads_enabled))
?.isChecked = checked
@ -135,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

@ -1,6 +1,5 @@
package io.github.wulkanowy.ui.modules.settings.ads
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
@ -13,18 +12,12 @@ class AdsPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val adsHelper: AdsHelper,
private val preferencesRepository: PreferencesRepository
) : BasePresenter<AdsView>(errorHandler, studentRepository) {
override fun onAttachView(view: AdsView) {
super.onAttachView(view)
view.initView()
Timber.i("Settings ads view was initialized")
view.showProcessingDataSummary(
preferencesRepository.isPersonalizedAdsEnabled.takeIf {
preferencesRepository.isAgreeToProcessData
})
}
fun onWatchSingleAdSelected() {
@ -50,38 +43,17 @@ class AdsPresenter @Inject constructor(
}
}
fun onConsentSelected(isChecked: Boolean) {
if (isChecked) {
view?.showPrivacyPolicyDialog()
} else {
view?.showProcessingDataSummary(null)
view?.setCheckedAdsEnabled(false)
}
}
fun onPrivacySelected() {
view?.openPrivacyPolicy()
}
fun onPrivacyDialogCanceled() {
view?.setCheckedProcessingData(false)
fun onAdsEnabledSelected(newValue: Boolean) {
if (newValue) {
adsHelper.initialize()
}
}
fun onNonPersonalizedAgree() {
preferencesRepository.isPersonalizedAdsEnabled = false
adsHelper.initialize()
view?.setCheckedProcessingData(true)
view?.showProcessingDataSummary(false)
}
fun onPersonalizedAgree() {
preferencesRepository.isPersonalizedAdsEnabled = true
adsHelper.initialize()
view?.setCheckedProcessingData(true)
view?.showProcessingDataSummary(true)
fun onUmpAgreementsSelected() {
adsHelper.openAdsUmpAgreements()
}
}

View File

@ -9,8 +9,6 @@ interface AdsView : BaseView {
fun showAd(ad: RewardedInterstitialAd)
fun showPrivacyPolicyDialog()
fun openPrivacyPolicy()
fun showLoadingSupportAd(show: Boolean)
@ -18,8 +16,4 @@ interface AdsView : BaseView {
fun showWatchAdOncePerVisit(show: Boolean)
fun setCheckedAdsEnabled(checked: Boolean)
fun setCheckedProcessingData(checked: Boolean)
fun showProcessingDataSummary(isPersonalized: Boolean?)
}

View File

@ -1,49 +1,110 @@
package io.github.wulkanowy.utils
import android.app.Activity
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.core.content.getSystemService
import com.google.ads.mediation.admob.AdMobAdapter
import com.google.android.gms.ads.*
import com.google.android.gms.ads.AdListener
import com.google.android.gms.ads.AdRequest
import com.google.android.gms.ads.AdSize
import com.google.android.gms.ads.AdView
import com.google.android.gms.ads.LoadAdError
import com.google.android.gms.ads.MobileAds
import com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAd
import com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAdLoadCallback
import com.google.android.ump.ConsentInformation
import com.google.android.ump.ConsentRequestParameters
import com.google.android.ump.UserMessagingPlatform
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ActivityScoped
import io.github.wulkanowy.BuildConfig
import io.github.wulkanowy.data.repositories.PreferencesRepository
import kotlinx.coroutines.flow.MutableStateFlow
import timber.log.Timber
import java.net.UnknownHostException
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
@ActivityScoped
class AdsHelper @Inject constructor(
private val activity: Activity,
@ApplicationContext private val context: Context,
private val preferencesRepository: PreferencesRepository
preferencesRepository: PreferencesRepository
) {
private var isMobileAdsInitializeCalled = AtomicBoolean(false)
private var consentInformation: ConsentInformation? = null
private val canRequestAd get() = consentInformation?.canRequestAds() == true
val isMobileAdsSdkInitialized = MutableStateFlow(false)
val canShowAd get() = isMobileAdsSdkInitialized.value && canRequestAd
init {
if (preferencesRepository.isAdsEnabled) {
initialize()
}
}
fun initialize() {
if (preferencesRepository.isAgreeToProcessData) {
MobileAds.initialize(context)
val consentRequestParameters = ConsentRequestParameters.Builder()
.build()
consentInformation = UserMessagingPlatform.getConsentInformation(context)
consentInformation?.requestConsentInfoUpdate(
activity,
consentRequestParameters,
{
UserMessagingPlatform.loadAndShowConsentFormIfRequired(
activity
) { loadAndShowError ->
if (loadAndShowError != null) {
Timber.e(IllegalStateException("${loadAndShowError.errorCode}: ${loadAndShowError.message}"))
}
if (canRequestAd) {
initializeMobileAds()
}
}
},
{ requestConsentError ->
Timber.e(IllegalStateException("${requestConsentError.errorCode}: ${requestConsentError.message}"))
})
if (canRequestAd) {
initializeMobileAds()
}
}
fun openAdsUmpAgreements() {
UserMessagingPlatform.showPrivacyOptionsForm(activity) {
if (it != null) {
Timber.e(IllegalStateException("${it.errorCode}: ${it.message}"))
}
}
}
private fun initializeMobileAds() {
if (isMobileAdsInitializeCalled.getAndSet(true)) return
MobileAds.initialize(context) {
isMobileAdsSdkInitialized.value = true
}
}
suspend fun getSupportAd(): RewardedInterstitialAd? {
if (!canRequestAd) return null
if (!context.isInternetConnected()) {
throw UnknownHostException()
}
val extra = Bundle().apply { putString("npa", "1") }
val adRequest = AdRequest.Builder()
.apply {
if (!preferencesRepository.isPersonalizedAdsEnabled) {
addNetworkExtrasBundle(AdMobAdapter::class.java, extra)
}
}
.build()
return suspendCoroutine {
@ -64,13 +125,8 @@ class AdsHelper @Inject constructor(
}
suspend fun getDashboardTileAdBanner(width: Int): AdBanner {
val extra = Bundle().apply { putString("npa", "1") }
if (!canShowAd) throw IllegalStateException("Cannot show ad")
val adRequest = AdRequest.Builder()
.apply {
if (!preferencesRepository.isPersonalizedAdsEnabled) {
addNetworkExtrasBundle(AdMobAdapter::class.java, extra)
}
}
.build()
return suspendCoroutine {

View File

@ -1,25 +1,24 @@
package io.github.wulkanowy.utils
import android.app.Activity
import android.content.Context
import android.os.Bundle
import com.google.firebase.Firebase
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.crashlytics.FirebaseCrashlytics
import dagger.hilt.android.qualifiers.ApplicationContext
import com.google.firebase.analytics.analytics
import com.google.firebase.crashlytics.crashlytics
import io.github.wulkanowy.data.repositories.PreferencesRepository
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AnalyticsHelper @Inject constructor(
@ApplicationContext private val context: Context,
preferencesRepository: PreferencesRepository,
appInfo: AppInfo,
) {
private val analytics by lazy { FirebaseAnalytics.getInstance(context) }
private val analytics by lazy { Firebase.analytics }
private val crashlytics by lazy { FirebaseCrashlytics.getInstance() }
private val crashlytics by lazy { Firebase.crashlytics }
init {
if (!appInfo.isDebug) {

View File

@ -1,5 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<SwitchPreferenceCompat
app:defaultValue="@string/pref_key_ads_enabled"
app:iconSpaceReserved="false"
app:key="@string/pref_key_ads_enabled"
app:singleLineTitle="false"
app:title="@string/pref_ads_show_in_app" />
<PreferenceCategory
app:iconSpaceReserved="false"
app:title="@string/pref_ads_agreements">
@ -8,25 +14,17 @@
app:key="@string/pref_key_ads_privacy_policy"
app:singleLineTitle="false"
app:title="@string/pref_ads_privacy_policy" />
<CheckBoxPreference
app:defaultValue="@bool/pref_default_ads_consent_data_processing"
<Preference
app:dependency="@string/pref_key_ads_enabled"
app:iconSpaceReserved="false"
app:key="@string/pref_key_ads_consent_data_processing"
app:key="@string/pref_key_ads_ump_agreements"
app:singleLineTitle="false"
app:title="@string/pref_ads_consent" />
</PreferenceCategory>
<PreferenceCategory
app:iconSpaceReserved="false"
app:title="@string/pref_ads_support_category_name">
<SwitchPreferenceCompat
app:defaultValue="@string/pref_key_ads_enabled"
app:dependency="@string/pref_key_ads_consent_data_processing"
app:iconSpaceReserved="false"
app:key="@string/pref_key_ads_enabled"
app:singleLineTitle="false"
app:title="@string/pref_ads_show_in_app" />
<Preference
app:dependency="@string/pref_key_ads_consent_data_processing"
app:iconSpaceReserved="false"
app:key="@string/pref_key_ads_single_support"
app:singleLineTitle="false"

View File

@ -1,8 +1,8 @@
buildscript {
ext {
kotlin_version = '1.9.10'
about_libraries = '10.9.1'
hilt_version = '2.48.1'
kotlin_version = '1.9.22'
about_libraries = '10.10.0'
hilt_version = '2.50'
}
repositories {
mavenCentral()
@ -13,14 +13,14 @@ buildscript {
dependencies {
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.13"
classpath 'com.android.tools.build:gradle:8.1.2'
classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$kotlin_version-1.0.16"
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.301'
classpath 'com.huawei.agconnect:agcp:1.9.1.303'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9'
classpath "com.github.triplet.gradle:play-publisher:3.8.4"
classpath "ru.cian:huawei-publish-gradle-plugin:1.4.0"
classpath "ru.cian:huawei-publish-gradle-plugin:1.4.2"
classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.4.1.3373"
classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0"
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries"
@ -29,6 +29,7 @@ buildscript {
allprojects {
repositories {
mavenLocal()
mavenCentral()
google()
maven { url "https://jitpack.io" }

View File

@ -6,8 +6,9 @@ kotlin.code.style=official
android.useAndroidX=true
android.enableJetifier=true
android.nonTransitiveRClass=false
android.defaults.buildfeatures.buildconfig=true
#
# https://developer.huawei.com/consumer/en/doc/development/AppGallery-connect-Guides/agc-common-faq-0000001063210244#section17273113244910
apmsInstrumentationEnabled=false
# https://community.sonarsource.com/t/sonarscanner-for-gradle-you-can-now-decide-when-to-compile/102069/2
systemProp.sonar.gradle.skipCompile=true

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" \