port more code to coroutines

This commit is contained in:
Phillip Thelen 2022-11-15 16:06:43 +01:00
parent bd4b04f86e
commit cedc06f1e9
38 changed files with 495 additions and 472 deletions

View file

@ -72,41 +72,38 @@ interface ApiService {
suspend fun getContent(@Query("language") language: String?): HabitResponse<ContentResult>
@PUT("user/")
fun updateUser(@Body updateDictionary: Map<String, Any>): Flowable<HabitResponse<User>>
suspend fun updateUser(@Body updateDictionary: Map<String, Any>): HabitResponse<User>
@PUT("user/")
fun registrationLanguage(@Header("Accept-Language") registrationLanguage: String): Flowable<HabitResponse<User>>
@GET("user/in-app-rewards")
fun retrieveInAppRewards(): Flowable<HabitResponse<List<ShopItem>>>
@GET("user/inventory/buy")
fun retrieveOldGearRewards(): Flowable<HabitResponse<List<ShopItem>>>
suspend fun retrieveInAppRewards(): HabitResponse<List<ShopItem>>
@POST("user/equip/{type}/{key}")
fun equipItem(@Path("type") type: String, @Path("key") itemKey: String): Flowable<HabitResponse<Items>>
@POST("user/buy/{key}")
fun buyItem(@Path("key") itemKey: String, @Body quantity: Map<String, Int>): Flowable<HabitResponse<BuyResponse>>
suspend fun buyItem(@Path("key") itemKey: String, @Body quantity: Map<String, Int>): HabitResponse<BuyResponse>
@POST("user/purchase/{type}/{key}")
fun purchaseItem(
suspend fun purchaseItem(
@Path("type") type: String,
@Path("key") itemKey: String,
@Body quantity: Map<String, Int>
): Flowable<HabitResponse<Void>>
): HabitResponse<Void>
@POST("user/purchase-hourglass/{type}/{key}")
fun purchaseHourglassItem(@Path("type") type: String, @Path("key") itemKey: String): Flowable<HabitResponse<Void>>
suspend fun purchaseHourglassItem(@Path("type") type: String, @Path("key") itemKey: String): HabitResponse<Void>
@POST("user/buy-mystery-set/{key}")
fun purchaseMysterySet(@Path("key") itemKey: String): Flowable<HabitResponse<Void>>
suspend fun purchaseMysterySet(@Path("key") itemKey: String): HabitResponse<Void>
@POST("user/buy-quest/{key}")
fun purchaseQuest(@Path("key") key: String): Flowable<HabitResponse<Void>>
suspend fun purchaseQuest(@Path("key") key: String): HabitResponse<Void>
@POST("user/buy-special-spell/{key}")
fun purchaseSpecialSpell(@Path("key") key: String): Flowable<HabitResponse<Void>>
suspend fun purchaseSpecialSpell(@Path("key") key: String): HabitResponse<Void>
@POST("user/sell/{type}/{key}")
fun sellItem(@Path("type") itemType: String, @Path("key") itemKey: String): Flowable<HabitResponse<User>>
@ -124,7 +121,7 @@ interface ApiService {
fun getTasks(@Query("type") type: String, @Query("dueDate") dueDate: String): Flowable<HabitResponse<TaskList>>
@POST("user/unlock")
fun unlockPath(@Query("path") path: String): Flowable<HabitResponse<UnlockResponse>>
suspend fun unlockPath(@Query("path") path: String): HabitResponse<UnlockResponse>
@GET("tasks/{id}")
fun getTask(@Path("id") id: String): Flowable<HabitResponse<Task>>
@ -183,14 +180,14 @@ interface ApiService {
suspend fun revive(): HabitResponse<User>
@POST("user/class/cast/{skill}")
fun useSkill(
suspend fun useSkill(
@Path("skill") skillName: String,
@Query("targetType") targetType: String,
@Query("targetId") targetId: String
): Flowable<HabitResponse<SkillResponse>>
): HabitResponse<SkillResponse>
@POST("user/class/cast/{skill}")
fun useSkill(@Path("skill") skillName: String, @Query("targetType") targetType: String): Flowable<HabitResponse<SkillResponse>>
suspend fun useSkill(@Path("skill") skillName: String, @Query("targetType") targetType: String): HabitResponse<SkillResponse>
@POST("user/change-class")
suspend fun changeClass(): HabitResponse<User>
@ -407,13 +404,13 @@ interface ApiService {
fun deleteAccount(@Body body: Map<String, String>): Flowable<HabitResponse<Void>>
@GET("user/toggle-pinned-item/{pinType}/{path}")
fun togglePinnedItem(@Path("pinType") pinType: String, @Path("path") path: String): Flowable<HabitResponse<Void>>
suspend fun togglePinnedItem(@Path("pinType") pinType: String, @Path("path") path: String): HabitResponse<Void>
@POST("user/reset-password")
fun sendPasswordResetEmail(@Body data: Map<String, String>): Flowable<HabitResponse<Void>>
@PUT("user/auth/update-username")
fun updateLoginName(@Body data: Map<String, String>): Flowable<HabitResponse<Void>>
suspend fun updateLoginName(@Body data: Map<String, String>): HabitResponse<Void>
@POST("user/auth/verify-username")
fun verifyUsername(@Body data: Map<String, String>): Flowable<HabitResponse<VerifyUsernameResponse>>

View file

@ -58,25 +58,24 @@ interface ApiClient {
fun setLanguageCode(languageCode: String)
suspend fun getContent(language: String? = null): ContentResult?
fun updateUser(updateDictionary: Map<String, Any>): Flowable<User>
suspend fun updateUser(updateDictionary: Map<String, Any>): User?
fun registrationLanguage(registrationLanguage: String): Flowable<User>
fun retrieveInAppRewards(): Flowable<List<ShopItem>>
fun retrieveOldGear(): Flowable<List<ShopItem>>
suspend fun retrieveInAppRewards(): List<ShopItem>?
fun equipItem(type: String, itemKey: String): Flowable<Items>
fun buyItem(itemKey: String, purchaseQuantity: Int): Flowable<BuyResponse>
suspend fun buyItem(itemKey: String, purchaseQuantity: Int): BuyResponse?
fun purchaseItem(type: String, itemKey: String, purchaseQuantity: Int): Flowable<Void>
suspend fun purchaseItem(type: String, itemKey: String, purchaseQuantity: Int): Void?
fun purchaseHourglassItem(type: String, itemKey: String): Flowable<Void>
suspend fun purchaseHourglassItem(type: String, itemKey: String): Void?
fun purchaseMysterySet(itemKey: String): Flowable<Void>
suspend fun purchaseMysterySet(itemKey: String): Void?
fun purchaseQuest(key: String): Flowable<Void>
fun purchaseSpecialSpell(key: String): Flowable<Void>
suspend fun purchaseQuest(key: String): Void?
suspend fun purchaseSpecialSpell(key: String): Void?
fun validateSubscription(request: PurchaseValidationRequest): Flowable<Any>
fun validateNoRenewSubscription(request: PurchaseValidationRequest): Flowable<Any>
suspend fun cancelSubscription(): Void?
@ -89,7 +88,7 @@ interface ApiClient {
fun getTasks(type: String): Flowable<TaskList>
fun getTasks(type: String, dueDate: String): Flowable<TaskList>
fun unlockPath(path: String): Flowable<UnlockResponse>
suspend fun unlockPath(path: String): UnlockResponse?
fun getTask(id: String): Flowable<Task>
@ -126,9 +125,9 @@ interface ApiClient {
suspend fun sleep(): Boolean?
suspend fun revive(): User?
fun useSkill(skillName: String, targetType: String, targetId: String): Flowable<SkillResponse>
suspend fun useSkill(skillName: String, targetType: String, targetId: String): SkillResponse?
fun useSkill(skillName: String, targetType: String): Flowable<SkillResponse>
suspend fun useSkill(skillName: String, targetType: String): SkillResponse?
suspend fun changeClass(className: String?): User?
@ -251,12 +250,12 @@ interface ApiClient {
fun resetAccount(): Flowable<Void>
fun deleteAccount(password: String): Flowable<Void>
fun togglePinnedItem(pinType: String, path: String): Flowable<Void>
suspend fun togglePinnedItem(pinType: String, path: String): Void?
fun sendPasswordResetEmail(email: String): Flowable<Void>
fun updateLoginName(newLoginName: String, password: String): Flowable<Void>
fun updateUsername(newLoginName: String): Flowable<Void>
suspend fun updateLoginName(newLoginName: String, password: String): Void?
suspend fun updateUsername(newLoginName: String): Void?
fun updateEmail(newEmail: String, password: String): Flowable<Void>

View file

@ -39,7 +39,7 @@ interface InventoryRepository : BaseRepository {
fun getQuestContent(keys: List<String>): Flow<List<QuestContent>>
fun getEquipment(searchedKeys: List<String>): Flowable<out List<Equipment>>
fun retrieveInAppRewards(): Flowable<List<ShopItem>>
suspend fun retrieveInAppRewards(): List<ShopItem>?
fun getOwnedEquipment(type: String): Flowable<out List<Equipment>>
fun getEquipmentType(type: String, set: String): Flowable<out List<Equipment>>
@ -71,21 +71,21 @@ interface InventoryRepository : BaseRepository {
fun inviteToQuest(quest: QuestContent): Flowable<Quest>
fun buyItem(user: User?, id: String, value: Double, purchaseQuantity: Int): Flowable<BuyResponse>
suspend fun buyItem(user: User?, id: String, value: Double, purchaseQuantity: Int): BuyResponse?
fun retrieveShopInventory(identifier: String): Flowable<Shop>
fun retrieveMarketGear(): Flowable<Shop>
fun purchaseMysterySet(categoryIdentifier: String): Flowable<Void>
suspend fun purchaseMysterySet(categoryIdentifier: String): Void?
fun purchaseHourglassItem(purchaseType: String, key: String): Flowable<Void>
suspend fun purchaseHourglassItem(purchaseType: String, key: String): Void?
fun purchaseQuest(key: String): Flowable<Void>
fun purchaseSpecialSpell(key: String): Flowable<Void>
suspend fun purchaseQuest(key: String): Void?
suspend fun purchaseSpecialSpell(key: String): Void?
fun purchaseItem(purchaseType: String, key: String, purchaseQuantity: Int): Flowable<Void>
suspend fun purchaseItem(purchaseType: String, key: String, purchaseQuantity: Int): Void?
fun togglePinnedItem(item: ShopItem): Flowable<List<ShopItem>>
suspend fun togglePinnedItem(item: ShopItem): List<ShopItem>?
fun getItems(itemClass: Class<out Item>, keys: Array<String>): Flow<List<Item>>
fun getItemsFlowable(itemClass: Class<out Item>): Flowable<out List<Item>>
fun getItems(itemClass: Class<out Item>): Flow<List<Item>>

View file

@ -1,6 +1,5 @@
package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.user.UserQuestStatus
import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.models.QuestAchievement
import com.habitrpg.android.habitica.models.Skill
@ -8,14 +7,14 @@ import com.habitrpg.android.habitica.models.TeamPlan
import com.habitrpg.android.habitica.models.inventory.Customization
import com.habitrpg.android.habitica.models.responses.SkillResponse
import com.habitrpg.android.habitica.models.responses.UnlockResponse
import com.habitrpg.shared.habitica.models.responses.VerifyUsernameResponse
import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.shared.habitica.models.tasks.Attribute
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.user.Stats
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.models.user.UserQuestStatus
import com.habitrpg.shared.habitica.models.responses.VerifyUsernameResponse
import com.habitrpg.shared.habitica.models.tasks.Attribute
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Maybe
import kotlinx.coroutines.flow.Flow
interface UserRepository : BaseRepository {
@ -23,14 +22,14 @@ interface UserRepository : BaseRepository {
fun getUserFlowable(): Flowable<User>
fun getUser(userID: String): Flow<User?>
fun updateUser(updateData: Map<String, Any>): Flowable<User>
fun updateUser(key: String, value: Any): Flowable<User>
suspend fun updateUser(updateData: Map<String, Any>): User?
suspend fun updateUser(key: String, value: Any): User?
suspend fun retrieveUser(withTasks: Boolean = false, forced: Boolean = false, overrideExisting: Boolean = false): User?
suspend fun revive(): User?
fun resetTutorial(): Maybe<User>
suspend fun resetTutorial(): User?
suspend fun sleep(user: User): User?
@ -38,14 +37,14 @@ interface UserRepository : BaseRepository {
fun getSpecialItems(user: User): Flowable<out List<Skill>>
fun useSkill(key: String, target: String?, taskId: String): Flowable<SkillResponse>
fun useSkill(key: String, target: String?): Flowable<SkillResponse>
suspend fun useSkill(key: String, target: String?, taskId: String): SkillResponse?
suspend fun useSkill(key: String, target: String?): SkillResponse?
suspend fun disableClasses(): User?
suspend fun changeClass(selectedClass: String? = null): User?
fun unlockPath(path: String, price: Int): Flowable<UnlockResponse>
fun unlockPath(customization: Customization): Flowable<UnlockResponse>
suspend fun unlockPath(path: String, price: Int): UnlockResponse?
suspend fun unlockPath(customization: Customization): UnlockResponse?
suspend fun runCron(tasks: MutableList<Task>)
suspend fun runCron()
@ -56,14 +55,14 @@ interface UserRepository : BaseRepository {
fun changeCustomDayStart(dayStartTime: Int): Flowable<User>
fun updateLanguage(languageCode: String): Flowable<User>
suspend fun updateLanguage(languageCode: String): User?
suspend fun resetAccount(): User?
fun deleteAccount(password: String): Flowable<Void>
fun sendPasswordResetEmail(email: String): Flowable<Void>
fun updateLoginName(newLoginName: String, password: String? = null): Maybe<User>
suspend fun updateLoginName(newLoginName: String, password: String? = null): User?
fun updateEmail(newEmail: String, password: String): Flowable<Void>
fun updatePassword(oldPassword: String, newPassword: String, newPasswordConfirmation: String): Flowable<Void>
fun verifyUsername(username: String): Flowable<VerifyUsernameResponse>
@ -71,7 +70,7 @@ interface UserRepository : BaseRepository {
fun allocatePoint(stat: Attribute): Flowable<Stats>
fun bulkAllocatePoints(strength: Int, intelligence: Int, constitution: Int, perception: Int): Flowable<Stats>
fun useCustomization(type: String, category: String?, identifier: String): Flowable<User>
suspend fun useCustomization(type: String, category: String?, identifier: String): User?
fun retrieveAchievements(): Flowable<List<Achievement>>
fun getAchievements(): Flow<List<Achievement>>
fun getQuestAchievements(): Flow<List<QuestAchievement>>

View file

@ -105,7 +105,7 @@ class ApiClientImpl(
return habitResponse.data
}
suspend fun <T> handleSuspendCall(apiCall: suspend () -> HabitResponse<T>): T? {
private suspend fun <T> process(apiCall: suspend () -> HabitResponse<T>): T? {
try {
return processResponse(apiCall())
} catch (throwable: Throwable) {
@ -280,14 +280,14 @@ class ApiClientImpl(
}
override suspend fun retrieveUser(withTasks: Boolean): User? {
val user = handleSuspendCall { apiService.getUser() }
val user = process { apiService.getUser() }
val tasks = getTasks()
user?.tasks = tasks
return user
}
override suspend fun retrieveInboxMessages(uuid: String, page: Int): List<ChatMessage>? {
return handleSuspendCall { apiService.getInboxMessages(uuid, page) }
return process { apiService.getInboxMessages(uuid, page) }
}
override fun retrieveInboxConversations(): Flowable<List<InboxConversation>> {
@ -346,34 +346,30 @@ class ApiClientImpl(
this.languageCode = languageCode
}
override suspend fun getStatus(): Status? = handleSuspendCall { apiService.getStatus() }
override suspend fun getStatus(): Status? = process { apiService.getStatus() }
override suspend fun getContent(language: String?): ContentResult? {
return handleSuspendCall { apiService.getContent(language) }
return process { apiService.getContent(language) }
}
override fun updateUser(updateDictionary: Map<String, Any>): Flowable<User> {
return apiService.updateUser(updateDictionary).compose(configureApiCallObserver())
override suspend fun updateUser(updateDictionary: Map<String, Any>): User? {
return process { apiService.updateUser(updateDictionary) }
}
override fun registrationLanguage(registrationLanguage: String): Flowable<User> {
return apiService.registrationLanguage(registrationLanguage).compose(configureApiCallObserver())
}
override fun retrieveInAppRewards(): Flowable<List<ShopItem>> {
return apiService.retrieveInAppRewards().compose(configureApiCallObserver())
}
override fun retrieveOldGear(): Flowable<List<ShopItem>> {
return apiService.retrieveOldGearRewards().compose(configureApiCallObserver())
override suspend fun retrieveInAppRewards(): List<ShopItem>? {
return process { apiService.retrieveInAppRewards() }
}
override fun equipItem(type: String, itemKey: String): Flowable<Items> {
return apiService.equipItem(type, itemKey).compose(configureApiCallObserver())
}
override fun buyItem(itemKey: String, purchaseQuantity: Int): Flowable<BuyResponse> {
return apiService.buyItem(itemKey, mapOf(Pair("quantity", purchaseQuantity))).compose(configureApiCallObserver())
override suspend fun buyItem(itemKey: String, purchaseQuantity: Int): BuyResponse? {
return process { apiService.buyItem(itemKey, mapOf(Pair("quantity", purchaseQuantity))) }
}
override fun unlinkAllTasks(challengeID: String?, keepOption: String): Flowable<Void> {
@ -384,8 +380,8 @@ class ApiClientImpl(
return apiService.blockMember(userID).compose(configureApiCallObserver())
}
override fun purchaseItem(type: String, itemKey: String, purchaseQuantity: Int): Flowable<Void> {
return apiService.purchaseItem(type, itemKey, mapOf(Pair("quantity", purchaseQuantity))).compose(configureApiCallObserver())
override suspend fun purchaseItem(type: String, itemKey: String, purchaseQuantity: Int): Void? {
return process { apiService.purchaseItem(type, itemKey, mapOf(Pair("quantity", purchaseQuantity))) }
}
override fun validateSubscription(request: PurchaseValidationRequest): Flowable<Any> {
@ -400,20 +396,20 @@ class ApiClientImpl(
return processResponse(apiService.cancelSubscription())
}
override fun purchaseHourglassItem(type: String, itemKey: String): Flowable<Void> {
return apiService.purchaseHourglassItem(type, itemKey).compose(configureApiCallObserver())
override suspend fun purchaseHourglassItem(type: String, itemKey: String): Void? {
return process { apiService.purchaseHourglassItem(type, itemKey) }
}
override fun purchaseMysterySet(itemKey: String): Flowable<Void> {
return apiService.purchaseMysterySet(itemKey).compose(configureApiCallObserver())
override suspend fun purchaseMysterySet(itemKey: String): Void? {
return process { apiService.purchaseMysterySet(itemKey) }
}
override fun purchaseQuest(key: String): Flowable<Void> {
return apiService.purchaseQuest(key).compose(configureApiCallObserver())
override suspend fun purchaseQuest(key: String): Void? {
return process { apiService.purchaseQuest(key) }
}
override fun purchaseSpecialSpell(key: String): Flowable<Void> {
return apiService.purchaseSpecialSpell(key).compose(configureApiCallObserver())
override suspend fun purchaseSpecialSpell(key: String): Void? {
return process { apiService.purchaseSpecialSpell(key) }
}
override fun sellItem(itemType: String, itemKey: String): Flowable<User> {
@ -433,7 +429,7 @@ class ApiClientImpl(
return apiService.hatchPet(eggKey, hatchingPotionKey).compose(configureApiCallObserver())
}
override suspend fun getTasks(): TaskList? = handleSuspendCall { apiService.getTasks() }
override suspend fun getTasks(): TaskList? = process { apiService.getTasks() }
override fun getTasks(type: String): Flowable<TaskList> {
return apiService.getTasks(type).compose(configureApiCallObserver())
@ -443,8 +439,8 @@ class ApiClientImpl(
return apiService.getTasks(type, dueDate).compose(configureApiCallObserver())
}
override fun unlockPath(path: String): Flowable<UnlockResponse> {
return apiService.unlockPath(path).compose(configureApiCallObserver())
override suspend fun unlockPath(path: String): UnlockResponse? {
return process { apiService.unlockPath(path) }
}
override fun getTask(id: String): Flowable<Task> {
@ -452,7 +448,7 @@ class ApiClientImpl(
}
override suspend fun postTaskDirection(id: String, direction: String): TaskDirectionData? {
return handleSuspendCall { apiService.postTaskDirection(id, direction) }
return process { apiService.postTaskDirection(id, direction) }
}
override fun bulkScoreTasks(data: List<Map<String, String>>): Flowable<BulkTaskScoringData> {
@ -464,7 +460,7 @@ class ApiClientImpl(
}
override suspend fun scoreChecklistItem(taskId: String, itemId: String): Task? {
return handleSuspendCall { apiService.scoreChecklistItem(taskId, itemId) }
return process { apiService.scoreChecklistItem(taskId, itemId) }
}
override fun createTask(item: Task): Flowable<Task> {
@ -495,20 +491,20 @@ class ApiClientImpl(
return apiService.deleteTag(id).compose(configureApiCallObserver())
}
override suspend fun sleep(): Boolean? = handleSuspendCall { apiService.sleep() }
override suspend fun sleep(): Boolean? = process { apiService.sleep() }
override suspend fun revive(): User? = handleSuspendCall { apiService.revive() }
override suspend fun revive(): User? = process { apiService.revive() }
override fun useSkill(skillName: String, targetType: String, targetId: String): Flowable<SkillResponse> {
return apiService.useSkill(skillName, targetType, targetId).compose(configureApiCallObserver())
suspend override fun useSkill(skillName: String, targetType: String, targetId: String): SkillResponse? {
return process { apiService.useSkill(skillName, targetType, targetId) }
}
override fun useSkill(skillName: String, targetType: String): Flowable<SkillResponse> {
return apiService.useSkill(skillName, targetType).compose(configureApiCallObserver())
suspend override fun useSkill(skillName: String, targetType: String): SkillResponse? {
return process { apiService.useSkill(skillName, targetType) }
}
override suspend fun changeClass(className: String?): User? {
return handleSuspendCall {
return process {
if (className != null) {
apiService.changeClass(className)
} else {
@ -517,7 +513,7 @@ class ApiClientImpl(
}
}
override suspend fun disableClasses(): User? = handleSuspendCall { apiService.disableClasses() }
override suspend fun disableClasses(): User? = process { apiService.disableClasses() }
override fun markPrivateMessagesRead(): Flowable<Void> {
// This is necessary, because the API call returns weird data.
@ -650,7 +646,7 @@ class ApiClientImpl(
}
override suspend fun postPrivateMessage(messageDetails: Map<String, String>): PostChatMessageResult? {
return handleSuspendCall { apiService.postPrivateMessage(messageDetails) }
return process { apiService.postPrivateMessage(messageDetails) }
}
override fun retrieveShopIventory(identifier: String): Flowable<Shop> {
@ -733,7 +729,7 @@ class ApiClientImpl(
return apiService.runCron().compose(configureApiCallObserver())
}
override suspend fun reroll(): User? = handleSuspendCall { apiService.reroll() }
override suspend fun reroll(): User? = process { apiService.reroll() }
override fun resetAccount(): Flowable<Void> {
return apiService.resetAccount().compose(configureApiCallObserver())
@ -745,8 +741,8 @@ class ApiClientImpl(
return apiService.deleteAccount(updateObject).compose(configureApiCallObserver())
}
override fun togglePinnedItem(pinType: String, path: String): Flowable<Void> {
return apiService.togglePinnedItem(pinType, path).compose(configureApiCallObserver())
override suspend fun togglePinnedItem(pinType: String, path: String): Void? {
return process { apiService.togglePinnedItem(pinType, path) }
}
override fun sendPasswordResetEmail(email: String): Flowable<Void> {
@ -755,17 +751,17 @@ class ApiClientImpl(
return apiService.sendPasswordResetEmail(data).compose(configureApiCallObserver())
}
override fun updateLoginName(newLoginName: String, password: String): Flowable<Void> {
override suspend fun updateLoginName(newLoginName: String, password: String): Void? {
val updateObject = HashMap<String, String>()
updateObject["username"] = newLoginName
updateObject["password"] = password
return apiService.updateLoginName(updateObject).compose(configureApiCallObserver())
return process { apiService.updateLoginName(updateObject) }
}
override fun updateUsername(newLoginName: String): Flowable<Void> {
override suspend fun updateUsername(newLoginName: String): Void? {
val updateObject = HashMap<String, String>()
updateObject["username"] = newLoginName
return apiService.updateLoginName(updateObject).compose(configureApiCallObserver())
return process { apiService.updateLoginName(updateObject) }
}
override fun verifyUsername(username: String): Flowable<VerifyUsernameResponse> {
@ -831,7 +827,7 @@ class ApiClientImpl(
return apiService.retrieveMarketGear(languageCode).compose(configureApiCallObserver())
}
override suspend fun getWorldState(): WorldState? = handleSuspendCall { apiService.worldState() }
override suspend fun getWorldState(): WorldState? = process { apiService.worldState() }
companion object {
fun createGsonFactory(): GsonConverterFactory {

View file

@ -47,8 +47,12 @@ class InventoryRepositoryImpl(
return localRepository.getInAppRewards()
}
override fun retrieveInAppRewards(): Flowable<List<ShopItem>> {
return apiClient.retrieveInAppRewards().doOnNext { localRepository.saveInAppRewards(it) }
override suspend fun retrieveInAppRewards(): List<ShopItem>? {
val rewards = apiClient.retrieveInAppRewards()
if (rewards != null) {
localRepository.saveInAppRewards(rewards)
}
return rewards
}
override fun getOwnedEquipment(type: String): Flowable<out List<Equipment>> {
@ -238,35 +242,32 @@ class InventoryRepositoryImpl(
.doOnNext { localRepository.changeOwnedCount("quests", quest.key, userID, -1) }
}
override fun buyItem(user: User?, id: String, value: Double, purchaseQuantity: Int): Flowable<BuyResponse> {
return apiClient.buyItem(id, purchaseQuantity)
.doOnNext { buyResponse ->
if (user == null) {
return@doOnNext
}
val copiedUser = localRepository.getUnmanagedCopy(user)
if (buyResponse.items != null) {
copiedUser.items = buyResponse.items
}
if (buyResponse.hp != null) {
copiedUser.stats?.hp = buyResponse.hp
}
if (buyResponse.exp != null) {
copiedUser.stats?.exp = buyResponse.exp
}
if (buyResponse.mp != null) {
copiedUser.stats?.mp = buyResponse.mp
}
if (buyResponse.gp != null) {
copiedUser.stats?.gp = buyResponse.gp
} else {
copiedUser.stats?.gp = copiedUser.stats?.gp ?: 0 - (value * purchaseQuantity)
}
if (buyResponse.lvl != null) {
copiedUser.stats?.lvl = buyResponse.lvl
}
localRepository.save(copiedUser)
}
override suspend fun buyItem(user: User?, id: String, value: Double, purchaseQuantity: Int): BuyResponse? {
val buyResponse = apiClient.buyItem(id, purchaseQuantity) ?: return null
val foundUser = user ?: localRepository.getLiveUser(userID) ?: return buyResponse
val copiedUser = localRepository.getUnmanagedCopy(foundUser)
if (buyResponse.items != null) {
copiedUser.items = buyResponse.items
}
if (buyResponse.hp != null) {
copiedUser.stats?.hp = buyResponse.hp
}
if (buyResponse.exp != null) {
copiedUser.stats?.exp = buyResponse.exp
}
if (buyResponse.mp != null) {
copiedUser.stats?.mp = buyResponse.mp
}
if (buyResponse.gp != null) {
copiedUser.stats?.gp = buyResponse.gp
} else {
copiedUser.stats?.gp = (copiedUser.stats?.gp ?: 0.0) - (value * purchaseQuantity)
}
if (buyResponse.lvl != null) {
copiedUser.stats?.lvl = buyResponse.lvl
}
localRepository.save(copiedUser)
return buyResponse
}
override fun getAvailableLimitedItems(): Flowable<List<Item>> {
@ -281,30 +282,30 @@ class InventoryRepositoryImpl(
return apiClient.retrieveMarketGear()
}
override fun purchaseMysterySet(categoryIdentifier: String): Flowable<Void> {
override suspend fun purchaseMysterySet(categoryIdentifier: String): Void? {
return apiClient.purchaseMysterySet(categoryIdentifier)
}
override fun purchaseHourglassItem(purchaseType: String, key: String): Flowable<Void> {
override suspend fun purchaseHourglassItem(purchaseType: String, key: String): Void? {
return apiClient.purchaseHourglassItem(purchaseType, key)
}
override fun purchaseQuest(key: String): Flowable<Void> {
override suspend fun purchaseQuest(key: String): Void? {
return apiClient.purchaseQuest(key)
}
override fun purchaseSpecialSpell(key: String): Flowable<Void> {
override suspend fun purchaseSpecialSpell(key: String): Void? {
return apiClient.purchaseSpecialSpell(key)
}
override fun purchaseItem(purchaseType: String, key: String, purchaseQuantity: Int): Flowable<Void> {
override suspend fun purchaseItem(purchaseType: String, key: String, purchaseQuantity: Int): Void? {
return apiClient.purchaseItem(purchaseType, key, purchaseQuantity)
}
override fun togglePinnedItem(item: ShopItem): Flowable<List<ShopItem>> {
return if (!item.isValid) {
Flowable.empty()
} else apiClient.togglePinnedItem(item.pinType ?: "", item.path ?: "")
.flatMap { retrieveInAppRewards() }
override suspend fun togglePinnedItem(item: ShopItem): List<ShopItem>? {
if (item.isValid) {
apiClient.togglePinnedItem(item.pinType ?: "", item.path ?: "")
}
return retrieveInAppRewards()
}
}

View file

@ -30,6 +30,7 @@ import io.reactivex.rxjava3.core.Maybe
import io.reactivex.rxjava3.functions.BiFunction
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withContext
import java.util.Date
import java.util.GregorianCalendar
@ -51,24 +52,23 @@ class UserRepositoryImpl(
override fun getUser(userID: String): Flow<User?> = localRepository.getUser(userID)
private fun updateUser(userID: String, updateData: Map<String, Any>): Flowable<User> {
return Flowable.zip(
apiClient.updateUser(updateData),
localRepository.getUserFlowable(userID).firstElement().toFlowable()
) { newUser, user -> mergeUser(user, newUser) }
private suspend fun updateUser(userID: String, updateData: Map<String, Any>): User? {
val networkUser = apiClient.updateUser(updateData) ?: return null
val oldUser = localRepository.getUser(userID).firstOrNull()
return mergeUser(oldUser, networkUser)
}
private fun updateUser(userID: String, key: String, value: Any): Flowable<User> {
private suspend fun updateUser(userID: String, key: String, value: Any): User? {
val updateData = HashMap<String, Any>()
updateData[key] = value
return updateUser(userID, updateData)
}
override fun updateUser(updateData: Map<String, Any>): Flowable<User> {
override suspend fun updateUser(updateData: Map<String, Any>): User? {
return updateUser(userID, updateData)
}
override fun updateUser(key: String, value: Any): Flowable<User> {
override suspend fun updateUser(key: String, value: Any): User? {
return updateUser(userID, key, value)
}
@ -90,12 +90,11 @@ class UserRepositoryImpl(
val calendar = GregorianCalendar()
val timeZone = calendar.timeZone
val offset = -TimeUnit.MINUTES.convert(timeZone.getOffset(calendar.timeInMillis).toLong(), TimeUnit.MILLISECONDS)
/*if (offset.toInt() != user.preferences?.timezoneOffset ?: 0) {
return@flatMap updateUser(user.id ?: "", "preferences.timezoneOffset", offset.toString())
return if (offset.toInt() != (user.preferences?.timezoneOffset ?: 0)) {
updateUser(user.id ?: "", "preferences.timezoneOffset", offset.toString())
} else {
return@flatMap Flowable.just(user)
}*/
return user
user
}
} else {
return null
}
@ -106,17 +105,13 @@ class UserRepositoryImpl(
return retrieveUser(false, true)
}
override fun resetTutorial(): Maybe<User> {
return localRepository.getTutorialSteps()
.firstElement()
.map<Map<String, Any>> { tutorialSteps ->
val updateData = HashMap<String, Any>()
for (step in tutorialSteps) {
updateData[step.flagPath] = false
}
updateData
}
.flatMap { updateData -> updateUser(updateData).firstElement() }
override suspend fun resetTutorial(): User? {
val tutorialSteps = localRepository.getTutorialSteps().firstOrNull() ?: return null
val updateData = HashMap<String, Any>()
for (step in tutorialSteps) {
updateData[step.flagPath] = false
}
return updateUser(updateData)
}
override suspend fun sleep(user: User): User {
@ -134,26 +129,26 @@ class UserRepositoryImpl(
override fun getSpecialItems(user: User): Flowable<out List<Skill>> =
localRepository.getSpecialItems(user)
override fun useSkill(key: String, target: String?, taskId: String): Flowable<SkillResponse> {
return zipWithLiveUser(apiClient.useSkill(key, target ?: "", taskId)) { response, user ->
response.hpDiff = (response.user?.stats?.hp ?: 0.0) - (user.stats?.hp ?: 0.0)
response.expDiff =(response.user?.stats?.exp ?: 0.0) - (user.stats?.exp ?: 0.0)
response.goldDiff = (response.user?.stats?.gp ?: 0.0) - (user.stats?.gp ?: 0.0)
response.damage = (response.user?.party?.quest?.progress?.up ?: 0.0f) - (user.party?.quest?.progress?.up ?: 0.0f)
response.user?.let { mergeUser(user, it) }
response
}
override suspend fun useSkill(key: String, target: String?, taskId: String): SkillResponse? {
val response = apiClient.useSkill(key, target ?: "", taskId) ?: return null
val user = getLiveUser() ?: return response
response.hpDiff = (response.user?.stats?.hp ?: 0.0) - (user.stats?.hp ?: 0.0)
response.expDiff =(response.user?.stats?.exp ?: 0.0) - (user.stats?.exp ?: 0.0)
response.goldDiff = (response.user?.stats?.gp ?: 0.0) - (user.stats?.gp ?: 0.0)
response.damage = (response.user?.party?.quest?.progress?.up ?: 0.0f) - (user.party?.quest?.progress?.up ?: 0.0f)
response.user?.let { mergeUser(user, it) }
return response
}
override fun useSkill(key: String, target: String?): Flowable<SkillResponse> {
return zipWithLiveUser(apiClient.useSkill(key, target ?: "")) { response, user ->
response.hpDiff = (response.user?.stats?.hp ?: 0.0) - (user.stats?.hp ?: 0.0)
response.expDiff =(response.user?.stats?.exp ?: 0.0) - (user.stats?.exp ?: 0.0)
response.goldDiff = (response.user?.stats?.gp ?: 0.0) - (user.stats?.gp ?: 0.0)
response.damage = (response.user?.party?.quest?.progress?.up ?: 0.0f) - (user.party?.quest?.progress?.up ?: 0.0f)
response.user?.let { mergeUser(user, it) }
response
}
override suspend fun useSkill(key: String, target: String?): SkillResponse? {
val response = apiClient.useSkill(key, target ?: "") ?: return null
val user = getLiveUser() ?: return response
response.hpDiff = (response.user?.stats?.hp ?: 0.0) - (user.stats?.hp ?: 0.0)
response.expDiff =(response.user?.stats?.exp ?: 0.0) - (user.stats?.exp ?: 0.0)
response.goldDiff = (response.user?.stats?.gp ?: 0.0) - (user.stats?.gp ?: 0.0)
response.damage = (response.user?.party?.quest?.progress?.up ?: 0.0f) - (user.party?.quest?.progress?.up ?: 0.0f)
response.user?.let { mergeUser(user, it) }
return response
}
override suspend fun disableClasses(): User? = apiClient.disableClasses()
@ -163,20 +158,19 @@ class UserRepositoryImpl(
return retrieveUser(false, forced = true)
}
override fun unlockPath(customization: Customization): Flowable<UnlockResponse> {
override suspend fun unlockPath(customization: Customization): UnlockResponse? {
return unlockPath(customization.path, customization.price ?: 0)
}
override fun unlockPath(path: String, price: Int): Flowable<UnlockResponse> {
return zipWithLiveUser(apiClient.unlockPath(path)) { unlockResponse, copiedUser ->
val user = localRepository.getUnmanagedCopy(copiedUser)
user.preferences = unlockResponse.preferences
user.purchased = unlockResponse.purchased
user.items = unlockResponse.items
user.balance = copiedUser.balance - (price / 4.0)
localRepository.saveUser(copiedUser, false)
unlockResponse
}
override suspend fun unlockPath(path: String, price: Int): UnlockResponse? {
val unlockResponse = apiClient.unlockPath(path) ?: return null
val user = localRepository.getUser(userID).firstOrNull() ?: return unlockResponse
user.preferences = unlockResponse.preferences
user.purchased = unlockResponse.purchased
user.items = unlockResponse.items
user.balance = user.balance - (price / 4.0)
localRepository.saveUser(user, false)
return unlockResponse
}
override suspend fun runCron() {
@ -204,9 +198,10 @@ class UserRepositoryImpl(
return apiClient.changeCustomDayStart(updateObject)
}
override fun updateLanguage(languageCode: String): Flowable<User> {
return updateUser("preferences.language", languageCode)
.doOnNext { apiClient.setLanguageCode(languageCode) }
override suspend fun updateLanguage(languageCode: String): User? {
val user = updateUser("preferences.language", languageCode)
apiClient.setLanguageCode(languageCode)
return user
}
override suspend fun resetAccount(): User? {
@ -220,21 +215,18 @@ class UserRepositoryImpl(
override fun sendPasswordResetEmail(email: String): Flowable<Void> =
apiClient.sendPasswordResetEmail(email)
override fun updateLoginName(newLoginName: String, password: String?): Maybe<User> {
return (
if (password != null && password.isNotEmpty()) {
apiClient.updateLoginName(newLoginName.trim(), password.trim())
} else {
apiClient.updateUsername(newLoginName.trim())
}
).flatMapMaybe { localRepository.getUserFlowable(userID).firstElement() }
.doOnNext { user ->
localRepository.modify(user) { liveUser ->
liveUser.authentication?.localAuthentication?.username = newLoginName
liveUser.flags?.verifiedUsername = true
}
}
.firstElement()
override suspend fun updateLoginName(newLoginName: String, password: String?): User? {
if (password != null && password.isNotEmpty()) {
apiClient.updateLoginName(newLoginName.trim(), password.trim())
} else {
apiClient.updateUsername(newLoginName.trim())
}
val user = localRepository.getUser(userID).firstOrNull() ?: return null
localRepository.modify(user) { liveUser ->
liveUser.authentication?.localAuthentication?.username = newLoginName
liveUser.flags?.verifiedUsername = true
}
return user
}
override fun verifyUsername(username: String): Flowable<VerifyUsernameResponse> = apiClient.verifyUsername(username.trim())
@ -250,7 +242,7 @@ class UserRepositoryImpl(
apiClient.updatePassword(oldPassword.trim(), newPassword.trim(), newPasswordConfirmation.trim())
override fun allocatePoint(stat: Attribute): Flowable<Stats> {
getLiveUser().firstElement().subscribe(
getLiveUserFlowable().firstElement().subscribe(
{ liveUser ->
localRepository.executeTransaction {
when (stat) {
@ -324,7 +316,7 @@ class UserRepositoryImpl(
}
}
override fun useCustomization(type: String, category: String?, identifier: String): Flowable<User> {
override suspend fun useCustomization(type: String, category: String?, identifier: String): User? {
if (appConfigManager.enableLocalChanges()) {
localRepository.getUserFlowable(userID).firstElement().subscribe(
{ liveUser ->
@ -401,14 +393,19 @@ class UserRepositoryImpl(
return localRepository.getTeamPlan(teamID)
}
private fun getLiveUser(): Flowable<User> {
private fun getLiveUserFlowable(): Flowable<User> {
return localRepository.getUserFlowable(userID)
.map { Optional(localRepository.getLiveObject(it)) }
.filterMapEmpty()
}
private suspend fun getLiveUser(): User? {
val user = localRepository.getUser(userID).firstOrNull() ?: return null
return localRepository.getLiveObject(user)
}
private fun <T : Any> zipWithLiveUser(flowable: Flowable<T>, mergeFunc: BiFunction<T, User, T>): Flowable<T> {
return Flowable.zip(flowable, getLiveUser().firstElement().toFlowable(), mergeFunc)
return Flowable.zip(flowable, getLiveUserFlowable().firstElement().toFlowable(), mergeFunc)
}
private fun mergeUser(oldUser: User?, newUser: User): User {

View file

@ -10,11 +10,12 @@ import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.models.user.UserQuestStatus
import io.reactivex.rxjava3.core.Flowable
import io.realm.RealmResults
import kotlinx.coroutines.flow.Flow
interface UserLocalRepository : BaseLocalRepository {
fun getTutorialSteps(): Flowable<List<TutorialStep>>
suspend fun getTutorialSteps(): Flow<RealmResults<TutorialStep>>
fun getUser(userID: String): Flow<User?>
fun getUserFlowable(userID: String): Flowable<User>

View file

@ -1,7 +1,6 @@
package com.habitrpg.android.habitica.data.local.implementation
import com.habitrpg.android.habitica.data.local.UserLocalRepository
import com.habitrpg.android.habitica.models.user.UserQuestStatus
import com.habitrpg.android.habitica.extensions.filterMap
import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.models.QuestAchievement
@ -12,6 +11,7 @@ import com.habitrpg.android.habitica.models.TutorialStep
import com.habitrpg.android.habitica.models.social.ChatMessage
import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.models.user.UserQuestStatus
import hu.akarnokd.rxjava3.bridge.RxJavaBridge
import io.reactivex.rxjava3.core.Flowable
import io.realm.Realm
@ -21,7 +21,8 @@ import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
class RealmUserLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm), UserLocalRepository {
class RealmUserLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm),
UserLocalRepository {
override fun getUserQuestStatus(userID: String): Flowable<UserQuestStatus> {
return getUserFlowable(userID)
.map { it.party?.id ?: "" }
@ -39,7 +40,8 @@ class RealmUserLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm),
.map {
when {
it.quest?.members?.find { questMember -> questMember.key == userID } === null -> UserQuestStatus.NO_QUEST
it.quest?.progress?.collect?.isNotEmpty() ?: false -> UserQuestStatus.QUEST_COLLECT
it.quest?.progress?.collect?.isNotEmpty()
?: false -> UserQuestStatus.QUEST_COLLECT
it.quest?.progress?.hp ?: 0.0 > 0.0 -> UserQuestStatus.QUEST_BOSS
else -> UserQuestStatus.QUEST_UNKNOWN
}
@ -48,25 +50,24 @@ class RealmUserLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm),
override fun getAchievements(): Flow<List<Achievement>> {
return realm.where(Achievement::class.java)
.sort("index")
.findAll()
.toFlow()
.filter { it.isLoaded }
.sort("index")
.findAll()
.toFlow()
.filter { it.isLoaded }
}
override fun getQuestAchievements(userID: String): Flow<List<QuestAchievement>> {
return realm.where(User::class.java)
.equalTo("id", userID)
.findAll()
.toFlow()
.filter { it.isLoaded && it.size > 0 }
.map { it.first()?.questAchievements ?: emptyList() }
.equalTo("id", userID)
.findAll()
.toFlow()
.filter { it.isLoaded && it.size > 0 }
.map { it.first()?.questAchievements ?: emptyList() }
}
override fun getTutorialSteps(): Flowable<List<TutorialStep>> = RxJavaBridge.toV3Flowable(
realm.where(TutorialStep::class.java).findAll().asFlowable()
override suspend fun getTutorialSteps() =
realm.where(TutorialStep::class.java).findAll().toFlow()
.filter { it.isLoaded }.map { it }
)
override fun getUser(userID: String): Flow<User?> {
if (realm.isClosed) return emptyFlow()
@ -82,11 +83,11 @@ class RealmUserLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm),
if (realm.isClosed) return Flowable.empty()
return RxJavaBridge.toV3Flowable(
realm.where(User::class.java)
.equalTo("id", userID)
.findAll()
.asFlowable()
.filter { realmObject -> realmObject.isLoaded && realmObject.isValid && !realmObject.isEmpty() }
.map { users -> users.first() })
.equalTo("id", userID)
.findAll()
.asFlowable()
.filter { realmObject -> realmObject.isLoaded && realmObject.isValid && !realmObject.isEmpty() }
.map { users -> users.first() })
}
override fun saveUser(user: User, overrideExisting: Boolean) {
@ -127,10 +128,10 @@ class RealmUserLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm),
override fun getTeamPlans(userID: String): Flow<List<TeamPlan>> {
return realm.where(TeamPlan::class.java)
.equalTo("userID", userID)
.findAll()
.toFlow()
.filter { it.isLoaded }
.equalTo("userID", userID)
.findAll()
.toFlow()
.filter { it.isLoaded }
}
override fun getTeamPlan(teamID: String): Flowable<Group> {
@ -146,7 +147,8 @@ class RealmUserLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm),
}
override fun getSkills(user: User): Flowable<out List<Skill>> {
val habitClass = if (user.preferences?.disableClasses == true) "none" else user.stats?.habitClass
val habitClass =
if (user.preferences?.disableClasses == true) "none" else user.stats?.habitClass
return RxJavaBridge.toV3Flowable(
realm.where(Skill::class.java)
.equalTo("habitClass", habitClass)

View file

@ -5,6 +5,8 @@ import com.habitrpg.android.habitica.BuildConfig
import com.habitrpg.android.habitica.proxy.AnalyticsManager
import io.reactivex.rxjava3.functions.Consumer
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import okhttp3.internal.http2.ConnectionShutdownException
import retrofit2.HttpException
import java.io.EOFException
@ -53,3 +55,7 @@ class ExceptionHandler {
}
}
}
fun CoroutineScope.launchCatching(function: suspend CoroutineScope.() -> Unit) {
launch((ExceptionHandler.coroutine()), block = function)
}

View file

@ -501,6 +501,10 @@ open class Task : RealmObject, BaseMainObject, Parcelable, BaseTask {
return daysOfMonth
}
fun canEdit(userID: String): Boolean {
return true
}
companion object CREATOR : Parcelable.Creator<Task> {
override fun createFromParcel(source: Parcel): Task = Task(source)

View file

@ -18,7 +18,6 @@ import com.habitrpg.android.habitica.helpers.AdHandler
import com.habitrpg.android.habitica.helpers.AdType
import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.android.habitica.helpers.ExceptionHandler
import com.habitrpg.common.habitica.extensions.loadImage
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
import com.habitrpg.android.habitica.ui.views.ads.AdButton
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaBottomSheetDialog
@ -26,6 +25,7 @@ import com.habitrpg.common.habitica.extensions.dpToPx
import com.habitrpg.common.habitica.extensions.loadImage
import com.habitrpg.common.habitica.helpers.Animations
import com.plattysoft.leonids.ParticleSystem
import kotlinx.coroutines.launch
import java.util.Locale
import javax.inject.Inject
@ -80,22 +80,20 @@ class ArmoireActivity : BaseActivity() {
Log.d("AdHandler", "Giving Armoire")
val user = userViewModel.user.value ?: return@AdHandler
val currentGold = user.stats?.gp ?: return@AdHandler
compositeSubscription.add(
lifecycleScope.launch(ExceptionHandler.coroutine()) {
userRepository.updateUser("stats.gp", currentGold + 100)
.flatMap { inventoryRepository.buyItem(user, "armoire", 100.0, 1) }
.subscribe({
configure(
it.armoire["type"] ?: "",
it.armoire["dropKey"] ?: "",
it.armoire["dropText"] ?: "",
it.armoire["value"] ?: ""
)
binding.adButton.state = AdButton.State.UNAVAILABLE
binding.adButton.visibility = View.INVISIBLE
hasAnimatedChanges = false
gold = null
}, ExceptionHandler.rx())
)
val buyResponse = inventoryRepository.buyItem(user, "armoire", 100.0, 1) ?: return@launch
configure(
buyResponse.armoire["type"] ?: "",
buyResponse.armoire["dropKey"] ?: "",
buyResponse.armoire["dropText"] ?: "",
buyResponse.armoire["value"] ?: ""
)
binding.adButton.state = AdButton.State.UNAVAILABLE
binding.adButton.visibility = View.INVISIBLE
hasAnimatedChanges = false
gold = null
}
}
handler.prepare {
if (it && binding.adButton.state == AdButton.State.LOADING) {

View file

@ -57,11 +57,10 @@ class DeathActivity: BaseActivity() {
return@AdHandler
}
Log.d("AdHandler", "Reviving user")
compositeSubscription.add(
userRepository.updateUser("stats.hp", 1).subscribe({
finish()
}, ExceptionHandler.rx())
)
lifecycleScope.launch(ExceptionHandler.coroutine()) {
userRepository.updateUser("stats.hp", 1)
finish()
}
}
handler.prepare {
if (it && binding.adButton.state == AdButton.State.LOADING) {

View file

@ -209,17 +209,10 @@ class SetupActivity : BaseActivity(), ViewPager.OnPageChangeListener {
additionalData["status"] = "completed"
AmplitudeManager.sendEvent("setup", AmplitudeManager.EVENT_CATEGORY_BEHAVIOUR, AmplitudeManager.EVENT_HITTYPE_EVENT, additionalData)
compositeSubscription.add(
userRepository.updateUser("flags.welcomed", true).subscribe(
{
if (!compositeSubscription.isDisposed) {
compositeSubscription.dispose()
}
startMainActivity()
},
ExceptionHandler.rx()
)
)
lifecycleScope.launch(ExceptionHandler.coroutine()) {
userRepository.updateUser("flags.welcomed", true)
startMainActivity()
}
return
}
this.user = user
@ -238,11 +231,10 @@ class SetupActivity : BaseActivity(), ViewPager.OnPageChangeListener {
}
private fun confirmNames(displayName: String, username: String) {
compositeSubscription.add(
lifecycleScope.launch(ExceptionHandler.coroutine()) {
userRepository.updateUser("profile.name", displayName)
.flatMap { userRepository.updateLoginName(username).toFlowable() }
.subscribe({ }, ExceptionHandler.rx())
)
userRepository.updateLoginName(username)
}
}
private inner class ViewPageAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT), IconPagerAdapter {

View file

@ -114,13 +114,12 @@ class VerifyUsernameActivity : BaseActivity() {
private fun confirmNames() {
binding.confirmUsernameButton.isClickable = false
compositeSubscription.add(
lifecycleScope.launch(ExceptionHandler.coroutine()) {
userRepository.updateUser("profile.name", binding.displayNameEditText.text.toString())
.flatMap { userRepository.updateLoginName(binding.usernameEditText.text.toString()).toFlowable() }
.doOnComplete { showConfirmationAndFinish() }
.doOnEach { binding.confirmUsernameButton.isClickable = true }
.subscribe({ }, ExceptionHandler.rx())
)
userRepository.updateLoginName(binding.usernameEditText.text.toString())
showConfirmationAndFinish()
binding.confirmUsernameButton.isClickable = true
}
}
private fun showConfirmationAndFinish() {

View file

@ -18,11 +18,8 @@ import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.shops.PurchaseDialog
import com.habitrpg.common.habitica.extensions.dpToPx
import com.habitrpg.common.habitica.extensions.loadImage
import com.habitrpg.shared.habitica.models.Avatar
import com.habitrpg.common.habitica.views.AvatarView
import io.reactivex.rxjava3.core.BackpressureStrategy
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.subjects.PublishSubject
import com.habitrpg.shared.habitica.models.Avatar
import java.util.Date
import java.util.EnumMap
import kotlin.math.min
@ -46,7 +43,7 @@ class CustomizationRecyclerViewAdapter() : androidx.recyclerview.widget.Recycler
var ownedCustomizations: List<String> = listOf()
private var pinnedItemKeys: List<String> = ArrayList()
private val selectCustomizationEvents = PublishSubject.create<Customization>()
var onCustomizationSelected: ((Customization) -> Unit)? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): androidx.recyclerview.widget.RecyclerView.ViewHolder {
return when (viewType) {
@ -152,10 +149,6 @@ class CustomizationRecyclerViewAdapter() : androidx.recyclerview.widget.Recycler
this.notifyDataSetChanged()
}
fun getSelectCustomizationEvents(): Flowable<Customization> {
return selectCustomizationEvents.toFlowable(BackpressureStrategy.DROP)
}
fun setPinnedItemKeys(pinnedItemKeys: List<String>) {
this.pinnedItemKeys = pinnedItemKeys
if (customizationList.size > 0) this.notifyDataSetChanged()
@ -234,7 +227,7 @@ class CustomizationRecyclerViewAdapter() : androidx.recyclerview.widget.Recycler
alert.setMessage(customization?.notes)
alert.addButton(R.string.equip, true) { _, _ ->
customization?.let {
selectCustomizationEvents.onNext(it)
onCustomizationSelected?.invoke(it)
}
}
alert.addButton(R.string.close, false) { _, _ ->
@ -243,7 +236,7 @@ class CustomizationRecyclerViewAdapter() : androidx.recyclerview.widget.Recycler
alert.show()
} else {
customization?.let {
selectCustomizationEvents.onNext(it)
onCustomizationSelected?.invoke(it)
}
}
}

View file

@ -24,8 +24,7 @@ internal class CustomizationSetupAdapter : RecyclerView.Adapter<CustomizationSet
private val equipGearEventSubject = PublishSubject.create<String>()
val equipGearEvents: Flowable<String> = equipGearEventSubject.toFlowable(BackpressureStrategy.DROP)
private val updateUserEventsSubject = PublishSubject.create<Map<String, Any>>()
val updateUserEvents: Flowable<Map<String, Any>> = updateUserEventsSubject.toFlowable(BackpressureStrategy.DROP)
var onUpdateUser: ((Map<String, Any>) -> Unit)? = null
fun setCustomizationList(newCustomizationList: List<SetupCustomization>) {
this.customizationList = newCustomizationList
@ -128,7 +127,7 @@ internal class CustomizationSetupAdapter : RecyclerView.Adapter<CustomizationSet
val updateData = HashMap<String, Any>()
val updatePath = "preferences." + selectedCustomization.path
updateData[updatePath] = selectedCustomization.key
updateUserEventsSubject.onNext(updateData)
onUpdateUser?.invoke(updateData)
}
}
}

View file

@ -9,11 +9,13 @@ import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.lifecycle.lifecycleScope
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.databinding.FragmentNewsBinding
import com.habitrpg.android.habitica.extensions.subscribeWithErrorHandler
import com.habitrpg.android.habitica.helpers.ExceptionHandler
import com.habitrpg.android.habitica.helpers.MainNavigationController
import kotlinx.coroutines.launch
class NewsFragment : BaseMainFragment<FragmentNewsBinding>() {
@ -75,6 +77,8 @@ class NewsFragment : BaseMainFragment<FragmentNewsBinding>() {
override fun onResume() {
super.onResume()
compositeSubscription.add(userRepository.updateUser("flags.newStuff", false).subscribeWithErrorHandler({}))
lifecycleScope.launch(ExceptionHandler.coroutine()) {
userRepository.updateUser("flags.newStuff", false)
}
}
}

View file

@ -5,23 +5,25 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import com.habitrpg.android.habitica.HabiticaBaseApplication
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.data.InventoryRepository
import com.habitrpg.android.habitica.databinding.FragmentStatsBinding
import com.habitrpg.android.habitica.extensions.addOkButton
import com.habitrpg.common.habitica.extensions.getThemeColor
import com.habitrpg.android.habitica.extensions.setScaledPadding
import com.habitrpg.android.habitica.helpers.ExceptionHandler
import com.habitrpg.android.habitica.helpers.UserStatComputer
import com.habitrpg.shared.habitica.models.tasks.Attribute
import com.habitrpg.android.habitica.helpers.launchCatching
import com.habitrpg.android.habitica.models.user.Stats
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.stats.BulkAllocateStatsDialog
import com.habitrpg.common.habitica.extensions.getThemeColor
import com.habitrpg.shared.habitica.models.tasks.Attribute
import javax.inject.Inject
import kotlin.math.min
@ -131,8 +133,9 @@ class StatsFragment : BaseMainFragment<FragmentStatsBinding>() {
}
binding?.automaticAllocationSwitch?.setOnCheckedChangeListener { _, isChecked ->
userRepository.updateUser("preferences.automaticAllocation", isChecked)
.subscribe({}, ExceptionHandler.rx())
lifecycleScope.launchCatching {
userRepository.updateUser("preferences.automaticAllocation", isChecked)
}
}
binding?.strengthStatsView?.allocateAction = { allocatePoint(Attribute.STRENGTH) }
@ -159,12 +162,12 @@ class StatsFragment : BaseMainFragment<FragmentStatsBinding>() {
}
private fun changeAutoAllocationMode(allocationMode: String) {
compositeSubscription.add(
lifecycleScope.launchCatching {
userRepository.updateUser(
"preferences.allocationMode",
allocationMode
).subscribe({}, ExceptionHandler.rx())
)
)
}
binding?.distributeEvenlyButton?.isChecked = allocationMode == Stats.AUTO_ALLOCATE_FLAT
binding?.distributeClassButton?.isChecked = allocationMode == Stats.AUTO_ALLOCATE_CLASSBASED
binding?.distributeTaskButton?.isChecked = allocationMode == Stats.AUTO_ALLOCATE_TASKBASED

View file

@ -26,6 +26,7 @@ import com.habitrpg.android.habitica.databinding.BottomSheetBackgroundsFilterBin
import com.habitrpg.android.habitica.databinding.FragmentRefreshRecyclerviewBinding
import com.habitrpg.android.habitica.extensions.setTintWith
import com.habitrpg.android.habitica.helpers.ExceptionHandler
import com.habitrpg.android.habitica.helpers.launchCatching
import com.habitrpg.android.habitica.models.CustomizationFilter
import com.habitrpg.android.habitica.models.inventory.Customization
import com.habitrpg.android.habitica.models.user.OwnedCustomization
@ -79,18 +80,20 @@ class AvatarCustomizationFragment :
savedInstanceState: Bundle?
): View? {
showsBackButton = true
compositeSubscription.add(
adapter.getSelectCustomizationEvents()
.flatMap { customization ->
adapter.onCustomizationSelected = { customization ->
lifecycleScope.launchCatching {
if (customization.type == "background") {
userRepository.unlockPath(customization)
//TODO: .flatMap { userRepository.retrieveUser(false, true, true) }
userRepository.retrieveUser(false, true, true)
} else {
userRepository.useCustomization(customization.type ?: "", customization.category, customization.identifier ?: "")
userRepository.useCustomization(
customization.type ?: "",
customization.category,
customization.identifier ?: ""
)
}
}
.subscribe({ }, ExceptionHandler.rx())
)
}
compositeSubscription.add(
this.inventoryRepository.getInAppRewards()

View file

@ -20,11 +20,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.lifecycleScope
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.databinding.FragmentComposeScrollingBinding
import com.habitrpg.android.habitica.helpers.ExceptionHandler
import com.habitrpg.android.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.helpers.launchCatching
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
import com.habitrpg.android.habitica.ui.theme.HabiticaTheme
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
@ -89,10 +90,9 @@ class AvatarOverviewFragment : BaseMainFragment<FragmentComposeScrollingBinding>
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
val newSize: String = if (position == 0) "slim" else "broad"
compositeSubscription.add(
lifecycleScope.launchCatching {
userRepository.updateUser("preferences.size", newSize)
.subscribe({ }, ExceptionHandler.rx())
)
}
}
override fun onNothingSelected(parent: AdapterView<*>) { /* no-on */

View file

@ -19,8 +19,9 @@ import com.habitrpg.android.habitica.databinding.FragmentItemsBinding
import com.habitrpg.android.habitica.extensions.addCloseButton
import com.habitrpg.android.habitica.extensions.observeOnce
import com.habitrpg.android.habitica.extensions.subscribeWithErrorHandler
import com.habitrpg.android.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.helpers.ExceptionHandler
import com.habitrpg.android.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.helpers.launchCatching
import com.habitrpg.android.habitica.interactors.HatchPetUseCase
import com.habitrpg.android.habitica.models.inventory.Egg
import com.habitrpg.android.habitica.models.inventory.Food
@ -28,7 +29,6 @@ import com.habitrpg.android.habitica.models.inventory.HatchingPotion
import com.habitrpg.android.habitica.models.inventory.Item
import com.habitrpg.android.habitica.models.inventory.QuestContent
import com.habitrpg.android.habitica.models.inventory.SpecialItem
import com.habitrpg.android.habitica.models.responses.SkillResponse
import com.habitrpg.android.habitica.models.user.OwnedItem
import com.habitrpg.android.habitica.models.user.OwnedPet
import com.habitrpg.android.habitica.models.user.User
@ -44,7 +44,6 @@ import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.OpenedMysteryitemDialog
import com.habitrpg.common.habitica.extensions.loadImage
import com.habitrpg.common.habitica.helpers.EmptyItem
import io.reactivex.rxjava3.core.Flowable
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
@ -322,16 +321,10 @@ class ItemRecyclerFragment : BaseFragment<FragmentItemsBinding>(), SwipeRefreshL
if (specialItem == null || memberID == null) {
return
}
val observable: Flowable<SkillResponse> =
lifecycleScope.launchCatching {
userRepository.useSkill(specialItem.key, specialItem.target, memberID)
compositeSubscription.add(
observable.subscribe(
{ this.displaySpecialItemResult(specialItem) },
ExceptionHandler.rx()
)
)
displaySpecialItemResult(specialItem)
}
}
private fun displaySpecialItemResult(specialItem: SpecialItem?) {

View file

@ -27,6 +27,7 @@ import com.habitrpg.common.habitica.extensions.dpToPx
import com.habitrpg.common.habitica.extensions.layoutInflater
import com.habitrpg.android.habitica.helpers.ExceptionHandler
import com.habitrpg.android.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.helpers.launchCatching
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.ui.activities.FixCharacterValuesActivity
import com.habitrpg.android.habitica.ui.fragments.preferences.HabiticaAccountDialog.AccountUpdateConfirmed
@ -256,8 +257,9 @@ class AccountPreferenceFragment :
private fun updateUser(path: String, value: String?, title: String) {
showSingleEntryDialog(value, title) {
if (value != it) {
userRepository.updateUser(path, it ?: "")
.subscribe({}, ExceptionHandler.rx())
lifecycleScope.launchCatching {
userRepository.updateUser(path, it ?: "")
}
}
}
}
@ -388,8 +390,9 @@ class AccountPreferenceFragment :
private fun showLoginNameDialog() {
showSingleEntryDialog(user?.username, getString(R.string.username)) {
userRepository.updateLoginName(it ?: "")
.subscribe({}, ExceptionHandler.rx())
lifecycleScope.launchCatching {
userRepository.updateLoginName(it ?: "")
}
}
}
@ -478,8 +481,11 @@ class AccountPreferenceFragment :
dialog.setTitle(R.string.confirm_username_title)
dialog.setMessage(R.string.confirm_username_description)
dialog.addButton(R.string.confirm, true) { _, _ ->
userRepository.updateLoginName(user?.authentication?.localAuthentication?.username ?: "")
.subscribe({ }, ExceptionHandler.rx())
lifecycleScope.launchCatching {
userRepository.updateLoginName(
user?.authentication?.localAuthentication?.username ?: ""
)
}
}
dialog.addCancelButton()
dialog.show()

View file

@ -2,9 +2,10 @@ package com.habitrpg.android.habitica.ui.fragments.preferences
import android.content.SharedPreferences
import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import androidx.preference.CheckBoxPreference
import com.habitrpg.android.habitica.HabiticaBaseApplication
import com.habitrpg.android.habitica.helpers.ExceptionHandler
import com.habitrpg.android.habitica.helpers.launchCatching
import com.habitrpg.android.habitica.models.user.User
class EmailNotificationsPreferencesFragment : BasePreferencesFragment(), SharedPreferences.OnSharedPreferenceChangeListener {
@ -73,7 +74,12 @@ class EmailNotificationsPreferencesFragment : BasePreferencesFragment(), SharedP
else -> null
}
if (pathKey != null) {
compositeSubscription.add(userRepository.updateUser("preferences.emailNotifications.$pathKey", sharedPreferences.getBoolean(key, false)).subscribe({ }, ExceptionHandler.rx()))
lifecycleScope.launchCatching {
userRepository.updateUser(
"preferences.emailNotifications.$pathKey",
sharedPreferences.getBoolean(key, false)
)
}
}
}
}

View file

@ -23,6 +23,7 @@ import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.android.habitica.helpers.ExceptionHandler
import com.habitrpg.android.habitica.helpers.SoundManager
import com.habitrpg.android.habitica.helpers.TaskAlarmManager
import com.habitrpg.android.habitica.helpers.launchCatching
import com.habitrpg.android.habitica.helpers.notifications.PushNotificationManager
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.prefs.TimePreference
@ -207,7 +208,9 @@ class PreferencesFragment : BasePreferencesFragment(), SharedPreferences.OnShare
"usePushNotifications" -> {
val usePushNotifications = sharedPreferences.getBoolean(key, true)
pushNotificationsPreference?.isEnabled = usePushNotifications
userRepository.updateUser("preferences.pushNotifications.unsubscribeFromAll", !usePushNotifications).subscribe()
lifecycleScope.launchCatching {
userRepository.updateUser("preferences.pushNotifications.unsubscribeFromAll", !usePushNotifications)
}
if (usePushNotifications) {
pushNotificationManager.addPushDeviceUsingStoredToken()
} else {
@ -217,7 +220,9 @@ class PreferencesFragment : BasePreferencesFragment(), SharedPreferences.OnShare
"useEmails" -> {
val useEmailNotifications = sharedPreferences.getBoolean(key, true)
emailNotificationsPreference?.isEnabled = useEmailNotifications
userRepository.updateUser("preferences.emailNotifications.unsubscribeFromAll", !useEmailNotifications).subscribe()
lifecycleScope.launchCatching {
userRepository.updateUser("preferences.emailNotifications.unsubscribeFromAll", !useEmailNotifications)
}
}
"cds_time" -> {
val timeval = sharedPreferences.getString("cds_time", "0") ?: "0"
@ -238,10 +243,10 @@ class PreferencesFragment : BasePreferencesFragment(), SharedPreferences.OnShare
if (user?.preferences?.language == languageHelper.languageCode) {
return
}
userRepository.updateLanguage(languageHelper.languageCode ?: "en")
.subscribe({ reloadContent(false) }, ExceptionHandler.rx())
lifecycleScope.launchCatching {
userRepository.updateLanguage(languageHelper.languageCode ?: "en")
reloadContent(false)
}
val intent = Intent(activity, MainActivity::class.java)
this.startActivity(intent)
activity?.finishAffinity()
@ -249,10 +254,9 @@ class PreferencesFragment : BasePreferencesFragment(), SharedPreferences.OnShare
"audioTheme" -> {
val newAudioTheme = sharedPreferences.getString(key, "off")
if (newAudioTheme != null) {
compositeSubscription.add(
lifecycleScope.launchCatching {
userRepository.updateUser("preferences.sound", newAudioTheme)
.subscribe({ }, ExceptionHandler.rx())
)
}
soundManager.soundTheme = newAudioTheme
soundManager.preloadAllFiles()
}
@ -265,8 +269,12 @@ class PreferencesFragment : BasePreferencesFragment(), SharedPreferences.OnShare
val activity = activity as? PrefsActivity ?: return
activity.reload()
}
"dailyDueDefaultView" -> userRepository.updateUser("preferences.dailyDueDefaultView", sharedPreferences.getBoolean(key, false))
.subscribe({ }, ExceptionHandler.rx())
"dailyDueDefaultView" -> lifecycleScope.launchCatching {
userRepository.updateUser(
"preferences.dailyDueDefaultView",
sharedPreferences.getBoolean(key, false)
)
}
"server_url" -> {
apiClient.updateServerUrl(sharedPreferences.getString(key, ""))
findPreference<Preference>(key)?.summary = sharedPreferences.getString(key, "")
@ -282,10 +290,9 @@ class PreferencesFragment : BasePreferencesFragment(), SharedPreferences.OnShare
"disablePMs" -> {
val isDisabled = sharedPreferences.getBoolean("disablePMs", false)
if (user?.inbox?.optOut != isDisabled) {
compositeSubscription.add(
lifecycleScope.launchCatching {
userRepository.updateUser("inbox.optOut", isDisabled)
.subscribe({ }, ExceptionHandler.rx())
)
}
}
}
"launch_screen" -> {
@ -390,7 +397,12 @@ class PreferencesFragment : BasePreferencesFragment(), SharedPreferences.OnShare
} else if (newValue == false && currentIds.contains(team.id)) {
currentIds.remove(team.id)
}
userRepository.updateUser("preferences.tasks.mirrorGroupTasks", currentIds).subscribe({}, ExceptionHandler.rx())
lifecycleScope.launchCatching {
userRepository.updateUser(
"preferences.tasks.mirrorGroupTasks",
currentIds
)
}
true
}
groupCategory?.addPreference(newPreference)

View file

@ -2,9 +2,10 @@ package com.habitrpg.android.habitica.ui.fragments.preferences
import android.content.SharedPreferences
import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import androidx.preference.CheckBoxPreference
import com.habitrpg.android.habitica.HabiticaBaseApplication
import com.habitrpg.android.habitica.helpers.ExceptionHandler
import com.habitrpg.android.habitica.helpers.launchCatching
import com.habitrpg.android.habitica.models.user.User
class PushNotificationsPreferencesFragment : BasePreferencesFragment(), SharedPreferences.OnSharedPreferenceChangeListener {
@ -75,7 +76,9 @@ class PushNotificationsPreferencesFragment : BasePreferencesFragment(), SharedPr
else -> null
}
if (pathKey != null) {
compositeSubscription.add(userRepository.updateUser("preferences.pushNotifications.$pathKey", sharedPreferences.getBoolean(key, false)).subscribe({ }, ExceptionHandler.rx()))
lifecycleScope.launchCatching {
userRepository.updateUser("preferences.pushNotifications.$pathKey", sharedPreferences.getBoolean(key, false))
}
}
}
}

View file

@ -6,6 +6,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.RelativeLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.tabs.TabLayout
import com.habitrpg.android.habitica.R
@ -15,6 +16,7 @@ import com.habitrpg.android.habitica.data.SetupCustomizationRepository
import com.habitrpg.android.habitica.data.UserRepository
import com.habitrpg.android.habitica.databinding.FragmentSetupAvatarBinding
import com.habitrpg.android.habitica.extensions.subscribeWithErrorHandler
import com.habitrpg.android.habitica.helpers.launchCatching
import com.habitrpg.android.habitica.models.SetupCustomization
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.ui.activities.SetupActivity
@ -56,7 +58,11 @@ class AvatarSetupFragment : BaseFragment<FragmentSetupAvatarBinding>() {
this.adapter = CustomizationSetupAdapter()
this.adapter?.userSize = this.user?.preferences?.size ?: "slim"
adapter?.updateUserEvents?.flatMap { userRepository.updateUser(it) }?.subscribeWithErrorHandler {}?.let { compositeSubscription.add(it) }
adapter?.onUpdateUser = {
lifecycleScope.launchCatching {
userRepository.updateUser(it)
}
}
adapter?.equipGearEvents?.flatMap { inventoryRepository.equip("equipped", it) }?.subscribeWithErrorHandler {}?.let { compositeSubscription.add(it) }
this.adapter?.user = this.user
@ -185,7 +191,9 @@ class AvatarSetupFragment : BaseFragment<FragmentSetupAvatarBinding>() {
updateData["preferences.hair.bangs"] = chooseRandomKey(customizationRepository.getCustomizations(SetupCustomizationRepository.CATEGORY_HAIR, SetupCustomizationRepository.SUBCATEGORY_BANGS, user), false)
updateData["preferences.hair.flower"] = chooseRandomKey(customizationRepository.getCustomizations(SetupCustomizationRepository.CATEGORY_EXTRAS, SetupCustomizationRepository.SUBCATEGORY_FLOWER, user), true)
updateData["preferences.chair"] = chooseRandomKey(customizationRepository.getCustomizations(SetupCustomizationRepository.CATEGORY_EXTRAS, SetupCustomizationRepository.SUBCATEGORY_WHEELCHAIR, user), true)
compositeSubscription.add(userRepository.updateUser(updateData).subscribeWithErrorHandler({}))
lifecycleScope.launchCatching {
userRepository.updateUser(updateData)
}
}
@Suppress("ReturnCount")
@ -215,4 +223,4 @@ class AvatarSetupFragment : BaseFragment<FragmentSetupAvatarBinding>() {
params?.marginStart = location[0] + px
binding?.customizationDrawer?.binding?.caretView?.layoutParams = params
}
}
}

View file

@ -15,6 +15,7 @@ import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.databinding.FragmentSkillsBinding
import com.habitrpg.android.habitica.extensions.subscribeWithErrorHandler
import com.habitrpg.android.habitica.helpers.ExceptionHandler
import com.habitrpg.android.habitica.helpers.launchCatching
import com.habitrpg.android.habitica.models.Skill
import com.habitrpg.android.habitica.models.responses.SkillResponse
import com.habitrpg.android.habitica.models.user.User
@ -161,16 +162,15 @@ class SkillsFragment : BaseMainFragment<FragmentSkillsBinding>() {
if (skill == null) {
return
}
val observable: Flowable<SkillResponse> = if (taskId != null) {
userRepository.useSkill(skill.key, skill.target, taskId)
} else {
userRepository.useSkill(skill.key, skill.target)
lifecycleScope.launchCatching {
val skillResponse = if (taskId != null) {
userRepository.useSkill(skill.key, skill.target, taskId)
} else {
userRepository.useSkill(skill.key, skill.target)
}
if (skillResponse != null) {
displaySkillResult(skill, skillResponse)
}
}
compositeSubscription.add(
observable.subscribe(
{ skillResponse -> this.displaySkillResult(skill, skillResponse) },
ExceptionHandler.rx()
)
)
}
}

View file

@ -6,16 +6,18 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.net.toUri
import androidx.lifecycle.lifecycleScope
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.data.FAQRepository
import com.habitrpg.android.habitica.databinding.FragmentSupportMainBinding
import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.android.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.helpers.ExceptionHandler
import com.habitrpg.android.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.modules.AppModule
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
import com.habitrpg.android.habitica.ui.views.HabiticaSnackbar
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Named
@ -59,9 +61,11 @@ class SupportMainFragment : BaseMainFragment<FragmentSupportMainBinding>() {
}
binding?.resetTutorialButton?.setOnClickListener {
userRepository.resetTutorial().subscribe({
lifecycleScope.launch(ExceptionHandler.coroutine()) {
userRepository.resetTutorial()
activity?.showSnackbar(null, null, getString(R.string.tutorial_reset_confirmation), displayType = HabiticaSnackbar.SnackbarDisplayType.SUCCESS)
}, ExceptionHandler.rx())
}
}
}

View file

@ -15,6 +15,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.extensions.subscribeWithErrorHandler
import com.habitrpg.android.habitica.helpers.ExceptionHandler
import com.habitrpg.android.habitica.helpers.launchCatching
import com.habitrpg.android.habitica.models.shops.ShopItem
import com.habitrpg.android.habitica.ui.activities.MainActivity
import com.habitrpg.android.habitica.ui.activities.SkillMemberActivity
@ -22,7 +23,6 @@ import com.habitrpg.android.habitica.ui.adapter.tasks.RewardsRecyclerViewAdapter
import com.habitrpg.android.habitica.ui.helpers.SafeDefaultItemAnimator
import com.habitrpg.android.habitica.ui.views.HabiticaSnackbar
import com.habitrpg.shared.habitica.models.tasks.TaskType
import io.reactivex.rxjava3.functions.Consumer
import kotlinx.coroutines.launch
class RewardsRecyclerviewFragment : TaskRecyclerViewFragment() {
@ -35,7 +35,9 @@ class RewardsRecyclerviewFragment : TaskRecyclerViewFragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
compositeSubscription.add(inventoryRepository.retrieveInAppRewards().subscribe({ }, ExceptionHandler.rx()))
lifecycleScope.launchCatching {
inventoryRepository.retrieveInAppRewards()
}
return super.onCreateView(inflater, container, savedInstanceState)
}
@ -97,13 +99,9 @@ class RewardsRecyclerviewFragment : TaskRecyclerViewFragment() {
binding?.refreshLayout?.isRefreshing = true
lifecycleScope.launch(ExceptionHandler.coroutine()) {
userRepository.retrieveUser(withTasks = true, forced = true)
}
compositeSubscription.add(
inventoryRepository.retrieveInAppRewards()
.doOnTerminate {
binding?.refreshLayout?.isRefreshing = false
}.subscribe({ }, ExceptionHandler.rx())
)
binding?.refreshLayout?.isRefreshing = false
}
}
private fun setGridSpanCount(width: Int) {
@ -122,21 +120,19 @@ class RewardsRecyclerviewFragment : TaskRecyclerViewFragment() {
private val cardSelectedResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
userRepository.useSkill(
selectedCard?.key ?: "",
"member",
it.data?.getStringExtra("member_id") ?: ""
)
.subscribeWithErrorHandler(
Consumer {
val activity = (activity as? MainActivity) ?: return@Consumer
HabiticaSnackbar.showSnackbar(
activity.snackbarContainer,
context?.getString(R.string.sent_card, selectedCard?.text),
HabiticaSnackbar.SnackbarDisplayType.BLUE
)
}
lifecycleScope.launchCatching {
userRepository.useSkill(
selectedCard?.key ?: "",
"member",
it.data?.getStringExtra("member_id") ?: ""
)
val activity = (activity as? MainActivity) ?: return@launchCatching
HabiticaSnackbar.showSnackbar(
activity.snackbarContainer,
context?.getString(R.string.sent_card, selectedCard?.text),
HabiticaSnackbar.SnackbarDisplayType.BLUE
)
}
}
}

View file

@ -2,12 +2,14 @@ package com.habitrpg.android.habitica.ui.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.habitrpg.android.habitica.HabiticaBaseApplication
import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.data.UserRepository
import com.habitrpg.android.habitica.helpers.ExceptionHandler
import com.habitrpg.android.habitica.models.user.User
import io.reactivex.rxjava3.disposables.CompositeDisposable
import kotlinx.coroutines.launch
import javax.inject.Inject
abstract class BaseViewModel(initializeComponent: Boolean = true) : ViewModel() {
@ -38,9 +40,8 @@ abstract class BaseViewModel(initializeComponent: Boolean = true) : ViewModel()
internal val disposable = CompositeDisposable()
fun updateUser(path: String, value: Any) {
disposable.add(
viewModelScope.launch(ExceptionHandler.coroutine()) {
userRepository.updateUser(path, value)
.subscribe({ }, ExceptionHandler.rx())
)
}
}
}

View file

@ -11,10 +11,12 @@ import com.habitrpg.android.habitica.models.members.Member
import com.habitrpg.android.habitica.models.user.User
import io.reactivex.rxjava3.disposables.CompositeDisposable
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.launch
class MainUserViewModel(private val providedUserID: String, val userRepository: UserRepository, val socialRepository: SocialRepository) {
@ -59,16 +61,14 @@ class MainUserViewModel(private val providedUserID: String, val userRepository:
internal val disposable = CompositeDisposable()
fun updateUser(path: String, value: Any) {
disposable.add(
MainScope().launch(ExceptionHandler.coroutine()) {
userRepository.updateUser(path, value)
.subscribe({ }, ExceptionHandler.rx())
)
}
}
fun updateUser(data: Map<String, Any>) {
disposable.add(
MainScope().launch(ExceptionHandler.coroutine()) {
userRepository.updateUser(data)
.subscribe({ }, ExceptionHandler.rx())
)
}
}
}

View file

@ -6,12 +6,14 @@ import android.view.LayoutInflater
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.lifecycle.lifecycleScope
import com.habitrpg.android.habitica.HabiticaBaseApplication
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.databinding.DialogHatchPetButtonBinding
import com.habitrpg.android.habitica.databinding.DialogPetSuggestHatchBinding
import com.habitrpg.android.habitica.extensions.subscribeWithErrorHandler
import com.habitrpg.android.habitica.helpers.ExceptionHandler
import com.habitrpg.android.habitica.helpers.launchCatching
import com.habitrpg.android.habitica.interactors.HatchPetUseCase
import com.habitrpg.android.habitica.models.inventory.Animal
import com.habitrpg.android.habitica.models.inventory.Egg
@ -22,7 +24,6 @@ import com.habitrpg.android.habitica.ui.activities.MainActivity
import com.habitrpg.common.habitica.extensions.DataBindingUtils
import com.habitrpg.common.habitica.extensions.loadImage
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Observable
import java.util.Locale
import javax.inject.Inject
@ -140,21 +141,16 @@ class PetSuggestHatchDialog(context: Context) : HabiticaAlertDialog(context) {
val activity = (getActivity() as? MainActivity) ?: return@addButton
val thisPotion = potion ?: return@addButton
val thisEgg = egg ?: return@addButton
var observable: Flowable<Any> = Flowable.just("")
if (!hasEgg) {
observable = observable.flatMap { activity.inventoryRepository.purchaseItem("eggs", thisEgg.key, 1) }
lifecycleScope.launchCatching {
if (!hasEgg) {
activity.inventoryRepository.purchaseItem("eggs", thisEgg.key, 1)
}
if (!hasPotion) {
activity.inventoryRepository.purchaseItem("hatchingPotions", thisPotion.key, 1)
}
activity.userRepository.retrieveUser(true, forced = true)
hatchPet(thisPotion, thisEgg)
}
if (!hasPotion) {
observable = observable.flatMap { activity.inventoryRepository.purchaseItem("hatchingPotions", thisPotion.key, 1) }
}
observable
.subscribe(
{
// TODO: activity.userRepository.retrieveUser(true, forced = true)
hatchPet(thisPotion, thisEgg)
},
ExceptionHandler.rx()
)
}
}

View file

@ -10,6 +10,7 @@ import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toDrawable
import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope
import com.google.firebase.analytics.FirebaseAnalytics
import com.habitrpg.android.habitica.HabiticaBaseApplication
import com.habitrpg.android.habitica.R
@ -23,6 +24,7 @@ import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.android.habitica.helpers.ExceptionHandler
import com.habitrpg.android.habitica.helpers.HapticFeedbackManager
import com.habitrpg.android.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.helpers.launchCatching
import com.habitrpg.android.habitica.models.shops.Shop
import com.habitrpg.android.habitica.models.shops.ShopItem
import com.habitrpg.android.habitica.models.user.OwnedItem
@ -38,7 +40,6 @@ import com.habitrpg.android.habitica.ui.views.insufficientCurrency.InsufficientG
import com.habitrpg.android.habitica.ui.views.insufficientCurrency.InsufficientHourglassesDialog
import com.habitrpg.android.habitica.ui.views.insufficientCurrency.InsufficientSubscriberGemsDialog
import com.habitrpg.android.habitica.ui.views.tasks.form.StepperValueFormView
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.disposables.CompositeDisposable
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
@ -254,7 +255,12 @@ class PurchaseDialog(context: Context, component: UserComponent?, val item: Shop
priceLabel = buyButton.findViewById(R.id.priceLabel)
priceLabel.animationDuration = 0L
buyLabel = buyButton.findViewById(R.id.buy_label)
pinButton.setOnClickListener { inventoryRepository.togglePinnedItem(shopItem).subscribe({ isPinned = !this.isPinned }, ExceptionHandler.rx()) }
pinButton.setOnClickListener {
lifecycleScope.launchCatching {
inventoryRepository.togglePinnedItem(shopItem)
isPinned = !isPinned
}
}
shopItem = item
@ -357,30 +363,31 @@ class PurchaseDialog(context: Context, component: UserComponent?, val item: Shop
)
HapticFeedbackManager.tap(contentView)
val snackbarText = arrayOf("")
val observable: Flowable<Any>
val observable: (suspend () -> Unit)
if (shopIdentifier != null && shopIdentifier == Shop.TIME_TRAVELERS_SHOP || "mystery_set" == shopItem.purchaseType || shopItem.currency == "hourglasses") {
observable = if (shopItem.purchaseType == "gear") {
inventoryRepository.purchaseMysterySet(shopItem.key).cast(Any::class.java)
{ inventoryRepository.purchaseMysterySet(shopItem.key) }
} else {
inventoryRepository.purchaseHourglassItem(shopItem.purchaseType, shopItem.key).cast(Any::class.java)
{ inventoryRepository.purchaseHourglassItem(shopItem.purchaseType, shopItem.key) }
}
// TODO: } else if (shopItem.purchaseType == "fortify") {
// observable = userRepository.reroll().cast(Any::class.java)
} else if (shopItem.purchaseType == "fortify") {
observable = { userRepository.reroll() }
} else if (shopItem.purchaseType == "quests" && shopItem.currency == "gold") {
observable = inventoryRepository.purchaseQuest(shopItem.key).cast(Any::class.java)
observable = { inventoryRepository.purchaseQuest(shopItem.key) }
} else if (shopItem.purchaseType == "debuffPotion") {
observable = userRepository.useSkill(shopItem.key, null).cast(Any::class.java)
observable = { userRepository.useSkill(shopItem.key, null) }
} else if (shopItem.purchaseType == "customization" || shopItem.purchaseType == "background" || shopItem.purchaseType == "backgrounds" || shopItem.purchaseType == "customizationSet") {
observable = userRepository.unlockPath(item.unlockPath ?: "${item.pinType}.${item.key}" ?: "", item.value).cast(Any::class.java)
observable = { userRepository.unlockPath(item.unlockPath ?: "${item.pinType}.${item.key}" ?: "", item.value) }
} else if (shopItem.purchaseType == "debuffPotion") {
observable = userRepository.useSkill(shopItem.key, null).cast(Any::class.java)
observable = { userRepository.useSkill(shopItem.key, null) }
} else if (shopItem.purchaseType == "card") {
purchaseCardAction?.invoke(shopItem)
dismiss()
return
} else if ("gold" == shopItem.currency && "gem" != shopItem.key) {
observable = inventoryRepository.buyItem(user, shopItem.key, shopItem.value.toDouble(), quantity).map { buyResponse ->
if (shopItem.key == "armoire" && configManager.enableNewArmoire()) {
observable = {
val buyResponse = inventoryRepository.buyItem(user, shopItem.key, shopItem.value.toDouble(), quantity)
if (shopItem.key == "armoire" && configManager.enableNewArmoire() && buyResponse != null) {
MainNavigationController.navigate(
R.id.armoireActivity,
ArmoireActivityDirections.openArmoireActivity(
@ -391,45 +398,34 @@ class PurchaseDialog(context: Context, component: UserComponent?, val item: Shop
).arguments
)
}
buyResponse
}
} else {
observable = inventoryRepository.purchaseItem(shopItem.purchaseType, shopItem.key, quantity).cast(Any::class.java)
observable = { inventoryRepository.purchaseItem(shopItem.purchaseType, shopItem.key, quantity) }
}
val subscription = observable
.doOnNext {
val text = if (snackbarText[0].isNotEmpty()) {
snackbarText[0]
} else {
context.getString(R.string.successful_purchase, shopItem.text)
}
val rightTextColor = when (item.currency) {
"gold" -> ContextCompat.getColor(context, R.color.text_yellow)
"gems" -> ContextCompat.getColor(context, R.color.text_green)
"hourglasses" -> ContextCompat.getColor(context, R.color.text_brand)
else -> 0
}
((application?.currentActivity?.get() ?: getActivity() ?: ownerActivity) as? SnackbarActivity)?.showSnackbar(
content = text,
rightIcon = priceLabel.compoundDrawables[0],
rightTextColor = rightTextColor,
rightText = "-" + priceLabel.text
)
lifecycleScope.launchCatching {
observable()
val text = if (snackbarText[0].isNotEmpty()) {
snackbarText[0]
} else {
context.getString(R.string.successful_purchase, shopItem.text)
}
// TODO: .flatMap { userRepository.retrieveUser(withTasks = false, forced = true) }
.flatMap { inventoryRepository.retrieveInAppRewards() }
.subscribe({
if (item.isTypeGear || item.currency == "hourglasses") {
onGearPurchased?.invoke(item)
}
}) { throwable ->
if (throwable.javaClass.isAssignableFrom(retrofit2.HttpException::class.java)) {
val error = throwable as retrofit2.HttpException
if (error.code() == 401 && shopItem.currency == "gems") {
MainNavigationController.navigate(R.id.gemPurchaseActivity, bundleOf(Pair("openSubscription", false)))
}
}
val rightTextColor = when (item.currency) {
"gold" -> ContextCompat.getColor(context, R.color.text_yellow)
"gems" -> ContextCompat.getColor(context, R.color.text_green)
"hourglasses" -> ContextCompat.getColor(context, R.color.text_brand)
else -> 0
}
((application?.currentActivity?.get() ?: getActivity() ?: ownerActivity) as? SnackbarActivity)?.showSnackbar(
content = text,
rightIcon = priceLabel.compoundDrawables[0],
rightTextColor = rightTextColor,
rightText = "-" + priceLabel.text
)
inventoryRepository.retrieveInAppRewards()
if (item.isTypeGear || item.currency == "hourglasses") {
onGearPurchased?.invoke(item)
}
}
}
private fun displayPurchaseConfirmationDialog(quantity: Int) {

View file

@ -21,7 +21,7 @@ kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0-native-mt")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
implementation("io.realm.kotlin:library-base:1.0.2")
}
}

View file

@ -14,6 +14,10 @@ actual class PlatformLogger actual constructor() {
Log.i(tag, message)
}
actual fun logWarning(tag: String, message: String) {
Log.w(tag, message)
}
actual fun logError(tag: String, message: String) {
Log.e(tag, message)
}

View file

@ -5,12 +5,13 @@ expect class PlatformLogger() {
fun logDebug(tag: String, message: String)
fun logInfo(tag: String, message: String)
fun logWarning(tag: String, message: String)
fun logError(tag: String, message: String)
fun logError(tag: String, message: String, exception: Throwable)
}
enum class LogLevel {
ERROR, INFO, DEBUG
ERROR, INFO, WARNING, DEBUG
}
class HLogger {
@ -26,6 +27,7 @@ class HLogger {
when (level) {
LogLevel.ERROR -> platformLogger.logError(tag, message)
LogLevel.INFO -> platformLogger.logInfo(tag, message)
LogLevel.WARNING -> platformLogger.logWarning(tag, message)
LogLevel.DEBUG -> platformLogger.logDebug(tag, message)
}
}

View file

@ -5,18 +5,22 @@ actual class PlatformLogger {
get() = true
actual fun logDebug(tag: String, message: String) {
println("[DEBUG] $tag: $message")
println("[🟢] $tag: $message")
}
actual fun logInfo(tag: String, message: String) {
println("[INFO] $tag: $message")
println("[🟡] $tag: $message")
}
actual fun logWarning(tag: String, message: String) {
println("[🟠] $tag: $message")
}
actual fun logError(tag: String, message: String) {
println("[ERROR] $tag: $message")
println("[🔴] $tag: $message")
}
actual fun logError(tag: String, message: String, exception: Throwable) {
println("[ERROR] $tag: $message\n${exception.getStackTrace().joinToString("\n")}")
println("[🔴] $tag: $message\n${exception.getStackTrace().joinToString("\n")}")
}
}