mirror of
https://github.com/sudoxnym/habitica-android.git
synced 2026-05-18 03:39:00 +00:00
Improve network handling
This commit is contained in:
parent
91b1f32d72
commit
2bada3fc35
14 changed files with 138 additions and 69 deletions
|
|
@ -31,7 +31,7 @@ buildscript {
|
|||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.1.3'
|
||||
classpath 'com.android.tools.build:gradle:7.2.1'
|
||||
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
|
||||
classpath 'com.google.gms:google-services:4.3.13'
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.1'
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
NAME=4.0
|
||||
CODE=4050
|
||||
CODE=4160
|
||||
|
|
@ -110,9 +110,6 @@ dependencies {
|
|||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
kapt("com.squareup.moshi:moshi-kotlin-codegen:$moshi_version")
|
||||
|
||||
//Analytics
|
||||
implementation "com.amplitude:android-sdk:$amplitude_version"
|
||||
|
||||
implementation platform('com.google.firebase:firebase-bom:30.2.0')
|
||||
implementation 'com.google.firebase:firebase-crashlytics-ktx'
|
||||
implementation 'com.google.firebase:firebase-analytics-ktx'
|
||||
|
|
|
|||
|
|
@ -59,8 +59,8 @@ class MainApplication : Application() {
|
|||
if (userRepository.hasAuthentication) {
|
||||
MainScope().launch(CoroutineExceptionHandler { _, _ ->
|
||||
}) {
|
||||
val user = userRepository.retrieveUser()
|
||||
taskRepository.retrieveTasks(user?.tasksOrder)
|
||||
val user = userRepository.retrieveUser(true)
|
||||
taskRepository.retrieveTasks(user?.tasksOrder, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -74,6 +74,9 @@ class MainApplication : Application() {
|
|||
private fun setupFirebase() {
|
||||
if (!BuildConfig.DEBUG) {
|
||||
val crashlytics = Firebase.crashlytics
|
||||
if (userRepository.hasAuthentication) {
|
||||
crashlytics.setUserId(userRepository.userID)
|
||||
}
|
||||
crashlytics.setCustomKey("is_wear", true)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ package com.habitrpg.wearos.habitica.data
|
|||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import com.amplitude.api.Amplitude
|
||||
import com.habitrpg.common.habitica.BuildConfig
|
||||
import com.habitrpg.common.habitica.api.HostConfig
|
||||
import com.habitrpg.common.habitica.api.Server
|
||||
import com.habitrpg.common.habitica.models.auth.UserAuth
|
||||
import com.habitrpg.common.habitica.models.auth.UserAuthSocial
|
||||
import com.habitrpg.wearos.habitica.models.NetworkResult
|
||||
import com.habitrpg.wearos.habitica.models.WearableHabitResponse
|
||||
import com.habitrpg.wearos.habitica.models.tasks.Task
|
||||
import okhttp3.Cache
|
||||
|
|
@ -17,6 +17,7 @@ import okhttp3.OkHttpClient
|
|||
import okhttp3.Request
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Converter
|
||||
import retrofit2.Response
|
||||
import retrofit2.Retrofit
|
||||
import java.io.File
|
||||
import java.util.GregorianCalendar
|
||||
|
|
@ -28,6 +29,8 @@ class ApiClient @Inject constructor(
|
|||
private val hostConfig: HostConfig,
|
||||
private val context: Context
|
||||
) {
|
||||
val userID: String
|
||||
get() = hostConfig.userID
|
||||
private lateinit var retrofitAdapter: Retrofit
|
||||
|
||||
// I think we don't need the ApiClientImpl anymore we could just use ApiService
|
||||
|
|
@ -117,6 +120,8 @@ class ApiClient @Inject constructor(
|
|||
.removeHeader("Pragma")
|
||||
.build()
|
||||
val response = chain.proceed(request)
|
||||
val responseBuilder = response.newBuilder()
|
||||
responseBuilder.header("was-cached", (response.networkResponse == null).toString())
|
||||
if (request.method == "GET") {
|
||||
if (response.code == 504) {
|
||||
// Cache miss. Network might be down, but retry call without cache to be sure.
|
||||
|
|
@ -124,12 +129,12 @@ class ApiClient @Inject constructor(
|
|||
.header("Cache-Control", "no-cache")
|
||||
.build())
|
||||
} else {
|
||||
response.newBuilder()
|
||||
responseBuilder
|
||||
.header("Cache-Control", request.header("Cache-Control") ?: "")
|
||||
.build()
|
||||
}
|
||||
} else {
|
||||
response
|
||||
responseBuilder.build()
|
||||
}
|
||||
}
|
||||
.readTimeout(2400, TimeUnit.SECONDS)
|
||||
|
|
@ -149,38 +154,57 @@ class ApiClient @Inject constructor(
|
|||
fun updateAuthenticationCredentials(userID: String?, apiToken: String?) {
|
||||
this.hostConfig.userID = userID ?: ""
|
||||
this.hostConfig.apiKey = apiToken ?: ""
|
||||
Amplitude.getInstance().userId = this.hostConfig.userID
|
||||
}
|
||||
|
||||
private fun <T> process(response: WearableHabitResponse<T>): T? {
|
||||
return response.data
|
||||
private suspend fun <T: Any> process(call: suspend () -> Response<WearableHabitResponse<T>>): NetworkResult<T> {
|
||||
val response: Response<WearableHabitResponse<T>>
|
||||
try {
|
||||
response = call.invoke()
|
||||
} catch (t: Exception) {
|
||||
return NetworkResult.Error(t, false)
|
||||
}
|
||||
|
||||
val wasCached = response.headers()["was-cached"] == "true"
|
||||
|
||||
return if (!response.isSuccessful) {
|
||||
val errorBody = response.errorBody()
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
NetworkResult.Error(Exception((response.message() + errorBody?.string())), !wasCached)
|
||||
} else {
|
||||
val body = response.body()
|
||||
return if (body?.data != null) {
|
||||
NetworkResult.Success(body.data!!, !wasCached)
|
||||
} else {
|
||||
NetworkResult.Error(Exception("response.body() can't be null"), !wasCached)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getUser(forced: Boolean = false) = if (forced) {
|
||||
process(apiService.getUserForced())
|
||||
process { apiService.getUserForced() }
|
||||
} else {
|
||||
process(apiService.getUser())
|
||||
process { apiService.getUser() }
|
||||
}
|
||||
suspend fun updateUser(data: Map<String, Any>) = process(apiService.updateUser(data))
|
||||
suspend fun sleep() = process(apiService.sleep())
|
||||
suspend fun revive() = process(apiService.revive())
|
||||
suspend fun updateUser(data: Map<String, Any>) = process { apiService.updateUser(data) }
|
||||
suspend fun sleep() = process { apiService.sleep() }
|
||||
suspend fun revive() = process { apiService.revive() }
|
||||
|
||||
suspend fun loginLocal(auth: UserAuth) = process(apiService.connectLocal(auth))
|
||||
suspend fun loginSocial(auth: UserAuthSocial) = process(apiService.connectSocial(auth))
|
||||
suspend fun loginLocal(auth: UserAuth) = process { apiService.connectLocal(auth) }
|
||||
suspend fun loginSocial(auth: UserAuthSocial) = process { apiService.connectSocial(auth) }
|
||||
|
||||
suspend fun addPushDevice(data: Map<String, String>) = process(apiService.addPushDevice(data))
|
||||
suspend fun removePushDevice(id: String) = process(apiService.removePushDevice(id))
|
||||
suspend fun addPushDevice(data: Map<String, String>) = process { apiService.addPushDevice(data) }
|
||||
suspend fun removePushDevice(id: String) = process { apiService.removePushDevice(id) }
|
||||
|
||||
suspend fun runCron() = process(apiService.runCron())
|
||||
suspend fun runCron() = process { apiService.runCron() }
|
||||
|
||||
suspend fun getTasks(forced: Boolean = false) = if (forced) {
|
||||
process(apiService.getTasksForced())
|
||||
process { apiService.getTasksForced() }
|
||||
} else {
|
||||
process(apiService.getTasks())
|
||||
process { apiService.getTasks() }
|
||||
}
|
||||
suspend fun scoreTask(id: String, direction: String) =
|
||||
process(apiService.scoreTask(id, direction))
|
||||
process { apiService.scoreTask(id, direction) }
|
||||
|
||||
suspend fun createTask(task: Task) = process(apiService.createTask(task))
|
||||
suspend fun createTask(task: Task) = process { apiService.createTask(task) }
|
||||
fun hasAuthentication() = hostConfig.hasAuthentication()
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import com.habitrpg.wearos.habitica.models.tasks.BulkTaskScoringData
|
|||
import com.habitrpg.wearos.habitica.models.tasks.Task
|
||||
import com.habitrpg.wearos.habitica.models.tasks.TaskList
|
||||
import com.habitrpg.wearos.habitica.models.user.User
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.DELETE
|
||||
import retrofit2.http.GET
|
||||
|
|
@ -22,83 +23,83 @@ import retrofit2.http.Query
|
|||
|
||||
interface ApiService {
|
||||
@GET("user/")
|
||||
suspend fun getUser(): WearableHabitResponse<User>
|
||||
suspend fun getUser(): Response<WearableHabitResponse<User>>
|
||||
@GET("user/")
|
||||
@Headers("Cache-Control: no-cache")
|
||||
suspend fun getUserForced(): WearableHabitResponse<User>
|
||||
suspend fun getUserForced(): Response<WearableHabitResponse<User>>
|
||||
|
||||
@PUT("user/")
|
||||
suspend fun updateUser(@Body updateDictionary: Map<String, Any>): WearableHabitResponse<User>
|
||||
suspend fun updateUser(@Body updateDictionary: Map<String, Any>): Response<WearableHabitResponse<User>>
|
||||
|
||||
@PUT("user/")
|
||||
suspend fun registrationLanguage(@Header("Accept-Language") registrationLanguage: String): WearableHabitResponse<User>
|
||||
suspend fun registrationLanguage(@Header("Accept-Language") registrationLanguage: String): Response<WearableHabitResponse<User>>
|
||||
|
||||
@GET("tasks/user")
|
||||
suspend fun getTasks(): WearableHabitResponse<TaskList>
|
||||
suspend fun getTasks(): Response<WearableHabitResponse<TaskList>>
|
||||
@GET("tasks/user")
|
||||
@Headers("Cache-Control: no-cache")
|
||||
suspend fun getTasksForced(): WearableHabitResponse<TaskList>
|
||||
suspend fun getTasksForced(): Response<WearableHabitResponse<TaskList>>
|
||||
|
||||
@GET("tasks/user")
|
||||
suspend fun getTasks(@Query("type") type: String): WearableHabitResponse<TaskList>
|
||||
suspend fun getTasks(@Query("type") type: String): Response<WearableHabitResponse<TaskList>>
|
||||
|
||||
@GET("tasks/user")
|
||||
suspend fun getTasks(@Query("type") type: String, @Query("dueDate") dueDate: String): WearableHabitResponse<TaskList>
|
||||
suspend fun getTasks(@Query("type") type: String, @Query("dueDate") dueDate: String): Response<WearableHabitResponse<TaskList>>
|
||||
|
||||
@GET("tasks/{id}")
|
||||
suspend fun getTask(@Path("id") id: String): WearableHabitResponse<Task>
|
||||
suspend fun getTask(@Path("id") id: String): Response<WearableHabitResponse<Task>>
|
||||
|
||||
@POST("tasks/{id}/score/{direction}")
|
||||
suspend fun scoreTask(@Path("id") id: String, @Path("direction") direction: String): WearableHabitResponse<TaskDirectionData>
|
||||
suspend fun scoreTask(@Path("id") id: String, @Path("direction") direction: String): Response<WearableHabitResponse<TaskDirectionData>>
|
||||
@POST("tasks/bulk-score")
|
||||
suspend fun bulkScoreTasks(@Body data: List<Map<String, String>>): WearableHabitResponse<BulkTaskScoringData>
|
||||
suspend fun bulkScoreTasks(@Body data: List<Map<String, String>>): Response<WearableHabitResponse<BulkTaskScoringData>>
|
||||
|
||||
@POST("tasks/{id}/move/to/{position}")
|
||||
suspend fun postTaskNewPosition(@Path("id") id: String, @Path("position") position: Int): WearableHabitResponse<List<String>>
|
||||
suspend fun postTaskNewPosition(@Path("id") id: String, @Path("position") position: Int): Response<WearableHabitResponse<List<String>>>
|
||||
|
||||
@POST("tasks/{taskId}/checklist/{itemId}/score")
|
||||
suspend fun scoreChecklistItem(@Path("taskId") taskId: String, @Path("itemId") itemId: String): WearableHabitResponse<Task>
|
||||
suspend fun scoreChecklistItem(@Path("taskId") taskId: String, @Path("itemId") itemId: String): Response<WearableHabitResponse<Task>>
|
||||
|
||||
@POST("tasks/user")
|
||||
suspend fun createTask(@Body item: Task): WearableHabitResponse<Task>
|
||||
suspend fun createTask(@Body item: Task): Response<WearableHabitResponse<Task>>
|
||||
|
||||
@POST("tasks/user")
|
||||
suspend fun createTasks(@Body tasks: List<Task>): WearableHabitResponse<List<Task>>
|
||||
suspend fun createTasks(@Body tasks: List<Task>): Response<WearableHabitResponse<List<Task>>>
|
||||
|
||||
@PUT("tasks/{id}")
|
||||
suspend fun updateTask(@Path("id") id: String, @Body item: Task): WearableHabitResponse<Task>
|
||||
suspend fun updateTask(@Path("id") id: String, @Body item: Task): Response<WearableHabitResponse<Task>>
|
||||
|
||||
@DELETE("tasks/{id}")
|
||||
suspend fun deleteTask(@Path("id") id: String): WearableHabitResponse<Void>
|
||||
suspend fun deleteTask(@Path("id") id: String): Response<WearableHabitResponse<Void>>
|
||||
|
||||
@POST("user/auth/local/register")
|
||||
suspend fun registerUser(@Body auth: UserAuth): WearableHabitResponse<UserAuthResponse>
|
||||
suspend fun registerUser(@Body auth: UserAuth): Response<WearableHabitResponse<UserAuthResponse>>
|
||||
|
||||
@POST("user/auth/local/login")
|
||||
suspend fun connectLocal(@Body auth: UserAuth): WearableHabitResponse<UserAuthResponse>
|
||||
suspend fun connectLocal(@Body auth: UserAuth): Response<WearableHabitResponse<UserAuthResponse>>
|
||||
|
||||
@POST("user/auth/social")
|
||||
suspend fun connectSocial(@Body auth: UserAuthSocial): WearableHabitResponse<UserAuthResponse>
|
||||
suspend fun connectSocial(@Body auth: UserAuthSocial): Response<WearableHabitResponse<UserAuthResponse>>
|
||||
|
||||
@DELETE("user/auth/social/{network}")
|
||||
suspend fun disconnectSocial(@Path("network") network: String): WearableHabitResponse<Void>
|
||||
suspend fun disconnectSocial(@Path("network") network: String): Response<WearableHabitResponse<Void>>
|
||||
|
||||
@POST("user/auth/apple")
|
||||
suspend fun loginApple(@Body auth: Map<String, Any>): WearableHabitResponse<UserAuthResponse>
|
||||
suspend fun loginApple(@Body auth: Map<String, Any>): Response<WearableHabitResponse<UserAuthResponse>>
|
||||
|
||||
@POST("user/sleep")
|
||||
suspend fun sleep(): WearableHabitResponse<Boolean>
|
||||
suspend fun sleep(): Response<WearableHabitResponse<Boolean>>
|
||||
|
||||
@POST("user/revive")
|
||||
suspend fun revive(): WearableHabitResponse<User>
|
||||
suspend fun revive(): Response<WearableHabitResponse<User>>
|
||||
|
||||
// Push notifications
|
||||
@POST("user/push-devices")
|
||||
suspend fun addPushDevice(@Body pushDeviceData: Map<String, String>): WearableHabitResponse<List<Void>>
|
||||
suspend fun addPushDevice(@Body pushDeviceData: Map<String, String>): Response<WearableHabitResponse<List<Void>>>
|
||||
|
||||
@DELETE("user/push-devices/{regId}")
|
||||
suspend fun removePushDevice(@Path("regId") regId: String): WearableHabitResponse<List<Void>>
|
||||
suspend fun removePushDevice(@Path("regId") regId: String): Response<WearableHabitResponse<List<Void>>>
|
||||
|
||||
@POST("cron")
|
||||
suspend fun runCron(): WearableHabitResponse<EmptyResponse>
|
||||
suspend fun runCron(): Response<WearableHabitResponse<EmptyResponse>>
|
||||
}
|
||||
|
|
@ -18,9 +18,14 @@ class TaskRepository @Inject constructor(
|
|||
private val userLocalRepository: UserLocalRepository
|
||||
) {
|
||||
|
||||
suspend fun retrieveTasks(order: TasksOrder?, forced: Boolean = false): TaskList? {
|
||||
val tasks = apiClient.getTasks(forced)
|
||||
tasks?.let { localRepository.saveTasks(tasks, order) }
|
||||
suspend fun retrieveTasks(order: TasksOrder?, ensureFresh: Boolean = false): TaskList? {
|
||||
val response = apiClient.getTasks()
|
||||
var tasks = response.responseData
|
||||
tasks?.let { localRepository.saveTasks(it, order) }
|
||||
if (ensureFresh && !response.isResponseFresh) {
|
||||
tasks = apiClient.getTasks(true).responseData
|
||||
tasks?.let { localRepository.saveTasks(tasks, order) }
|
||||
}
|
||||
return tasks
|
||||
}
|
||||
|
||||
|
|
@ -28,7 +33,7 @@ class TaskRepository @Inject constructor(
|
|||
|
||||
suspend fun scoreTask(user: User?, task: Task, direction: TaskDirection): TaskScoringResult? {
|
||||
val id = task.id ?: return null
|
||||
val result = apiClient.scoreTask(id, direction.text)
|
||||
val result = apiClient.scoreTask(id, direction.text).responseData
|
||||
if (result != null) {
|
||||
task.completed = direction == TaskDirection.UP
|
||||
task.value += result.delta
|
||||
|
|
@ -65,7 +70,7 @@ class TaskRepository @Inject constructor(
|
|||
}
|
||||
|
||||
suspend fun createTask(task: Task) {
|
||||
val newTask = apiClient.createTask(task)
|
||||
val newTask = apiClient.createTask(task).responseData
|
||||
if (newTask != null) {
|
||||
localRepository.updateTask(newTask)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.habitrpg.wearos.habitica.data.repositories
|
||||
|
||||
import com.habitrpg.wearos.habitica.data.ApiClient
|
||||
import com.habitrpg.wearos.habitica.models.NetworkResult
|
||||
import com.habitrpg.wearos.habitica.models.user.User
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -8,25 +9,33 @@ class UserRepository @Inject constructor(
|
|||
private val apiClient: ApiClient,
|
||||
private val localRepository: UserLocalRepository
|
||||
) {
|
||||
val userID: String
|
||||
get() = apiClient.userID
|
||||
val hasAuthentication: Boolean
|
||||
get() = apiClient.hasAuthentication()
|
||||
|
||||
fun getUser() = localRepository.getUser()
|
||||
|
||||
suspend fun retrieveUser(forced: Boolean = false): User? {
|
||||
val user = apiClient.getUser(forced)
|
||||
suspend fun retrieveUser(ensureFresh: Boolean = false): User? {
|
||||
var response = apiClient.getUser()
|
||||
var user = (response as? NetworkResult.Success)?.data
|
||||
user?.let { localRepository.saveUser(it) }
|
||||
if (ensureFresh && !response.isResponseFresh) {
|
||||
response = apiClient.getUser(true)
|
||||
user = (response as? NetworkResult.Success)?.data
|
||||
user?.let { localRepository.saveUser(it) }
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
suspend fun updateUser(data: Map<String, Any>): User? {
|
||||
val user = apiClient.updateUser(data)
|
||||
val user = apiClient.updateUser(data).responseData
|
||||
user?.let { localRepository.saveUser(it) }
|
||||
return user
|
||||
}
|
||||
|
||||
suspend fun sleep() = apiClient.sleep()
|
||||
suspend fun revive() = apiClient.revive()
|
||||
suspend fun sleep() = apiClient.sleep().responseData
|
||||
suspend fun revive() = apiClient.revive().responseData
|
||||
suspend fun runCron() {
|
||||
apiClient.runCron()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
package com.habitrpg.wearos.habitica.models
|
||||
|
||||
sealed class NetworkResult<out T : Any> {
|
||||
val isResponseFresh: Boolean
|
||||
get() = if (this is Success) {
|
||||
this.isFresh
|
||||
} else if (this is Error) {
|
||||
this.isFresh
|
||||
} else {
|
||||
false
|
||||
}
|
||||
val responseData: T?
|
||||
get() {
|
||||
return if (this is Success) {
|
||||
this.data
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val isSuccess: Boolean
|
||||
get() = this is Success
|
||||
val isError: Boolean
|
||||
get() = this is Error
|
||||
|
||||
data class Success<out T : Any>(val data: T, val isFresh: Boolean) : NetworkResult<T>()
|
||||
data class Error(val exception: Exception, val isFresh: Boolean) : NetworkResult<Nothing>()
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.habitrpg.wearos.habitica.models
|
|||
|
||||
class WearableHabitResponse<T> {
|
||||
var data: T? = null
|
||||
var success: Boolean? = null
|
||||
var success: Boolean = false
|
||||
var message: String? = null
|
||||
val isFresh: Boolean = true
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ abstract class CheckedTaskViewHolder(itemView: View) : TaskViewHolder(itemView)
|
|||
if (data.completed) {
|
||||
checkbox.setImageResource(R.drawable.checkmark)
|
||||
checkboxWrapper.backgroundTintList =
|
||||
ColorStateList.valueOf(ContextCompat.getColor(itemView.context, R.color.amp_transparent))
|
||||
ColorStateList.valueOf(ContextCompat.getColor(itemView.context, R.color.transparent))
|
||||
checkbox.backgroundTintList = ColorStateList.valueOf(
|
||||
ContextCompat.getColor(
|
||||
itemView.context,
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ class LoginViewModel @Inject constructor(userRepository: UserRepository,
|
|||
auth.authResponse?.client_id = account?.email
|
||||
auth.authResponse?.access_token = token
|
||||
val response = apiClient.loginSocial(auth)
|
||||
handleAuthResponse(response)
|
||||
handleAuthResponse(response.responseData)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -153,7 +153,7 @@ class LoginViewModel @Inject constructor(userRepository: UserRepository,
|
|||
|
||||
fun login(username: String, password: String, onResult: (Boolean) -> Unit) {
|
||||
viewModelScope.launch(exceptionBuilder.userFacing(this)) {
|
||||
val response = apiClient.loginLocal(UserAuth(username, password))
|
||||
val response = apiClient.loginLocal(UserAuth(username, password)).responseData
|
||||
handleAuthResponse(response)
|
||||
onResult(response?.id != null)
|
||||
}.invokeOnCompletion {
|
||||
|
|
|
|||
|
|
@ -60,4 +60,5 @@
|
|||
<color name="exp_bar_color">@color/watch_yellow_100</color>
|
||||
<color name="mp_bar_color">@color/watch_blue_100</color>
|
||||
<color name="hp_bar_color">@color/watch_red_100</color>
|
||||
<color name="transparent">#00ffffff</color>
|
||||
</resources>
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
<string name="you_found">You found…</string>
|
||||
<string name="one_quest_item">1 Quest item</string>
|
||||
<string name="x_quest_item">%d Quest items</string>
|
||||
<string name="x_and_y">%s and %s</string>
|
||||
<string name="x_and_y">%1$s and %2$s</string>
|
||||
<string name="some_x">some %s</string>
|
||||
<string name="edit_on_phone">Edit on phone</string>
|
||||
<string name="sign_in">Sign in</string>
|
||||
|
|
|
|||
Loading…
Reference in a new issue