diff --git a/build.gradle b/build.gradle index 97a6b9d7c..2dfd25719 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/version.properties b/version.properties index 836b20a4b..872b2b01f 100644 --- a/version.properties +++ b/version.properties @@ -1,2 +1,2 @@ NAME=4.0 -CODE=4050 \ No newline at end of file +CODE=4160 \ No newline at end of file diff --git a/wearos/build.gradle b/wearos/build.gradle index 0b2ac1396..cfce73bd7 100644 --- a/wearos/build.gradle +++ b/wearos/build.gradle @@ -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' diff --git a/wearos/src/main/java/com/habitrpg/wearos/habitica/MainApplication.kt b/wearos/src/main/java/com/habitrpg/wearos/habitica/MainApplication.kt index fa3a383fb..0de723f56 100644 --- a/wearos/src/main/java/com/habitrpg/wearos/habitica/MainApplication.kt +++ b/wearos/src/main/java/com/habitrpg/wearos/habitica/MainApplication.kt @@ -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) } } diff --git a/wearos/src/main/java/com/habitrpg/wearos/habitica/data/ApiClient.kt b/wearos/src/main/java/com/habitrpg/wearos/habitica/data/ApiClient.kt index c6778bbe3..604c0acd3 100644 --- a/wearos/src/main/java/com/habitrpg/wearos/habitica/data/ApiClient.kt +++ b/wearos/src/main/java/com/habitrpg/wearos/habitica/data/ApiClient.kt @@ -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 process(response: WearableHabitResponse): T? { - return response.data + private suspend fun process(call: suspend () -> Response>): NetworkResult { + val response: Response> + 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) = process(apiService.updateUser(data)) - suspend fun sleep() = process(apiService.sleep()) - suspend fun revive() = process(apiService.revive()) + suspend fun updateUser(data: Map) = 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) = process(apiService.addPushDevice(data)) - suspend fun removePushDevice(id: String) = process(apiService.removePushDevice(id)) + suspend fun addPushDevice(data: Map) = 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() } \ No newline at end of file diff --git a/wearos/src/main/java/com/habitrpg/wearos/habitica/data/ApiService.kt b/wearos/src/main/java/com/habitrpg/wearos/habitica/data/ApiService.kt index 4e1c1b4ea..b87455033 100644 --- a/wearos/src/main/java/com/habitrpg/wearos/habitica/data/ApiService.kt +++ b/wearos/src/main/java/com/habitrpg/wearos/habitica/data/ApiService.kt @@ -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 + suspend fun getUser(): Response> @GET("user/") @Headers("Cache-Control: no-cache") - suspend fun getUserForced(): WearableHabitResponse + suspend fun getUserForced(): Response> @PUT("user/") - suspend fun updateUser(@Body updateDictionary: Map): WearableHabitResponse + suspend fun updateUser(@Body updateDictionary: Map): Response> @PUT("user/") - suspend fun registrationLanguage(@Header("Accept-Language") registrationLanguage: String): WearableHabitResponse + suspend fun registrationLanguage(@Header("Accept-Language") registrationLanguage: String): Response> @GET("tasks/user") - suspend fun getTasks(): WearableHabitResponse + suspend fun getTasks(): Response> @GET("tasks/user") @Headers("Cache-Control: no-cache") - suspend fun getTasksForced(): WearableHabitResponse + suspend fun getTasksForced(): Response> @GET("tasks/user") - suspend fun getTasks(@Query("type") type: String): WearableHabitResponse + suspend fun getTasks(@Query("type") type: String): Response> @GET("tasks/user") - suspend fun getTasks(@Query("type") type: String, @Query("dueDate") dueDate: String): WearableHabitResponse + suspend fun getTasks(@Query("type") type: String, @Query("dueDate") dueDate: String): Response> @GET("tasks/{id}") - suspend fun getTask(@Path("id") id: String): WearableHabitResponse + suspend fun getTask(@Path("id") id: String): Response> @POST("tasks/{id}/score/{direction}") - suspend fun scoreTask(@Path("id") id: String, @Path("direction") direction: String): WearableHabitResponse + suspend fun scoreTask(@Path("id") id: String, @Path("direction") direction: String): Response> @POST("tasks/bulk-score") - suspend fun bulkScoreTasks(@Body data: List>): WearableHabitResponse + suspend fun bulkScoreTasks(@Body data: List>): Response> @POST("tasks/{id}/move/to/{position}") - suspend fun postTaskNewPosition(@Path("id") id: String, @Path("position") position: Int): WearableHabitResponse> + suspend fun postTaskNewPosition(@Path("id") id: String, @Path("position") position: Int): Response>> @POST("tasks/{taskId}/checklist/{itemId}/score") - suspend fun scoreChecklistItem(@Path("taskId") taskId: String, @Path("itemId") itemId: String): WearableHabitResponse + suspend fun scoreChecklistItem(@Path("taskId") taskId: String, @Path("itemId") itemId: String): Response> @POST("tasks/user") - suspend fun createTask(@Body item: Task): WearableHabitResponse + suspend fun createTask(@Body item: Task): Response> @POST("tasks/user") - suspend fun createTasks(@Body tasks: List): WearableHabitResponse> + suspend fun createTasks(@Body tasks: List): Response>> @PUT("tasks/{id}") - suspend fun updateTask(@Path("id") id: String, @Body item: Task): WearableHabitResponse + suspend fun updateTask(@Path("id") id: String, @Body item: Task): Response> @DELETE("tasks/{id}") - suspend fun deleteTask(@Path("id") id: String): WearableHabitResponse + suspend fun deleteTask(@Path("id") id: String): Response> @POST("user/auth/local/register") - suspend fun registerUser(@Body auth: UserAuth): WearableHabitResponse + suspend fun registerUser(@Body auth: UserAuth): Response> @POST("user/auth/local/login") - suspend fun connectLocal(@Body auth: UserAuth): WearableHabitResponse + suspend fun connectLocal(@Body auth: UserAuth): Response> @POST("user/auth/social") - suspend fun connectSocial(@Body auth: UserAuthSocial): WearableHabitResponse + suspend fun connectSocial(@Body auth: UserAuthSocial): Response> @DELETE("user/auth/social/{network}") - suspend fun disconnectSocial(@Path("network") network: String): WearableHabitResponse + suspend fun disconnectSocial(@Path("network") network: String): Response> @POST("user/auth/apple") - suspend fun loginApple(@Body auth: Map): WearableHabitResponse + suspend fun loginApple(@Body auth: Map): Response> @POST("user/sleep") - suspend fun sleep(): WearableHabitResponse + suspend fun sleep(): Response> @POST("user/revive") - suspend fun revive(): WearableHabitResponse + suspend fun revive(): Response> // Push notifications @POST("user/push-devices") - suspend fun addPushDevice(@Body pushDeviceData: Map): WearableHabitResponse> + suspend fun addPushDevice(@Body pushDeviceData: Map): Response>> @DELETE("user/push-devices/{regId}") - suspend fun removePushDevice(@Path("regId") regId: String): WearableHabitResponse> + suspend fun removePushDevice(@Path("regId") regId: String): Response>> @POST("cron") - suspend fun runCron(): WearableHabitResponse + suspend fun runCron(): Response> } \ No newline at end of file diff --git a/wearos/src/main/java/com/habitrpg/wearos/habitica/data/repositories/TaskRepository.kt b/wearos/src/main/java/com/habitrpg/wearos/habitica/data/repositories/TaskRepository.kt index d5ec83c93..5f1efd5f5 100644 --- a/wearos/src/main/java/com/habitrpg/wearos/habitica/data/repositories/TaskRepository.kt +++ b/wearos/src/main/java/com/habitrpg/wearos/habitica/data/repositories/TaskRepository.kt @@ -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) } diff --git a/wearos/src/main/java/com/habitrpg/wearos/habitica/data/repositories/UserRepository.kt b/wearos/src/main/java/com/habitrpg/wearos/habitica/data/repositories/UserRepository.kt index 29baa4726..891c6b133 100644 --- a/wearos/src/main/java/com/habitrpg/wearos/habitica/data/repositories/UserRepository.kt +++ b/wearos/src/main/java/com/habitrpg/wearos/habitica/data/repositories/UserRepository.kt @@ -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): 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() } diff --git a/wearos/src/main/java/com/habitrpg/wearos/habitica/models/Result.kt b/wearos/src/main/java/com/habitrpg/wearos/habitica/models/Result.kt new file mode 100644 index 000000000..a21ed6f23 --- /dev/null +++ b/wearos/src/main/java/com/habitrpg/wearos/habitica/models/Result.kt @@ -0,0 +1,28 @@ +package com.habitrpg.wearos.habitica.models + +sealed class NetworkResult { + 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(val data: T, val isFresh: Boolean) : NetworkResult() + data class Error(val exception: Exception, val isFresh: Boolean) : NetworkResult() +} \ No newline at end of file diff --git a/wearos/src/main/java/com/habitrpg/wearos/habitica/models/WearableHabitResponse.kt b/wearos/src/main/java/com/habitrpg/wearos/habitica/models/WearableHabitResponse.kt index 1b949dd12..54302c16e 100644 --- a/wearos/src/main/java/com/habitrpg/wearos/habitica/models/WearableHabitResponse.kt +++ b/wearos/src/main/java/com/habitrpg/wearos/habitica/models/WearableHabitResponse.kt @@ -2,6 +2,7 @@ package com.habitrpg.wearos.habitica.models class WearableHabitResponse { var data: T? = null - var success: Boolean? = null + var success: Boolean = false var message: String? = null + val isFresh: Boolean = true } \ No newline at end of file diff --git a/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/viewHolders/tasks/CheckedTaskViewHolder.kt b/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/viewHolders/tasks/CheckedTaskViewHolder.kt index 863093323..b9ab7c41b 100644 --- a/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/viewHolders/tasks/CheckedTaskViewHolder.kt +++ b/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/viewHolders/tasks/CheckedTaskViewHolder.kt @@ -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, diff --git a/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/viewmodels/LoginViewModel.kt b/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/viewmodels/LoginViewModel.kt index 6e3739759..9c2100d56 100644 --- a/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/viewmodels/LoginViewModel.kt +++ b/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/viewmodels/LoginViewModel.kt @@ -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 { diff --git a/wearos/src/main/res/values/colors.xml b/wearos/src/main/res/values/colors.xml index 706e492a2..5799d09b1 100644 --- a/wearos/src/main/res/values/colors.xml +++ b/wearos/src/main/res/values/colors.xml @@ -60,4 +60,5 @@ @color/watch_yellow_100 @color/watch_blue_100 @color/watch_red_100 + #00ffffff \ No newline at end of file diff --git a/wearos/src/main/res/values/strings.xml b/wearos/src/main/res/values/strings.xml index fdbb2ba62..7ae3faeb5 100644 --- a/wearos/src/main/res/values/strings.xml +++ b/wearos/src/main/res/values/strings.xml @@ -5,7 +5,7 @@ You found… 1 Quest item %d Quest items - %s and %s + %1$s and %2$s some %s Edit on phone Sign in