Improve notification handling

This commit is contained in:
Phillip Thelen 2022-01-06 13:36:08 +01:00
parent 910b2f2b0a
commit 2f9f342768
22 changed files with 358 additions and 316 deletions

View file

@ -13,7 +13,6 @@ buildscript {
mavenLocal()
google()
mavenCentral()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.4'
@ -47,8 +46,8 @@ dependencies {
implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0'
//Dependency Injection
implementation 'com.google.dagger:dagger:2.39.1'
kapt 'com.google.dagger:dagger-compiler:2.39.1'
implementation 'com.google.dagger:dagger:2.40.5'
kapt 'com.google.dagger:dagger-compiler:2.40.5'
compileOnly 'javax.annotation:javax.annotation-api:1.3.2'
compileOnly 'com.github.pengrad:jdk9-deps:1.0'
//App Compatibility and Material Design
@ -64,44 +63,51 @@ dependencies {
implementation "io.noties.markwon:image:4.6.2"
implementation "io.noties.markwon:recycler:4.6.2"
//Eventbus
implementation 'org.greenrobot:eventbus:3.2.0'
implementation 'org.greenrobot:eventbus:3.3.1'
// IAP Handling / Verification
implementation 'org.solovyev.android:checkout:1.2.3'
//Facebook
implementation('com.facebook.android:facebook-android-sdk:11.3.0') {
implementation('com.facebook.android:facebook-android-sdk:12.2.0') {
transitive = true
}
implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1@aar'
//RxJava
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
implementation 'io.reactivex.rxjava3:rxjava:3.1.1'
implementation 'io.reactivex.rxjava3:rxjava:3.1.3'
implementation 'io.reactivex.rxjava3:rxkotlin:3.0.1'
implementation 'io.reactivex.rxjava2:rxjava:2.2.21'
implementation "com.github.akarnokd:rxjava3-bridge:3.0.0"
implementation "com.github.akarnokd:rxjava3-bridge:3.0.2"
//Analytics
implementation 'com.amplitude:android-sdk:2.30.0'
implementation 'com.amplitude:android-sdk:3.35.1'
// Image Management Library
implementation("io.coil-kt:coil:1.4.0")
implementation("io.coil-kt:coil-gif:1.4.0")
//Tests
testImplementation 'io.kotest:kotest-runner-junit5:4.6.2'
testImplementation 'io.kotest:kotest-runner-junit5:5.0.3'
testImplementation 'androidx.test:core:1.4.0'
testImplementation "io.mockk:mockk:1.12.0"
testImplementation "io.mockk:mockk-android:1.12.0"
testImplementation 'io.kotest:kotest-assertions-core:4.6.2'
testImplementation "io.mockk:mockk:1.12.2"
testImplementation "io.mockk:mockk-android:1.12.2"
testImplementation 'io.kotest:kotest-assertions-core:5.0.3'
testImplementation 'io.kotest:kotest-framework-datatest:4.6.2'
androidTestImplementation 'com.kaspersky.android-components:kaspresso:1.2.1'
androidTestImplementation 'com.kaspersky.android-components:kaspresso:1.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation "io.mockk:mockk-android:1.12.0"
androidTestImplementation "io.mockk:mockk-android:1.12.2"
implementation 'androidx.activity:activity-compose:1.4.0'
implementation 'androidx.compose.material:material:1.0.5'
implementation 'androidx.compose.animation:animation:1.0.5'
implementation 'androidx.compose.ui:ui-tooling:1.0.5'
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0'
implementation "com.google.accompanist:accompanist-appcompat-theme:0.16.0"
//Leak Detection
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8'
//Push Notifications
implementation platform('com.google.firebase:firebase-bom:29.0.0')
implementation 'com.google.firebase:firebase-crashlytics-ktx'
@ -109,7 +115,7 @@ dependencies {
implementation 'com.google.firebase:firebase-messaging-ktx'
implementation 'com.google.firebase:firebase-config-ktx'
implementation 'com.google.firebase:firebase-perf-ktx'
implementation 'com.google.android.gms:play-services-auth:19.2.0'
implementation 'com.google.android.gms:play-services-auth:20.0.0'
implementation 'com.nex3z:flow-layout:1.2.2'
implementation 'androidx.core:core-ktx:1.7.0'
@ -121,14 +127,14 @@ dependencies {
implementation 'com.plattysoft.leonids:LeonidsLib:1.3.2'
implementation "androidx.fragment:fragment-ktx:1.4.0"
implementation "androidx.paging:paging-runtime-ktx:3.1.0"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0'
implementation 'com.willowtreeapps:signinwithapplebutton:0.3'
implementation project(':shared')
ktlint("com.pinterest:ktlint:0.42.1") {
ktlint("com.pinterest:ktlint:0.43.2") {
attributes {
attribute(Bundling.BUNDLING_ATTRIBUTE, getObjects().named(Bundling, Bundling.EXTERNAL))
}
@ -136,7 +142,7 @@ dependencies {
}
android {
compileSdkVersion 31
compileSdkVersion 32
buildToolsVersion '30.0.2'
testOptions {
unitTests {
@ -153,9 +159,9 @@ android {
resConfigs "en", "bg", "de", "en-rGB", "es", "fr", "hr-rHR", "in", "it", "iw", "ja", "ko", "lt", "nl", "pl", "pt-rBR", "pt-rPT", "ru", "tr", "zh", "zh-rTW"
versionCode 3128
versionName "3.4.2"
versionName "3.5"
targetSdkVersion 31
targetSdkVersion 32
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

View file

@ -80,8 +80,6 @@ class ApiClientImpl // private OnHabitsAPIResult mResultListener;
private var lastAPICallURL: String? = null
init {
this.notificationsManager.setApiClient(this)
HabiticaBaseApplication.userComponent?.inject(this)
analyticsManager.setUserIdentifier(this.hostConfig.userID)
buildRetrofit()

View file

@ -1,6 +0,0 @@
package com.habitrpg.android.habitica.events
/**
* Created by Negue on 29.11.2015.
*/
class BoughtGemsEvent(var NewGemsToAdd: Int)

View file

@ -2,4 +2,4 @@ package com.habitrpg.android.habitica.events
import com.habitrpg.android.habitica.models.shops.ShopItem
class GearPurchasedEvent(val item: ShopItem)
class GearPurchasedEvent(val item: ShopItem)

View file

@ -1,3 +0,0 @@
package com.habitrpg.android.habitica.events
class ShowAchievementDialog(var type: String, val id: String, val message: String? = null, val text: String? = null, val isLastOnboardingAchievement: Boolean = false)

View file

@ -1,5 +0,0 @@
package com.habitrpg.android.habitica.events
import com.habitrpg.android.habitica.models.Notification
class ShowCheckinDialog(var notification: Notification, var nextUnlockText: String, var nextUnlockCount: Int)

View file

@ -1,3 +0,0 @@
package com.habitrpg.android.habitica.events
class ShowFirstDropDialog(val egg: String, val hatchingPotion: String, val id: String)

View file

@ -1,5 +0,0 @@
package com.habitrpg.android.habitica.events
import com.habitrpg.android.habitica.models.notifications.ChallengeWonData
class ShowWonChallengeDialog(val id: String, val data: ChallengeWonData?)

View file

@ -1,6 +1,7 @@
package com.habitrpg.android.habitica.helpers
import android.content.Context
import androidx.lifecycle.MutableLiveData
import com.google.firebase.analytics.FirebaseAnalytics
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.data.ApiClient
@ -12,22 +13,35 @@ import com.habitrpg.android.habitica.models.notifications.FirstDropData
import com.habitrpg.android.habitica.models.notifications.LoginIncentiveData
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.ui.views.HabiticaSnackbar
import com.habitrpg.android.habitica.ui.views.dialogs.WonChallengeDialog
import io.reactivex.rxjava3.core.BackpressureStrategy
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.subjects.BehaviorSubject
import io.reactivex.rxjava3.subjects.PublishSubject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import java.lang.ref.WeakReference
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.coroutines.CoroutineContext
class NotificationsManager {
private val displayNotificationSubject = PublishSubject.create<Notification>()
class NotificationsManager(private val context: Context) {
private val seenNotifications: MutableMap<String, Boolean>
private var apiClient: ApiClient? = null
lateinit var apiClient: WeakReference<ApiClient>
private val notifications: BehaviorSubject<List<Notification>>
private var lastNotificationHandling: Date? = null
val displayNotificationEvents: Flowable<Notification>
get() {
return displayNotificationSubject.toFlowable(BackpressureStrategy.DROP)
}
init {
this.seenNotifications = HashMap()
this.notifications = BehaviorSubject.create()
@ -48,11 +62,7 @@ class NotificationsManager(private val context: Context) {
return this.notifications.value?.find { it.id == id }
}
fun setApiClient(apiClient: ApiClient?) {
this.apiClient = apiClient
}
private fun handlePopupNotifications(notifications: List<Notification>): Boolean? {
private fun handlePopupNotifications(notifications: List<Notification>): Boolean {
val now = Date()
if (now.time - (lastNotificationHandling?.time ?: 0) < 300) {
return true
@ -62,116 +72,56 @@ class NotificationsManager(private val context: Context) {
.filter { !this.seenNotifications.containsKey(it.id) }
.map {
val notificationDisplayed = when (it.type) {
Notification.Type.LOGIN_INCENTIVE.type -> displayLoginIncentiveNotification(it)
Notification.Type.ACHIEVEMENT_PARTY_UP.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_PARTY_ON.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_BEAST_MASTER.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_MOUNT_MASTER.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_TRIAD_BINGO.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_GUILD_JOINED.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_CHALLENGE_JOINED.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_INVITED_FRIEND.type -> displayAchievementNotification(it)
Notification.Type.WON_CHALLENGE.type -> displayWonChallengeNotificaiton(it)
Notification.Type.ACHIEVEMENT_PARTY_UP.type -> true
Notification.Type.ACHIEVEMENT_PARTY_ON.type -> true
Notification.Type.ACHIEVEMENT_BEAST_MASTER.type -> true
Notification.Type.ACHIEVEMENT_MOUNT_MASTER.type -> true
Notification.Type.ACHIEVEMENT_TRIAD_BINGO.type -> true
Notification.Type.ACHIEVEMENT_GUILD_JOINED.type -> true
Notification.Type.ACHIEVEMENT_CHALLENGE_JOINED.type -> true
Notification.Type.ACHIEVEMENT_INVITED_FRIEND.type -> true
Notification.Type.ACHIEVEMENT_ALL_YOUR_BASE.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_BACK_TO_BASICS.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_JUST_ADD_WATER.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_LOST_MASTERCLASSER.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_MIND_OVER_MATTER.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_DUST_DEVIL.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_ARID_AUTHORITY.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_MONSTER_MAGUS.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_UNDEAD_UNDERTAKER.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_PRIMED_FOR_PAINTING.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_PEARLY_PRO.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_TICKLED_PINK.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_ROSY_OUTLOOK.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_BUG_BONANZA.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_BARE_NECESSITIES.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_FRESHWATER_FRIENDS.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_GOOD_AS_GOLD.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_ALL_THAT_GLITTERS.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_GOOD_AS_GOLD.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_BONE_COLLECTOR.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_SKELETON_CREW.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_SEEING_RED.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_RED_LETTER_DAY.type -> displayAchievementNotification(it)
Notification.Type.ACHIEVEMENT_ALL_YOUR_BASE.type -> true
Notification.Type.ACHIEVEMENT_BACK_TO_BASICS.type -> true
Notification.Type.ACHIEVEMENT_JUST_ADD_WATER.type -> true
Notification.Type.ACHIEVEMENT_LOST_MASTERCLASSER.type -> true
Notification.Type.ACHIEVEMENT_MIND_OVER_MATTER.type -> true
Notification.Type.ACHIEVEMENT_DUST_DEVIL.type -> true
Notification.Type.ACHIEVEMENT_ARID_AUTHORITY.type -> true
Notification.Type.ACHIEVEMENT_MONSTER_MAGUS.type -> true
Notification.Type.ACHIEVEMENT_UNDEAD_UNDERTAKER.type -> true
Notification.Type.ACHIEVEMENT_PRIMED_FOR_PAINTING.type -> true
Notification.Type.ACHIEVEMENT_PEARLY_PRO.type -> true
Notification.Type.ACHIEVEMENT_TICKLED_PINK.type -> true
Notification.Type.ACHIEVEMENT_ROSY_OUTLOOK.type -> true
Notification.Type.ACHIEVEMENT_BUG_BONANZA.type -> true
Notification.Type.ACHIEVEMENT_BARE_NECESSITIES.type -> true
Notification.Type.ACHIEVEMENT_FRESHWATER_FRIENDS.type -> true
Notification.Type.ACHIEVEMENT_GOOD_AS_GOLD.type -> true
Notification.Type.ACHIEVEMENT_ALL_THAT_GLITTERS.type -> true
Notification.Type.ACHIEVEMENT_GOOD_AS_GOLD.type -> true
Notification.Type.ACHIEVEMENT_BONE_COLLECTOR.type -> true
Notification.Type.ACHIEVEMENT_SKELETON_CREW.type -> true
Notification.Type.ACHIEVEMENT_SEEING_RED.type -> true
Notification.Type.ACHIEVEMENT_RED_LETTER_DAY.type -> true
Notification.Type.ACHIEVEMENT_GENERIC.type -> displayAchievementNotification(
it,
notifications.find { notif ->
notif.type == Notification.Type.ACHIEVEMENT_ONBOARDING_COMPLETE.type
} != null
)
Notification.Type.ACHIEVEMENT_ONBOARDING_COMPLETE.type -> displayAchievementNotification(it)
Notification.Type.FIRST_DROP.type -> displayFirstDropNotification(it)
Notification.Type.ACHIEVEMENT_GENERIC.type -> true
Notification.Type.ACHIEVEMENT_ONBOARDING_COMPLETE.type -> true
else -> false
}
if (notificationDisplayed == true) {
if (notificationDisplayed) {
displayNotificationSubject.onNext(it)
this.seenNotifications[it.id] = true
readNotification(it)
}
}
return true
}
private fun displayWonChallengeNotificaiton(notification: Notification): Boolean {
EventBus.getDefault().post(ShowWonChallengeDialog(notification.id, notification.data as? ChallengeWonData))
return true
}
private fun displayFirstDropNotification(notification: Notification): Boolean {
val data = (notification.data as? FirstDropData)
EventBus.getDefault().post(ShowFirstDropDialog(data?.egg ?: "", data?.hatchingPotion ?: "", notification.id))
return true
}
private fun displayLoginIncentiveNotification(notification: Notification): Boolean? {
val notificationData = notification.data as? LoginIncentiveData
val nextUnlockText = context.getString(R.string.nextPrizeUnlocks, notificationData?.nextRewardAt)
if (notificationData?.rewardKey != null) {
val event = ShowCheckinDialog(notification, nextUnlockText, notificationData.nextRewardAt ?: 0)
EventBus.getDefault().post(event)
} else {
val event = ShowSnackbarEvent()
event.title = notificationData?.message
event.text = nextUnlockText
event.type = HabiticaSnackbar.SnackbarDisplayType.BLUE
EventBus.getDefault().post(event)
if (apiClient != null) {
apiClient?.readNotification(notification.id)
?.subscribe({}, RxErrorHandler.handleEmptyError())
}
}
return true
}
private fun displayAchievementNotification(notification: Notification, isLastOnboardingAchievement: Boolean = false): Boolean {
val data = (notification.data as? AchievementData)
val achievement = data?.achievement ?: notification.type ?: ""
val delay: Long = if (achievement == "createdTask" || achievement == Notification.Type.ACHIEVEMENT_ONBOARDING_COMPLETE.type) {
1000
} else {
200
}
val sub = Completable.complete()
.delay(delay, TimeUnit.MILLISECONDS)
.subscribe(
{
EventBus.getDefault().post(ShowAchievementDialog(achievement, notification.id, data?.message, data?.modalText, isLastOnboardingAchievement))
},
RxErrorHandler.handleEmptyError()
)
logOnboardingEvents(achievement)
return true
}
private fun logOnboardingEvents(type: String) {
if (User.ONBOARDING_ACHIEVEMENT_KEYS.contains(type)) {
FirebaseAnalytics.getInstance(context).logEvent(type, null)
} else if (type == Notification.Type.ACHIEVEMENT_ONBOARDING_COMPLETE.type) {
FirebaseAnalytics.getInstance(context).logEvent(type, null)
}
private fun readNotification(notification: Notification) {
apiClient.get()?.readNotification(notification.id)
?.subscribe({ }, RxErrorHandler.handleEmptyError())
}
}

View file

@ -156,7 +156,6 @@ open class PurchaseHandler(activity: Activity, val analyticsManager: AnalyticsMa
purchase.token,
object : RequestListener<Any> {
override fun onSuccess(o: Any) {
// EventBus.getDefault().post(new BoughtGemsEvent(GEMS_TO_ADD));
}
override fun onError(i: Int, e: Exception) {

View file

@ -0,0 +1,169 @@
package com.habitrpg.android.habitica.interactors
import android.app.Activity
import android.view.LayoutInflater
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.lifecycleScope
import com.google.firebase.analytics.FirebaseAnalytics
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.events.*
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.models.Notification
import com.habitrpg.android.habitica.models.notifications.AchievementData
import com.habitrpg.android.habitica.models.notifications.ChallengeWonData
import com.habitrpg.android.habitica.models.notifications.FirstDropData
import com.habitrpg.android.habitica.models.notifications.LoginIncentiveData
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.ui.helpers.DataBindingUtils
import com.habitrpg.android.habitica.ui.views.HabiticaSnackbar
import com.habitrpg.android.habitica.ui.views.dialogs.AchievementDialog
import com.habitrpg.android.habitica.ui.views.dialogs.FirstDropDialog
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import com.habitrpg.android.habitica.ui.views.dialogs.WonChallengeDialog
import io.reactivex.rxjava3.core.Completable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import java.util.concurrent.TimeUnit
class ShowNotificationInteractor(private val activity: Activity, private val lifecycleScope: LifecycleCoroutineScope) {
fun handleNotification(notification: Notification) {
when (notification.type) {
Notification.Type.LOGIN_INCENTIVE.type -> showCheckinDialog(notification)
Notification.Type.ACHIEVEMENT_PARTY_UP.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_PARTY_ON.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_BEAST_MASTER.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_MOUNT_MASTER.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_TRIAD_BINGO.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_GUILD_JOINED.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_CHALLENGE_JOINED.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_INVITED_FRIEND.type -> showAchievementDialog(notification)
Notification.Type.WON_CHALLENGE.type -> showWonChallengeDialog(notification)
Notification.Type.ACHIEVEMENT_ALL_YOUR_BASE.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_BACK_TO_BASICS.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_JUST_ADD_WATER.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_LOST_MASTERCLASSER.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_MIND_OVER_MATTER.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_DUST_DEVIL.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_ARID_AUTHORITY.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_MONSTER_MAGUS.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_UNDEAD_UNDERTAKER.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_PRIMED_FOR_PAINTING.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_PEARLY_PRO.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_TICKLED_PINK.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_ROSY_OUTLOOK.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_BUG_BONANZA.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_BARE_NECESSITIES.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_FRESHWATER_FRIENDS.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_GOOD_AS_GOLD.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_ALL_THAT_GLITTERS.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_GOOD_AS_GOLD.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_BONE_COLLECTOR.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_SKELETON_CREW.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_SEEING_RED.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_RED_LETTER_DAY.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_GENERIC.type -> showAchievementDialog(notification)
Notification.Type.ACHIEVEMENT_ONBOARDING_COMPLETE.type -> showAchievementDialog(notification)
Notification.Type.FIRST_DROP.type -> showFirstDropDialog(notification)
else -> false
}
}
@Subscribe
fun showCheckinDialog(notification: Notification) {
val notificationData = notification.data as? LoginIncentiveData
val nextUnlockText = activity.getString(R.string.nextPrizeUnlocks, notificationData?.nextRewardAt)
if (notificationData?.rewardKey != null) {
val title = notificationData.message
val factory = LayoutInflater.from(activity)
val view = factory.inflate(R.layout.dialog_login_incentive, null)
val imageView = view.findViewById(R.id.imageView) as? ImageView
var imageKey = notificationData?.rewardKey?.get(0)
if (imageKey?.contains("armor") == true) {
imageKey = "slim_$imageKey"
}
DataBindingUtils.loadImage(imageView, imageKey)
val youEarnedMessage = activity.getString(R.string.checkInRewardEarned, notificationData?.rewardText)
val youEarnedTexView = view.findViewById(R.id.you_earned_message) as? TextView
youEarnedTexView?.text = youEarnedMessage
val nextUnlockTextView = view.findViewById(R.id.next_unlock_message) as? TextView
if ((notificationData.nextRewardAt ?: 0) > 0) {
nextUnlockTextView?.text = nextUnlockText
} else {
nextUnlockTextView?.visibility = View.GONE
}
lifecycleScope.launch(context = Dispatchers.Main) {
val alert = HabiticaAlertDialog(activity)
alert.setAdditionalContentView(view)
alert.setTitle(title)
alert.addButton(R.string.see_you_tomorrow, true)
alert.show()
}
} else {
val event = ShowSnackbarEvent()
event.title = notificationData?.message
event.text = nextUnlockText
event.type = HabiticaSnackbar.SnackbarDisplayType.BLUE
EventBus.getDefault().post(event)
}
}
@Subscribe
fun showAchievementDialog(notification: Notification) {
val data = (notification.data as? AchievementData) ?: return
val achievement = data.achievement ?: notification.type ?: ""
val delayTime: Long = if (achievement == "createdTask" || achievement == Notification.Type.ACHIEVEMENT_ONBOARDING_COMPLETE.type) {
1000
} else {
200
}
lifecycleScope.launch() {
delay(delayTime)
lifecycleScope.launch(context = Dispatchers.Main) {
val dialog = AchievementDialog(activity)
dialog.isLastOnboardingAchievement = data.isLastOnboardingAchievement
dialog.setType(data.achievement ?: "", data.message, data.modalText)
dialog.enqueue()
}
}
logOnboardingEvents(achievement)
}
private fun showFirstDropDialog(notification: Notification) {
val data = notification.data as? FirstDropData ?: return
lifecycleScope.launch(context = Dispatchers.Main) {
val dialog = FirstDropDialog(activity)
dialog.configure(data.egg ?: "", data.hatchingPotion ?: "")
dialog.enqueue()
}
}
private fun showWonChallengeDialog(notification: Notification) {
lifecycleScope.launch(context = Dispatchers.Main) {
val dialog = WonChallengeDialog(activity)
dialog.configure(notification.data as? ChallengeWonData)
dialog.enqueue()
}
}
private fun logOnboardingEvents(type: String) {
if (User.ONBOARDING_ACHIEVEMENT_KEYS.contains(type)) {
FirebaseAnalytics.getInstance(activity).logEvent(type, null)
} else if (type == Notification.Type.ACHIEVEMENT_ONBOARDING_COMPLETE.type) {
FirebaseAnalytics.getInstance(activity).logEvent(type, null)
}
}
}

View file

@ -2,6 +2,7 @@ package com.habitrpg.android.habitica.models.notifications
open class AchievementData : NotificationData {
val isLastOnboardingAchievement: Boolean = false
var achievement: String? = null
var message: String? = null
var modalText: String? = null

View file

@ -1,58 +0,0 @@
package com.habitrpg.android.habitica.modules;
import android.content.Context;
import android.content.SharedPreferences;
import com.habitrpg.android.habitica.api.HostConfig;
import com.habitrpg.android.habitica.api.MaintenanceApiService;
import com.habitrpg.android.habitica.data.ApiClient;
import com.habitrpg.android.habitica.data.implementation.ApiClientImpl;
import com.habitrpg.android.habitica.helpers.NotificationsManager;
import com.habitrpg.android.habitica.helpers.KeyHelper;
import com.habitrpg.android.habitica.proxy.AnalyticsManager;
import javax.annotation.Nullable;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;
@Module
public class ApiModule {
@Provides
@Singleton
public HostConfig providesHostConfig(SharedPreferences sharedPreferences, @Nullable KeyHelper keyHelper, Context context) {
return new HostConfig(sharedPreferences, keyHelper, context);
}
@Provides
public GsonConverterFactory providesGsonConverterFactory() {
return ApiClientImpl.Companion.createGsonFactory();
}
@Provides
@Singleton
public NotificationsManager providesPopupNotificationsManager(Context context) {
return new NotificationsManager(context);
}
@Provides
@Singleton
public ApiClient providesApiHelper(GsonConverterFactory gsonConverter, HostConfig hostConfig, AnalyticsManager analyticsManager, NotificationsManager notificationsManager, Context context) {
return new ApiClientImpl(gsonConverter, hostConfig, analyticsManager, notificationsManager, context);
}
@Provides
public MaintenanceApiService providesMaintenanceApiService(GsonConverterFactory gsonConverter) {
Retrofit adapter = new Retrofit.Builder()
.baseUrl("https://habitica-assets.s3.amazonaws.com/mobileApp/endpoint/")
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.addConverterFactory(gsonConverter)
.build();
return adapter.create(MaintenanceApiService.class);
}
}

View file

@ -0,0 +1,73 @@
package com.habitrpg.android.habitica.modules
import android.content.Context
import com.habitrpg.android.habitica.data.implementation.ApiClientImpl.Companion.createGsonFactory
import android.content.SharedPreferences
import com.habitrpg.android.habitica.helpers.KeyHelper
import com.habitrpg.android.habitica.api.HostConfig
import retrofit2.converter.gson.GsonConverterFactory
import com.habitrpg.android.habitica.data.implementation.ApiClientImpl
import com.habitrpg.android.habitica.helpers.NotificationsManager
import com.habitrpg.android.habitica.proxy.AnalyticsManager
import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.api.MaintenanceApiService
import dagger.Module
import dagger.Provides
import retrofit2.Retrofit
import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory
import java.lang.ref.WeakReference
import javax.inject.Singleton
@Module
open class ApiModule {
@Provides
@Singleton
fun providesHostConfig(
sharedPreferences: SharedPreferences,
keyHelper: KeyHelper?,
context: Context
): HostConfig {
return HostConfig(sharedPreferences, keyHelper, context)
}
@Provides
fun providesGsonConverterFactory(): GsonConverterFactory {
return createGsonFactory()
}
@Provides
@Singleton
fun providesPopupNotificationsManager(): NotificationsManager {
return NotificationsManager()
}
@Provides
@Singleton
fun providesApiHelper(
gsonConverter: GsonConverterFactory,
hostConfig: HostConfig,
analyticsManager: AnalyticsManager,
notificationsManager: NotificationsManager,
context: Context
): ApiClient {
val apiClient = ApiClientImpl(
gsonConverter,
hostConfig,
analyticsManager,
notificationsManager,
context
)
notificationsManager.apiClient = WeakReference(apiClient)
return apiClient
}
@Provides
fun providesMaintenanceApiService(gsonConverter: GsonConverterFactory): MaintenanceApiService {
val adapter = Retrofit.Builder()
.baseUrl("https://habitica-assets.s3.amazonaws.com/mobileApp/endpoint/")
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.addConverterFactory(gsonConverter)
.build()
return adapter.create(MaintenanceApiService::class.java)
}
}

View file

@ -9,7 +9,6 @@ import androidx.core.os.bundleOf
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.data.UserRepository
import com.habitrpg.android.habitica.databinding.AvatarWithBarsBinding
import com.habitrpg.android.habitica.events.BoughtGemsEvent
import com.habitrpg.android.habitica.helpers.HealthFormatter
import com.habitrpg.android.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.models.Avatar
@ -17,7 +16,6 @@ import com.habitrpg.android.habitica.models.user.Stats
import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
import io.reactivex.rxjava3.disposables.Disposable
import org.greenrobot.eventbus.Subscribe
import java.util.*
import kotlin.math.floor
@ -111,13 +109,6 @@ class AvatarWithBarsViewModel(private val context: Context, private val binding:
binding.mpBar.set(floor(value.toDouble()), cachedMaxMana.toDouble())
}
@Subscribe
fun onEvent(gemsEvent: BoughtGemsEvent) {
var gems = userObject?.gemCount ?: 0
gems += gemsEvent.NewGemsToAdd
binding.currencyView.gems = gems.toDouble()
}
companion object {
private fun setUserLevel(context: Context, textView: TextView, level: Int?) {
textView.text = context.getString(R.string.user_level, level)

View file

@ -14,6 +14,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import com.habitrpg.android.habitica.HabiticaApplication
import com.habitrpg.android.habitica.HabiticaBaseApplication
@ -24,14 +25,21 @@ import com.habitrpg.android.habitica.extensions.getThemeColor
import com.habitrpg.android.habitica.extensions.isUsingNightModeResources
import com.habitrpg.android.habitica.extensions.updateStatusBarColor
import com.habitrpg.android.habitica.helpers.LanguageHelper
import com.habitrpg.android.habitica.helpers.NotificationsManager
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.interactors.ShowNotificationInteractor
import com.habitrpg.android.habitica.ui.helpers.ToolbarColorHelper
import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import java.util.*
import javax.inject.Inject
abstract class BaseActivity : AppCompatActivity() {
@Inject
lateinit var notificationsManager: NotificationsManager
private var currentTheme: String? = null
private var isNightMode: Boolean = false
internal var forcedTheme: String? = null
@ -76,6 +84,12 @@ abstract class BaseActivity : AppCompatActivity() {
injectActivity(HabiticaBaseApplication.userComponent)
setContentView(getContentView())
compositeSubscription = CompositeDisposable()
compositeSubscription.add(notificationsManager.displayNotificationEvents.subscribe(
{
ShowNotificationInteractor(this, lifecycleScope).handleNotification(it)
},
RxErrorHandler.handleEmptyError()
))
}
override fun onRestart() {

View file

@ -103,7 +103,6 @@ class GemPurchaseActivity : BaseActivity() {
fun onConsumablePurchased(event: ConsumablePurchasedEvent) {
if (isActivityVisible) {
purchaseHandler?.consumePurchase(event.purchase)
compositeSubscription.add(userRepository.retrieveUser(false).subscribe({}, RxErrorHandler.handleEmptyError()))
}
}

View file

@ -783,81 +783,6 @@ open class MainActivity : BaseActivity(), TutorialView.OnTutorialReaction {
HabiticaSnackbar.showSnackbar(snackbarContainer, event.leftImage, event.title, event.text, event.specialView, event.rightIcon, event.rightTextColor, event.rightText, event.type)
}
@Subscribe
fun showCheckinDialog(event: ShowCheckinDialog) {
val notificationData = event.notification.data as? LoginIncentiveData
val title = notificationData?.message
val factory = LayoutInflater.from(this)
val view = factory.inflate(R.layout.dialog_login_incentive, null)
val imageView = view.findViewById(R.id.imageView) as? ImageView
var imageKey = notificationData?.rewardKey?.get(0)
if (imageKey?.contains("armor") == true) {
imageKey = "slim_$imageKey"
}
DataBindingUtils.loadImage(imageView, imageKey)
val youEarnedMessage = this.getString(R.string.checkInRewardEarned, notificationData?.rewardText)
val youEarnedTexView = view.findViewById(R.id.you_earned_message) as? TextView
youEarnedTexView?.text = youEarnedMessage
val nextUnlockTextView = view.findViewById(R.id.next_unlock_message) as? TextView
if (event.nextUnlockCount > 0) {
nextUnlockTextView?.text = event.nextUnlockText
} else {
nextUnlockTextView?.visibility = View.GONE
}
lifecycleScope.launch(context = Dispatchers.Main) {
val alert = HabiticaAlertDialog(this@MainActivity)
alert.setAdditionalContentView(view)
alert.setTitle(title)
alert.addButton(R.string.see_you_tomorrow, true) { _, _ ->
apiClient.readNotification(event.notification.id)
.subscribe({ }, RxErrorHandler.handleEmptyError())
}
alert.show()
}
}
@Subscribe
fun showAchievementDialog(event: ShowAchievementDialog) {
retrieveUser(true)
lifecycleScope.launch(context = Dispatchers.Main) {
val dialog = AchievementDialog(this@MainActivity)
dialog.isLastOnboardingAchievement = event.isLastOnboardingAchievement
dialog.setType(event.type, event.message, event.text)
dialog.enqueue()
apiClient.readNotification(event.id)
.subscribe({ }, RxErrorHandler.handleEmptyError())
}
}
@Subscribe
fun showFirstDropDialog(event: ShowFirstDropDialog) {
retrieveUser(true)
lifecycleScope.launch(context = Dispatchers.Main) {
val dialog = FirstDropDialog(this@MainActivity)
dialog.configure(event.egg, event.hatchingPotion)
dialog.enqueue()
apiClient.readNotification(event.id)
.subscribe({ }, RxErrorHandler.handleEmptyError())
}
}
@Subscribe
fun showWonChallengeDialog(event: ShowWonChallengeDialog) {
retrieveUser(true)
lifecycleScope.launch(context = Dispatchers.Main) {
val dialog = WonChallengeDialog(this@MainActivity)
dialog.configure(event.data)
dialog.enqueue()
apiClient.readNotification(event.id)
.subscribe({ }, RxErrorHandler.handleEmptyError())
}
}
override fun onEvent(event: ShowConnectionProblemEvent) {
if (event.title != null) {
super.onEvent(event)

View file

@ -8,7 +8,6 @@ import android.content.SharedPreferences
import android.os.Build
import androidx.activity.result.ActivityResultLauncher
import androidx.core.content.edit
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import com.facebook.AccessToken
import com.facebook.CallbackManager
@ -92,6 +91,9 @@ class AuthenticationViewModel() {
response.newUser = result.newUser
onSuccess(response)
}
else -> {
}
}
}.show()
}
@ -101,7 +103,7 @@ class AuthenticationViewModel() {
loginManager.registerCallback(
callbackManager,
object : FacebookCallback<LoginResult> {
override fun onSuccess(loginResult: LoginResult) {
override fun onSuccess(result: LoginResult) {
val accessToken = AccessToken.getCurrentAccessToken()
compositeSubscription.add(
apiClient.connectSocial("facebook", accessToken?.userId ?: "", accessToken?.token ?: "")
@ -113,8 +115,8 @@ class AuthenticationViewModel() {
override fun onCancel() { /* no-on */ }
override fun onError(exception: FacebookException) {
RxErrorHandler.reportError(exception)
override fun onError(error: FacebookException) {
RxErrorHandler.reportError(error)
}
}
)
@ -124,10 +126,6 @@ class AuthenticationViewModel() {
loginManager.logInWithReadPermissions(activity, listOf("user_friends"))
}
fun handleFacebookLogin(fragment: Fragment) {
loginManager.logInWithReadPermissions(fragment, listOf("user_friends"))
}
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?, onSuccess: (UserAuthResponse) -> Unit) {
callbackManager.onActivityResult(requestCode, resultCode, data)
@ -180,7 +178,7 @@ class AuthenticationViewModel() {
Flowable.defer {
try {
@Suppress("Deprecation")
return@defer Flowable.just(GoogleAuthUtil.getToken(activity, googleEmail, scopes))
return@defer Flowable.just(GoogleAuthUtil.getToken(activity, googleEmail ?: "", scopes))
} catch (e: IOException) {
throw Exceptions.propagate(e)
} catch (e: GoogleAuthException) {
@ -225,7 +223,7 @@ class AuthenticationViewModel() {
e.connectionStatusCode,
activity,
null,
AuthenticationViewModel.REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR
REQUEST_CODE_RECOVER_FROM_PLAY_SERVICES_ERROR
) {
}
return
@ -247,8 +245,8 @@ class AuthenticationViewModel() {
if (result != ConnectionResult.SUCCESS) {
if (googleAPI.isUserResolvableError(result)) {
googleAPI.getErrorDialog(activity, result,
AuthenticationViewModel.PLAY_SERVICES_RESOLUTION_REQUEST
).show()
PLAY_SERVICES_RESOLUTION_REQUEST
)?.show()
}
return false
}

View file

@ -1,11 +1,10 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.6.0'
ext.kotlin_version = '1.6.10'
repositories {
google()
jcenter()
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
@ -15,7 +14,7 @@ buildscript {
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1'
classpath "io.realm:realm-gradle-plugin:10.8.1"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.18.1"
classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.19.0"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5"
classpath 'com.google.firebase:perf-plugin:1.4.0'
}

View file

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip

View file

@ -3,11 +3,11 @@ apply plugin: 'kotlin-multiplatform'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 31
compileSdkVersion 32
defaultConfig {
minSdkVersion 21
targetSdkVersion 31
targetSdkVersion 32
}
buildTypes {
release {