Implement ad cooldown

This commit is contained in:
Phillip Thelen 2022-04-21 11:08:36 +02:00
parent c1b29a547a
commit ce76c40eec
18 changed files with 408 additions and 84 deletions

View file

@ -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

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item>
<!-- create gradient you want to use with the angle you want to use -->
<shape android:shape="rectangle" >
<gradient
android:angle="0"
android:startColor="@color/green_100"
android:endColor="@color/green_500" />
<corners android:radius="8dp" />
</shape>
</item>
<item
android:bottom="3dp"
android:left="3dp"
android:right="3dp"
android:top="3dp">
<shape android:shape="rectangle" >
<solid android:color="@color/brand_400" />
<corners android:radius="6dp" />
</shape>
</item>
</layer-list>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape android:shape="rectangle"
xmlns:android="http://schemas.android.com/apk/res/android">
<stroke android:color="@color/brand_500" android:width="3dp" />
<corners android:radius="8dp" />
</shape>

View file

@ -1,10 +1,8 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/transparent" />
<corners android:radius="@dimen/rounded_button_radius" />
<stroke
android:width="0.5dip"
android:color="#1f000000"/>
</shape>

View file

@ -69,7 +69,8 @@
android:background="@drawable/armoire_background"
android:orientation="vertical"
android:gravity="center"
android:padding="12dp">
android:paddingHorizontal="12dp"
android:paddingTop="24dp">
<TextView
android:id="@+id/equipment_count_view"
android:layout_width="wrap_content"
@ -96,11 +97,8 @@
android:layout_height="60dp"
android:text="@string/equip"
android:textStyle="bold"
style="@style/HabiticaButton.White"/>
<Space
android:id="@+id/button_spacer"
android:layout_width="12dp"
android:layout_height="wrap_content" />
style="@style/HabiticaButton.White"
android:layout_marginEnd="12dp"/>
<Button
android:id="@+id/close_button"
android:layout_width="0dp"
@ -110,7 +108,16 @@
android:textStyle="bold"
style="@style/HabiticaButton.White"/>
</LinearLayout>
<com.habitrpg.android.habitica.ui.views.ads.AdButton
android:id="@+id/ad_button"
android:layout_width="match_parent"
android:layout_height="60dp"
app:text="@string/watch_ad_to_open"
android:layout_marginTop="4dp"
app:currency="gold" />
<TextView
android:id="@+id/drop_rate_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/armoire_drop_rates"

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:parentTag="android.widget.LinearLayout">
<TextView
android:id="@+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
android:textSize="16sp" />
<com.habitrpg.android.habitica.ui.views.CurrencyView
android:id="@+id/currency_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:currency="gold"
android:layout_marginStart="24dp" />
</merge>

View file

@ -27,6 +27,8 @@
<attr name="headerOffsetColor" format="color" />
<attr name="headerTextColor" format="color" />
<attr name="widgetBackgroundRadius" format="dimension" />
<attr name="currency" format="string" />
<declare-styleable name="AvatarView">
<attr name="showBackground" format="boolean" />
<attr name="showMount" format="boolean" />
@ -77,7 +79,7 @@
<attr name="barBackgroundColor" />
</declare-styleable>
<declare-styleable name="CurrencyView">
<attr name="currency" format="string" />
<attr name="currency" />
<attr name="hasLightBackground" />
</declare-styleable>
<declare-styleable name="CurrencyViews">
@ -153,4 +155,8 @@
<attr name="android:inputType" />
<attr name="android:maxLines" />
</declare-styleable>
<declare-styleable name="AdButton">
<attr name="text" />
<attr name="currency" />
</declare-styleable>
</resources>

View file

@ -1230,4 +1230,7 @@
<string name="equipment_remaining">Equipment Remaining: %d</string>
<string name="new_pieces_added_every_month">New pieces added every month</string>
<string name="watch_ad">Watch Ad</string>
<string name="available_in">Available in %s</string>
<string name="watch_ad_to_open">Watch ad to open again</string>
<string name="watch_ad_to_revive">Watch Ad to revive</string>
</resources>

View file

@ -104,5 +104,22 @@
<key>enableTeamBoards</key>
<value>false</value>
</entry>
<entry>
<key>enableNewArmoire</key>
<value>true</value>
</entry>
<entry>
<key>enableArmoireAds</key>
<value>true</value>
</entry>
<entry>
<key>enableFaintAds</key>
<value>false</value>
</entry>
<entry>
<key>enableSpellAds</key>
<value>false</value>
</entry>
</defaultsMap>
<!-- END xml_defaults -->

View file

@ -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

View file

@ -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
}

View file

@ -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<AdType, Date> = 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)
}
}

View file

@ -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")
}
}

View file

@ -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(

View file

@ -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()

View file

@ -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())

View file

@ -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
}
}
}
}

View file

@ -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)