From 196f208533286145c6ca1f6469041759e7e159e7 Mon Sep 17 00:00:00 2001 From: Carl Vuorinen Date: Fri, 19 Apr 2019 21:25:39 +0300 Subject: [PATCH] Implement dismiss notifications & mark notifications as seen --- .../android/habitica/api/ApiService.java | 6 + .../android/habitica/data/ApiClient.kt | 2 + .../android/habitica/data/UserRepository.kt | 2 + .../data/implementation/ApiClientImpl.kt | 1534 +++++++++-------- .../data/implementation/UserRepositoryImpl.kt | 6 + .../ui/activities/NotificationsActivity.kt | 12 +- .../ui/viewmodels/NotificationsViewModel.kt | 43 +- 7 files changed, 828 insertions(+), 777 deletions(-) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/api/ApiService.java b/Habitica/src/main/java/com/habitrpg/android/habitica/api/ApiService.java index 0979c8a8f..1df43e0c6 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/api/ApiService.java +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/api/ApiService.java @@ -348,6 +348,12 @@ public interface ApiService { @POST("notifications/{notificationId}/read") Flowable> readNotification(@Path("notificationId") String notificationId); + @POST("notifications/read") + Flowable> readNotifications(@Body Map> notificationIds); + + @POST("notifications/see") + Flowable> seeNotifications(@Body Map> notificationIds); + @POST("user/open-mystery-item") Flowable> openMysteryItem(); diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/ApiClient.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/ApiClient.kt index eb8b6e574..79ebc7a8f 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/ApiClient.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/ApiClient.kt @@ -212,6 +212,8 @@ interface ApiClient { // Notifications fun readNotification(notificationId: String): Flowable> + fun readNotifications(notificationIds: Map>): Flowable> + fun seeNotifications(notificationIds: Map>): Flowable> fun getErrorResponse(throwable: HttpException): ErrorResponse diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/UserRepository.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/UserRepository.kt index 590912b04..1b77e4b9f 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/UserRepository.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/UserRepository.kt @@ -50,6 +50,8 @@ interface UserRepository : BaseRepository { fun runCron() fun readNotification(id: String): Flowable> + fun readNotifications(notificationIds: Map>): Flowable> + fun seeNotifications(notificationIds: Map>): Flowable> fun changeCustomDayStart(dayStartTime: Int): Flowable diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/ApiClientImpl.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/ApiClientImpl.kt index d1c920078..74669d750 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/ApiClientImpl.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/ApiClientImpl.kt @@ -1,763 +1,771 @@ -package com.habitrpg.android.habitica.data.implementation - -import android.content.Context -import android.util.Log -import androidx.appcompat.app.AlertDialog -import com.amplitude.api.Amplitude -import com.google.gson.JsonSyntaxException -import com.habitrpg.android.habitica.BuildConfig -import com.habitrpg.android.habitica.HabiticaBaseApplication -import com.habitrpg.android.habitica.R -import com.habitrpg.android.habitica.api.ApiService -import com.habitrpg.android.habitica.api.GSonFactoryCreator -import com.habitrpg.android.habitica.api.HostConfig -import com.habitrpg.android.habitica.api.Server -import com.habitrpg.android.habitica.data.ApiClient -import com.habitrpg.android.habitica.events.ShowConnectionProblemEvent -import com.habitrpg.android.habitica.helpers.PopupNotificationsManager -import com.habitrpg.android.habitica.models.* -import com.habitrpg.android.habitica.models.auth.UserAuth -import com.habitrpg.android.habitica.models.auth.UserAuthResponse -import com.habitrpg.android.habitica.models.auth.UserAuthSocial -import com.habitrpg.android.habitica.models.auth.UserAuthSocialTokens -import com.habitrpg.android.habitica.models.inventory.Equipment -import com.habitrpg.android.habitica.models.inventory.Quest -import com.habitrpg.android.habitica.models.members.Member -import com.habitrpg.android.habitica.models.responses.* -import com.habitrpg.android.habitica.models.shops.Shop -import com.habitrpg.android.habitica.models.shops.ShopItem -import com.habitrpg.android.habitica.models.social.Challenge -import com.habitrpg.android.habitica.models.social.ChatMessage -import com.habitrpg.android.habitica.models.social.FindUsernameResult -import com.habitrpg.android.habitica.models.social.Group -import com.habitrpg.android.habitica.models.tasks.Task -import com.habitrpg.android.habitica.models.tasks.TaskList -import com.habitrpg.android.habitica.models.user.Items -import com.habitrpg.android.habitica.models.user.Stats -import com.habitrpg.android.habitica.models.user.User -import com.habitrpg.android.habitica.proxy.CrashlyticsProxy -import io.reactivex.Flowable -import io.reactivex.FlowableTransformer -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.functions.BiFunction -import io.reactivex.functions.Consumer -import io.reactivex.schedulers.Schedulers -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.logging.HttpLoggingInterceptor -import org.greenrobot.eventbus.EventBus -import retrofit2.HttpException -import retrofit2.Retrofit -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory -import retrofit2.converter.gson.GsonConverterFactory -import java.io.IOException -import java.net.SocketException -import java.net.SocketTimeoutException -import java.net.UnknownHostException -import java.util.* -import java.util.concurrent.TimeUnit -import javax.net.ssl.SSLException - - -class ApiClientImpl//private OnHabitsAPIResult mResultListener; -//private HostConfig mConfig; -(private val gsonConverter: GsonConverterFactory, override val hostConfig: HostConfig, private val crashlyticsProxy: CrashlyticsProxy, private val popupNotificationsManager: PopupNotificationsManager, private val context: Context) : Consumer, ApiClient { - - - private lateinit var retrofitAdapter: Retrofit - - // I think we don't need the ApiClientImpl anymore we could just use ApiService - private lateinit var apiService: ApiService - - private val apiCallTransformer = FlowableTransformer, Any> { observable -> - observable - .filter { it.data != null } - .map { habitResponse -> - if (habitResponse.notifications != null) { - popupNotificationsManager.showNotificationDialog(habitResponse.notifications) - } - habitResponse.data - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnError(this) - } - private val displayedAlert: AlertDialog? = null - private var languageCode: String? = null - private var lastAPICallURL: String? = null - - init { - this.popupNotificationsManager.setApiClient(this) - - HabiticaBaseApplication.component?.inject(this) - crashlyticsProxy.setUserIdentifier(this.hostConfig.user) - crashlyticsProxy.setUserName(this.hostConfig.user) - Amplitude.getInstance().userId = this.hostConfig.user - buildRetrofit() - } - - fun buildRetrofit() { - val logging = HttpLoggingInterceptor() - if (BuildConfig.DEBUG) { - logging.level = HttpLoggingInterceptor.Level.BODY - } - - val userAgent = System.getProperty("http.agent") - - val calendar = GregorianCalendar() - val timeZone = calendar.timeZone - val timezoneOffset = -TimeUnit.MINUTES.convert(timeZone.getOffset(calendar.timeInMillis).toLong(), TimeUnit.MILLISECONDS) - - val client = OkHttpClient.Builder() - .addInterceptor(logging) - .addNetworkInterceptor { chain -> - val original = chain.request() - var builder: Request.Builder = original.newBuilder() - if (this.hostConfig.hasAuthentication()) { - builder = builder - .header("x-api-key", this.hostConfig.api) - .header("x-api-user", this.hostConfig.user) - } - builder = builder.header("x-client", "habitica-android") - .header("x-user-timezoneOffset", timezoneOffset.toString()) - if (userAgent != null) { - builder = builder.header("user-agent", userAgent) - } - if (!BuildConfig.STAGING_KEY.isEmpty()) { - builder = builder.header("Authorization", "Basic " + BuildConfig.STAGING_KEY) - } - val request = builder.method(original.method(), original.body()) - .build() - lastAPICallURL = original.url().toString() - Log.d("NETWORK", lastAPICallURL) - chain.proceed(request) - } - .readTimeout(2400, TimeUnit.SECONDS) - .build() - - val server = Server(this.hostConfig.address) - - retrofitAdapter = Retrofit.Builder() - .client(client) - .baseUrl(server.toString()) - .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) - .addConverterFactory(gsonConverter) - .build() - - this.apiService = retrofitAdapter.create(ApiService::class.java) - } - - override fun updateServerUrl(newAddress: String?) { - if (newAddress != null) { - hostConfig.address = newAddress - buildRetrofit() - } - } - - override fun registerUser(username: String, email: String, password: String, confirmPassword: String): Flowable { - val auth = UserAuth() - auth.username = username - auth.password = password - auth.confirmPassword = confirmPassword - auth.email = email - return this.apiService.registerUser(auth).compose(configureApiCallObserver()) - } - - override fun connectUser(username: String, password: String): Flowable { - val auth = UserAuth() - auth.username = username - auth.password = password - return this.apiService.connectLocal(auth).compose(configureApiCallObserver()) - } - - override fun connectSocial(network: String, userId: String, accessToken: String): Flowable { - val auth = UserAuthSocial() - auth.network = network - val authResponse = UserAuthSocialTokens() - authResponse.client_id = userId - authResponse.access_token = accessToken - auth.authResponse = authResponse - - return this.apiService.connectSocial(auth).compose(configureApiCallObserver()) - } - - override fun accept(throwable: Throwable) { - val throwableClass = throwable.javaClass - if (SocketTimeoutException::class.java.isAssignableFrom(throwableClass)) { - return - } - @Suppress("DEPRECATION") - if (SocketException::class.java.isAssignableFrom(throwableClass) || SSLException::class.java.isAssignableFrom(throwableClass)) { - this.showConnectionProblemDialog(R.string.internal_error_api) - } else if (throwableClass == SocketTimeoutException::class.java || UnknownHostException::class.java == throwableClass) { - this.showConnectionProblemDialog(R.string.network_error_no_network_body) - } else if (throwableClass == retrofit2.adapter.rxjava2.HttpException::class.java) { - val error = throwable as HttpException - val res = getErrorResponse(error) - val status = error.code() - - if (error.response().raw().request().url().toString().endsWith("/user/push-devices")) { - //workaround for an error that sometimes displays that the user already has this push device - return - } - - if (status == 404) { - return - } - - if (status in 400..499) { - if (res.displayMessage.isNotEmpty()) { - showConnectionProblemDialog("", res.displayMessage) - } else if (status == 401) { - showConnectionProblemDialog(R.string.authentication_error_title, R.string.authentication_error_body) - } - } else if (status in 500..599) { - this.showConnectionProblemDialog(R.string.internal_error_api) - } else { - showConnectionProblemDialog(R.string.internal_error_api) - } - } else if (JsonSyntaxException::class.java.isAssignableFrom(throwableClass)) { - crashlyticsProxy.log("Json Error: " + lastAPICallURL + ", " + throwable.message) - } else { - crashlyticsProxy.logException(throwable) - } - } - - override fun getErrorResponse(throwable: HttpException): ErrorResponse { - val errorResponse = throwable.response().errorBody() ?: return ErrorResponse() - val errorConverter = gsonConverter - .responseBodyConverter(ErrorResponse::class.java, arrayOfNulls(0), retrofitAdapter) - return try { - errorConverter?.convert(errorResponse) as ErrorResponse - } catch (e: IOException) { - ErrorResponse() - } - - } - - override fun retrieveUser(withTasks: Boolean): Flowable { - - var userObservable = this.user - - if (withTasks) { - val tasksObservable = this.tasks - - userObservable = Flowable.zip(userObservable, tasksObservable, - BiFunction { habitRPGUser, tasks -> - habitRPGUser.tasks = tasks - habitRPGUser - }) - } - return userObservable - } - - override fun retrieveInboxMessages(): Flowable> { - return apiService.inboxMessages.compose(configureApiCallObserver()) - } - - override fun hasAuthenticationKeys(): Boolean { - return this.hostConfig.user.isNotEmpty() && hostConfig.api.isNotEmpty() - } - - private fun showConnectionProblemDialog(resourceMessageString: Int) { - showConnectionProblemDialog(R.string.network_error_title, resourceMessageString) - } - - private fun showConnectionProblemDialog(resourceTitleString: Int, resourceMessageString: Int) { - showConnectionProblemDialog(context.getString(resourceTitleString), context.getString(resourceMessageString)) - } - - private fun showConnectionProblemDialog(resourceTitleString: String, resourceMessageString: String) { - val event = ShowConnectionProblemEvent(resourceTitleString, resourceMessageString) - EventBus.getDefault().post(event) - if (BuildConfig.DEBUG) { - Log.d(TAG, "showConnectionProblemDialog: $resourceTitleString $resourceMessageString") - } - } - - /* - This function is used with Observer.compose to reuse transformers across the application. - See here for more info: http://blog.danlew.net/2015/03/02/dont-break-the-chain/ - */ - - override fun configureApiCallObserver(): FlowableTransformer, T> { - return apiCallTransformer as FlowableTransformer, T> - } - - override fun updateAuthenticationCredentials(userID: String?, apiToken: String?) { - this.hostConfig.user = userID ?: "" - this.hostConfig.api = apiToken ?: "" - crashlyticsProxy.setUserIdentifier(this.hostConfig.user) - crashlyticsProxy.setUserName(this.hostConfig.user) - Amplitude.getInstance().userId = this.hostConfig.user - } - - override fun setLanguageCode(languageCode: String) { - this.languageCode = languageCode - } - - override val status: Flowable - get() = apiService.status.compose(configureApiCallObserver()) - - override fun getContent(language: String): Flowable { - return apiService.getContent(language).compose(configureApiCallObserver()) - } - - override val user: Flowable - get() = apiService.user.compose(configureApiCallObserver()) - - override fun updateUser(updateDictionary: Map): Flowable { - return apiService.updateUser(updateDictionary).compose(configureApiCallObserver()) - } - - override fun registrationLanguage(registrationLanguage: String): Flowable { - return apiService.registrationLanguage(registrationLanguage).compose(configureApiCallObserver()) - } - - override fun retrieveInAppRewards(): Flowable> { - return apiService.retrieveInAppRewards().compose(configureApiCallObserver()) - } - - override fun retrieveOldGear(): Flowable> { - return apiService.retrieveOldGearRewards().compose(configureApiCallObserver()) - } - - override fun equipItem(type: String, itemKey: String): Flowable { - return apiService.equipItem(type, itemKey).compose(configureApiCallObserver()) - } - - override fun buyItem(itemKey: String): Flowable { - return apiService.buyItem(itemKey).compose(configureApiCallObserver()) - } - - override fun purchaseItem(type: String, itemKey: String): Flowable { - return apiService.purchaseItem(type, itemKey).compose(configureApiCallObserver()) - } - - override fun validateSubscription(request: SubscriptionValidationRequest): Flowable { - return apiService.validateSubscription(request).map { habitResponse -> - if (habitResponse.notifications != null) { - popupNotificationsManager.showNotificationDialog(habitResponse.notifications) - } - habitResponse.getData() - } - } - - override fun validateNoRenewSubscription(request: PurchaseValidationRequest): Flowable { - return apiService.validateNoRenewSubscription(request).map { habitResponse -> - if (habitResponse.notifications != null) { - popupNotificationsManager.showNotificationDialog(habitResponse.notifications) - } - habitResponse.getData() - } - } - - override fun purchaseHourglassItem(type: String, itemKey: String): Flowable { - return apiService.purchaseHourglassItem(type, itemKey).compose(configureApiCallObserver()) - } - - override fun purchaseMysterySet(itemKey: String): Flowable { - return apiService.purchaseMysterySet(itemKey).compose(configureApiCallObserver()) - } - - override fun purchaseQuest(key: String): Flowable { - return apiService.purchaseQuest(key).compose(configureApiCallObserver()) - } - - override fun sellItem(itemType: String, itemKey: String): Flowable { - return apiService.sellItem(itemType, itemKey).compose(configureApiCallObserver()) - } - - override fun feedPet(petKey: String, foodKey: String): Flowable { - return apiService.feedPet(petKey, foodKey).compose(configureApiCallObserver()) - } - - override fun hatchPet(eggKey: String, hatchingPotionKey: String): Flowable { - return apiService.hatchPet(eggKey, hatchingPotionKey).compose(configureApiCallObserver()) - } - - override val tasks: Flowable - get() = apiService.tasks.compose(configureApiCallObserver()) - - override fun getTasks(type: String): Flowable { - return apiService.getTasks(type).compose(configureApiCallObserver()) - } - - - override fun getTasks(type: String, dueDate: String): Flowable { - return apiService.getTasks(type, dueDate).compose(configureApiCallObserver()) - } - - - override fun unlockPath(path: String): Flowable { - return apiService.unlockPath(path).compose(configureApiCallObserver()) - } - - override fun getTask(id: String): Flowable { - return apiService.getTask(id).compose(configureApiCallObserver()) - } - - override fun postTaskDirection(id: String, direction: String): Flowable { - return apiService.postTaskDirection(id, direction).compose(configureApiCallObserver()) - } - - override fun postTaskNewPosition(id: String, position: Int): Flowable> { - return apiService.postTaskNewPosition(id, position).compose(configureApiCallObserver()) - } - - override fun scoreChecklistItem(taskId: String, itemId: String): Flowable { - return apiService.scoreChecklistItem(taskId, itemId).compose(configureApiCallObserver()) - } - - override fun createTask(item: Task): Flowable { - return apiService.createTask(item).compose(configureApiCallObserver()) - } - - override fun createTasks(tasks: List): Flowable> { - return apiService.createTasks(tasks).compose(configureApiCallObserver()) - } - - override fun updateTask(id: String, item: Task): Flowable { - return apiService.updateTask(id, item).compose(configureApiCallObserver()) - } - - override fun deleteTask(id: String): Flowable { - return apiService.deleteTask(id).compose(configureApiCallObserver()) - } - - override fun createTag(tag: Tag): Flowable { - return apiService.createTag(tag).compose(configureApiCallObserver()) - } - - override fun updateTag(id: String, tag: Tag): Flowable { - return apiService.updateTag(id, tag).compose(configureApiCallObserver()) - } - - override fun deleteTag(id: String): Flowable { - return apiService.deleteTag(id).compose(configureApiCallObserver()) - } - - override fun sleep(): Flowable { - return apiService.sleep().compose(configureApiCallObserver()) - } - - override fun revive(): Flowable { - return apiService.revive().compose(configureApiCallObserver()) - } - - override fun useSkill(skillName: String, targetType: String, targetId: String): Flowable { - return apiService.useSkill(skillName, targetType, targetId).compose(configureApiCallObserver()) - } - - override fun useSkill(skillName: String, targetType: String): Flowable { - return apiService.useSkill(skillName, targetType).compose(configureApiCallObserver()) - } - - override fun changeClass(): Flowable { - return apiService.changeClass().compose(configureApiCallObserver()) - } - - override fun changeClass(className: String): Flowable { - return apiService.changeClass(className).compose(configureApiCallObserver()) - } - - override fun disableClasses(): Flowable { - return apiService.disableClasses().compose(configureApiCallObserver()) - } - - override fun markPrivateMessagesRead(): Flowable { - //This is necessary, because the API call returns weird data. - return apiService.markPrivateMessagesRead() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnError(this) - } - - override fun listGroups(type: String): Flowable> { - return apiService.listGroups(type).compose(configureApiCallObserver()) - } - - override fun getGroup(groupId: String): Flowable { - return apiService.getGroup(groupId).compose(configureApiCallObserver()) - } - - override fun createGroup(group: Group): Flowable { - return apiService.createGroup(group).compose(configureApiCallObserver()) - } - - override fun updateGroup(id: String, item: Group): Flowable { - return apiService.updateGroup(id, item).compose(configureApiCallObserver()) - } - - override fun listGroupChat(groupId: String): Flowable> { - return apiService.listGroupChat(groupId).compose(configureApiCallObserver()) - } - - override fun joinGroup(groupId: String): Flowable { - return apiService.joinGroup(groupId).compose(configureApiCallObserver()) - } - - override fun leaveGroup(groupId: String): Flowable { - return apiService.leaveGroup(groupId).compose(configureApiCallObserver()) - } - - override fun postGroupChat(groupId: String, message: Map): Flowable { - return apiService.postGroupChat(groupId, message).compose(configureApiCallObserver()) - } - - override fun deleteMessage(groupId: String, messageId: String): Flowable { - return apiService.deleteMessage(groupId, messageId).compose(configureApiCallObserver()) - } - override fun deleteInboxMessage(id: String): Flowable { - return apiService.deleteInboxMessage(id).compose(configureApiCallObserver()) - } - - override fun getGroupMembers(groupId: String, includeAllPublicFields: Boolean?): Flowable> { - return apiService.getGroupMembers(groupId, includeAllPublicFields).compose(configureApiCallObserver()) - } - - override fun getGroupMembers(groupId: String, includeAllPublicFields: Boolean?, lastId: String): Flowable> { - return apiService.getGroupMembers(groupId, includeAllPublicFields, lastId).compose(configureApiCallObserver()) - } - - override fun likeMessage(groupId: String, mid: String): Flowable { - return apiService.likeMessage(groupId, mid).compose(configureApiCallObserver()) - } - - override fun flagMessage(groupId: String, mid: String, data: MutableMap): Flowable { - return apiService.flagMessage(groupId, mid, data).compose(configureApiCallObserver()) - } - - override fun seenMessages(groupId: String): Flowable { - return apiService.seenMessages(groupId).compose(configureApiCallObserver()) - } - - override fun inviteToGroup(groupId: String, inviteData: Map): Flowable> { - return apiService.inviteToGroup(groupId, inviteData).compose(configureApiCallObserver()) - } - - override fun rejectGroupInvite(groupId: String): Flowable { - return apiService.rejectGroupInvite(groupId).compose(configureApiCallObserver()) - } - - override fun acceptQuest(groupId: String): Flowable { - return apiService.acceptQuest(groupId).compose(configureApiCallObserver()) - } - - override fun rejectQuest(groupId: String): Flowable { - return apiService.rejectQuest(groupId).compose(configureApiCallObserver()) - } - - override fun cancelQuest(groupId: String): Flowable { - return apiService.cancelQuest(groupId).compose(configureApiCallObserver()) - } - - override fun forceStartQuest(groupId: String, group: Group): Flowable { - return apiService.forceStartQuest(groupId, group).compose(configureApiCallObserver()) - } - - override fun inviteToQuest(groupId: String, questKey: String): Flowable { - return apiService.inviteToQuest(groupId, questKey).compose(configureApiCallObserver()) - } - - override fun abortQuest(groupId: String): Flowable { - return apiService.abortQuest(groupId).compose(configureApiCallObserver()) - } - - override fun leaveQuest(groupId: String): Flowable { - return apiService.leaveQuest(groupId).compose(configureApiCallObserver()) - } - - override fun validatePurchase(request: PurchaseValidationRequest): Flowable { - return apiService.validatePurchase(request).map { habitResponse -> - if (habitResponse.notifications != null) { - popupNotificationsManager.showNotificationDialog(habitResponse.notifications) - } - habitResponse.getData() - } - } - - override fun changeCustomDayStart(updateObject: Map): Flowable { - return apiService.changeCustomDayStart(updateObject).compose(configureApiCallObserver()) - } - - override fun getMember(memberId: String): Flowable { - return apiService.getMember(memberId).compose(configureApiCallObserver()) - } - - override fun getMemberWithUsername(username: String): Flowable { - return apiService.getMemberWithUsername(username).compose(configureApiCallObserver()) - } - - override fun getMemberAchievements(memberId: String): Flowable { - return apiService.getMemberAchievements(memberId).compose(configureApiCallObserver()) - } - - override fun findUsernames(username: String, context: String?, id: String?): Flowable> { - return apiService.findUsernames(username, context, id).compose(configureApiCallObserver()) - } - - override fun postPrivateMessage(messageDetails: Map): Flowable { - return apiService.postPrivateMessage(messageDetails).compose(configureApiCallObserver()) - } - - override fun retrieveShopIventory(identifier: String): Flowable { - return apiService.retrieveShopInventory(identifier).compose(configureApiCallObserver()) - } - - override fun addPushDevice(pushDeviceData: Map): Flowable> { - return apiService.addPushDevice(pushDeviceData).compose(configureApiCallObserver()) - } - - override fun deletePushDevice(regId: String): Flowable> { - return apiService.deletePushDevice(regId).compose(configureApiCallObserver()) - } - - override fun getUserChallenges(page: Int, memberOnly: Boolean): Flowable> { - return if (memberOnly) { - apiService.getUserChallenges(page, memberOnly).compose(configureApiCallObserver()) - } else { - apiService.getUserChallenges(page).compose(configureApiCallObserver()) - } - } - - override fun getChallengeTasks(challengeId: String): Flowable { - return apiService.getChallengeTasks(challengeId).compose(configureApiCallObserver()) - } - - override fun getChallenge(challengeId: String): Flowable { - return apiService.getChallenge(challengeId).compose(configureApiCallObserver()) - } - - override fun joinChallenge(challengeId: String): Flowable { - return apiService.joinChallenge(challengeId).compose(configureApiCallObserver()) - } - - override fun leaveChallenge(challengeId: String, body: LeaveChallengeBody): Flowable { - return apiService.leaveChallenge(challengeId, body).compose(configureApiCallObserver()) - } - - - override fun createChallenge(challenge: Challenge): Flowable { - return apiService.createChallenge(challenge).compose(configureApiCallObserver()) - } - - override fun createChallengeTasks(challengeId: String, tasks: List): Flowable> { - return apiService.createChallengeTasks(challengeId, tasks).compose(configureApiCallObserver()) - } - - override fun createChallengeTask(challengeId: String, task: Task): Flowable { - return apiService.createChallengeTask(challengeId, task).compose(configureApiCallObserver()) - } - - override fun updateChallenge(challenge: Challenge): Flowable { - return apiService.updateChallenge(challenge.id, challenge).compose(configureApiCallObserver()) - } - - override fun deleteChallenge(challengeId: String): Flowable { - return apiService.deleteChallenge(challengeId).compose(configureApiCallObserver()) - } - - override fun debugAddTenGems(): Flowable { - return apiService.debugAddTenGems().compose(configureApiCallObserver()) - } - - override fun readNotification(notificationId: String): Flowable> { - return apiService.readNotification(notificationId).compose(configureApiCallObserver()) - } - - override val content: Flowable - get() = apiService.getContent(languageCode).compose(configureApiCallObserver()) - - override fun openMysteryItem(): Flowable { - return apiService.openMysteryItem().compose(configureApiCallObserver()) - } - - override fun runCron(): Flowable { - return apiService.runCron().compose(configureApiCallObserver()) - } - - override fun resetAccount(): Flowable { - return apiService.resetAccount().compose(configureApiCallObserver()) - } - - override fun deleteAccount(password: String): Flowable { - val updateObject = HashMap() - updateObject["password"] = password - return apiService.deleteAccount(updateObject).compose(configureApiCallObserver()) - } - - override fun togglePinnedItem(pinType: String, path: String): Flowable { - return apiService.togglePinnedItem(pinType, path).compose(configureApiCallObserver()) - } - - override fun sendPasswordResetEmail(email: String): Flowable { - val data = HashMap() - data["email"] = email - return apiService.sendPasswordResetEmail(data).compose(configureApiCallObserver()) - } - - override fun updateLoginName(newLoginName: String, password: String): Flowable { - val updateObject = HashMap() - updateObject["username"] = newLoginName - updateObject["password"] = password - return apiService.updateLoginName(updateObject).compose(configureApiCallObserver()) - } - - override fun updateUsername(newLoginName: String): Flowable { - val updateObject = HashMap() - updateObject["username"] = newLoginName - return apiService.updateLoginName(updateObject).compose(configureApiCallObserver()) - } - - override fun verifyUsername(username: String): Flowable { - val updateObject = HashMap() - updateObject["username"] = username - return this.apiService.verifyUsername(updateObject).compose(configureApiCallObserver()) - } - - override fun updateEmail(newEmail: String, password: String): Flowable { - val updateObject = HashMap() - updateObject["newEmail"] = newEmail - updateObject["password"] = password - return apiService.updateEmail(updateObject).compose(configureApiCallObserver()) - } - - override fun updatePassword(oldPassword: String, newPassword: String, newPasswordConfirmation: String): Flowable { - val updateObject = HashMap() - updateObject["password"] = oldPassword - updateObject["newPassword"] = newPassword - updateObject["confirmPassword"] = newPasswordConfirmation - return apiService.updatePassword(updateObject).compose(configureApiCallObserver()) - } - - override fun allocatePoint(stat: String): Flowable { - return apiService.allocatePoint(stat).compose(configureApiCallObserver()) - } - - override fun bulkAllocatePoints(strength: Int, intelligence: Int, constitution: Int, perception: Int): Flowable { - val body = HashMap>() - val stats = HashMap() - stats["str"] = strength - stats["int"] = intelligence - stats["con"] = constitution - stats["per"] = perception - body["stats"] = stats - return apiService.bulkAllocatePoints(body).compose(configureApiCallObserver()) - } - - override fun retrieveMarketGear(): Flowable { - return apiService.retrieveMarketGear().compose(configureApiCallObserver()) - } - - override val worldState: Flowable - get() = apiService.worldState.compose(configureApiCallObserver()) - - companion object { - private const val TAG = "ApiClientImpl" - - fun createGsonFactory(): GsonConverterFactory { - return GSonFactoryCreator.create() - } - } -} +package com.habitrpg.android.habitica.data.implementation + +import android.content.Context +import android.util.Log +import androidx.appcompat.app.AlertDialog +import com.amplitude.api.Amplitude +import com.google.gson.JsonSyntaxException +import com.habitrpg.android.habitica.BuildConfig +import com.habitrpg.android.habitica.HabiticaBaseApplication +import com.habitrpg.android.habitica.R +import com.habitrpg.android.habitica.api.ApiService +import com.habitrpg.android.habitica.api.GSonFactoryCreator +import com.habitrpg.android.habitica.api.HostConfig +import com.habitrpg.android.habitica.api.Server +import com.habitrpg.android.habitica.data.ApiClient +import com.habitrpg.android.habitica.events.ShowConnectionProblemEvent +import com.habitrpg.android.habitica.helpers.PopupNotificationsManager +import com.habitrpg.android.habitica.models.* +import com.habitrpg.android.habitica.models.auth.UserAuth +import com.habitrpg.android.habitica.models.auth.UserAuthResponse +import com.habitrpg.android.habitica.models.auth.UserAuthSocial +import com.habitrpg.android.habitica.models.auth.UserAuthSocialTokens +import com.habitrpg.android.habitica.models.inventory.Equipment +import com.habitrpg.android.habitica.models.inventory.Quest +import com.habitrpg.android.habitica.models.members.Member +import com.habitrpg.android.habitica.models.responses.* +import com.habitrpg.android.habitica.models.shops.Shop +import com.habitrpg.android.habitica.models.shops.ShopItem +import com.habitrpg.android.habitica.models.social.Challenge +import com.habitrpg.android.habitica.models.social.ChatMessage +import com.habitrpg.android.habitica.models.social.FindUsernameResult +import com.habitrpg.android.habitica.models.social.Group +import com.habitrpg.android.habitica.models.tasks.Task +import com.habitrpg.android.habitica.models.tasks.TaskList +import com.habitrpg.android.habitica.models.user.Items +import com.habitrpg.android.habitica.models.user.Stats +import com.habitrpg.android.habitica.models.user.User +import com.habitrpg.android.habitica.proxy.CrashlyticsProxy +import io.reactivex.Flowable +import io.reactivex.FlowableTransformer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.functions.BiFunction +import io.reactivex.functions.Consumer +import io.reactivex.schedulers.Schedulers +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.logging.HttpLoggingInterceptor +import org.greenrobot.eventbus.EventBus +import retrofit2.HttpException +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory +import java.io.IOException +import java.net.SocketException +import java.net.SocketTimeoutException +import java.net.UnknownHostException +import java.util.* +import java.util.concurrent.TimeUnit +import javax.net.ssl.SSLException + + +class ApiClientImpl//private OnHabitsAPIResult mResultListener; +//private HostConfig mConfig; +(private val gsonConverter: GsonConverterFactory, override val hostConfig: HostConfig, private val crashlyticsProxy: CrashlyticsProxy, private val popupNotificationsManager: PopupNotificationsManager, private val context: Context) : Consumer, ApiClient { + + + private lateinit var retrofitAdapter: Retrofit + + // I think we don't need the ApiClientImpl anymore we could just use ApiService + private lateinit var apiService: ApiService + + private val apiCallTransformer = FlowableTransformer, Any> { observable -> + observable + .filter { it.data != null } + .map { habitResponse -> + if (habitResponse.notifications != null) { + popupNotificationsManager.showNotificationDialog(habitResponse.notifications) + } + habitResponse.data + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnError(this) + } + private val displayedAlert: AlertDialog? = null + private var languageCode: String? = null + private var lastAPICallURL: String? = null + + init { + this.popupNotificationsManager.setApiClient(this) + + HabiticaBaseApplication.component?.inject(this) + crashlyticsProxy.setUserIdentifier(this.hostConfig.user) + crashlyticsProxy.setUserName(this.hostConfig.user) + Amplitude.getInstance().userId = this.hostConfig.user + buildRetrofit() + } + + fun buildRetrofit() { + val logging = HttpLoggingInterceptor() + if (BuildConfig.DEBUG) { + logging.level = HttpLoggingInterceptor.Level.BODY + } + + val userAgent = System.getProperty("http.agent") + + val calendar = GregorianCalendar() + val timeZone = calendar.timeZone + val timezoneOffset = -TimeUnit.MINUTES.convert(timeZone.getOffset(calendar.timeInMillis).toLong(), TimeUnit.MILLISECONDS) + + val client = OkHttpClient.Builder() + .addInterceptor(logging) + .addNetworkInterceptor { chain -> + val original = chain.request() + var builder: Request.Builder = original.newBuilder() + if (this.hostConfig.hasAuthentication()) { + builder = builder + .header("x-api-key", this.hostConfig.api) + .header("x-api-user", this.hostConfig.user) + } + builder = builder.header("x-client", "habitica-android") + .header("x-user-timezoneOffset", timezoneOffset.toString()) + if (userAgent != null) { + builder = builder.header("user-agent", userAgent) + } + if (!BuildConfig.STAGING_KEY.isEmpty()) { + builder = builder.header("Authorization", "Basic " + BuildConfig.STAGING_KEY) + } + val request = builder.method(original.method(), original.body()) + .build() + lastAPICallURL = original.url().toString() + Log.d("NETWORK", lastAPICallURL) + chain.proceed(request) + } + .readTimeout(2400, TimeUnit.SECONDS) + .build() + + val server = Server(this.hostConfig.address) + + retrofitAdapter = Retrofit.Builder() + .client(client) + .baseUrl(server.toString()) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .addConverterFactory(gsonConverter) + .build() + + this.apiService = retrofitAdapter.create(ApiService::class.java) + } + + override fun updateServerUrl(newAddress: String?) { + if (newAddress != null) { + hostConfig.address = newAddress + buildRetrofit() + } + } + + override fun registerUser(username: String, email: String, password: String, confirmPassword: String): Flowable { + val auth = UserAuth() + auth.username = username + auth.password = password + auth.confirmPassword = confirmPassword + auth.email = email + return this.apiService.registerUser(auth).compose(configureApiCallObserver()) + } + + override fun connectUser(username: String, password: String): Flowable { + val auth = UserAuth() + auth.username = username + auth.password = password + return this.apiService.connectLocal(auth).compose(configureApiCallObserver()) + } + + override fun connectSocial(network: String, userId: String, accessToken: String): Flowable { + val auth = UserAuthSocial() + auth.network = network + val authResponse = UserAuthSocialTokens() + authResponse.client_id = userId + authResponse.access_token = accessToken + auth.authResponse = authResponse + + return this.apiService.connectSocial(auth).compose(configureApiCallObserver()) + } + + override fun accept(throwable: Throwable) { + val throwableClass = throwable.javaClass + if (SocketTimeoutException::class.java.isAssignableFrom(throwableClass)) { + return + } + @Suppress("DEPRECATION") + if (SocketException::class.java.isAssignableFrom(throwableClass) || SSLException::class.java.isAssignableFrom(throwableClass)) { + this.showConnectionProblemDialog(R.string.internal_error_api) + } else if (throwableClass == SocketTimeoutException::class.java || UnknownHostException::class.java == throwableClass) { + this.showConnectionProblemDialog(R.string.network_error_no_network_body) + } else if (throwableClass == retrofit2.adapter.rxjava2.HttpException::class.java) { + val error = throwable as HttpException + val res = getErrorResponse(error) + val status = error.code() + + if (error.response().raw().request().url().toString().endsWith("/user/push-devices")) { + //workaround for an error that sometimes displays that the user already has this push device + return + } + + if (status == 404) { + return + } + + if (status in 400..499) { + if (res.displayMessage.isNotEmpty()) { + showConnectionProblemDialog("", res.displayMessage) + } else if (status == 401) { + showConnectionProblemDialog(R.string.authentication_error_title, R.string.authentication_error_body) + } + } else if (status in 500..599) { + this.showConnectionProblemDialog(R.string.internal_error_api) + } else { + showConnectionProblemDialog(R.string.internal_error_api) + } + } else if (JsonSyntaxException::class.java.isAssignableFrom(throwableClass)) { + crashlyticsProxy.log("Json Error: " + lastAPICallURL + ", " + throwable.message) + } else { + crashlyticsProxy.logException(throwable) + } + } + + override fun getErrorResponse(throwable: HttpException): ErrorResponse { + val errorResponse = throwable.response().errorBody() ?: return ErrorResponse() + val errorConverter = gsonConverter + .responseBodyConverter(ErrorResponse::class.java, arrayOfNulls(0), retrofitAdapter) + return try { + errorConverter?.convert(errorResponse) as ErrorResponse + } catch (e: IOException) { + ErrorResponse() + } + + } + + override fun retrieveUser(withTasks: Boolean): Flowable { + + var userObservable = this.user + + if (withTasks) { + val tasksObservable = this.tasks + + userObservable = Flowable.zip(userObservable, tasksObservable, + BiFunction { habitRPGUser, tasks -> + habitRPGUser.tasks = tasks + habitRPGUser + }) + } + return userObservable + } + + override fun retrieveInboxMessages(): Flowable> { + return apiService.inboxMessages.compose(configureApiCallObserver()) + } + + override fun hasAuthenticationKeys(): Boolean { + return this.hostConfig.user.isNotEmpty() && hostConfig.api.isNotEmpty() + } + + private fun showConnectionProblemDialog(resourceMessageString: Int) { + showConnectionProblemDialog(R.string.network_error_title, resourceMessageString) + } + + private fun showConnectionProblemDialog(resourceTitleString: Int, resourceMessageString: Int) { + showConnectionProblemDialog(context.getString(resourceTitleString), context.getString(resourceMessageString)) + } + + private fun showConnectionProblemDialog(resourceTitleString: String, resourceMessageString: String) { + val event = ShowConnectionProblemEvent(resourceTitleString, resourceMessageString) + EventBus.getDefault().post(event) + if (BuildConfig.DEBUG) { + Log.d(TAG, "showConnectionProblemDialog: $resourceTitleString $resourceMessageString") + } + } + + /* + This function is used with Observer.compose to reuse transformers across the application. + See here for more info: http://blog.danlew.net/2015/03/02/dont-break-the-chain/ + */ + + override fun configureApiCallObserver(): FlowableTransformer, T> { + return apiCallTransformer as FlowableTransformer, T> + } + + override fun updateAuthenticationCredentials(userID: String?, apiToken: String?) { + this.hostConfig.user = userID ?: "" + this.hostConfig.api = apiToken ?: "" + crashlyticsProxy.setUserIdentifier(this.hostConfig.user) + crashlyticsProxy.setUserName(this.hostConfig.user) + Amplitude.getInstance().userId = this.hostConfig.user + } + + override fun setLanguageCode(languageCode: String) { + this.languageCode = languageCode + } + + override val status: Flowable + get() = apiService.status.compose(configureApiCallObserver()) + + override fun getContent(language: String): Flowable { + return apiService.getContent(language).compose(configureApiCallObserver()) + } + + override val user: Flowable + get() = apiService.user.compose(configureApiCallObserver()) + + override fun updateUser(updateDictionary: Map): Flowable { + return apiService.updateUser(updateDictionary).compose(configureApiCallObserver()) + } + + override fun registrationLanguage(registrationLanguage: String): Flowable { + return apiService.registrationLanguage(registrationLanguage).compose(configureApiCallObserver()) + } + + override fun retrieveInAppRewards(): Flowable> { + return apiService.retrieveInAppRewards().compose(configureApiCallObserver()) + } + + override fun retrieveOldGear(): Flowable> { + return apiService.retrieveOldGearRewards().compose(configureApiCallObserver()) + } + + override fun equipItem(type: String, itemKey: String): Flowable { + return apiService.equipItem(type, itemKey).compose(configureApiCallObserver()) + } + + override fun buyItem(itemKey: String): Flowable { + return apiService.buyItem(itemKey).compose(configureApiCallObserver()) + } + + override fun purchaseItem(type: String, itemKey: String): Flowable { + return apiService.purchaseItem(type, itemKey).compose(configureApiCallObserver()) + } + + override fun validateSubscription(request: SubscriptionValidationRequest): Flowable { + return apiService.validateSubscription(request).map { habitResponse -> + if (habitResponse.notifications != null) { + popupNotificationsManager.showNotificationDialog(habitResponse.notifications) + } + habitResponse.getData() + } + } + + override fun validateNoRenewSubscription(request: PurchaseValidationRequest): Flowable { + return apiService.validateNoRenewSubscription(request).map { habitResponse -> + if (habitResponse.notifications != null) { + popupNotificationsManager.showNotificationDialog(habitResponse.notifications) + } + habitResponse.getData() + } + } + + override fun purchaseHourglassItem(type: String, itemKey: String): Flowable { + return apiService.purchaseHourglassItem(type, itemKey).compose(configureApiCallObserver()) + } + + override fun purchaseMysterySet(itemKey: String): Flowable { + return apiService.purchaseMysterySet(itemKey).compose(configureApiCallObserver()) + } + + override fun purchaseQuest(key: String): Flowable { + return apiService.purchaseQuest(key).compose(configureApiCallObserver()) + } + + override fun sellItem(itemType: String, itemKey: String): Flowable { + return apiService.sellItem(itemType, itemKey).compose(configureApiCallObserver()) + } + + override fun feedPet(petKey: String, foodKey: String): Flowable { + return apiService.feedPet(petKey, foodKey).compose(configureApiCallObserver()) + } + + override fun hatchPet(eggKey: String, hatchingPotionKey: String): Flowable { + return apiService.hatchPet(eggKey, hatchingPotionKey).compose(configureApiCallObserver()) + } + + override val tasks: Flowable + get() = apiService.tasks.compose(configureApiCallObserver()) + + override fun getTasks(type: String): Flowable { + return apiService.getTasks(type).compose(configureApiCallObserver()) + } + + + override fun getTasks(type: String, dueDate: String): Flowable { + return apiService.getTasks(type, dueDate).compose(configureApiCallObserver()) + } + + + override fun unlockPath(path: String): Flowable { + return apiService.unlockPath(path).compose(configureApiCallObserver()) + } + + override fun getTask(id: String): Flowable { + return apiService.getTask(id).compose(configureApiCallObserver()) + } + + override fun postTaskDirection(id: String, direction: String): Flowable { + return apiService.postTaskDirection(id, direction).compose(configureApiCallObserver()) + } + + override fun postTaskNewPosition(id: String, position: Int): Flowable> { + return apiService.postTaskNewPosition(id, position).compose(configureApiCallObserver()) + } + + override fun scoreChecklistItem(taskId: String, itemId: String): Flowable { + return apiService.scoreChecklistItem(taskId, itemId).compose(configureApiCallObserver()) + } + + override fun createTask(item: Task): Flowable { + return apiService.createTask(item).compose(configureApiCallObserver()) + } + + override fun createTasks(tasks: List): Flowable> { + return apiService.createTasks(tasks).compose(configureApiCallObserver()) + } + + override fun updateTask(id: String, item: Task): Flowable { + return apiService.updateTask(id, item).compose(configureApiCallObserver()) + } + + override fun deleteTask(id: String): Flowable { + return apiService.deleteTask(id).compose(configureApiCallObserver()) + } + + override fun createTag(tag: Tag): Flowable { + return apiService.createTag(tag).compose(configureApiCallObserver()) + } + + override fun updateTag(id: String, tag: Tag): Flowable { + return apiService.updateTag(id, tag).compose(configureApiCallObserver()) + } + + override fun deleteTag(id: String): Flowable { + return apiService.deleteTag(id).compose(configureApiCallObserver()) + } + + override fun sleep(): Flowable { + return apiService.sleep().compose(configureApiCallObserver()) + } + + override fun revive(): Flowable { + return apiService.revive().compose(configureApiCallObserver()) + } + + override fun useSkill(skillName: String, targetType: String, targetId: String): Flowable { + return apiService.useSkill(skillName, targetType, targetId).compose(configureApiCallObserver()) + } + + override fun useSkill(skillName: String, targetType: String): Flowable { + return apiService.useSkill(skillName, targetType).compose(configureApiCallObserver()) + } + + override fun changeClass(): Flowable { + return apiService.changeClass().compose(configureApiCallObserver()) + } + + override fun changeClass(className: String): Flowable { + return apiService.changeClass(className).compose(configureApiCallObserver()) + } + + override fun disableClasses(): Flowable { + return apiService.disableClasses().compose(configureApiCallObserver()) + } + + override fun markPrivateMessagesRead(): Flowable { + //This is necessary, because the API call returns weird data. + return apiService.markPrivateMessagesRead() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnError(this) + } + + override fun listGroups(type: String): Flowable> { + return apiService.listGroups(type).compose(configureApiCallObserver()) + } + + override fun getGroup(groupId: String): Flowable { + return apiService.getGroup(groupId).compose(configureApiCallObserver()) + } + + override fun createGroup(group: Group): Flowable { + return apiService.createGroup(group).compose(configureApiCallObserver()) + } + + override fun updateGroup(id: String, item: Group): Flowable { + return apiService.updateGroup(id, item).compose(configureApiCallObserver()) + } + + override fun listGroupChat(groupId: String): Flowable> { + return apiService.listGroupChat(groupId).compose(configureApiCallObserver()) + } + + override fun joinGroup(groupId: String): Flowable { + return apiService.joinGroup(groupId).compose(configureApiCallObserver()) + } + + override fun leaveGroup(groupId: String): Flowable { + return apiService.leaveGroup(groupId).compose(configureApiCallObserver()) + } + + override fun postGroupChat(groupId: String, message: Map): Flowable { + return apiService.postGroupChat(groupId, message).compose(configureApiCallObserver()) + } + + override fun deleteMessage(groupId: String, messageId: String): Flowable { + return apiService.deleteMessage(groupId, messageId).compose(configureApiCallObserver()) + } + override fun deleteInboxMessage(id: String): Flowable { + return apiService.deleteInboxMessage(id).compose(configureApiCallObserver()) + } + + override fun getGroupMembers(groupId: String, includeAllPublicFields: Boolean?): Flowable> { + return apiService.getGroupMembers(groupId, includeAllPublicFields).compose(configureApiCallObserver()) + } + + override fun getGroupMembers(groupId: String, includeAllPublicFields: Boolean?, lastId: String): Flowable> { + return apiService.getGroupMembers(groupId, includeAllPublicFields, lastId).compose(configureApiCallObserver()) + } + + override fun likeMessage(groupId: String, mid: String): Flowable { + return apiService.likeMessage(groupId, mid).compose(configureApiCallObserver()) + } + + override fun flagMessage(groupId: String, mid: String, data: MutableMap): Flowable { + return apiService.flagMessage(groupId, mid, data).compose(configureApiCallObserver()) + } + + override fun seenMessages(groupId: String): Flowable { + return apiService.seenMessages(groupId).compose(configureApiCallObserver()) + } + + override fun inviteToGroup(groupId: String, inviteData: Map): Flowable> { + return apiService.inviteToGroup(groupId, inviteData).compose(configureApiCallObserver()) + } + + override fun rejectGroupInvite(groupId: String): Flowable { + return apiService.rejectGroupInvite(groupId).compose(configureApiCallObserver()) + } + + override fun acceptQuest(groupId: String): Flowable { + return apiService.acceptQuest(groupId).compose(configureApiCallObserver()) + } + + override fun rejectQuest(groupId: String): Flowable { + return apiService.rejectQuest(groupId).compose(configureApiCallObserver()) + } + + override fun cancelQuest(groupId: String): Flowable { + return apiService.cancelQuest(groupId).compose(configureApiCallObserver()) + } + + override fun forceStartQuest(groupId: String, group: Group): Flowable { + return apiService.forceStartQuest(groupId, group).compose(configureApiCallObserver()) + } + + override fun inviteToQuest(groupId: String, questKey: String): Flowable { + return apiService.inviteToQuest(groupId, questKey).compose(configureApiCallObserver()) + } + + override fun abortQuest(groupId: String): Flowable { + return apiService.abortQuest(groupId).compose(configureApiCallObserver()) + } + + override fun leaveQuest(groupId: String): Flowable { + return apiService.leaveQuest(groupId).compose(configureApiCallObserver()) + } + + override fun validatePurchase(request: PurchaseValidationRequest): Flowable { + return apiService.validatePurchase(request).map { habitResponse -> + if (habitResponse.notifications != null) { + popupNotificationsManager.showNotificationDialog(habitResponse.notifications) + } + habitResponse.getData() + } + } + + override fun changeCustomDayStart(updateObject: Map): Flowable { + return apiService.changeCustomDayStart(updateObject).compose(configureApiCallObserver()) + } + + override fun getMember(memberId: String): Flowable { + return apiService.getMember(memberId).compose(configureApiCallObserver()) + } + + override fun getMemberWithUsername(username: String): Flowable { + return apiService.getMemberWithUsername(username).compose(configureApiCallObserver()) + } + + override fun getMemberAchievements(memberId: String): Flowable { + return apiService.getMemberAchievements(memberId).compose(configureApiCallObserver()) + } + + override fun findUsernames(username: String, context: String?, id: String?): Flowable> { + return apiService.findUsernames(username, context, id).compose(configureApiCallObserver()) + } + + override fun postPrivateMessage(messageDetails: Map): Flowable { + return apiService.postPrivateMessage(messageDetails).compose(configureApiCallObserver()) + } + + override fun retrieveShopIventory(identifier: String): Flowable { + return apiService.retrieveShopInventory(identifier).compose(configureApiCallObserver()) + } + + override fun addPushDevice(pushDeviceData: Map): Flowable> { + return apiService.addPushDevice(pushDeviceData).compose(configureApiCallObserver()) + } + + override fun deletePushDevice(regId: String): Flowable> { + return apiService.deletePushDevice(regId).compose(configureApiCallObserver()) + } + + override fun getUserChallenges(page: Int, memberOnly: Boolean): Flowable> { + return if (memberOnly) { + apiService.getUserChallenges(page, memberOnly).compose(configureApiCallObserver()) + } else { + apiService.getUserChallenges(page).compose(configureApiCallObserver()) + } + } + + override fun getChallengeTasks(challengeId: String): Flowable { + return apiService.getChallengeTasks(challengeId).compose(configureApiCallObserver()) + } + + override fun getChallenge(challengeId: String): Flowable { + return apiService.getChallenge(challengeId).compose(configureApiCallObserver()) + } + + override fun joinChallenge(challengeId: String): Flowable { + return apiService.joinChallenge(challengeId).compose(configureApiCallObserver()) + } + + override fun leaveChallenge(challengeId: String, body: LeaveChallengeBody): Flowable { + return apiService.leaveChallenge(challengeId, body).compose(configureApiCallObserver()) + } + + + override fun createChallenge(challenge: Challenge): Flowable { + return apiService.createChallenge(challenge).compose(configureApiCallObserver()) + } + + override fun createChallengeTasks(challengeId: String, tasks: List): Flowable> { + return apiService.createChallengeTasks(challengeId, tasks).compose(configureApiCallObserver()) + } + + override fun createChallengeTask(challengeId: String, task: Task): Flowable { + return apiService.createChallengeTask(challengeId, task).compose(configureApiCallObserver()) + } + + override fun updateChallenge(challenge: Challenge): Flowable { + return apiService.updateChallenge(challenge.id, challenge).compose(configureApiCallObserver()) + } + + override fun deleteChallenge(challengeId: String): Flowable { + return apiService.deleteChallenge(challengeId).compose(configureApiCallObserver()) + } + + override fun debugAddTenGems(): Flowable { + return apiService.debugAddTenGems().compose(configureApiCallObserver()) + } + + override fun readNotification(notificationId: String): Flowable> { + return apiService.readNotification(notificationId).compose(configureApiCallObserver()) + } + + override fun readNotifications(notificationIds: Map>): Flowable> { + return apiService.readNotifications(notificationIds).compose(configureApiCallObserver()) + } + + override fun seeNotifications(notificationIds: Map>): Flowable> { + return apiService.seeNotifications(notificationIds).compose(configureApiCallObserver()) + } + + override val content: Flowable + get() = apiService.getContent(languageCode).compose(configureApiCallObserver()) + + override fun openMysteryItem(): Flowable { + return apiService.openMysteryItem().compose(configureApiCallObserver()) + } + + override fun runCron(): Flowable { + return apiService.runCron().compose(configureApiCallObserver()) + } + + override fun resetAccount(): Flowable { + return apiService.resetAccount().compose(configureApiCallObserver()) + } + + override fun deleteAccount(password: String): Flowable { + val updateObject = HashMap() + updateObject["password"] = password + return apiService.deleteAccount(updateObject).compose(configureApiCallObserver()) + } + + override fun togglePinnedItem(pinType: String, path: String): Flowable { + return apiService.togglePinnedItem(pinType, path).compose(configureApiCallObserver()) + } + + override fun sendPasswordResetEmail(email: String): Flowable { + val data = HashMap() + data["email"] = email + return apiService.sendPasswordResetEmail(data).compose(configureApiCallObserver()) + } + + override fun updateLoginName(newLoginName: String, password: String): Flowable { + val updateObject = HashMap() + updateObject["username"] = newLoginName + updateObject["password"] = password + return apiService.updateLoginName(updateObject).compose(configureApiCallObserver()) + } + + override fun updateUsername(newLoginName: String): Flowable { + val updateObject = HashMap() + updateObject["username"] = newLoginName + return apiService.updateLoginName(updateObject).compose(configureApiCallObserver()) + } + + override fun verifyUsername(username: String): Flowable { + val updateObject = HashMap() + updateObject["username"] = username + return this.apiService.verifyUsername(updateObject).compose(configureApiCallObserver()) + } + + override fun updateEmail(newEmail: String, password: String): Flowable { + val updateObject = HashMap() + updateObject["newEmail"] = newEmail + updateObject["password"] = password + return apiService.updateEmail(updateObject).compose(configureApiCallObserver()) + } + + override fun updatePassword(oldPassword: String, newPassword: String, newPasswordConfirmation: String): Flowable { + val updateObject = HashMap() + updateObject["password"] = oldPassword + updateObject["newPassword"] = newPassword + updateObject["confirmPassword"] = newPasswordConfirmation + return apiService.updatePassword(updateObject).compose(configureApiCallObserver()) + } + + override fun allocatePoint(stat: String): Flowable { + return apiService.allocatePoint(stat).compose(configureApiCallObserver()) + } + + override fun bulkAllocatePoints(strength: Int, intelligence: Int, constitution: Int, perception: Int): Flowable { + val body = HashMap>() + val stats = HashMap() + stats["str"] = strength + stats["int"] = intelligence + stats["con"] = constitution + stats["per"] = perception + body["stats"] = stats + return apiService.bulkAllocatePoints(body).compose(configureApiCallObserver()) + } + + override fun retrieveMarketGear(): Flowable { + return apiService.retrieveMarketGear().compose(configureApiCallObserver()) + } + + override val worldState: Flowable + get() = apiService.worldState.compose(configureApiCallObserver()) + + companion object { + private const val TAG = "ApiClientImpl" + + fun createGsonFactory(): GsonConverterFactory { + return GSonFactoryCreator.create() + } + } +} diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/UserRepositoryImpl.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/UserRepositoryImpl.kt index 5e7a39eb5..ffa739e6d 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/UserRepositoryImpl.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/UserRepositoryImpl.kt @@ -170,6 +170,12 @@ class UserRepositoryImpl(localRepository: UserLocalRepository, apiClient: ApiCli override fun readNotification(id: String): Flowable> = apiClient.readNotification(id) + override fun readNotifications(notificationIds: Map>): Flowable> = + apiClient.readNotifications(notificationIds) + + override fun seeNotifications(notificationIds: Map>): Flowable> = + apiClient.seeNotifications(notificationIds) + override fun changeCustomDayStart(dayStartTime: Int): Flowable { val updateObject = HashMap() updateObject["dayStart"] = dayStartTime diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/NotificationsActivity.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/NotificationsActivity.kt index 02616d93b..9cb2d236e 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/NotificationsActivity.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/NotificationsActivity.kt @@ -25,6 +25,8 @@ class NotificationsActivity : BaseActivity(), androidx.swiperefreshlayout.widget override fun getLayoutResId(): Int = R.layout.activity_notifications + private var notifications: List = emptyList() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -37,7 +39,7 @@ class NotificationsActivity : BaseActivity(), androidx.swiperefreshlayout.widget compositeSubscription.add(viewModel.getNotifications().subscribe(Consumer { this.setNotifications(it) - viewModel.markNotificationsAsSeen() + viewModel.markNotificationsAsSeen(it) }, RxErrorHandler.handleEmptyError())) notifications_refresh_layout?.setOnRefreshListener(this) @@ -64,6 +66,8 @@ class NotificationsActivity : BaseActivity(), androidx.swiperefreshlayout.widget } private fun setNotifications(notifications: List) { + this.notifications = notifications + if (notification_items == null) { return } @@ -108,14 +112,14 @@ class NotificationsActivity : BaseActivity(), androidx.swiperefreshlayout.widget badge?.text = notificationCount.toString() val dismissAllButton = header?.findViewById(R.id.dismiss_all_button) as? Button - dismissAllButton?.setOnClickListener({ viewModel.dismissAllNotifications() }) + dismissAllButton?.setOnClickListener { viewModel.dismissAllNotifications(notifications) } return header } private fun createNewChatMessageNotification(notification: GlobalNotification): View? { val data = notification.getData() as? NewChatMessageData - val stringId = if (viewModel.isPartyMessage(data)) R.string.new_msg_party else R.string.new_msg_guild; + val stringId = if (viewModel.isPartyMessage(data)) R.string.new_msg_party else R.string.new_msg_guild return createNotificationItem( notification, @@ -156,7 +160,7 @@ class NotificationsActivity : BaseActivity(), androidx.swiperefreshlayout.widget val item = inflater.inflate(R.layout.notification_item, notification_items, false) val dismissButton = item?.findViewById(R.id.dismiss_button) as? ImageView - dismissButton?.setOnClickListener({ viewModel.dismissNotification(notification) }) + dismissButton?.setOnClickListener { viewModel.dismissNotification(notification) } val messageTextView = item?.findViewById(R.id.message_text) as? TextView messageTextView?.text = messageText diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/NotificationsViewModel.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/NotificationsViewModel.kt index 20df9220b..81ff69290 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/NotificationsViewModel.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/NotificationsViewModel.kt @@ -6,12 +6,11 @@ import com.habitrpg.android.habitica.models.notifications.GlobalNotification import com.habitrpg.android.habitica.models.notifications.NewChatMessageData import com.habitrpg.android.habitica.models.notifications.NotificationType import com.habitrpg.android.habitica.models.social.UserParty -import com.playseeds.android.sdk.inappmessaging.Log import io.reactivex.Flowable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.functions.Consumer import io.realm.RealmList - +import java.util.HashMap open class NotificationsViewModel : BaseViewModel() { var party: UserParty? = null @@ -56,18 +55,42 @@ open class NotificationsViewModel : BaseViewModel() { } fun dismissNotification(notification: GlobalNotification) { - Log.d("NotificationsViewModel.dismissNotification " + notification.type + " " + notification.id) - //TODO("not implemented") + disposable.add(userRepository.readNotification(notification.id) + .subscribe(Consumer { + // TODO better way to handle updates than reload whole user ?? + refreshNotifications() + }, RxErrorHandler.handleEmptyError())) } - fun dismissAllNotifications() { - Log.d("NotificationsViewModel.dismissAllNotifications") - //TODO("not implemented") + fun dismissAllNotifications(notifications: List) { + if (notifications.isEmpty()) { + return + } + + val notificationIds = HashMap>() + notificationIds["notificationIds"] = notifications.map { notification -> notification.id } + + disposable.add(userRepository.readNotifications(notificationIds) + .subscribe(Consumer { + refreshNotifications() + }, RxErrorHandler.handleEmptyError())) } - fun markNotificationsAsSeen() { - Log.d("NotificationsViewModel.markNotificationsAsSeen") - //TODO("not implemented") + fun markNotificationsAsSeen(notifications: List) { + val unseenIds = notifications.filter { notification -> notification.seen != true } + .map { notification -> notification.id } + + if (unseenIds.isEmpty()) { + return + } + + val notificationIds = HashMap>() + notificationIds["notificationIds"] = unseenIds + + disposable.add(userRepository.seeNotifications(notificationIds) + .subscribe(Consumer { + refreshNotifications() + }, RxErrorHandler.handleEmptyError())) } }