diff --git a/.github/workflows/on_push_development.yml b/.github/workflows/on_push_development.yml
index 6233bc5d..cca70992 100644
--- a/.github/workflows/on_push_development.yml
+++ b/.github/workflows/on_push_development.yml
@@ -20,7 +20,7 @@ jobs:
uses: actions/checkout@v4.1.1
- name: set up JDK 17
- uses: actions/setup-java@v4.1.0
+ uses: actions/setup-java@v4.2.1
with:
distribution: 'zulu' # See 'Supported distributions' for available options
java-version: '17'
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 8d81632f..fe63bb67 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 8ec0f4be..07edca29 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -21,7 +21,7 @@ android {
defaultConfig {
applicationId = "com.joeloewi.croissant"
- versionCode = 59
+ versionCode = 60
versionName = "1.3.0"
targetSdk = 34
@@ -68,9 +68,9 @@ baselineProfile {
}
dependencies {
- implementation(project(":data"))
- implementation(project(":domain"))
- baselineProfile(project(":baselineprofile"))
+ implementation(projects.data)
+ implementation(projects.domain)
+ baselineProfile(projects.baselineprofile)
implementation(libs.androidx.core.ktx)
implementation(libs.android.material)
@@ -111,7 +111,6 @@ dependencies {
implementation(libs.accompanist.webview)
implementation(libs.accompanist.pager.indicators)
implementation(libs.accompanist.swiperefresh)
- implementation(libs.accompanist.themeadapter.material3)
implementation(libs.accompanist.navigation.material)
//work
diff --git a/app/src/main/kotlin/com/joeloewi/croissant/ui/navigation/main/attendances/screen/AttendancesScreen.kt b/app/src/main/kotlin/com/joeloewi/croissant/ui/navigation/main/attendances/screen/AttendancesScreen.kt
index 941a2ba8..4ba67359 100644
--- a/app/src/main/kotlin/com/joeloewi/croissant/ui/navigation/main/attendances/screen/AttendancesScreen.kt
+++ b/app/src/main/kotlin/com/joeloewi/croissant/ui/navigation/main/attendances/screen/AttendancesScreen.kt
@@ -45,9 +45,9 @@ import androidx.compose.material3.rememberSwipeToDismissBoxState
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
@@ -58,6 +58,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString
@@ -65,12 +66,14 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.flowWithLifecycle
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemKey
import androidx.work.ExistingWorkPolicy
import androidx.work.WorkInfo
import androidx.work.WorkManager
+import androidx.work.WorkQuery
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.google.firebase.Firebase
@@ -252,12 +255,14 @@ fun AttendanceWithGamesItem(
{ attendance ->
Firebase.analytics.logEvent("instant_attend_click", bundleOf())
- val oneTimeWork =
- AttendCheckInEventWorker.buildOneTimeWork(attendanceId = attendance.id)
+ val oneTimeWork = AttendCheckInEventWorker.buildOneTimeWork(
+ attendanceId = attendance.id,
+ isInstantAttendance = true
+ )
WorkManager.getInstance(context).beginUniqueWork(
attendance.oneTimeAttendCheckInEventWorkerName.toString(),
- ExistingWorkPolicy.APPEND_OR_REPLACE,
+ ExistingWorkPolicy.REPLACE,
oneTimeWork
).enqueue()
@@ -401,14 +406,24 @@ private fun DismissContent(
},
trailingContent = {
val context = LocalContext.current
+ val lifecycleOwner = LocalLifecycleOwner.current
val workerName =
attendanceWithGames().value.attendance.oneTimeAttendCheckInEventWorkerName.toString()
- val isRunning by remember(context, workerName) {
+ val isRunningFlow = remember(context, workerName) {
WorkManager.getInstance(context)
- .getWorkInfosForUniqueWorkFlow(workerName)
- .map { list -> list.any { it.state == WorkInfo.State.RUNNING } }
+ .getWorkInfosFlow(
+ WorkQuery.Builder
+ .fromUniqueWorkNames(listOf(workerName))
+ .addStates(listOf(WorkInfo.State.RUNNING))
+ .build()
+ )
+ .catch { }
+ .map { it.isNotEmpty() }
.flowOn(Dispatchers.IO)
- }.collectAsState(initial = false)
+ }
+ val isRunning by produceState(initialValue = false) {
+ isRunningFlow.flowWithLifecycle(lifecycleOwner.lifecycle).collect { value = it }
+ }
IconButton(
enabled = !isRunning,
diff --git a/app/src/main/kotlin/com/joeloewi/croissant/util/NotificationGenerator.kt b/app/src/main/kotlin/com/joeloewi/croissant/util/NotificationGenerator.kt
index eb9601f0..a58cbb8a 100644
--- a/app/src/main/kotlin/com/joeloewi/croissant/util/NotificationGenerator.kt
+++ b/app/src/main/kotlin/com/joeloewi/croissant/util/NotificationGenerator.kt
@@ -8,7 +8,6 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.net.Uri
-import android.os.Build
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
@@ -216,21 +215,15 @@ class NotificationGenerator(
.setContentTitle(context.getString(R.string.attendance_foreground_notification_title))
.setContentText(context.getString(R.string.wait_for_a_moment))
.setSmallIcon(R.drawable.ic_baseline_bakery_dining_24)
- .setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE)
+ .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.build()
.run {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- return@run ForegroundInfo(
- notificationId,
- this,
- ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
- )
- }
- return@run ForegroundInfo(
+ ForegroundInfo(
notificationId,
- this
+ this,
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
)
}
diff --git a/app/src/main/kotlin/com/joeloewi/croissant/worker/AttendCheckInEventWorker.kt b/app/src/main/kotlin/com/joeloewi/croissant/worker/AttendCheckInEventWorker.kt
index 144ad298..4ed3f145 100644
--- a/app/src/main/kotlin/com/joeloewi/croissant/worker/AttendCheckInEventWorker.kt
+++ b/app/src/main/kotlin/com/joeloewi/croissant/worker/AttendCheckInEventWorker.kt
@@ -61,6 +61,7 @@ class AttendCheckInEventWorker @AssistedInject constructor(
private val _firstTriggeredTimestamp by lazy {
inputData.getLong(FIRST_TRIGGERED_TIMESTAMP, Instant.now().toEpochMilli())
}
+ private val _isInstantAttendance by lazy { inputData.getBoolean(IS_INSTANT_ATTENDANCE, false) }
override suspend fun getForegroundInfo(): ForegroundInfo =
notificationGenerator.createForegroundInfo(_attendanceId.toInt())
@@ -205,21 +206,41 @@ class AttendCheckInEventWorker @AssistedInject constructor(
is HoYoLABUnsuccessfulResponseException -> {
when (val retCode = HoYoLABRetCode.findByCode(cause.retCode)) {
HoYoLABRetCode.TooManyRequests, HoYoLABRetCode.TooManyRequestsGenshinImpact -> {
- //do not make log, not to skip this game when retry
-
- notificationGenerator.createAttendanceRetryScheduledNotification(
- nickname = attendanceWithGames.attendance.nickname,
- contentText = context.getString(R.string.attendance_retry_too_many_requests_error)
- ).let { notification ->
- notificationGenerator.safeNotify(
- UUID.randomUUID().toString(),
- game.type.gameId,
- notification
- )
+ if (_isInstantAttendance) {
+ //make log and do not retry if this work was enqueued by clicking instant attendance
+
+ addFailureLog(_attendanceId, game.type, cause)
+
+ createUnsuccessfulAttendanceNotification(
+ nickname = attendanceWithGames.attendance.nickname,
+ hoYoLABGame = game.type,
+ region = game.region,
+ hoYoLABUnsuccessfulResponseException = cause
+ ).let { notification ->
+ notificationGenerator.safeNotify(
+ UUID.randomUUID().toString(),
+ game.type.gameId,
+ notification
+ )
+ }
+
+ Result.success()
+ } else {
+ //do not make log, not to skip this game when retry
+ notificationGenerator.createAttendanceRetryScheduledNotification(
+ nickname = attendanceWithGames.attendance.nickname,
+ contentText = context.getString(R.string.attendance_retry_too_many_requests_error)
+ ).let { notification ->
+ notificationGenerator.safeNotify(
+ UUID.randomUUID().toString(),
+ game.type.gameId,
+ notification
+ )
+ }
+
+ //good to retry
+ Result.retry()
}
-
- //good to retry
- Result.retry()
}
else -> {
@@ -238,11 +259,11 @@ class AttendCheckInEventWorker @AssistedInject constructor(
)
}
- if (!listOf(
+ if (retCode !in listOf(
HoYoLABRetCode.AlreadyCheckedIn,
HoYoLABRetCode.CharacterNotExists,
HoYoLABRetCode.LoginFailed
- ).contains(retCode)
+ )
) {
//we don't know which error was occurred
//record this error for monitoring
@@ -265,21 +286,40 @@ class AttendCheckInEventWorker @AssistedInject constructor(
}
else -> {
- //do not make log, not to pass this game when retry
- notificationGenerator.createAttendanceRetryScheduledNotification(
- nickname = attendanceWithGames.attendance.nickname
- ).let { notification ->
- notificationGenerator.safeNotify(
- UUID.randomUUID().toString(),
- game.type.gameId,
- notification
- )
- }
+ if (_isInstantAttendance) {
+ //make log and do not retry if this work was enqueued by clicking instant attendance
+ addFailureLog(_attendanceId, game.type, cause)
+
+ notificationGenerator.createUnsuccessfulAttendanceNotification(
+ nickname = attendanceWithGames.attendance.nickname,
+ hoYoLABGame = game.type,
+ attendanceId = _attendanceId
+ ).let { notification ->
+ notificationGenerator.safeNotify(
+ UUID.randomUUID().toString(),
+ game.type.gameId,
+ notification
+ )
+ }
- //these errors are not hoyolab server's errors, but networks errors, generally
- //do retry
- Firebase.crashlytics.log("runAttemptCount: $runAttemptCount")
- Result.retry()
+ Result.success()
+ } else {
+ //do not make log, not to pass this game when retry
+ notificationGenerator.createAttendanceRetryScheduledNotification(
+ nickname = attendanceWithGames.attendance.nickname
+ ).let { notification ->
+ notificationGenerator.safeNotify(
+ UUID.randomUUID().toString(),
+ game.type.gameId,
+ notification
+ )
+ }
+
+ //these errors are not hoyolab server's errors, but networks errors, generally
+ //do retry
+ Firebase.crashlytics.log("runAttemptCount: $runAttemptCount")
+ Result.retry()
+ }
}
}
}
@@ -287,12 +327,13 @@ class AttendCheckInEventWorker @AssistedInject constructor(
}
}.fold(
onSuccess = { results ->
- if (results.contains(Result.retry())) {
+ //do not retry if this work was enqueued by clicking instant attendance
+ if (results.contains(Result.retry()) && !_isInstantAttendance) {
return@withContext Result.retry()
}
runAttemptCount.takeIf { count -> count > 0 }?.let {
- Firebase.crashlytics.log("success after run attempts: $it")
+ Firebase.crashlytics.log("succeed after run attempts: $it")
}
Result.success()
@@ -313,10 +354,12 @@ class AttendCheckInEventWorker @AssistedInject constructor(
companion object {
const val ATTENDANCE_ID = "attendanceId"
const val FIRST_TRIGGERED_TIMESTAMP = "triggeredTimestamp"
+ const val IS_INSTANT_ATTENDANCE = "isInstantAttendance"
fun buildOneTimeWork(
attendanceId: Long,
triggeredTimestamp: Long = Instant.now().toEpochMilli(),
+ isInstantAttendance: Boolean = false,
constraints: Constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
@@ -324,7 +367,8 @@ class AttendCheckInEventWorker @AssistedInject constructor(
.setInputData(
workDataOf(
ATTENDANCE_ID to attendanceId,
- FIRST_TRIGGERED_TIMESTAMP to triggeredTimestamp
+ FIRST_TRIGGERED_TIMESTAMP to triggeredTimestamp,
+ IS_INSTANT_ATTENDANCE to isInstantAttendance
)
)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
diff --git a/app/src/main/kotlin/com/joeloewi/croissant/worker/CheckSessionWorker.kt b/app/src/main/kotlin/com/joeloewi/croissant/worker/CheckSessionWorker.kt
index f0fc272b..e06eb3dc 100644
--- a/app/src/main/kotlin/com/joeloewi/croissant/worker/CheckSessionWorker.kt
+++ b/app/src/main/kotlin/com/joeloewi/croissant/worker/CheckSessionWorker.kt
@@ -85,7 +85,7 @@ class CheckSessionWorker @AssistedInject constructor(
)
runAttemptCount.takeIf { count -> count > 0 }?.let {
- Firebase.crashlytics.log("success after run attempts: $it")
+ Firebase.crashlytics.log("succeed after run attempts: $it")
}
Result.success()
diff --git a/data/build.gradle.kts b/data/build.gradle.kts
index 8a5c373b..1e35e354 100644
--- a/data/build.gradle.kts
+++ b/data/build.gradle.kts
@@ -21,7 +21,7 @@ android {
}
dependencies {
- implementation(project(":domain"))
+ implementation(projects.domain)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
@@ -31,9 +31,12 @@ dependencies {
androidTestImplementation(libs.androidx.test.espresso.core)
//retrofit2
+ implementation(platform(libs.retrofit.bom))
implementation(libs.retrofit)
implementation(libs.retrofit.converter.moshi)
implementation(libs.retrofit.converter.scalars)
+ ksp(libs.retrofit.response.type.keeper)
+
implementation(platform(libs.okhttp3.bom))
implementation(libs.okhttp3.logging.interceptor)
implementation(libs.okhttp3.dnsoverhttps)
diff --git a/data/consumer-proguard-rules.pro b/data/consumer-proguard-rules.pro
index 73c8f35f..63e51592 100644
--- a/data/consumer-proguard-rules.pro
+++ b/data/consumer-proguard-rules.pro
@@ -1,18 +1 @@
--keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
-
--dontwarn okhttp3.internal.platform.**
--dontwarn org.conscrypt.**
--dontwarn org.bouncycastle.**
--dontwarn org.openjsse.**
-
- # With R8 full mode generic signatures are stripped for classes that are not
- # kept. Suspend functions are wrapped in continuations where the type argument
- # is used.
- -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
-
- # R8 full mode strips generic signatures from return types if not kept.
- -if interface * { @retrofit2.http.* public *** *(...); }
- -keep,allowoptimization,allowshrinking,allowobfuscation class <3>
-
- # With R8 full mode generic signatures are stripped for classes that are not kept.
- -keep,allowobfuscation,allowshrinking class retrofit2.Response
\ No newline at end of file
+-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
\ No newline at end of file
diff --git a/data/src/main/kotlin/com/joeloewi/croissant/data/util/ApiResponseExtensions.kt b/data/src/main/kotlin/com/joeloewi/croissant/data/util/ApiResponseExtensions.kt
index c0818121..d6e35ec6 100644
--- a/data/src/main/kotlin/com/joeloewi/croissant/data/util/ApiResponseExtensions.kt
+++ b/data/src/main/kotlin/com/joeloewi/croissant/data/util/ApiResponseExtensions.kt
@@ -21,7 +21,7 @@ suspend fun runAndRetryWithExponentialBackOff(
task = task
)
-private tailrec suspend fun retryTask(
+internal tailrec suspend fun retryTask(
attempt: Int = 1,
initialDelay: Int,
retryFactor: Float,
@@ -46,25 +46,17 @@ private tailrec suspend fun retryTask(
return when (val apiResponse = task()) {
is ApiResponse.Success -> apiResponse
is ApiResponse.Failure -> {
- when (apiResponse) {
- is ApiResponse.Failure.Error -> {
- apiResponse
- }
-
- is ApiResponse.Failure.Exception -> {
- if (attempt < maxAttempts) {
- retryTask(
- attempt = attempt + 1,
- initialDelay = initialDelay,
- retryFactor = retryFactor,
- maxAttempts = maxAttempts,
- maxDelayMillis = maxDelayMillis,
- task = task
- )
- } else {
- apiResponse
- }
- }
+ if (attempt < maxAttempts) {
+ retryTask(
+ attempt = attempt + 1,
+ initialDelay = initialDelay,
+ retryFactor = retryFactor,
+ maxAttempts = maxAttempts,
+ maxDelayMillis = maxDelayMillis,
+ task = task
+ )
+ } else {
+ apiResponse
}
}
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 484fc0ec..1e2a7a1a 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,15 +1,15 @@
[versions]
accompanist = "0.34.0"
androidDesugarJdkLibs = "2.0.4"
-androidGradlePlugin = "8.3.0"
-androidxActivity = "1.8.2"
+androidGradlePlugin = "8.3.2"
+androidxActivity = "1.9.0"
androidxAppCompat = "1.6.1"
androidxCompose = "1.3.1"
-androidxComposeCompiler = "1.5.10"
+androidxComposeCompiler = "1.5.12"
androidxComposeMaterial3 = "1.0.1"
-androidxCore = "1.12.0"
+androidxCore = "1.13.0"
androidxCoreSplashscreen = "1.0.1"
-androidxDataStore = "1.0.0"
+androidxDataStore = "1.1.0"
androidxEspresso = "3.5.1"
androidxHiltNavigationCompose = "1.2.0"
androidxLifecycle = "2.7.0"
@@ -23,45 +23,45 @@ androidxTracing = "1.1.0"
androidxUiAutomator = "2.2.0"
androidxWork = "2.9.0"
androidxWebkit = "1.10.0"
-androidxComposeBom = "2024.02.02"
+androidxComposeBom = "2024.04.01"
androidxPagingCommon = "3.2.1"
androidxPagingCompose = "3.2.1"
androidMaterial = "1.11.0"
coil = "2.6.0"
-hilt = "2.51"
+hilt = "2.51.1"
hiltExt = "1.2.0"
-kotlin = "1.9.22"
+kotlin = "1.9.23"
kotlinxCoroutines = "1.8.0"
kotlinxCollectionsImmutable = "0.3.7"
androidxProfileInstaller = "1.3.1"
androidxTestRunner = "1.5.2"
androidxTestCore = "1.5.0"
-androidxBenchmarkMacroJunit4 = "1.2.3"
+androidxBenchmarkMacroJunit4 = "1.2.4"
androidxTestRules = "1.5.0"
-protobuf = "3.25.3"
+protobuf = "4.26.1"
protobufPlugin = "0.9.4"
-retrofit = "2.9.0"
+retrofit = "2.11.0"
room = "2.6.1"
moshi = "1.15.1"
androidPlayAppUpdateKtx = "2.1.0"
androidPlayReviewKtx = "2.0.1"
firebaseCrashlyticsPlugin = "2.9.9"
gmsGoogleServicesGradlePlugin = "4.4.1"
-firebaseBom = "32.7.4"
-leakcanaryAndroid = "2.13"
+firebaseBom = "32.8.1"
+leakcanaryAndroid = "2.14"
okhttp3Bom = "4.12.0"
inject = "1"
-sandwich = "2.0.5"
+sandwich = "2.0.6"
jsoup = "1.17.2"
junit = "4.13.2"
-kspPlugin = "1.9.22-1.0.18"
-tts = "2.5.0"
+kspPlugin = "1.9.23-1.0.20"
+tts = "2.6.0"
gmsOssLicensesPlugin = "0.10.6"
gmsPlayServicesOssLicenses = "17.0.1"
-androidxBaselineProfile = "1.2.3"
-androidTools = "31.3.0"
+androidxBaselineProfile = "1.2.4"
+androidTools = "31.3.2"
fornewidPlaceholderMaterial3 = "1.1.2"
-kotlinxAtomicfu = "0.23.2"
+kotlinxAtomicfu = "0.24.0"
[libraries]
accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanist" }
@@ -71,7 +71,6 @@ accompanist-systemuicontroller = { group = "com.google.accompanist", name = "acc
accompanist-webview = { group = "com.google.accompanist", name = "accompanist-webview", version.ref = "accompanist" }
accompanist-pager-indicators = { group = "com.google.accompanist", name = "accompanist-pager-indicators", version.ref = "accompanist" }
accompanist-swiperefresh = { group = "com.google.accompanist", name = "accompanist-swiperefresh", version.ref = "accompanist" }
-accompanist-themeadapter-material3 = { group = "com.google.accompanist", name = "accompanist-themeadapter-material3", version.ref = "accompanist" }
accompanist-navigation-material = { group = "com.google.accompanist", name = "accompanist-navigation-material", version.ref = "accompanist" }
gms-google-services-gradlePlugin = { group = "com.google.gms", name = "google-services", version.ref = "gmsGoogleServicesGradlePlugin" }
android-desugarJdkLibs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" }
@@ -129,10 +128,12 @@ kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotli
protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" }
protobuf-javalite = { group = "com.google.protobuf", name = "protobuf-javalite", version.ref = "protobuf" }
protobuf-kotlin-lite = { group = "com.google.protobuf", name = "protobuf-kotlin-lite", version.ref = "protobuf" }
-retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
-retrofit-converter-moshi = { group = "com.squareup.retrofit2", name = "converter-moshi", version.ref = "retrofit" }
-retrofit-converter-scalars = { group = "com.squareup.retrofit2", name = "converter-scalars", version.ref = "retrofit" }
-okhttp3-dnsoverhttps = { group = "com.squareup.okhttp3", name = "okhttp-dnsoverhttps", version.ref = "okhttp3Bom" }
+retrofit-bom = { group = "com.squareup.retrofit2", name = "retrofit-bom", version.ref = "retrofit" }
+retrofit = { group = "com.squareup.retrofit2", name = "retrofit" }
+retrofit-converter-moshi = { group = "com.squareup.retrofit2", name = "converter-moshi" }
+retrofit-converter-scalars = { group = "com.squareup.retrofit2", name = "converter-scalars" }
+retrofit-response-type-keeper = { group = "com.squareup.retrofit2", name = "response-type-keeper", version.ref = "retrofit" }
+okhttp3-dnsoverhttps = { group = "com.squareup.okhttp3", name = "okhttp-dnsoverhttps" }
okhttp3-bom = { group = "com.squareup.okhttp3", name = "okhttp-bom", version.ref = "okhttp3Bom" }
okhttp3-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor" }
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 943f0cbf..e6441136 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 509c4a29..b82aa23a 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
networkTimeout=10000
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index 31042a67..1aa94a42 100644
--- a/gradlew
+++ b/gradlew
@@ -246,4 +246,4 @@ eval "set -- $(
tr '\n' ' '
)" '"$@"'
-exec "$JAVACMD" "$@"
\ No newline at end of file
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
index 6689b85b..7101f8e4 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
goto fail
@@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
goto fail