Compare commits

...

25 Commits
2.2.7 ... 2.3.3

Author SHA1 Message Date
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
49 changed files with 492 additions and 535 deletions

View File

@ -27,8 +27,8 @@ android {
testApplicationId "io.github.tests.wulkanowy" testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 34 targetSdkVersion 34
versionCode 139 versionCode 143
versionName "2.2.7" versionName "2.3.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "app_name", "Wulkanowy" resValue "string", "app_name", "Wulkanowy"
@ -163,7 +163,7 @@ play {
track = 'production' track = 'production'
releaseStatus = ReleaseStatus.IN_PROGRESS releaseStatus = ReleaseStatus.IN_PROGRESS
userFraction = 0.99d userFraction = 0.99d
updatePriority = 5 updatePriority = 3
enabled.set(false) enabled.set(false)
} }
@ -188,12 +188,12 @@ ext {
android_hilt = "1.1.0" android_hilt = "1.1.0"
room = "2.6.1" room = "2.6.1"
chucker = "4.0.0" chucker = "4.0.0"
mockk = "1.13.8" mockk = "1.13.9"
coroutines = "1.7.3" coroutines = "1.7.3"
} }
dependencies { dependencies {
implementation 'io.github.wulkanowy:sdk:2.2.7' implementation 'io.github.wulkanowy:sdk:2.3.5'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
@ -240,7 +240,7 @@ dependencies {
implementation "com.squareup.okhttp3:logging-interceptor:4.12.0" implementation "com.squareup.okhttp3:logging-interceptor:4.12.0"
implementation "com.jakewharton.timber:timber:5.0.1" implementation "com.jakewharton.timber:timber:5.0.1"
implementation "at.favre.lib:slf4j-timber:1.0.1" implementation 'com.github.Faierbel:slf4j-timber:2.0'
implementation 'com.github.bastienpaulfr:Treessence:1.1.2' implementation 'com.github.bastienpaulfr:Treessence:1.1.2'
implementation "com.mikepenz:aboutlibraries-core:$about_libraries" implementation "com.mikepenz:aboutlibraries-core:$about_libraries"
implementation 'io.coil-kt:coil:2.5.0' implementation 'io.coil-kt:coil:2.5.0'
@ -250,17 +250,19 @@ dependencies {
implementation 'org.apache.commons:commons-text:1.11.0' implementation 'org.apache.commons:commons-text:1.11.0'
playImplementation platform('com.google.firebase:firebase-bom:32.7.0') playImplementation platform('com.google.firebase:firebase-bom:32.7.0')
playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-analytics'
playImplementation 'com.google.firebase:firebase-messaging' playImplementation 'com.google.firebase:firebase-messaging'
playImplementation 'com.google.firebase:firebase-crashlytics:' playImplementation 'com.google.firebase:firebase-crashlytics:'
playImplementation 'com.google.firebase:firebase-config-ktx' playImplementation 'com.google.firebase:firebase-config'
playImplementation 'com.google.android.gms:play-services-ads:22.6.0' 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:integrity:1.3.0"
playImplementation 'com.google.android.play:app-update-ktx:2.1.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.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.hms:hianalytics:6.12.0.300'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.9.1.302' 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"

View File

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

View File

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

View File

@ -12,7 +12,6 @@ import fr.bipi.treessence.file.FileLoggerTree
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.ui.base.ThemeManager import io.github.wulkanowy.ui.base.ThemeManager
import io.github.wulkanowy.utils.ActivityLifecycleLogger import io.github.wulkanowy.utils.ActivityLifecycleLogger
import io.github.wulkanowy.utils.AdsHelper
import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.CrashLogExceptionTree import io.github.wulkanowy.utils.CrashLogExceptionTree
@ -37,9 +36,6 @@ class WulkanowyApp : Application(), Configuration.Provider {
@Inject @Inject
lateinit var analyticsHelper: AnalyticsHelper lateinit var analyticsHelper: AnalyticsHelper
@Inject
lateinit var adsHelper: AdsHelper
@Inject @Inject
lateinit var remoteConfigHelper: RemoteConfigHelper lateinit var remoteConfigHelper: RemoteConfigHelper
@ -56,7 +52,6 @@ class WulkanowyApp : Application(), Configuration.Provider {
super.onCreate() super.onCreate()
initializeAppLanguage() initializeAppLanguage()
themeManager.applyDefaultTheme() themeManager.applyDefaultTheme()
adsHelper.initialize()
remoteConfigHelper.initialize() remoteConfigHelper.initialize()
initLogging() initLogging()
} }

View File

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

View File

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

View File

@ -9,7 +9,12 @@ import com.fredporciuncula.flow.preferences.Preference
import com.fredporciuncula.flow.preferences.Serializer import com.fredporciuncula.flow.preferences.Serializer
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R 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.dashboard.DashboardItem
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode import io.github.wulkanowy.ui.modules.grade.GradeAverageMode
import io.github.wulkanowy.ui.modules.settings.appearance.menuorder.AppMenuItem 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.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.time.Instant import java.time.Instant
import java.util.* import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -303,19 +308,6 @@ class PreferencesRepository @Inject constructor(
get() = sharedPref.getBoolean(PREF_KEY_APP_SUPPORT_SHOWN, false) get() = sharedPref.getBoolean(PREF_KEY_APP_SUPPORT_SHOWN, false)
set(value) = sharedPref.edit { putBoolean(PREF_KEY_APP_SUPPORT_SHOWN, value) } 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( val isAdsEnabledFlow = flowSharedPref.getBoolean(
context.getString(R.string.pref_key_ads_enabled), context.getString(R.string.pref_key_ads_enabled),
context.resources.getBoolean(R.bool.pref_default_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_DATE = "in_app_review_date"
private const val PREF_KEY_IN_APP_REVIEW_DONE = "in_app_review_done" 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_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" 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 package io.github.wulkanowy.data.repositories
import android.content.Context
import androidx.room.withTransaction import androidx.room.withTransaction
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.dao.SemesterDao import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao import io.github.wulkanowy.data.db.dao.StudentDao
@ -17,20 +15,19 @@ import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.DispatchersProvider import io.github.wulkanowy.utils.DispatchersProvider
import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.security.decrypt import io.github.wulkanowy.utils.security.Scrambler
import io.github.wulkanowy.utils.security.encrypt
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class StudentRepository @Inject constructor( class StudentRepository @Inject constructor(
@ApplicationContext private val context: Context,
private val dispatchers: DispatchersProvider, private val dispatchers: DispatchersProvider,
private val studentDb: StudentDao, private val studentDb: StudentDao,
private val semesterDb: SemesterDao, private val semesterDb: SemesterDao,
private val sdk: Sdk, private val sdk: Sdk,
private val appDatabase: AppDatabase private val appDatabase: AppDatabase,
private val scrambler: Scrambler,
) { ) {
suspend fun isCurrentStudentSet() = studentDb.loadCurrent()?.isCurrent ?: false suspend fun isCurrentStudentSet() = studentDb.loadCurrent()?.isCurrent ?: false
@ -68,7 +65,7 @@ class StudentRepository @Inject constructor(
student = student.apply { student = student.apply {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
student.password = withContext(dispatchers.io) { student.password = withContext(dispatchers.io) {
decrypt(student.password) scrambler.decrypt(student.password)
} }
} }
}, },
@ -86,7 +83,7 @@ class StudentRepository @Inject constructor(
}.apply { }.apply {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
student.password = withContext(dispatchers.io) { student.password = withContext(dispatchers.io) {
decrypt(student.password) scrambler.decrypt(student.password)
} }
} }
} }
@ -96,7 +93,7 @@ class StudentRepository @Inject constructor(
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
student.password = withContext(dispatchers.io) { student.password = withContext(dispatchers.io) {
decrypt(student.password) scrambler.decrypt(student.password)
} }
} }
return student return student
@ -107,7 +104,7 @@ class StudentRepository @Inject constructor(
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
student.password = withContext(dispatchers.io) { student.password = withContext(dispatchers.io) {
decrypt(student.password) scrambler.decrypt(student.password)
} }
} }
return student return student
@ -120,7 +117,7 @@ class StudentRepository @Inject constructor(
it.apply { it.apply {
if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.HEBE) { if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.HEBE) {
password = withContext(dispatchers.io) { password = withContext(dispatchers.io) {
encrypt(password, context) scrambler.encrypt(password)
} }
} }
} }
@ -166,4 +163,15 @@ class StudentRepository @Inject constructor(
studentDb.update(studentName) studentDb.update(studentName)
} }
suspend fun deleteStudentsAssociatedWithAccount(student: Student) {
studentDb.deleteByEmailAndUserName(student.email, student.userName)
}
suspend fun clearAll() {
withContext(dispatchers.io) {
scrambler.clearKeyPair()
appDatabase.clearAllTables()
}
}
} }

View File

@ -68,11 +68,20 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
} else Toast.makeText(this, text, Toast.LENGTH_LONG).show() } else Toast.makeText(this, text, Toast.LENGTH_LONG).show()
} }
override fun showExpiredDialog() { override fun showExpiredCredentialsDialog() {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.main_expired_credentials_title)
.setMessage(R.string.main_expired_credentials_description)
.setPositiveButton(R.string.main_log_in) { _, _ -> presenter.onConfirmExpiredCredentialsSelected() }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
}
override fun showDecryptionFailedDialog() {
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setTitle(R.string.main_session_expired) .setTitle(R.string.main_session_expired)
.setMessage(R.string.main_session_relogin) .setMessage(R.string.main_session_relogin)
.setPositiveButton(R.string.main_log_in) { _, _ -> presenter.onExpiredLoginSelected() } .setPositiveButton(R.string.main_log_in) { _, _ -> presenter.onConfirmDecryptionFailedSelected() }
.setNegativeButton(android.R.string.cancel) { _, _ -> } .setNegativeButton(android.R.string.cancel) { _, _ -> }
.show() .show()
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -30,7 +30,12 @@ import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.notificationscenter.NotificationsCenterFragment import io.github.wulkanowy.ui.modules.notificationscenter.NotificationsCenterFragment
import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
import io.github.wulkanowy.utils.* import io.github.wulkanowy.utils.capitalise
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getErrorString
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.openInternetBrowser
import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalDate import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject

View File

@ -1,19 +1,46 @@
package io.github.wulkanowy.ui.modules.dashboard 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.AdminMessage
import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.enums.MessageType 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.domain.adminmessage.GetAppropriateAdminMessageUseCase
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AdsHelper import io.github.wulkanowy.utils.AdsHelper
import io.github.wulkanowy.utils.calculatePercentage import io.github.wulkanowy.utils.calculatePercentage
import io.github.wulkanowy.utils.nextOrSameSchoolDay 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 kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.time.Instant import java.time.Instant
@ -48,6 +75,11 @@ class DashboardPresenter @Inject constructor(
private val firstLoadedItemList = mutableListOf<DashboardItem.Type>() 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 private lateinit var lastError: Throwable
override fun onAttachView(view: DashboardView) { override fun onAttachView(view: DashboardView) {
@ -59,10 +91,19 @@ class DashboardPresenter @Inject constructor(
showContent(false) 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( merge(
preferencesRepository.selectedDashboardTilesFlow, selectedDashboardTilesFlow,
preferencesRepository.isAdsEnabledFlow isAdsEnabledFlow,
.map { preferencesRepository.selectedDashboardTiles } isMobileAdsSdkInitializedFlow
) )
.onEach { loadData(tilesToLoad = it) } .onEach { loadData(tilesToLoad = it) }
.launch("dashboard_pref") .launch("dashboard_pref")
@ -71,7 +112,7 @@ class DashboardPresenter @Inject constructor(
fun onAdminMessageDismissed(adminMessage: AdminMessage) { fun onAdminMessageDismissed(adminMessage: AdminMessage) {
preferencesRepository.dismissedAdminMessageIds += adminMessage.id preferencesRepository.dismissedAdminMessageIds += adminMessage.id
loadData(preferencesRepository.selectedDashboardTiles) loadData(selectedDashboardTiles)
} }
fun onDragAndDropEnd(list: List<DashboardItem>) { fun onDragAndDropEnd(list: List<DashboardItem>) {
@ -187,7 +228,7 @@ class DashboardPresenter @Inject constructor(
fun onSwipeRefresh() { fun onSwipeRefresh() {
Timber.i("Force refreshing the dashboard") Timber.i("Force refreshing the dashboard")
loadData(preferencesRepository.selectedDashboardTiles, forceRefresh = true) loadData(selectedDashboardTiles, forceRefresh = true)
} }
fun onRetry() { fun onRetry() {
@ -195,7 +236,7 @@ class DashboardPresenter @Inject constructor(
showErrorView(false) showErrorView(false)
showProgress(true) showProgress(true)
} }
loadData(preferencesRepository.selectedDashboardTiles, forceRefresh = true) loadData(selectedDashboardTiles, forceRefresh = true)
} }
fun onViewReselected() { fun onViewReselected() {
@ -216,7 +257,7 @@ class DashboardPresenter @Inject constructor(
} }
fun onDashboardTileSettingsSelected(): Boolean { fun onDashboardTileSettingsSelected(): Boolean {
view?.showDashboardTileSettings(preferencesRepository.selectedDashboardTiles.toList()) view?.showDashboardTileSettings(selectedDashboardTiles.toList())
return true return true
} }
@ -232,7 +273,7 @@ class DashboardPresenter @Inject constructor(
private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) { private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) {
flow { flow {
val selectedTiles = preferencesRepository.selectedDashboardTiles val selectedTiles = selectedDashboardTiles
val flowSuccess = flowOf(Resource.Success(null)) val flowSuccess = flowOf(Resource.Success(null))
val luckyNumberFlow = luckyNumberRepository.getLuckyNumber(student, forceRefresh) val luckyNumberFlow = luckyNumberRepository.getLuckyNumber(student, forceRefresh)

View File

@ -9,7 +9,11 @@ import android.view.MenuItem
import android.view.ViewGroup.MarginLayoutParams import android.view.ViewGroup.MarginLayoutParams
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.addCallback 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.DialogFragment
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.preference.Preference import androidx.preference.Preference
@ -23,12 +27,19 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.databinding.ActivityMainBinding import io.github.wulkanowy.databinding.ActivityMainBinding
import io.github.wulkanowy.databinding.DialogAdsConsentBinding
import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog
import io.github.wulkanowy.ui.modules.settings.appearance.menuorder.AppMenuItem 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.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import timber.log.Timber import timber.log.Timber
@ -312,40 +323,6 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
.show() .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() }
}
override fun openPrivacyPolicy() {
openInternetBrowser(
"https://wulkanowy.github.io/polityka-prywatnosci.html",
::showMessage
)
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
navController.onSaveInstanceState(outState) navController.onSaveInstanceState(outState)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,6 +16,7 @@ import android.util.Base64.DEFAULT
import android.util.Base64.decode import android.util.Base64.decode
import android.util.Base64.encode import android.util.Base64.encode
import android.util.Base64.encodeToString import android.util.Base64.encodeToString
import dagger.hilt.android.qualifiers.ApplicationContext
import timber.log.Timber import timber.log.Timber
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
@ -33,108 +34,124 @@ import javax.crypto.CipherInputStream
import javax.crypto.CipherOutputStream import javax.crypto.CipherOutputStream
import javax.crypto.spec.OAEPParameterSpec import javax.crypto.spec.OAEPParameterSpec
import javax.crypto.spec.PSource.PSpecified import javax.crypto.spec.PSource.PSpecified
import javax.inject.Inject
import javax.inject.Singleton
import javax.security.auth.x500.X500Principal import javax.security.auth.x500.X500Principal
private const val KEYSTORE_NAME = "AndroidKeyStore" @Singleton
class Scrambler @Inject constructor(
@ApplicationContext private val context: Context,
) {
private val keyCharset = Charset.forName("UTF-8")
private const val KEY_ALIAS = "wulkanowy_password" private val isKeyPairExists: Boolean
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 private val cipher: Cipher
get() = keyStore.getKey(KEY_ALIAS, null) != null get() {
return if (SDK_INT >= M) Cipher.getInstance(
"RSA/ECB/OAEPWithSHA-256AndMGF1Padding",
"AndroidKeyStoreBCWorkaround"
)
else Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL")
}
private val keyStore: KeyStore fun encrypt(plainText: String): String {
get() = KeyStore.getInstance(KEYSTORE_NAME).apply { load(null) } if (plainText.isEmpty()) throw ScramblerException("Text to be encrypted is empty")
private val cipher: Cipher return try {
get() { if (!isKeyPairExists) generateKeyPair()
return if (SDK_INT >= M) Cipher.getInstance(
"RSA/ECB/OAEPWithSHA-256AndMGF1Padding", cipher.let {
"AndroidKeyStoreBCWorkaround" if (SDK_INT >= M) {
) OAEPParameterSpec("SHA-256", "MGF1", SHA1, PSpecified.DEFAULT).let { spec ->
else Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL") 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 { fun decrypt(cipherText: String): String {
if (plainText.isEmpty()) throw ScramblerException("Text to be encrypted is empty") if (cipherText.isEmpty()) throw ScramblerException("Text to be encrypted is empty")
return try { return try {
if (!isKeyPairExists) generateKeyPair(context) if (!isKeyPairExists) throw ScramblerException("KeyPair doesn't exist")
cipher.let { cipher.let {
if (SDK_INT >= M) { if (SDK_INT >= M) {
OAEPParameterSpec("SHA-256", "MGF1", SHA1, PSpecified.DEFAULT).let { spec -> OAEPParameterSpec("SHA-256", "MGF1", SHA1, PSpecified.DEFAULT).let { spec ->
it.init(ENCRYPT_MODE, keyStore.getCertificate(KEY_ALIAS).publicKey, 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 -> private fun generateKeyPair() {
CipherOutputStream(output, it).apply { (if (SDK_INT >= M) {
write(plainText.toByteArray(KEY_CHARSET)) KeyGenParameterSpec.Builder(KEY_ALIAS, PURPOSE_DECRYPT or PURPOSE_ENCRYPT)
close() .setDigests(DIGEST_SHA256, DIGEST_SHA512)
} .setEncryptionPaddings(ENCRYPTION_PADDING_RSA_OAEP)
encodeToString(output.toByteArray(), DEFAULT) .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.i("A new KeyPair has been generated")
Timber.e(exception, "An error occurred while encrypting text") }
String(encode(plainText.toByteArray(KEY_CHARSET), DEFAULT), KEY_CHARSET)
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,5 @@
Wersja 2.2.7 Wersja 2.3.3
naprawiliśmy logowanie do aplikacji i odświeżanie danych na odmianie standardowej i podobnych poprawiliśmy kolejne usterki przy odświeżaniu danych (teraz to powinno działać już dużo lepiej)
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases

View File

@ -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

@ -96,6 +96,8 @@
<string name="main_log_in">Přihlásit se</string> <string name="main_log_in">Přihlásit se</string>
<string name="main_session_expired">Relace vypršela</string> <string name="main_session_expired">Relace vypršela</string>
<string name="main_session_relogin">Relace vypršela. Přihlaste se prosím znovu</string> <string name="main_session_relogin">Relace vypršela. Přihlaste se prosím znovu</string>
<string name="main_expired_credentials_description">Heslo k vašemu účtu bylo změněno. Musíte se znovu přihlásit do Wulkanového</string>
<string name="main_expired_credentials_title">Heslo bylo změněno</string>
<string name="main_support_title">Podpora aplikace</string> <string name="main_support_title">Podpora aplikace</string>
<string name="main_support_description">Líbí se Vám tato aplikace? Podpořte její vývoj tím, že povolíte neinvazivní reklamy, které můžete kdykoliv vypnout</string> <string name="main_support_description">Líbí se Vám tato aplikace? Podpořte její vývoj tím, že povolíte neinvazivní reklamy, které můžete kdykoliv vypnout</string>
<string name="main_support_positive">Zapnout reklamy</string> <string name="main_support_positive">Zapnout reklamy</string>
@ -760,7 +762,7 @@
<string name="pref_ads_support_category_name">Podpora</string> <string name="pref_ads_support_category_name">Podpora</string>
<string name="pref_ads_privacy_policy">Ochrana osobních údajů</string> <string name="pref_ads_privacy_policy">Ochrana osobních údajů</string>
<string name="pref_ads_agreements">Souhlasy</string> <string name="pref_ads_agreements">Souhlasy</string>
<string name="pref_ads_consent">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_show_in_app">Zobrazit reklamy v aplikaci</string>
<string name="pref_ads_support">Podívejte se na jednu reklamu pro podporu projektu</string> <string name="pref_ads_support">Podívejte se na jednu reklamu pro podporu projektu</string>
<string name="pref_ads_privacy_title">Souhlas se zpracováním dat</string> <string name="pref_ads_privacy_title">Souhlas se zpracováním dat</string>
@ -769,13 +771,6 @@
<string name="pref_ads_privacy_link">Ochrana osobních údajů</string> <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_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_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_advanced_title">Pokročilé</string>
<string name="pref_settings_appearance_title">Vzhled a chování</string> <string name="pref_settings_appearance_title">Vzhled a chování</string>
<string name="pref_settings_notifications_title">Oznámení</string> <string name="pref_settings_notifications_title">Oznámení</string>

View File

@ -96,6 +96,8 @@
<string name="main_log_in">Log in</string> <string name="main_log_in">Log in</string>
<string name="main_session_expired">Session expired</string> <string name="main_session_expired">Session expired</string>
<string name="main_session_relogin">Session expired, log in again</string> <string name="main_session_relogin">Session expired, log in again</string>
<string name="main_expired_credentials_description">Your account password has been changed. You need to log in to Wulkanowy again</string>
<string name="main_expired_credentials_title">Password changed</string>
<string name="main_support_title">Application support</string> <string name="main_support_title">Application support</string>
<string name="main_support_description">Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time</string> <string name="main_support_description">Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time</string>
<string name="main_support_positive">Enable ads</string> <string name="main_support_positive">Enable ads</string>
@ -670,7 +672,7 @@
<string name="pref_ads_support_category_name">Support</string> <string name="pref_ads_support_category_name">Support</string>
<string name="pref_ads_privacy_policy">Privacy Policy</string> <string name="pref_ads_privacy_policy">Privacy Policy</string>
<string name="pref_ads_agreements">Agreements</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_show_in_app">Show ads in app</string>
<string name="pref_ads_support">Watch single ad to support project</string> <string name="pref_ads_support">Watch single ad to support project</string>
<string name="pref_ads_privacy_title">Consent to data processing</string> <string name="pref_ads_privacy_title">Consent to data processing</string>
@ -679,13 +681,6 @@
<string name="pref_ads_privacy_link">Privacy policy</string> <string name="pref_ads_privacy_link">Privacy policy</string>
<string name="pref_ads_loading">Ad is loading</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_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_advanced_title">Advanced</string>
<string name="pref_settings_appearance_title">Appearance &amp; Behavior</string> <string name="pref_settings_appearance_title">Appearance &amp; Behavior</string>
<string name="pref_settings_notifications_title">Notifications</string> <string name="pref_settings_notifications_title">Notifications</string>

View File

@ -96,6 +96,8 @@
<string name="main_log_in">Anmelden</string> <string name="main_log_in">Anmelden</string>
<string name="main_session_expired">Die Sitzung ist abgelaufen</string> <string name="main_session_expired">Die Sitzung ist abgelaufen</string>
<string name="main_session_relogin">Die Sitzung ist abgelaufen, bitte loggen Sie sich erneut ein</string> <string name="main_session_relogin">Die Sitzung ist abgelaufen, bitte loggen Sie sich erneut ein</string>
<string name="main_expired_credentials_description">Your account password has been changed. You need to log in to Wulkanowy again</string>
<string name="main_expired_credentials_title">Password changed</string>
<string name="main_support_title">Anwendungsunterstützung</string> <string name="main_support_title">Anwendungsunterstützung</string>
<string name="main_support_description">Gefällt Ihnen diese App? Unterstützen Sie ihre Entwicklung, indem Sie nicht-invasive Werbung aktivieren, die Sie jederzeit deaktivieren können</string> <string name="main_support_description">Gefällt Ihnen diese App? Unterstützen Sie ihre Entwicklung, indem Sie nicht-invasive Werbung aktivieren, die Sie jederzeit deaktivieren können</string>
<string name="main_support_positive">Werbung aktivieren</string> <string name="main_support_positive">Werbung aktivieren</string>
@ -670,7 +672,7 @@
<string name="pref_ads_support_category_name">Unterstützung</string> <string name="pref_ads_support_category_name">Unterstützung</string>
<string name="pref_ads_privacy_policy">Datenschutz-Bestimmungen</string> <string name="pref_ads_privacy_policy">Datenschutz-Bestimmungen</string>
<string name="pref_ads_agreements">Vereinbarungen</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_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_support">Einzelanzeige ansehen, um Projekt zu unterstützen</string>
<string name="pref_ads_privacy_title">Einwilligung in die Datenverarbeitung</string> <string name="pref_ads_privacy_title">Einwilligung in die Datenverarbeitung</string>
@ -679,13 +681,6 @@
<string name="pref_ads_privacy_link">Datenschutzerklärung</string> <string name="pref_ads_privacy_link">Datenschutzerklärung</string>
<string name="pref_ads_loading">Anzeige wird geladen</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_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_advanced_title">Erweitert</string>
<string name="pref_settings_appearance_title">Aussehen &amp; Verhalten</string> <string name="pref_settings_appearance_title">Aussehen &amp; Verhalten</string>
<string name="pref_settings_notifications_title">Benachrichtigungen</string> <string name="pref_settings_notifications_title">Benachrichtigungen</string>

View File

@ -96,6 +96,8 @@
<string name="main_log_in">Log in</string> <string name="main_log_in">Log in</string>
<string name="main_session_expired">Session expired</string> <string name="main_session_expired">Session expired</string>
<string name="main_session_relogin">Session expired, log in again</string> <string name="main_session_relogin">Session expired, log in again</string>
<string name="main_expired_credentials_description">Your account password has been changed. You need to log in to Wulkanowy again</string>
<string name="main_expired_credentials_title">Password changed</string>
<string name="main_support_title">Application support</string> <string name="main_support_title">Application support</string>
<string name="main_support_description">Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time</string> <string name="main_support_description">Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time</string>
<string name="main_support_positive">Enable ads</string> <string name="main_support_positive">Enable ads</string>
@ -670,7 +672,7 @@
<string name="pref_ads_support_category_name">Support</string> <string name="pref_ads_support_category_name">Support</string>
<string name="pref_ads_privacy_policy">Privacy Policy</string> <string name="pref_ads_privacy_policy">Privacy Policy</string>
<string name="pref_ads_agreements">Agreements</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_show_in_app">Show ads in app</string>
<string name="pref_ads_support">Watch single ad to support project</string> <string name="pref_ads_support">Watch single ad to support project</string>
<string name="pref_ads_privacy_title">Consent to data processing</string> <string name="pref_ads_privacy_title">Consent to data processing</string>
@ -679,13 +681,6 @@
<string name="pref_ads_privacy_link">Privacy policy</string> <string name="pref_ads_privacy_link">Privacy policy</string>
<string name="pref_ads_loading">Ad is loading</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_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_advanced_title">Advanced</string>
<string name="pref_settings_appearance_title">Appearance &amp; Behavior</string> <string name="pref_settings_appearance_title">Appearance &amp; Behavior</string>
<string name="pref_settings_notifications_title">Notifications</string> <string name="pref_settings_notifications_title">Notifications</string>

View File

@ -96,6 +96,8 @@
<string name="main_log_in">Log in</string> <string name="main_log_in">Log in</string>
<string name="main_session_expired">Session expired</string> <string name="main_session_expired">Session expired</string>
<string name="main_session_relogin">Session expired, log in again</string> <string name="main_session_relogin">Session expired, log in again</string>
<string name="main_expired_credentials_description">Your account password has been changed. You need to log in to Wulkanowy again</string>
<string name="main_expired_credentials_title">Password changed</string>
<string name="main_support_title">Application support</string> <string name="main_support_title">Application support</string>
<string name="main_support_description">Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time</string> <string name="main_support_description">Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time</string>
<string name="main_support_positive">Enable ads</string> <string name="main_support_positive">Enable ads</string>
@ -670,7 +672,7 @@
<string name="pref_ads_support_category_name">Support</string> <string name="pref_ads_support_category_name">Support</string>
<string name="pref_ads_privacy_policy">Privacy Policy</string> <string name="pref_ads_privacy_policy">Privacy Policy</string>
<string name="pref_ads_agreements">Agreements</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_show_in_app">Show ads in app</string>
<string name="pref_ads_support">Watch single ad to support project</string> <string name="pref_ads_support">Watch single ad to support project</string>
<string name="pref_ads_privacy_title">Consent to data processing</string> <string name="pref_ads_privacy_title">Consent to data processing</string>
@ -679,13 +681,6 @@
<string name="pref_ads_privacy_link">Privacy policy</string> <string name="pref_ads_privacy_link">Privacy policy</string>
<string name="pref_ads_loading">Ad is loading</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_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_advanced_title">Advanced</string>
<string name="pref_settings_appearance_title">Appearance &amp; Behavior</string> <string name="pref_settings_appearance_title">Appearance &amp; Behavior</string>
<string name="pref_settings_notifications_title">Notifications</string> <string name="pref_settings_notifications_title">Notifications</string>

View File

@ -96,6 +96,8 @@
<string name="main_log_in">Zaloguj się</string> <string name="main_log_in">Zaloguj się</string>
<string name="main_session_expired">Sesja wygasła</string> <string name="main_session_expired">Sesja wygasła</string>
<string name="main_session_relogin">Sesja wygasła, zaloguj się ponownie</string> <string name="main_session_relogin">Sesja wygasła, zaloguj się ponownie</string>
<string name="main_expired_credentials_description">Hasło do Twojego konta zostało zmienione. Musisz zalogować się ponownie do Wulkanowego</string>
<string name="main_expired_credentials_title">Hasło zostało zmienione</string>
<string name="main_support_title">Wparcie aplikacji</string> <string name="main_support_title">Wparcie aplikacji</string>
<string name="main_support_description">Podoba Ci się ta aplikacja? Wspieraj jej rozwój poprzez włączenie nieinwazyjnych reklam, które możesz wyłączyć w dowolnym momencie</string> <string name="main_support_description">Podoba Ci się ta aplikacja? Wspieraj jej rozwój poprzez włączenie nieinwazyjnych reklam, które możesz wyłączyć w dowolnym momencie</string>
<string name="main_support_positive">Włącz reklamy</string> <string name="main_support_positive">Włącz reklamy</string>
@ -760,7 +762,7 @@
<string name="pref_ads_support_category_name">Wsparcie</string> <string name="pref_ads_support_category_name">Wsparcie</string>
<string name="pref_ads_privacy_policy">Polityka prywatności</string> <string name="pref_ads_privacy_policy">Polityka prywatności</string>
<string name="pref_ads_agreements">Zgody</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_show_in_app">Pokazuj reklamy w aplikacji</string>
<string name="pref_ads_support">Obejrzyj pojedynczą reklamę, aby wesprzeć projekt</string> <string name="pref_ads_support">Obejrzyj pojedynczą reklamę, aby wesprzeć projekt</string>
<string name="pref_ads_privacy_title">Zgoda na przetwarzanie danych</string> <string name="pref_ads_privacy_title">Zgoda na przetwarzanie danych</string>
@ -769,13 +771,6 @@
<string name="pref_ads_privacy_link">Polityka prywatności</string> <string name="pref_ads_privacy_link">Polityka prywatności</string>
<string name="pref_ads_loading">Ładowanie reklamy</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_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_advanced_title">Zaawansowane</string>
<string name="pref_settings_appearance_title">Wygląd i zachowanie</string> <string name="pref_settings_appearance_title">Wygląd i zachowanie</string>
<string name="pref_settings_notifications_title">Powiadomienia</string> <string name="pref_settings_notifications_title">Powiadomienia</string>

View File

@ -96,6 +96,8 @@
<string name="main_log_in">Войти</string> <string name="main_log_in">Войти</string>
<string name="main_session_expired">Сеанс истёк</string> <string name="main_session_expired">Сеанс истёк</string>
<string name="main_session_relogin">Сеанс истёк, авторизуйтесь снова</string> <string name="main_session_relogin">Сеанс истёк, авторизуйтесь снова</string>
<string name="main_expired_credentials_description">Your account password has been changed. You need to log in to Wulkanowy again</string>
<string name="main_expired_credentials_title">Password changed</string>
<string name="main_support_title">Поддержка приложения</string> <string name="main_support_title">Поддержка приложения</string>
<string name="main_support_description">Вам нравится это приложение? Поддержите его разработку, включив неинвазивную рекламу, которую можно отключить в любое время</string> <string name="main_support_description">Вам нравится это приложение? Поддержите его разработку, включив неинвазивную рекламу, которую можно отключить в любое время</string>
<string name="main_support_positive">Включить рекламу</string> <string name="main_support_positive">Включить рекламу</string>
@ -760,7 +762,7 @@
<string name="pref_ads_support_category_name">Поддержка</string> <string name="pref_ads_support_category_name">Поддержка</string>
<string name="pref_ads_privacy_policy">Политика приватности</string> <string name="pref_ads_privacy_policy">Политика приватности</string>
<string name="pref_ads_agreements">Соглашения</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_show_in_app">Показать рекламу в приложении</string>
<string name="pref_ads_support">Посмотреть рекламу для поддержки проекта</string> <string name="pref_ads_support">Посмотреть рекламу для поддержки проекта</string>
<string name="pref_ads_privacy_title">Согласие на обработку данных</string> <string name="pref_ads_privacy_title">Согласие на обработку данных</string>
@ -769,13 +771,6 @@
<string name="pref_ads_privacy_link">Политика конфиденциальности</string> <string name="pref_ads_privacy_link">Политика конфиденциальности</string>
<string name="pref_ads_loading">Реклама загружается</string> <string name="pref_ads_loading">Реклама загружается</string>
<string name="pref_ads_once_per_visit">Спасибо за вашу поддержку, возвращайтесь позже для дополнительной рекламы</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_advanced_title">Расширенные</string>
<string name="pref_settings_appearance_title">Внешний вид и поведение</string> <string name="pref_settings_appearance_title">Внешний вид и поведение</string>
<string name="pref_settings_notifications_title">Уведомления</string> <string name="pref_settings_notifications_title">Уведомления</string>

View File

@ -96,6 +96,8 @@
<string name="main_log_in">Prihlásiť sa</string> <string name="main_log_in">Prihlásiť sa</string>
<string name="main_session_expired">Relácia vypršala</string> <string name="main_session_expired">Relácia vypršala</string>
<string name="main_session_relogin">Relácia vypršala. Prihláste sa prosím znovu</string> <string name="main_session_relogin">Relácia vypršala. Prihláste sa prosím znovu</string>
<string name="main_expired_credentials_description">Heslo k vášmu účtu bolo zmenené. Musíte sa znovu prihlásiť do Wulkanového</string>
<string name="main_expired_credentials_title">Heslo bolo zmenené</string>
<string name="main_support_title">Podpora aplikácie</string> <string name="main_support_title">Podpora aplikácie</string>
<string name="main_support_description">Páči sa Vám táto aplikácia? Podporte jej vývoj tým, že povolíte neinvazívne reklamy, ktoré môžete kedykoľvek vypnúť</string> <string name="main_support_description">Páči sa Vám táto aplikácia? Podporte jej vývoj tým, že povolíte neinvazívne reklamy, ktoré môžete kedykoľvek vypnúť</string>
<string name="main_support_positive">Zapnúť reklamy</string> <string name="main_support_positive">Zapnúť reklamy</string>
@ -760,7 +762,7 @@
<string name="pref_ads_support_category_name">Podpora</string> <string name="pref_ads_support_category_name">Podpora</string>
<string name="pref_ads_privacy_policy">Ochrana osobných údajov</string> <string name="pref_ads_privacy_policy">Ochrana osobných údajov</string>
<string name="pref_ads_agreements">Súhlasy</string> <string name="pref_ads_agreements">Súhlasy</string>
<string name="pref_ads_consent">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_show_in_app">Zobraziť reklamy v aplikácii</string>
<string name="pref_ads_support">Pozrite sa na jednu reklamu pre podporu projektu</string> <string name="pref_ads_support">Pozrite sa na jednu reklamu pre podporu projektu</string>
<string name="pref_ads_privacy_title">Súhlas so spracovaním dát</string> <string name="pref_ads_privacy_title">Súhlas so spracovaním dát</string>
@ -769,13 +771,6 @@
<string name="pref_ads_privacy_link">Ochrana osobných údajov</string> <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_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_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_advanced_title">Pokročilé</string>
<string name="pref_settings_appearance_title">Vzhľad a správanie</string> <string name="pref_settings_appearance_title">Vzhľad a správanie</string>
<string name="pref_settings_notifications_title">Oznámenia</string> <string name="pref_settings_notifications_title">Oznámenia</string>

View File

@ -96,6 +96,8 @@
<string name="main_log_in">Увійти</string> <string name="main_log_in">Увійти</string>
<string name="main_session_expired">Минув термін дії сесії</string> <string name="main_session_expired">Минув термін дії сесії</string>
<string name="main_session_relogin">Минув термін дії сесії, авторизуйтеся знову</string> <string name="main_session_relogin">Минув термін дії сесії, авторизуйтеся знову</string>
<string name="main_expired_credentials_description">Your account password has been changed. You need to log in to Wulkanowy again</string>
<string name="main_expired_credentials_title">Password changed</string>
<string name="main_support_title">Підтримка додатку</string> <string name="main_support_title">Підтримка додатку</string>
<string name="main_support_description">Вам подобається цей додаток? Підтримайте його розвиток, увімкнувши неінвазивну рекламу, яку ви можете відключити в будь-який час</string> <string name="main_support_description">Вам подобається цей додаток? Підтримайте його розвиток, увімкнувши неінвазивну рекламу, яку ви можете відключити в будь-який час</string>
<string name="main_support_positive">Увімкнути рекламу</string> <string name="main_support_positive">Увімкнути рекламу</string>
@ -760,7 +762,7 @@
<string name="pref_ads_support_category_name">Підтримка</string> <string name="pref_ads_support_category_name">Підтримка</string>
<string name="pref_ads_privacy_policy">Політика конфіденційності</string> <string name="pref_ads_privacy_policy">Політика конфіденційності</string>
<string name="pref_ads_agreements">Угоди</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_show_in_app">Показувати рекламу в додатку</string>
<string name="pref_ads_support">Подивіться одну рекламу для підтримки проєкту</string> <string name="pref_ads_support">Подивіться одну рекламу для підтримки проєкту</string>
<string name="pref_ads_privacy_title">Згода в обробці даних</string> <string name="pref_ads_privacy_title">Згода в обробці даних</string>
@ -769,13 +771,6 @@
<string name="pref_ads_privacy_link">Політика конфіденційності</string> <string name="pref_ads_privacy_link">Політика конфіденційності</string>
<string name="pref_ads_loading">Реклама завантажується</string> <string name="pref_ads_loading">Реклама завантажується</string>
<string name="pref_ads_once_per_visit">Дякуємо за вашу підтримку, повертайтеся пізніше для більшої кількості реклам</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_advanced_title">Додатково</string>
<string name="pref_settings_appearance_title">Вигляд та поведінка</string> <string name="pref_settings_appearance_title">Вигляд та поведінка</string>
<string name="pref_settings_notifications_title">Сповіщення</string> <string name="pref_settings_notifications_title">Сповіщення</string>

View File

@ -37,8 +37,7 @@
<string name="pref_key_ads_single_support">single_ad_support</string> <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_enabled">ads_enabled</string>
<string name="pref_key_ads_privacy_policy">ads_privacy_policy</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_ump_agreements">ads_ump_agreements</string>
<string name="pref_key_ads_over_eighteen">ads_over_eighteen</string>
<string name="pref_key_incognito_moge">incognito_mode</string> <string name="pref_key_incognito_moge">incognito_mode</string>
<string name="pref_key_menu_order">appearance_menu_order</string> <string name="pref_key_menu_order">appearance_menu_order</string>
</resources> </resources>

View File

@ -107,6 +107,8 @@
<string name="main_log_in">Log in</string> <string name="main_log_in">Log in</string>
<string name="main_session_expired">Session expired</string> <string name="main_session_expired">Session expired</string>
<string name="main_session_relogin">Session expired, log in again</string> <string name="main_session_relogin">Session expired, log in again</string>
<string name="main_expired_credentials_description">Your account password has been changed. You need to log in to Wulkanowy again</string>
<string name="main_expired_credentials_title">Password changed</string>
<string name="main_support_title">Application support</string> <string name="main_support_title">Application support</string>
<string name="main_support_description">Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time</string> <string name="main_support_description">Do you like this app? Support its development by enabling non-invasive ads that you can disable at any time</string>
<string name="main_support_positive">Enable ads</string> <string name="main_support_positive">Enable ads</string>
@ -749,7 +751,7 @@
<string name="pref_ads_support_category_name">Support</string> <string name="pref_ads_support_category_name">Support</string>
<string name="pref_ads_privacy_policy">Privacy Policy</string> <string name="pref_ads_privacy_policy">Privacy Policy</string>
<string name="pref_ads_agreements">Agreements</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_show_in_app">Show ads in app</string>
<string name="pref_ads_support">Watch single ad to support project</string> <string name="pref_ads_support">Watch single ad to support project</string>
<string name="pref_ads_privacy_title">Consent to data processing</string> <string name="pref_ads_privacy_title">Consent to data processing</string>
@ -758,13 +760,6 @@
<string name="pref_ads_privacy_link">Privacy policy</string> <string name="pref_ads_privacy_link">Privacy policy</string>
<string name="pref_ads_loading">Ad is loading</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_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_advanced_title">Advanced</string>
<string name="pref_settings_appearance_title">Appearance &amp; Behavior</string> <string name="pref_settings_appearance_title">Appearance &amp; Behavior</string>
<string name="pref_settings_notifications_title">Notifications</string> <string name="pref_settings_notifications_title">Notifications</string>

View File

@ -2,19 +2,17 @@ package io.github.wulkanowy.ui.modules.settings.ads
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.preference.CheckBoxPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAd import com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAd
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.DialogAdsConsentBinding
import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.base.ErrorDialog import io.github.wulkanowy.ui.base.ErrorDialog
import io.github.wulkanowy.ui.modules.auth.AuthDialog import io.github.wulkanowy.ui.modules.auth.AuthDialog
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.AdsHelper
import io.github.wulkanowy.utils.openInternetBrowser import io.github.wulkanowy.utils.openInternetBrowser
import javax.inject.Inject import javax.inject.Inject
@ -24,6 +22,9 @@ class AdsFragment : PreferenceFragmentCompat(), MainView.TitledView, AdsView {
@Inject @Inject
lateinit var presenter: AdsPresenter lateinit var presenter: AdsPresenter
@Inject
lateinit var adsHelper: AdsHelper
override val titleStringId = R.string.pref_settings_ads_title override val titleStringId = R.string.pref_settings_ads_title
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@ -46,11 +47,18 @@ class AdsFragment : PreferenceFragmentCompat(), MainView.TitledView, AdsView {
true true
} }
findPreference<CheckBoxPreference>(getString(R.string.pref_key_ads_consent_data_processing)) findPreference<Preference>(getString(R.string.pref_key_ads_ump_agreements))?.setOnPreferenceClickListener {
?.setOnPreferenceChangeListener { _, newValue -> presenter.onUmpAgreementsSelected()
presenter.onConsentSelected(newValue as Boolean) true
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) { 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) { override fun setCheckedAdsEnabled(checked: Boolean) {
findPreference<SwitchPreferenceCompat>(getString(R.string.pref_key_ads_enabled)) findPreference<SwitchPreferenceCompat>(getString(R.string.pref_key_ads_enabled))
?.isChecked = checked ?.isChecked = checked
@ -135,8 +101,12 @@ class AdsFragment : PreferenceFragmentCompat(), MainView.TitledView, AdsView {
(activity as? BaseActivity<*, *>)?.showMessage(text) (activity as? BaseActivity<*, *>)?.showMessage(text)
} }
override fun showExpiredDialog() { override fun showExpiredCredentialsDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredDialog() (activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog()
}
override fun showDecryptionFailedDialog() {
(activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog()
} }
override fun showChangePasswordSnackbar(redirectUrl: String) { override fun showChangePasswordSnackbar(redirectUrl: String) {

View File

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

View File

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

View File

@ -1,49 +1,110 @@
package io.github.wulkanowy.utils package io.github.wulkanowy.utils
import android.app.Activity
import android.content.Context import android.content.Context
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.os.Build import android.os.Build
import android.os.Bundle
import android.view.View import android.view.View
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import com.google.ads.mediation.admob.AdMobAdapter import com.google.android.gms.ads.AdListener
import com.google.android.gms.ads.* 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.RewardedInterstitialAd
import com.google.android.gms.ads.rewardedinterstitial.RewardedInterstitialAdLoadCallback 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.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ActivityScoped
import io.github.wulkanowy.BuildConfig import io.github.wulkanowy.BuildConfig
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import kotlinx.coroutines.flow.MutableStateFlow
import timber.log.Timber
import java.net.UnknownHostException import java.net.UnknownHostException
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject import javax.inject.Inject
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
@ActivityScoped
class AdsHelper @Inject constructor( class AdsHelper @Inject constructor(
private val activity: Activity,
@ApplicationContext private val context: Context, @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() { fun initialize() {
if (preferencesRepository.isAgreeToProcessData) { val consentRequestParameters = ConsentRequestParameters.Builder()
MobileAds.initialize(context) .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? { suspend fun getSupportAd(): RewardedInterstitialAd? {
if (!canRequestAd) return null
if (!context.isInternetConnected()) { if (!context.isInternetConnected()) {
throw UnknownHostException() throw UnknownHostException()
} }
val extra = Bundle().apply { putString("npa", "1") }
val adRequest = AdRequest.Builder() val adRequest = AdRequest.Builder()
.apply {
if (!preferencesRepository.isPersonalizedAdsEnabled) {
addNetworkExtrasBundle(AdMobAdapter::class.java, extra)
}
}
.build() .build()
return suspendCoroutine { return suspendCoroutine {
@ -64,13 +125,8 @@ class AdsHelper @Inject constructor(
} }
suspend fun getDashboardTileAdBanner(width: Int): AdBanner { suspend fun getDashboardTileAdBanner(width: Int): AdBanner {
val extra = Bundle().apply { putString("npa", "1") } if (!canShowAd) throw IllegalStateException("Cannot show ad")
val adRequest = AdRequest.Builder() val adRequest = AdRequest.Builder()
.apply {
if (!preferencesRepository.isPersonalizedAdsEnabled) {
addNetworkExtrasBundle(AdMobAdapter::class.java, extra)
}
}
.build() .build()
return suspendCoroutine { return suspendCoroutine {

View File

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

View File

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

View File

@ -1,7 +1,7 @@
buildscript { buildscript {
ext { ext {
kotlin_version = '1.9.22' kotlin_version = '1.9.22'
about_libraries = '10.9.2' about_libraries = '10.10.0'
hilt_version = '2.50' hilt_version = '2.50'
} }
repositories { repositories {
@ -14,10 +14,10 @@ buildscript {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$kotlin_version-1.0.16" classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$kotlin_version-1.0.16"
classpath 'com.android.tools.build:gradle:8.2.0' classpath 'com.android.tools.build:gradle:8.2.1'
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
classpath 'com.google.gms:google-services:4.4.0' classpath 'com.google.gms:google-services:4.4.0'
classpath 'com.huawei.agconnect:agcp:1.9.1.302' classpath 'com.huawei.agconnect:agcp:1.9.1.303'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9'
classpath "com.github.triplet.gradle:play-publisher:3.8.4" classpath "com.github.triplet.gradle:play-publisher:3.8.4"
classpath "ru.cian:huawei-publish-gradle-plugin:1.4.2" classpath "ru.cian:huawei-publish-gradle-plugin:1.4.2"
@ -29,6 +29,7 @@ buildscript {
allprojects { allprojects {
repositories { repositories {
mavenLocal()
mavenCentral() mavenCentral()
google() google()
maven { url "https://jitpack.io" } maven { url "https://jitpack.io" }

Binary file not shown.

View File

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

17
gradlew vendored
View File

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