diff --git a/Habitica/build.gradle b/Habitica/build.gradle
index 8be186bdd..7b9c30450 100644
--- a/Habitica/build.gradle
+++ b/Habitica/build.gradle
@@ -129,8 +129,8 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1"
implementation "androidx.lifecycle:lifecycle-common-java8:2.4.1"
- implementation 'androidx.navigation:navigation-fragment-ktx:2.4.1'
- implementation 'androidx.navigation:navigation-ui-ktx:2.4.1'
+ implementation 'androidx.navigation:navigation-fragment-ktx:2.4.2'
+ implementation 'androidx.navigation:navigation-ui-ktx:2.4.2'
implementation "androidx.fragment:fragment-ktx:1.4.1"
implementation "androidx.paging:paging-runtime-ktx:3.1.1"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0'
@@ -145,7 +145,7 @@ dependencies {
attribute(Bundling.BUNDLING_ATTRIBUTE, getObjects().named(Bundling, Bundling.EXTERNAL))
}
}
- androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.6.10"
+ androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.6.20"
}
android {
@@ -166,8 +166,8 @@ android {
buildConfigField "String", "TESTING_LEVEL", "\"production\""
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 3274
- versionName "3.5.1.3"
+ versionCode 3300
+ versionName "3.6"
targetSdkVersion 32
@@ -179,7 +179,6 @@ android {
viewBinding true
}
-
signingConfigs {
release
}
@@ -387,14 +386,6 @@ jacoco {
toolVersion = "0.8.7"
}
-// packages to exclude for example generated classes, R class and models package, add all packages that you wish to exclude from test coverage
-def fileFilter = [
- '**/*$ViewInjector*.*','**/*$ViewBinder*.*', '**/HabiticaIcons*.*', '**/DeviceName.*', '**/databinding/*Binding.*',
- '**/R.class', '**/R.styleable', '**/R$*.class', '**/BuildConfig.*', '**/EmojiMap.*',
- '**/Manifest*.*', 'android/**/*.*', '**/*RealmProxy*.*', '**/io/realm/*']
-def debugTree = fileTree(dir: "${buildDir}/intermediates/asm_instrumented_project_classes/prodDebug", excludes: fileFilter)
-def mainSrc = "${project.projectDir}/src/main/java"
-
task ktlint(type: JavaExec, group: "verification") {
description = "Check Kotlin code style."
classpath = configurations.ktlint
diff --git a/Habitica/res/drawable/ad_button_background.xml b/Habitica/res/drawable/ad_button_background.xml
new file mode 100644
index 000000000..70e52471b
--- /dev/null
+++ b/Habitica/res/drawable/ad_button_background.xml
@@ -0,0 +1,23 @@
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
diff --git a/Habitica/res/drawable/ad_button_background_disabled.xml b/Habitica/res/drawable/ad_button_background_disabled.xml
new file mode 100644
index 000000000..144721661
--- /dev/null
+++ b/Habitica/res/drawable/ad_button_background_disabled.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Habitica/res/drawable/border_1f000000.xml b/Habitica/res/drawable/border_1f000000.xml
index b2336280b..7329bc223 100644
--- a/Habitica/res/drawable/border_1f000000.xml
+++ b/Habitica/res/drawable/border_1f000000.xml
@@ -1,10 +1,8 @@
-
-
diff --git a/Habitica/res/layout/activity_armoire.xml b/Habitica/res/layout/activity_armoire.xml
index 6467cb8fb..e6867e5ce 100644
--- a/Habitica/res/layout/activity_armoire.xml
+++ b/Habitica/res/layout/activity_armoire.xml
@@ -69,7 +69,8 @@
android:background="@drawable/armoire_background"
android:orientation="vertical"
android:gravity="center"
- android:padding="12dp">
+ android:paddingHorizontal="12dp"
+ android:paddingTop="24dp">
-
+ style="@style/HabiticaButton.White"
+ android:layout_marginEnd="12dp"/>
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Habitica/res/values/attrs.xml b/Habitica/res/values/attrs.xml
index 07b0dcc1c..4a25555fe 100644
--- a/Habitica/res/values/attrs.xml
+++ b/Habitica/res/values/attrs.xml
@@ -27,6 +27,8 @@
+
+
@@ -77,7 +79,7 @@
-
+
@@ -153,4 +155,8 @@
+
+
+
+
diff --git a/Habitica/res/values/strings.xml b/Habitica/res/values/strings.xml
index 3840b8834..7f4fa2fa4 100644
--- a/Habitica/res/values/strings.xml
+++ b/Habitica/res/values/strings.xml
@@ -1230,4 +1230,7 @@
Equipment Remaining: %d
New pieces added every month
Watch Ad
+ Available in %s
+ Watch ad to open again
+ Watch Ad to revive
diff --git a/Habitica/res/xml/remote_config_defaults.xml b/Habitica/res/xml/remote_config_defaults.xml
index 920e9c679..0da9826af 100644
--- a/Habitica/res/xml/remote_config_defaults.xml
+++ b/Habitica/res/xml/remote_config_defaults.xml
@@ -104,5 +104,22 @@
enableTeamBoards
false
+
+
+ enableNewArmoire
+ true
+
+
+ enableArmoireAds
+ true
+
+
+ enableFaintAds
+ false
+
+
+ enableSpellAds
+ false
+
\ No newline at end of file
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/HabiticaBaseApplication.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/HabiticaBaseApplication.kt
index 6df86c481..ad02a63e7 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/HabiticaBaseApplication.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/HabiticaBaseApplication.kt
@@ -34,6 +34,7 @@ import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings
import com.habitrpg.android.habitica.components.AppComponent
import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.data.ApiClient
+import com.habitrpg.android.habitica.helpers.AdHandler
import com.habitrpg.android.habitica.helpers.LanguageHelper
import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.helpers.notifications.PushNotificationManager
@@ -73,6 +74,7 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife
setLocale()
setupRemoteConfig()
setupNotifications()
+ setupAdHandler()
HabiticaIconsHelper.init(this)
MarkdownParser.setup(this)
@@ -110,6 +112,10 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife
checkIfNewVersion()
}
+ private fun setupAdHandler() {
+ AdHandler.setup(sharedPrefs, analyticsManager)
+ }
+
private fun setLocale() {
val resources = resources
val configuration: Configuration = resources.configuration
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/Date-Extensions.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/Date-Extensions.kt
index 7582f866a..db8cbe6df 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/Date-Extensions.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/Date-Extensions.kt
@@ -4,16 +4,12 @@ import android.content.res.Resources
import com.habitrpg.android.habitica.R
import java.util.Calendar
import java.util.Date
-import kotlin.math.round
import kotlin.time.Duration
import kotlin.time.DurationUnit
-import kotlin.time.ExperimentalTime
-import kotlin.time.milliseconds
+import kotlin.time.toDuration
class DateUtils {
-
companion object {
-
fun createDate(year: Int, month: Int, day: Int): Date {
val cal = Calendar.getInstance()
cal.set(Calendar.YEAR, year)
@@ -33,11 +29,11 @@ fun Date.getAgoString(res: Resources): String {
}
fun Long.getAgoString(res: Resources): String {
- val diff = Date().time - this
+ val diff = (Date().time - this).toDuration(DurationUnit.MILLISECONDS)
- val diffMinutes = diff / (60 * 1000) % 60
- val diffHours = diff / (60 * 60 * 1000) % 24
- val diffDays = diff / (24 * 60 * 60 * 1000)
+ val diffMinutes = diff.inWholeMinutes
+ val diffHours = diff.inWholeHours
+ val diffDays = diff.inWholeDays
val diffWeeks = diffDays / 7
val diffMonths = diffDays / 30
@@ -63,30 +59,29 @@ fun Date.getRemainingString(res: Resources): String {
return this.time.getRemainingString(res)
}
-@OptIn(ExperimentalTime::class)
fun Long.getRemainingString(res: Resources): String {
- val diff = (this - Date().time).milliseconds
+ val diff = (this - Date().time).toDuration(DurationUnit.MILLISECONDS)
- val diffMinutes = diff.inMinutes
- val diffHours = diff.inHours
- val diffDays = diff.inDays
- val diffWeeks = diffDays / 7f
- val diffMonths = diffDays / 30f
+ val diffMinutes = diff.inWholeMinutes
+ val diffHours = diff.inWholeHours
+ val diffDays = diff.inWholeDays
+ val diffWeeks = diffDays / 7
+ val diffMonths = diffDays / 30
return when {
- diffMonths != 0.0 -> if (round(diffMonths) == 1.0) {
+ diffMonths != 0L -> if (diffMonths == 1L) {
res.getString(R.string.remaining_1month)
- } else res.getString(R.string.remaining_months, round(diffMonths).toInt())
- diffWeeks != 0.0 -> if (round(diffWeeks) == 1.0) {
+ } else res.getString(R.string.remaining_months, diffMonths)
+ diffWeeks != 0L -> if (diffWeeks == 1L) {
res.getString(R.string.remaining_1week)
- } else res.getString(R.string.remaining_weeks, round(diffWeeks).toInt())
- diffDays != 0.0 -> if (diffDays == 1.0) {
+ } else res.getString(R.string.remaining_weeks, diffWeeks)
+ diffDays != 0L -> if (diffDays == 1L) {
res.getString(R.string.remaining_1day)
} else res.getString(R.string.remaining_days, diffDays)
- diffHours != 0.0 -> if (diffHours == 1.0) {
+ diffHours != 0L -> if (diffHours == 1L) {
res.getString(R.string.remaining_1hour)
} else res.getString(R.string.remaining_hours, diffHours)
- diffMinutes == 1.0 -> res.getString(R.string.remaining_1Minute)
+ diffMinutes == 1L -> res.getString(R.string.remaining_1Minute)
else -> res.getString(R.string.remaining_minutes, diffMinutes)
}
}
@@ -95,16 +90,15 @@ fun Date.getShortRemainingString(): String {
return time.getShortRemainingString()
}
-@OptIn(ExperimentalTime::class)
fun Long.getShortRemainingString(): String {
- var diff = Duration.milliseconds((this - Date().time))
+ var diff = (this - Date().time).toDuration(DurationUnit.MILLISECONDS)
val diffDays = diff.toInt(DurationUnit.DAYS)
- diff -= Duration.days(diffDays)
+ diff -= diffDays.toDuration(DurationUnit.DAYS)
val diffHours = diff.toInt(DurationUnit.HOURS)
- diff -= Duration.hours(diffHours)
+ diff -= diffDays.toDuration(DurationUnit.HOURS)
val diffMinutes = diff.toInt(DurationUnit.MINUTES)
- diff -= Duration.minutes(diffMinutes)
+ diff -= diffMinutes.toDuration(DurationUnit.MINUTES)
val diffSeconds = diff.toInt(DurationUnit.SECONDS)
var str = "${diffMinutes}m"
@@ -119,3 +113,7 @@ fun Long.getShortRemainingString(): String {
}
return str
}
+
+fun Duration.getMinuteOrSeconds(): DurationUnit {
+ return if (this.inWholeHours < 1) DurationUnit.SECONDS else DurationUnit.MINUTES
+}
\ No newline at end of file
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/AdHandler.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/AdHandler.kt
index 0074a8d6d..04013b870 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/AdHandler.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/AdHandler.kt
@@ -2,18 +2,63 @@ package com.habitrpg.android.habitica.helpers
import android.app.Activity
import android.content.Context
+import android.content.SharedPreferences
+import android.provider.Settings
import android.util.Log
+import androidx.core.content.edit
+import androidx.core.os.bundleOf
import com.google.android.gms.ads.AdRequest
import com.google.android.gms.ads.FullScreenContentCallback
import com.google.android.gms.ads.LoadAdError
import com.google.android.gms.ads.MobileAds
import com.google.android.gms.ads.OnUserEarnedRewardListener
+import com.google.android.gms.ads.RequestConfiguration
import com.google.android.gms.ads.rewarded.RewardItem
import com.google.android.gms.ads.rewarded.RewardedAd
import com.google.android.gms.ads.rewarded.RewardedAdLoadCallback
+import com.google.firebase.analytics.FirebaseAnalytics
+import com.habitrpg.android.habitica.BuildConfig
+import com.habitrpg.android.habitica.proxy.AnalyticsManager
+import java.io.UnsupportedEncodingException
+import java.security.MessageDigest
+import java.util.Date
+import kotlin.time.DurationUnit
+import kotlin.time.toDuration
+enum class AdType {
+ ARMOIRE,
+ SPELL,
+ FAINT;
-class AdHandler(var activity: Activity, var rewardAction: (Boolean) -> Unit): OnUserEarnedRewardListener {
+ val adUnitID: String
+ get() {
+ if (BuildConfig.DEBUG) {
+ return "ca-app-pub-3940256099942544/5224354917"
+ }
+ return when (this) {
+ ARMOIRE -> "ca-app-pub-5911973472413421/9392092486\n"
+ SPELL -> "ca-app-pub-5911973472413421/1738504765"
+ FAINT -> "ca-app-pub-5911973472413421/1738504765"
+ }
+ }
+}
+
+fun String.md5(): String? {
+ try {
+ val md = MessageDigest.getInstance("MD5")
+ val array = md.digest(this.toByteArray())
+ val sb = StringBuffer()
+ for (i in array.indices) {
+ sb.append(Integer.toHexString(array[i].toInt() and 0xFF or 0x100).substring(1, 3))
+ }
+ return sb.toString()
+ } catch (e: java.security.NoSuchAlgorithmException) {
+ } catch (ex: UnsupportedEncodingException) {
+ }
+ return null
+}
+
+class AdHandler(val activity: Activity, val type: AdType, val rewardAction: (Boolean) -> Unit): OnUserEarnedRewardListener {
private var rewardedAd: RewardedAd? = null
companion object {
@@ -24,13 +69,40 @@ class AdHandler(var activity: Activity, var rewardAction: (Boolean) -> Unit): On
DISABLED
}
+ private lateinit var analyticsManager: AnalyticsManager
+ private lateinit var sharedPreferences: SharedPreferences
const val TAG = "AdHandler"
- const val adUnitID = "ca-app-pub-3940256099942544/5224354917"
private var currentAdStatus = AdStatus.UNINITIALIZED
+ private var nextAdAllowed: MutableMap = mutableMapOf()
+
+ fun nextAdAllowedDate(type: AdType): Date? {
+ return nextAdAllowed[type]
+ }
+
+ fun isAllowed(type: AdType): Boolean {
+ return nextAdAllowedDate(type)?.after(Date()) == true
+ }
+
+ fun setNextAllowedDate(type: AdType, date: Date) {
+ nextAdAllowed[type] = date
+ sharedPreferences.edit {
+ putLong("nextAd${type.name}", date.time)
+ }
+ }
+
fun initialize(context: Context, onComplete: () -> Unit) {
if (currentAdStatus != AdStatus.UNINITIALIZED) return
+
+ if (BuildConfig.DEBUG || BuildConfig.TESTING_LEVEL == "staff" || BuildConfig.TESTING_LEVEL == "alpha") {
+ val android_id: String =
+ Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
+ val deviceId: String = android_id.md5()?.uppercase() ?: ""
+ val configuration = RequestConfiguration.Builder().setTestDeviceIds(listOf(deviceId)).build()
+ MobileAds.setRequestConfiguration(configuration)
+ }
+
currentAdStatus = AdStatus.INITIALIZING
MobileAds.initialize(context) {
currentAdStatus = AdStatus.READY
@@ -56,13 +128,34 @@ class AdHandler(var activity: Activity, var rewardAction: (Boolean) -> Unit): On
}
}
}
+
+ fun setup(sharedPrefs: SharedPreferences, analyticsManager: AnalyticsManager) {
+ this.sharedPreferences = sharedPrefs
+ this.analyticsManager = analyticsManager
+
+ for (type in AdType.values()) {
+ val time = sharedPrefs.getLong("nextAd${type.name}", 0)
+ if (time > 0) {
+ nextAdAllowed[type] = Date(time)
+ }
+ }
+ }
}
fun prepare() {
whenAdsInitialized(activity) {
- val adRequest = AdRequest.Builder().build()
+ val adRequest = AdRequest.Builder()
+ .build()
- RewardedAd.load(activity, adUnitID, adRequest, object : RewardedAdLoadCallback() {
+ if (BuildConfig.DEBUG || BuildConfig.TESTING_LEVEL == "staff" || BuildConfig.TESTING_LEVEL == "alpha") {
+ if (!adRequest.isTestDevice(activity)) {
+ // users in this group need to be configured as Test device. better to fail if they aren't
+ currentAdStatus = AdStatus.DISABLED
+ return@whenAdsInitialized
+ }
+ }
+
+ RewardedAd.load(activity, type.adUnitID, adRequest, object : RewardedAdLoadCallback() {
override fun onAdFailedToLoad(adError: LoadAdError) {
rewardAction(false)
}
@@ -103,17 +196,24 @@ class AdHandler(var activity: Activity, var rewardAction: (Boolean) -> Unit): On
}
private fun showRewardedAd() {
+ if (nextAdAllowedDate(type)?.after(Date()) == true) {
+ return
+ }
if (rewardedAd != null) {
rewardedAd?.show(activity, this)
+ setNextAllowedDate(type, Date(Date().time + 1.toDuration(DurationUnit.HOURS).inWholeMilliseconds))
} else {
Log.d(TAG, "The rewarded ad wasn't ready yet.")
}
}
override fun onUserEarnedReward(rewardItem: RewardItem) {
- val rewardAmount = rewardItem.amount
- val rewardType = rewardItem.type
- Log.d(TAG, "User earned the reward. ${rewardAmount}, ${rewardType}")
+ analyticsManager.logEvent("adRewardEarned", bundleOf(
+ Pair("type", type.name)
+ ))
+ FirebaseAnalytics.getInstance(activity).logEvent("adRewardEarned", bundleOf(
+ Pair("type", type.name)
+ ))
rewardAction(true)
}
}
\ No newline at end of file
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/AppConfigManager.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/AppConfigManager.kt
index bbf4a5b3c..9979cb65d 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/AppConfigManager.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/AppConfigManager.kt
@@ -138,4 +138,20 @@ class AppConfigManager(contentRepository: ContentRepository?) {
fun enableTeamBoards(): Boolean {
return remoteConfig.getBoolean("enableTeamBoards")
}
+
+ fun enableArmoireAds(): Boolean {
+ return remoteConfig.getBoolean("enableArmoireAds")
+ }
+
+ fun enableFaintAds(): Boolean {
+ return remoteConfig.getBoolean("enableFaintAds")
+ }
+
+ fun enableSpellAds(): Boolean {
+ return remoteConfig.getBoolean("enableSpellAds")
+ }
+
+ fun enableNewArmoire(): Boolean {
+ return remoteConfig.getBoolean("enableNewArmoire")
+ }
}
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/ArmoireActivity.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/ArmoireActivity.kt
index 407ba8ff7..a7746a226 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/ArmoireActivity.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/ArmoireActivity.kt
@@ -1,17 +1,22 @@
package com.habitrpg.android.habitica.ui.activities
import android.os.Bundle
+import android.util.Log
import android.view.Gravity
import android.view.View
import android.view.animation.AccelerateInterpolator
import android.widget.FrameLayout
import androidx.core.content.ContextCompat
+import androidx.lifecycle.lifecycleScope
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.components.UserComponent
import com.habitrpg.android.habitica.data.InventoryRepository
import com.habitrpg.android.habitica.databinding.ActivityArmoireBinding
import com.habitrpg.android.habitica.extensions.observeOnce
+import com.habitrpg.android.habitica.helpers.AdHandler
+import com.habitrpg.android.habitica.helpers.AdType
import com.habitrpg.android.habitica.helpers.AppConfigManager
+import com.habitrpg.android.habitica.helpers.RxErrorHandler
import com.habitrpg.android.habitica.ui.helpers.loadImage
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
import com.plattysoft.leonids.ParticleSystem
@@ -58,12 +63,40 @@ class ArmoireActivity: BaseActivity() {
binding.noEquipmentView.visibility = if (remaining > 0) View.GONE else View.VISIBLE
}
+ if (appConfigManager.enableArmoireAds()) {
+ val handler = AdHandler(this, AdType.ARMOIRE) {
+ Log.d("AdHandler", "Giving Armoire")
+ val user = userViewModel.user.value ?: return@AdHandler
+ val currentGold = user.stats?.gp ?: return@AdHandler
+ compositeSubscription.add(userRepository.updateUser("stats.gp", currentGold + 100)
+ .flatMap { inventoryRepository.buyItem(user, "armoire", 100.0, 1) }
+ .subscribe({
+ configure(it.armoire["type"] ?: "",
+ it.armoire["dropKey"] ?: "",
+ it.armoire["dropText"] ?: "")
+ binding.adButton.updateForAdType(AdType.ARMOIRE, lifecycleScope)
+ hasAnimatedChanges = false
+ gold = null
+ }, RxErrorHandler.handleEmptyError()))
+ }
+ handler.prepare()
+ binding.adButton.updateForAdType(AdType.ARMOIRE, lifecycleScope)
+ binding.adButton.setOnClickListener {
+ handler.show()
+ }
+ } else {
+ binding.adButton.visibility = View.GONE
+ }
+
binding.closeButton.setOnClickListener {
finish()
}
binding.equipButton.setOnClickListener {
equipmentKey?.let { it1 -> inventoryRepository.equip("gear", it1).subscribe() }
finish()
+ }
+ binding.dropRateButton.setOnClickListener {
+
}
intent.extras?.let {
val args = ArmoireActivityArgs.fromBundle(it)
@@ -79,9 +112,11 @@ class ArmoireActivity: BaseActivity() {
private fun startAnimation() {
val gold = gold?.toInt()
- if (hasAnimatedChanges || gold == null) return
- binding.goldView.value = (gold).toDouble()
- binding.goldView.value = (gold - 100).toDouble()
+ if (hasAnimatedChanges) return
+ if (gold != null) {
+ binding.goldView.value = (gold).toDouble()
+ binding.goldView.value = (gold - 100).toDouble()
+ }
val container = binding.confettiAnchor
container.postDelayed(
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt
index 0cd14b815..f8cecf4d6 100755
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt
@@ -37,6 +37,7 @@ import com.habitrpg.android.habitica.extensions.isUsingNightModeResources
import com.habitrpg.android.habitica.extensions.subscribeWithErrorHandler
import com.habitrpg.android.habitica.extensions.updateStatusBarColor
import com.habitrpg.android.habitica.helpers.AdHandler
+import com.habitrpg.android.habitica.helpers.AdType
import com.habitrpg.android.habitica.helpers.AmplitudeManager
import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.android.habitica.helpers.MainNavigationController
@@ -203,9 +204,6 @@ open class MainActivity : BaseActivity(), SnackbarActivity {
setupBottomnavigationLayoutListener()
viewModel.onCreate()
-
- val args = ArmoireActivityDirections.openArmoireActivity("experience", "", "")
- MainNavigationController.navigate(R.id.armoireActivity, args.arguments)
}
override fun setTitle(title: CharSequence?) {
@@ -450,11 +448,7 @@ open class MainActivity : BaseActivity(), SnackbarActivity {
}
if (this.faintDialog == null && !this.isFinishing) {
- val handler = AdHandler(this) {
- Log.d("AdHandler", "Reviving user")
- compositeSubscription.add(userRepository.updateUser("stats.hp", 50).subscribe({}, RxErrorHandler.handleEmptyError()))
- }
- handler.prepare()
+
val binding = DialogFaintBinding.inflate(this.layoutInflater)
binding.hpBar.setLightBackground(true)
binding.hpBar.setIcon(HabiticaIconsHelper.imageOfHeartLightBg())
@@ -467,9 +461,19 @@ open class MainActivity : BaseActivity(), SnackbarActivity {
faintDialog = null
userRepository.revive().subscribe({ }, RxErrorHandler.handleEmptyError())
}
- faintDialog?.addButton(R.string.watch_ad, true) { _, _ ->
- faintDialog = null
- handler.show()
+ if (AdHandler.isAllowed(AdType.FAINT)) {
+ val handler = AdHandler(this, AdType.FAINT) {
+ Log.d("AdHandler", "Reviving user")
+ compositeSubscription.add(
+ userRepository.updateUser("stats.hp", 50)
+ .subscribe({}, RxErrorHandler.handleEmptyError())
+ )
+ }
+ handler.prepare()
+ faintDialog?.addButton(R.string.watch_ad_to_revive, true) { _, _ ->
+ faintDialog = null
+ handler.show()
+ }
}
soundManager.loadAndPlayAudio(SoundManager.SoundDeath)
this.faintDialog?.enqueue()
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NavigationDrawerFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NavigationDrawerFragment.kt
index fad356ae5..cc7a08049 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NavigationDrawerFragment.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/NavigationDrawerFragment.kt
@@ -24,6 +24,7 @@ import com.habitrpg.android.habitica.data.InventoryRepository
import com.habitrpg.android.habitica.data.SocialRepository
import com.habitrpg.android.habitica.data.UserRepository
import com.habitrpg.android.habitica.databinding.DrawerMainBinding
+import com.habitrpg.android.habitica.extensions.getMinuteOrSeconds
import com.habitrpg.android.habitica.extensions.getRemainingString
import com.habitrpg.android.habitica.extensions.getShortRemainingString
import com.habitrpg.android.habitica.extensions.getThemeColor
@@ -49,16 +50,18 @@ import com.habitrpg.android.habitica.ui.viewmodels.NotificationsViewModel
import com.habitrpg.android.habitica.ui.views.HabiticaSnackbar
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.disposables.CompositeDisposable
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
import java.util.Calendar
import java.util.Date
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.time.Duration
+import kotlin.time.DurationUnit
import kotlin.time.ExperimentalTime
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
+import kotlin.time.toDuration
class NavigationDrawerFragment : DialogFragment() {
@@ -227,11 +230,9 @@ class NavigationDrawerFragment : DialogFragment() {
subscriptions?.add(
Flowable.combineLatest(
- contentRepository.getWorldState(), inventoryRepository.getAvailableLimitedItems(),
- { state, items ->
- return@combineLatest Pair(state, items)
- }
- ).subscribe(
+ contentRepository.getWorldState(), inventoryRepository.getAvailableLimitedItems()) { state, items ->
+ return@combineLatest Pair(state, items)
+ }.subscribe(
{ pair ->
val gearEvent = pair.first.events.firstOrNull { it.gear }
createUpdatingJob("seasonal", {
@@ -675,8 +676,8 @@ class NavigationDrawerFragment : DialogFragment() {
createUpdatingJob(activePromo.promoType.name, {
activePromo.isActive
}, {
- val diff = activePromo.endDate.time - Date().time
- if (diff < (Duration.hours(1).inWholeMilliseconds)) Duration.seconds(1) else Duration.minutes(1)
+ val diff = (activePromo.endDate.time - Date().time).toDuration(DurationUnit.SECONDS)
+ 1.toDuration(diff.getMinuteOrSeconds())
}) {
if (activePromo.isActive) {
promotedItem.subtitle = context?.getString(R.string.sale_ends_in, activePromo.endDate.getShortRemainingString())
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/ads/AdButton.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/ads/AdButton.kt
new file mode 100644
index 000000000..c0b385ae3
--- /dev/null
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/ads/AdButton.kt
@@ -0,0 +1,92 @@
+package com.habitrpg.android.habitica.ui.views.ads
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.Gravity
+import android.view.View
+import android.widget.LinearLayout
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.LifecycleCoroutineScope
+import com.habitrpg.android.habitica.R
+import com.habitrpg.android.habitica.databinding.AdButtonBinding
+import com.habitrpg.android.habitica.extensions.getMinuteOrSeconds
+import com.habitrpg.android.habitica.extensions.getShortRemainingString
+import com.habitrpg.android.habitica.extensions.layoutInflater
+import com.habitrpg.android.habitica.helpers.AdHandler
+import com.habitrpg.android.habitica.helpers.AdType
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import java.util.Date
+import kotlin.time.DurationUnit
+import kotlin.time.toDuration
+
+class AdButton @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null
+) : LinearLayout(context, attrs) {
+ private var updateJob: Job? = null
+ private var nextAdDate: Date? = null
+ private val binding = AdButtonBinding.inflate(context.layoutInflater, this)
+
+ var text: String = ""
+ set(value) {
+ field = value
+ updateViews()
+ }
+
+ var isAvailable: Boolean = true
+ set(value) {
+ field = value
+ updateViews()
+ }
+
+ init {
+ context.theme?.obtainStyledAttributes(
+ attrs,
+ R.styleable.AdButton,
+ 0, 0
+ )?.let { attributes ->
+ text = attributes.getString(R.styleable.AdButton_text) ?: ""
+ binding.currencyView.currency = attributes.getString(R.styleable.AdButton_currency)
+ }
+ binding.textView.setTextColor(ContextCompat.getColor(context, R.color.white))
+ binding.currencyView.setTextColor(ContextCompat.getColor(context, R.color.white))
+ binding.currencyView.value = 0.0
+ gravity = Gravity.CENTER
+ }
+
+ private fun updateViews() {
+ if (isAvailable) {
+ binding.textView.text = text
+ binding.textView.alpha = 1.0f
+ binding.currencyView.visibility = View.VISIBLE
+ setBackgroundResource(R.drawable.ad_button_background)
+ } else {
+ binding.textView.text = context.getString(R.string.available_in, nextAdDate?.getShortRemainingString() ?: "")
+ binding.textView.alpha = 0.75f
+ binding.currencyView.visibility = View.GONE
+ setBackgroundResource(R.drawable.ad_button_background_disabled)
+ }
+ isEnabled = isAvailable
+ }
+
+ fun updateForAdType(type: AdType, lifecycleScope: LifecycleCoroutineScope) {
+ if (updateJob?.isActive == true) {
+ updateJob?.cancel()
+ }
+ nextAdDate = AdHandler.nextAdAllowedDate(type)
+ if (nextAdDate?.after(Date()) == true) {
+ updateJob = lifecycleScope.launch(Dispatchers.Main) {
+ while (nextAdDate?.after(Date()) == true) {
+ val remaining = ((nextAdDate?.time ?: 0L) - Date().time).toDuration(DurationUnit.MILLISECONDS)
+ isAvailable = remaining.isNegative()
+ updateViews()
+ delay(1.toDuration(remaining.getMinuteOrSeconds()))
+ }
+ isAvailable = true
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/shops/PurchaseDialog.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/shops/PurchaseDialog.kt
index 4800f1812..be0f8ae14 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/shops/PurchaseDialog.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/shops/PurchaseDialog.kt
@@ -359,7 +359,7 @@ class PurchaseDialog(context: Context, component: UserComponent?, val item: Shop
return
} else if ("gold" == shopItem.currency && "gem" != shopItem.key) {
observable = inventoryRepository.buyItem(user, shopItem.key, shopItem.value.toDouble(), quantity).map { buyResponse ->
- if (shopItem.key == "armoire") {
+ if (shopItem.key == "armoire" && configManager.enableNewArmoire()) {
MainNavigationController.navigate(R.id.armoireActivity, ArmoireActivityDirections.openArmoireActivity(buyResponse.armoire["type"] ?: "",
buyResponse.armoire["dropText"] ?: "",
buyResponse.armoire["dropKey"] ?: "").arguments)