linting fixes

This commit is contained in:
Phillip Thelen 2024-04-22 16:06:37 +02:00
parent deeed81e79
commit ce28bf83f3
612 changed files with 40442 additions and 33570 deletions

View file

@ -1,2 +1,14 @@
[*.{kt,kts}] [*.{kt,kts}]
max_line_length=off max_line_length=off
ktlint_function_naming_ignore_when_annotated_with=Composable
[*.gradle.kts]
property_naming=off
[shared/src/commonMain/kotlin/com/habitrpg/shared/habitica/models/responses/TaskDirectionData.kt]
ktlint_standard_backing-property-naming=disabled
[**/generated/**/*.kt]
ktlint_standard_property-naming=disabled
ktlint_standard_backing-property-naming=disabled

View file

@ -25,6 +25,11 @@ jobs:
unit-test: unit-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
module:
- "common"
- "Habitica"
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: set up JDK 17 - name: set up JDK 17
@ -38,7 +43,7 @@ jobs:
- name: Run with Gradle - name: Run with Gradle
uses: gradle/gradle-build-action@v2 uses: gradle/gradle-build-action@v2
with: with:
arguments: testProdDebugUnitTest arguments: ${{ matrix.module }}:testProdDebugUnitTest
# ui-test: # ui-test:
# runs-on: ubuntu-latest # runs-on: ubuntu-latest

View file

@ -1,8 +1,9 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<!-- Application theme. --> <!-- Application theme. -->
<style name="AppTheme" parent="Theme.Material3.DayNight"> <style name="AppTheme" parent="Theme.Material3.DayNight">
<item name="android:listSeparatorTextViewStyle">@style/MyOwnListSeperatorTextViewStyle</item> <item name="android:listSeparatorTextViewStyle">@style/MyOwnListSeperatorTextViewStyle
</item>
<item name="android:elevation">0dp</item> <item name="android:elevation">0dp</item>
<item name="elevation">0dp</item> <item name="elevation">0dp</item>
@ -69,8 +70,7 @@
<item name="headerTextColor">@color/text_title</item> <item name="headerTextColor">@color/text_title</item>
</style> </style>
<style name="MainAppTheme" parent="AppTheme"> <style name="MainAppTheme" parent="AppTheme"></style>
</style>
<style name="MainAppTheme.Dark"> <style name="MainAppTheme.Dark">
<item name="colorPrimary">@color/brand_400</item> <item name="colorPrimary">@color/brand_400</item>
@ -581,6 +581,7 @@
<style name="Pill.Content" parent="Pill"> <style name="Pill.Content" parent="Pill">
<item name="android:background">@drawable/pill_bg_content</item> <item name="android:background">@drawable/pill_bg_content</item>
</style> </style>
<style name="Pill.Purple"> <style name="Pill.Purple">
<item name="android:textColor">@color/white</item> <item name="android:textColor">@color/white</item>
<item name="android:background">@drawable/pill_bg_purple_300</item> <item name="android:background">@drawable/pill_bg_purple_300</item>
@ -881,12 +882,15 @@
<style name="RedTextLabel" parent="TextAppearance.AppCompat"> <style name="RedTextLabel" parent="TextAppearance.AppCompat">
<item name="android:textColor">@color/red_10</item> <item name="android:textColor">@color/red_10</item>
</style> </style>
<style name="YellowTextLabel" parent="TextAppearance.AppCompat"> <style name="YellowTextLabel" parent="TextAppearance.AppCompat">
<item name="android:textColor">@color/yellow_5</item> <item name="android:textColor">@color/yellow_5</item>
</style> </style>
<style name="BlueTextLabel" parent="TextAppearance.AppCompat"> <style name="BlueTextLabel" parent="TextAppearance.AppCompat">
<item name="android:textColor">@color/blue_10</item> <item name="android:textColor">@color/blue_10</item>
</style> </style>
<style name="PurpleTextLabel" parent="TextAppearance.AppCompat"> <style name="PurpleTextLabel" parent="TextAppearance.AppCompat">
<item name="android:textColor">@color/brand_300</item> <item name="android:textColor">@color/brand_300</item>
</style> </style>
@ -919,7 +923,6 @@
</style> </style>
<style name="TaskFormTextInputLayoutAppearance" parent="Widget.MaterialComponents.TextInputLayout.FilledBox"> <style name="TaskFormTextInputLayoutAppearance" parent="Widget.MaterialComponents.TextInputLayout.FilledBox">
<!-- reference our hint & error styles --> <!-- reference our hint & error styles -->
<item name="boxBackgroundColor">?colorTintedBackground</item> <item name="boxBackgroundColor">?colorTintedBackground</item>
@ -1021,6 +1024,7 @@
<item name="android:textSize">12sp</item> <item name="android:textSize">12sp</item>
<item name="android:textStyle">bold</item> <item name="android:textStyle">bold</item>
</style> </style>
<style name="ActiveLabel"> <style name="ActiveLabel">
<item name="android:background">@drawable/pill_bg_teal_100</item> <item name="android:background">@drawable/pill_bg_teal_100</item>
<item name="android:paddingStart">4dp</item> <item name="android:paddingStart">4dp</item>

View file

@ -13,7 +13,6 @@ import com.habitrpg.android.habitica.data.TaskRepository
import com.habitrpg.android.habitica.data.TutorialRepository import com.habitrpg.android.habitica.data.TutorialRepository
import com.habitrpg.android.habitica.data.UserRepository import com.habitrpg.android.habitica.data.UserRepository
import com.habitrpg.android.habitica.helpers.AppConfigManager import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.common.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.helpers.NotificationsManager import com.habitrpg.android.habitica.helpers.NotificationsManager
import com.habitrpg.android.habitica.helpers.SoundManager import com.habitrpg.android.habitica.helpers.SoundManager
import com.habitrpg.android.habitica.interactors.FeedPetUseCase import com.habitrpg.android.habitica.interactors.FeedPetUseCase
@ -28,6 +27,7 @@ import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
import com.habitrpg.common.habitica.api.HostConfig import com.habitrpg.common.habitica.api.HostConfig
import com.habitrpg.common.habitica.helpers.ExceptionHandler import com.habitrpg.common.habitica.helpers.ExceptionHandler
import com.habitrpg.common.habitica.helpers.MainNavigationController
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
import io.mockk.clearAllMocks import io.mockk.clearAllMocks
import io.mockk.every import io.mockk.every
@ -104,7 +104,10 @@ open class HabiticaTestCase : TestCase() {
every { inventoryRepository.getItems(QuestContent::class.java, any()) } returns flowOf(content.quests) every { inventoryRepository.getItems(QuestContent::class.java, any()) } returns flowOf(content.quests)
} }
internal fun <T> loadJsonFile(s: String, type: Type): T { internal fun <T> loadJsonFile(
s: String,
type: Type,
): T {
val userStream = javaClass.classLoader?.getResourceAsStream("$s.json") val userStream = javaClass.classLoader?.getResourceAsStream("$s.json")
return gson.fromJson(gson.newJsonReader(InputStreamReader(userStream)), type) return gson.fromJson(gson.newJsonReader(InputStreamReader(userStream)), type)
} }
@ -132,7 +135,11 @@ open class HabiticaTestCase : TestCase() {
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
private fun <P, C> assign(it: KCallable<*>, obj: C, value: P) { private fun <P, C> assign(
it: KCallable<*>,
obj: C,
value: P,
) {
if ((it as KMutableProperty1<C, P>).javaField!!.get(obj) == null) { if ((it as KMutableProperty1<C, P>).javaField!!.get(obj) == null) {
it.set(obj, value) it.set(obj, value)
} }

View file

@ -19,7 +19,6 @@ class IntroActivityScreen : Screen<IntroActivityScreen>() {
@LargeTest @LargeTest
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class IntroActivityTest : ActivityTestCase() { class IntroActivityTest : ActivityTestCase() {
@Rule @Rule
@JvmField @JvmField
var mActivityTestRule = ActivityScenarioRule(IntroActivity::class.java) var mActivityTestRule = ActivityScenarioRule(IntroActivity::class.java)

View file

@ -22,7 +22,6 @@ class MainActivityScreen : Screen<MainActivityScreen>() {
@LargeTest @LargeTest
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class MainActivityTest : ActivityTestCase() { class MainActivityTest : ActivityTestCase() {
val screen = MainActivityScreen() val screen = MainActivityScreen()
lateinit var scenario: ActivityScenario<MainActivity> lateinit var scenario: ActivityScenario<MainActivity>

View file

@ -48,7 +48,6 @@ class TaskFormScreen : Screen<TaskFormScreen>() {
@LargeTest @LargeTest
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class TaskFormActivityTest : ActivityTestCase() { class TaskFormActivityTest : ActivityTestCase() {
val screen = TaskFormScreen() val screen = TaskFormScreen()
lateinit var scenario: ActivityScenario<TaskFormActivity> lateinit var scenario: ActivityScenario<TaskFormActivity>
@ -297,7 +296,7 @@ class TaskFormActivityTest : ActivityTestCase() {
} }
KSpinner( KSpinner(
builder = { withId(R.id.repeats_every_spinner) }, builder = { withId(R.id.repeats_every_spinner) },
itemTypeBuilder = { itemType(::KSpinnerItem) } itemTypeBuilder = { itemType(::KSpinnerItem) },
) perform { ) perform {
open() open()
childAt<KSpinnerItem>(1) { childAt<KSpinnerItem>(1) {

View file

@ -9,13 +9,15 @@ import io.github.kakaocup.kakao.screen.Screen
import org.junit.Before import org.junit.Before
abstract class FragmentTestCase<F : Fragment, VB : ViewBinding, S : Screen<S>>( abstract class FragmentTestCase<F : Fragment, VB : ViewBinding, S : Screen<S>>(
val shouldLaunchFragment: Boolean = true val shouldLaunchFragment: Boolean = true,
) : HabiticaTestCase() { ) : HabiticaTestCase() {
lateinit var scenario: FragmentScenario<F> lateinit var scenario: FragmentScenario<F>
lateinit var fragment: F lateinit var fragment: F
abstract fun makeFragment() abstract fun makeFragment()
abstract fun launchFragment(args: Bundle? = null) abstract fun launchFragment(args: Bundle? = null)
abstract val screen: S abstract val screen: S
@Before @Before

View file

@ -23,7 +23,8 @@ class MainItem(parent: Matcher<View>) : KRecyclerItem<MainItem>(parent) {
} }
class NavigationDrawerScreen : Screen<NavigationDrawerScreen>() { class NavigationDrawerScreen : Screen<NavigationDrawerScreen>() {
val recycler: KRecyclerView = KRecyclerView({ val recycler: KRecyclerView =
KRecyclerView({
withId(R.id.recyclerView) withId(R.id.recyclerView)
}, itemTypeBuilder = { }, itemTypeBuilder = {
itemType(::SectionHeaderItem) itemType(::SectionHeaderItem)
@ -38,7 +39,8 @@ internal class NavigationDrawerFragmentTest : FragmentTestCase<NavigationDrawerF
} }
override fun launchFragment(args: Bundle?) { override fun launchFragment(args: Bundle?) {
scenario = launchFragmentInContainer(args, R.style.MainAppTheme) { scenario =
launchFragmentInContainer(args, R.style.MainAppTheme) {
return@launchFragmentInContainer fragment return@launchFragmentInContainer fragment
} }
} }

View file

@ -50,7 +50,8 @@ class PartyDetailFragmentTest : FragmentTestCase<PartyDetailFragment, FragmentPa
} }
override fun launchFragment(args: Bundle?) { override fun launchFragment(args: Bundle?) {
scenario = launchFragmentInContainer(args, R.style.MainAppTheme) { scenario =
launchFragmentInContainer(args, R.style.MainAppTheme) {
return@launchFragmentInContainer fragment return@launchFragmentInContainer fragment
} }
} }

View file

@ -21,22 +21,26 @@ import org.junit.runner.RunWith
class StatsScreen : Screen<StatsScreen>() { class StatsScreen : Screen<StatsScreen>() {
val strengthStatsView = KView { withId(R.id.strengthStatsView) } val strengthStatsView = KView { withId(R.id.strengthStatsView) }
val strengthAllocateButton = KButton { val strengthAllocateButton =
KButton {
withId(R.id.allocateButton) withId(R.id.allocateButton)
isDescendantOfA { withId(R.id.strengthStatsView) } isDescendantOfA { withId(R.id.strengthStatsView) }
} }
val intelligenceStatsView = KView { withId(R.id.intelligenceStatsView) } val intelligenceStatsView = KView { withId(R.id.intelligenceStatsView) }
val intelligenceAllocateButton = KButton { val intelligenceAllocateButton =
KButton {
withId(R.id.allocateButton) withId(R.id.allocateButton)
isDescendantOfA { withId(R.id.intelligenceStatsView) } isDescendantOfA { withId(R.id.intelligenceStatsView) }
} }
val constitutionStatsView = KView { withId(R.id.constitutionStatsView) } val constitutionStatsView = KView { withId(R.id.constitutionStatsView) }
val constitutionAllocateButton = KButton { val constitutionAllocateButton =
KButton {
withId(R.id.allocateButton) withId(R.id.allocateButton)
isDescendantOfA { withId(R.id.constitutionStatsView) } isDescendantOfA { withId(R.id.constitutionStatsView) }
} }
val perceptionStatsView = KView { withId(R.id.perceptionStatsView) } val perceptionStatsView = KView { withId(R.id.perceptionStatsView) }
val perceptionAllocateButton = KButton { val perceptionAllocateButton =
KButton {
withId(R.id.allocateButton) withId(R.id.allocateButton)
isDescendantOfA { withId(R.id.perceptionStatsView) } isDescendantOfA { withId(R.id.perceptionStatsView) }
} }
@ -46,7 +50,6 @@ class StatsScreen : Screen<StatsScreen>() {
@LargeTest @LargeTest
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class StatsFragmentTest : FragmentTestCase<StatsFragment, FragmentStatsBinding, StatsScreen>() { class StatsFragmentTest : FragmentTestCase<StatsFragment, FragmentStatsBinding, StatsScreen>() {
override val screen = StatsScreen() override val screen = StatsScreen()
override fun makeFragment() { override fun makeFragment() {
@ -55,7 +58,8 @@ class StatsFragmentTest : FragmentTestCase<StatsFragment, FragmentStatsBinding,
} }
override fun launchFragment(args: Bundle?) { override fun launchFragment(args: Bundle?) {
scenario = launchFragmentInContainer(args, R.style.MainAppTheme) { scenario =
launchFragmentInContainer(args, R.style.MainAppTheme) {
return@launchFragmentInContainer fragment return@launchFragmentInContainer fragment
} }
} }

View file

@ -34,7 +34,8 @@ private val KTextView.text: CharSequence?
get() { get() {
var string: CharSequence? = null var string: CharSequence? = null
( (
this.view.perform(object : ViewAction { this.view.perform(
object : ViewAction {
override fun getConstraints(): Matcher<View> { override fun getConstraints(): Matcher<View> {
return isA(TextView::class.java) return isA(TextView::class.java)
} }
@ -43,11 +44,15 @@ private val KTextView.text: CharSequence?
return "getting text from a TextView" return "getting text from a TextView"
} }
override fun perform(uiController: UiController?, view: View?) { override fun perform(
uiController: UiController?,
view: View?,
) {
val tv = view as TextView val tv = view as TextView
string = tv.text string = tv.text
} }
}) },
)
) )
return string return string
} }
@ -58,7 +63,8 @@ class ItemItem(parent: Matcher<View>) : KRecyclerItem<ItemItem>(parent) {
} }
class ItemScreen : Screen<ItemScreen>() { class ItemScreen : Screen<ItemScreen>() {
val recycler: KRecyclerView = KRecyclerView({ val recycler: KRecyclerView =
KRecyclerView({
withId(R.id.recyclerView) withId(R.id.recyclerView)
}, itemTypeBuilder = { }, itemTypeBuilder = {
itemType(::ItemItem) itemType(::ItemItem)
@ -78,7 +84,8 @@ internal class ItemRecyclerFragmentTest : FragmentTestCase<ItemRecyclerFragment,
} }
override fun launchFragment(args: Bundle?) { override fun launchFragment(args: Bundle?) {
scenario = launchFragmentInContainer(args, R.style.MainAppTheme) { scenario =
launchFragmentInContainer(args, R.style.MainAppTheme) {
return@launchFragmentInContainer fragment return@launchFragmentInContainer fragment
} }
} }

View file

@ -21,7 +21,8 @@ import kotlinx.coroutines.flow.flowOf
import org.junit.Test import org.junit.Test
class PetDetailScreen : Screen<PetDetailScreen>() { class PetDetailScreen : Screen<PetDetailScreen>() {
val recycler: KRecyclerView = KRecyclerView({ val recycler: KRecyclerView =
KRecyclerView({
withId(R.id.recyclerView) withId(R.id.recyclerView)
}, itemTypeBuilder = { }, itemTypeBuilder = {
itemType(::SectionItem) itemType(::SectionItem)
@ -37,13 +38,14 @@ internal class PetDetailRecyclerFragmentTest :
every { inventoryRepository.getOwnedItems("food") } returns flowOf(user.items?.food!!.filter { it.numberOwned > 0 }) every { inventoryRepository.getOwnedItems("food") } returns flowOf(user.items?.food!!.filter { it.numberOwned > 0 })
val saddle = OwnedItem() val saddle = OwnedItem()
saddle.numberOwned = 1 saddle.numberOwned = 1
every { inventoryRepository.getOwnedItems(true) } returns flowOf( every { inventoryRepository.getOwnedItems(true) } returns
flowOf(
mapOf( mapOf(
Pair( Pair(
"Saddle-food", "Saddle-food",
saddle saddle,
) ),
) ),
) )
fragment = spyk() fragment = spyk()
@ -51,7 +53,8 @@ internal class PetDetailRecyclerFragmentTest :
} }
override fun launchFragment(args: Bundle?) { override fun launchFragment(args: Bundle?) {
scenario = launchFragmentInContainer(args, R.style.MainAppTheme) { scenario =
launchFragmentInContainer(args, R.style.MainAppTheme) {
return@launchFragmentInContainer fragment return@launchFragmentInContainer fragment
} }
} }
@ -66,18 +69,18 @@ internal class PetDetailRecyclerFragmentTest :
inventoryRepository.getPets( inventoryRepository.getPets(
any(), any(),
any(), any(),
any() any(),
) )
} returns flowOf(content.pets.filter { it.animal == "Cactus" }) } returns flowOf(content.pets.filter { it.animal == "Cactus" })
every { every {
inventoryRepository.getMounts( inventoryRepository.getMounts(
any(), any(),
any(), any(),
any() any(),
) )
} returns flowOf(content.mounts.filter { it.animal == "Cactus" }) } returns flowOf(content.mounts.filter { it.animal == "Cactus" })
launchFragment( launchFragment(
PetDetailRecyclerFragmentArgs.Builder("cactus", "drop", "").build().toBundle() PetDetailRecyclerFragmentArgs.Builder("cactus", "drop", "").build().toBundle(),
) )
screen { screen {
recycler { recycler {
@ -99,14 +102,14 @@ internal class PetDetailRecyclerFragmentTest :
inventoryRepository.getPets( inventoryRepository.getPets(
any(), any(),
any(), any(),
any() any(),
) )
} returns flowOf(content.pets.filter { it.animal == "Fox" }) } returns flowOf(content.pets.filter { it.animal == "Fox" })
every { every {
inventoryRepository.getMounts( inventoryRepository.getMounts(
any(), any(),
any(), any(),
any() any(),
) )
} returns flowOf(content.mounts.filter { it.animal == "Fox" }) } returns flowOf(content.mounts.filter { it.animal == "Fox" })
launchFragment(PetDetailRecyclerFragmentArgs.Builder("fox", "drop", "").build().toBundle()) launchFragment(PetDetailRecyclerFragmentArgs.Builder("fox", "drop", "").build().toBundle())

View file

@ -5,8 +5,8 @@ import android.view.View
import androidx.fragment.app.testing.launchFragmentInContainer import androidx.fragment.app.testing.launchFragmentInContainer
import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.databinding.FragmentRecyclerviewBinding import com.habitrpg.android.habitica.databinding.FragmentRecyclerviewBinding
import com.habitrpg.common.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.ui.fragments.FragmentTestCase import com.habitrpg.android.habitica.ui.fragments.FragmentTestCase
import com.habitrpg.common.habitica.helpers.MainNavigationController
import io.github.kakaocup.kakao.common.views.KView import io.github.kakaocup.kakao.common.views.KView
import io.github.kakaocup.kakao.recycler.KRecyclerItem import io.github.kakaocup.kakao.recycler.KRecyclerItem
import io.github.kakaocup.kakao.recycler.KRecyclerView import io.github.kakaocup.kakao.recycler.KRecyclerView
@ -30,7 +30,8 @@ class SectionItem(parent: Matcher<View>) : KRecyclerItem<PetItem>(parent) {
} }
class StableScreen : Screen<StableScreen>() { class StableScreen : Screen<StableScreen>() {
val recycler: KRecyclerView = KRecyclerView({ val recycler: KRecyclerView =
KRecyclerView({
withId(R.id.recyclerView) withId(R.id.recyclerView)
}, itemTypeBuilder = { }, itemTypeBuilder = {
itemType(::SectionItem) itemType(::SectionItem)
@ -48,7 +49,8 @@ internal class StableRecyclerFragmentTest : FragmentTestCase<StableRecyclerFragm
} }
override fun launchFragment(args: Bundle?) { override fun launchFragment(args: Bundle?) {
scenario = launchFragmentInContainer(args, R.style.MainAppTheme) { scenario =
launchFragmentInContainer(args, R.style.MainAppTheme) {
return@launchFragmentInContainer fragment return@launchFragmentInContainer fragment
} }
} }

View file

@ -1,143 +0,0 @@
package com.habitrpg.android.habitica.ui.fragments.purchases
/*
class GemPurchaseScreen: Screen<GemPurchaseScreen>() {
val gems4View = KView { withId(R.id.gems_4_view) }
val gems4Button = KTextView {
withId(R.id.purchase_button)
isDescendantOfA { withId(R.id.gems_4_view) }
}
val gems21View = KView { withId(R.id.gems_21_view) }
val gems21Button = KTextView {
withId(R.id.purchase_button)
isDescendantOfA { withId(R.id.gems_21_view) }
}
val gems42View = KView { withId(R.id.gems_42_view) }
val gems42Button = KTextView {
withId(R.id.purchase_button)
isDescendantOfA { withId(R.id.gems_42_view) }
}
val gems84View = KView { withId(R.id.gems_84_view) }
val gems84Button = KTextView {
withId(R.id.purchase_button)
isDescendantOfA { withId(R.id.gems_84_view) }
}
}
@LargeTest
@RunWith(AndroidJUnit4::class)
class GemsPurchaseFragmentTest :
FragmentTestCase<GemsPurchaseFragment, FragmentGemPurchaseBinding, GemPurchaseScreen>() {
private lateinit var gemSkuMock: MockKAdditionalAnswerScope<List<Sku>, List<Sku>>
private var purchaseHandler: PurchaseHandler = mockk(relaxed = true)
override val screen = GemPurchaseScreen()
private fun makeTestSKU(
product: String,
code: String,
price: Long,
title: String,
description: String
): Sku {
return Sku(
product,
code,
"$${price}",
Sku.Price(price, ""),
title,
description,
"",
Sku.Price(10L, ""),
"",
"",
"",
0
)
}
override fun makeFragment() {
gemSkuMock = coEvery { purchaseHandler.getAllGemSKUs() } returns listOf(
makeTestSKU("4Gems", PurchaseTypes.Purchase4Gems, 99, "4 Gems", "smol amount of gems"),
makeTestSKU("21Gems", PurchaseTypes.Purchase21Gems, 499, "21 Gems", "medium amount of gems"),
makeTestSKU("42Gems", PurchaseTypes.Purchase42Gems, 999, "42 Gems", "lorge amount of gems"),
makeTestSKU("84Gems", PurchaseTypes.Purchase84Gems, 1999, "84 Gems", "huge amount of gems")
)
scenario = launchFragmentInContainer(null, R.style.MainAppTheme) {
fragment = spyk()
fragment.shouldInitializeComponent = false
fragment.userRepository = userRepository
fragment.tutorialRepository = tutorialRepository
fragment.appConfigManager = appConfigManager
fragment.setPurchaseHandler(purchaseHandler)
return@launchFragmentInContainer fragment
}
}
@Test
fun displaysGemOptions() {
screen {
fragment.setupCheckout()
gems4View.hasDescendant { withText("4") }
gems4View.hasDescendant { withText("$99") }
gems21View.hasDescendant { withText("21") }
gems21View.hasDescendant { withText("$499") }
gems42View.hasDescendant { withText("42") }
gems42View.hasDescendant { withText("$999") }
gems84View.hasDescendant { withText("84") }
gems84View.hasDescendant { withText("$1999") }
}
}
@Test
fun callsCorrectPurchaseFunction() {
screen {
fragment.setupCheckout()
gems4Button.click()
verify(exactly = 1) { purchaseHandler.purchaseGems(PurchaseTypes.Purchase4Gems) }
gems21Button.click()
verify(exactly = 1) { purchaseHandler.purchaseGems(PurchaseTypes.Purchase21Gems) }
gems42Button.click()
verify(exactly = 1) { purchaseHandler.purchaseGems(PurchaseTypes.Purchase42Gems) }
gems84Button.click()
verify(exactly = 1) { purchaseHandler.purchaseGems(PurchaseTypes.Purchase84Gems) }
}
}
@Test
fun disablesButtonsWithoutData() {
gemSkuMock = coEvery { purchaseHandler.getAllGemSKUs() } returns emptyList()
screen {
fragment.setupCheckout()
gems4Button.click()
gems21Button.click()
gems42Button.click()
gems84Button.click()
verify(exactly = 0) { purchaseHandler.purchaseGems(any()) }
}
}
@Test
fun displaysSubscriptionBannerForUnsubscribed() {
screen {
subscriptionPromo.isVisible()
subscriptionPromoButton.isClickable()
}
}
@Test
fun hidesSubscriptionBannerForSubscribed() {
user.purchased = Purchases()
user.purchased?.plan = SubscriptionPlan()
user.purchased?.plan?.customerId = "plan"
userSubject.onNext(user)
screen {
subscriptionPromo.isGone()
}
}
}*/

View file

@ -1,84 +0,0 @@
package com.habitrpg.android.habitica.ui.fragments.purchases
/*
class SubscriptionScreen: Screen<SubscriptionScreen>() {
val sub1MonthView = KView { withId(R.id.subscription1month) }
val sub3MonthView = KView { withId(R.id.subscription3month) }
val sub6MonthView = KView { withId(R.id.subscription6month) }
val sub12MonthView = KView { withId(R.id.subscription12month) }
val subscribeButton = KView { withId(R.id.subscribeButton) }
val subscriptionDetails = KView { withId(R.id.subscriptionDetails) }
}
@LargeTest
@RunWith(AndroidJUnit4::class)
class SubscriptionFragmentTest :
FragmentTestCase<SubscriptionFragment, FragmentSubscriptionBinding, SubscriptionScreen>() {
private lateinit var subSkuMock: MockKAdditionalAnswerScope<Inventory.Product?, Inventory.Product?>
private var purchaseHandler: PurchaseHandler = mockk(relaxed = true)
private fun makeTestSKU(
product: String,
code: String,
price: Long,
title: String,
description: String
): Sku {
return Sku(
product,
code,
"$${price}",
Sku.Price(price, ""),
title,
description,
"",
Sku.Price(10L, ""),
"",
"",
"",
0
)
}
override fun makeFragment() {
subSkuMock = coEvery { purchaseHandler.getAllSubscriptionProducts() } answers {
val product = mockk<Inventory.Product>()
every { product.skus } returns listOf(
makeTestSKU("1Month", PurchaseTypes.Subscription1Month, 99, "1 Month", "smol amount of gems"),
makeTestSKU("3Month", PurchaseTypes.Subscription3Month, 499, "3 Months", "medium amount of gems"),
makeTestSKU("6Month", PurchaseTypes.Subscription6Month, 999, "6 Months", "lorge amount of gems"),
makeTestSKU("12Month", PurchaseTypes.Subscription12Month, 1999, "12 Months", "huge amount of gems")
)
product
}
scenario = launchFragmentInContainer(null, R.style.MainAppTheme) {
fragment = spyk()
fragment.shouldInitializeComponent = false
fragment.userRepository = userRepository
fragment.inventoryRepository = inventoryRepository
fragment.tutorialRepository = tutorialRepository
fragment.appConfigManager = appConfigManager
fragment.setPurchaseHandler(purchaseHandler)
return@launchFragmentInContainer fragment
}
}
override val screen = SubscriptionScreen()
@Test
fun showsSubscriptionOptions() {
fragment.setupCheckout()
screen {
sub1MonthView.isVisible()
sub1MonthView.hasDescendant { withText("1 Month") }
sub3MonthView.isVisible()
sub3MonthView.hasDescendant { withText("3 Months") }
sub6MonthView.isVisible()
sub6MonthView.hasDescendant { withText("6 Months") }
sub12MonthView.isVisible()
sub12MonthView.hasDescendant { withText("12 Months") }
}
}
}*/

View file

@ -28,7 +28,8 @@ open class TaskItem(val parent: Matcher<View>) : KRecyclerItem<TaskItem>(parent)
} }
class TaskListScreen : Screen<TaskListScreen>() { class TaskListScreen : Screen<TaskListScreen>() {
val recycler: KRecyclerView = KRecyclerView({ val recycler: KRecyclerView =
KRecyclerView({
withId(R.id.recyclerView) withId(R.id.recyclerView)
}, itemTypeBuilder = { }, itemTypeBuilder = {
itemType(::TaskItem) itemType(::TaskItem)
@ -36,7 +37,6 @@ class TaskListScreen : Screen<TaskListScreen>() {
} }
internal class TaskRecyclerViewFragmentTest : FragmentTestCase<TaskRecyclerViewFragment, FragmentRefreshRecyclerviewBinding, TaskListScreen>(false) { internal class TaskRecyclerViewFragmentTest : FragmentTestCase<TaskRecyclerViewFragment, FragmentRefreshRecyclerviewBinding, TaskListScreen>(false) {
lateinit var tasks: MutableCollection<Task> lateinit var tasks: MutableCollection<Task>
override fun makeFragment() { override fun makeFragment() {
@ -46,7 +46,8 @@ internal class TaskRecyclerViewFragmentTest : FragmentTestCase<TaskRecyclerViewF
} }
override fun launchFragment(args: Bundle?) { override fun launchFragment(args: Bundle?) {
scenario = launchFragmentInContainer(args, R.style.MainAppTheme) { scenario =
launchFragmentInContainer(args, R.style.MainAppTheme) {
return@launchFragmentInContainer fragment return@launchFragmentInContainer fragment
} }
} }

View file

@ -48,8 +48,10 @@ import java.lang.ref.WeakReference
import java.util.Date import java.util.Date
import javax.inject.Inject import javax.inject.Inject
class ApplicationLifecycleTracker(private val sharedPreferences: SharedPreferences): DefaultLifecycleObserver { class ApplicationLifecycleTracker(private val sharedPreferences: SharedPreferences) :
DefaultLifecycleObserver {
private var lastResumeTime = 0L private var lastResumeTime = 0L
override fun onResume(owner: LifecycleOwner) { override fun onResume(owner: LifecycleOwner) {
super.onResume(owner) super.onResume(owner)
lastResumeTime = Date().time lastResumeTime = Date().time
@ -107,9 +109,6 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife
private lateinit var lifecycleTracker: ApplicationLifecycleTracker private lateinit var lifecycleTracker: ApplicationLifecycleTracker
/**
* For better performance billing class should be used as singleton
*/
// endregion // endregion
override fun onCreate() { override fun onCreate() {
@ -148,7 +147,6 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife
checkIfNewVersion() checkIfNewVersion()
} }
private fun setupAdHandler() { private fun setupAdHandler() {
AdHandler.setup(sharedPrefs) AdHandler.setup(sharedPrefs)
} }
@ -171,7 +169,8 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife
protected open fun setupRealm() { protected open fun setupRealm() {
Realm.init(this) Realm.init(this)
val builder = RealmConfiguration.Builder() val builder =
RealmConfiguration.Builder()
.schemaVersion(1) .schemaVersion(1)
.deleteRealmIfMigrationNeeded() .deleteRealmIfMigrationNeeded()
.allowWritesOnUiThread(true) .allowWritesOnUiThread(true)
@ -212,7 +211,7 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife
override fun openOrCreateDatabase( override fun openOrCreateDatabase(
name: String, name: String,
mode: Int, mode: Int,
factory: SQLiteDatabase.CursorFactory? factory: SQLiteDatabase.CursorFactory?,
): SQLiteDatabase { ): SQLiteDatabase {
return super.openOrCreateDatabase(getDatabasePath(name).absolutePath, mode, factory) return super.openOrCreateDatabase(getDatabasePath(name).absolutePath, mode, factory)
} }
@ -221,9 +220,14 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife
name: String, name: String,
mode: Int, mode: Int,
factory: SQLiteDatabase.CursorFactory?, factory: SQLiteDatabase.CursorFactory?,
errorHandler: DatabaseErrorHandler? errorHandler: DatabaseErrorHandler?,
): SQLiteDatabase { ): SQLiteDatabase {
return super.openOrCreateDatabase(getDatabasePath(name).absolutePath, mode, factory, errorHandler) return super.openOrCreateDatabase(
getDatabasePath(name).absolutePath,
mode,
factory,
errorHandler,
)
} }
// endregion // endregion
@ -241,7 +245,8 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife
private fun setupRemoteConfig() { private fun setupRemoteConfig() {
val remoteConfig = FirebaseRemoteConfig.getInstance() val remoteConfig = FirebaseRemoteConfig.getInstance()
val configSettings = FirebaseRemoteConfigSettings.Builder() val configSettings =
FirebaseRemoteConfigSettings.Builder()
.setMinimumFetchIntervalInSeconds(if (BuildConfig.DEBUG) 0 else 3600) .setMinimumFetchIntervalInSeconds(if (BuildConfig.DEBUG) 0 else 3600)
.build() .build()
remoteConfig.setConfigSettingsAsync(configSettings) remoteConfig.setConfigSettingsAsync(configSettings)
@ -278,13 +283,19 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife
} }
} }
override fun onActivityCreated(p0: Activity, p1: Bundle?) { override fun onActivityCreated(
p0: Activity,
p1: Bundle?,
) {
} }
override fun onActivityDestroyed(p0: Activity) { override fun onActivityDestroyed(p0: Activity) {
} }
override fun onActivitySaveInstanceState(p0: Activity, p1: Bundle) { override fun onActivitySaveInstanceState(
p0: Activity,
p1: Bundle,
) {
} }
override fun onActivityStopped(p0: Activity) { override fun onActivityStopped(p0: Activity) {
@ -319,7 +330,10 @@ abstract class HabiticaBaseApplication : Application(), Application.ActivityLife
} }
} }
private fun startActivity(activityClass: Class<*>, context: Context) { private fun startActivity(
activityClass: Class<*>,
context: Context,
) {
val intent = Intent(context, activityClass) val intent = Intent(context, activityClass)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent) context.startActivity(intent)

View file

@ -6,9 +6,6 @@ import com.google.gson.reflect.TypeToken;
import com.habitrpg.android.habitica.models.Achievement; import com.habitrpg.android.habitica.models.Achievement;
import com.habitrpg.android.habitica.models.ContentResult; import com.habitrpg.android.habitica.models.ContentResult;
import com.habitrpg.android.habitica.models.FAQArticle; import com.habitrpg.android.habitica.models.FAQArticle;
import com.habitrpg.android.habitica.models.tasks.GroupAssignedDetails;
import com.habitrpg.android.habitica.utils.AssignedDetailsDeserializer;
import com.habitrpg.common.habitica.models.Notification;
import com.habitrpg.android.habitica.models.Skill; import com.habitrpg.android.habitica.models.Skill;
import com.habitrpg.android.habitica.models.Tag; import com.habitrpg.android.habitica.models.Tag;
import com.habitrpg.android.habitica.models.TutorialStep; import com.habitrpg.android.habitica.models.TutorialStep;
@ -19,11 +16,11 @@ import com.habitrpg.android.habitica.models.inventory.Quest;
import com.habitrpg.android.habitica.models.inventory.QuestCollect; import com.habitrpg.android.habitica.models.inventory.QuestCollect;
import com.habitrpg.android.habitica.models.inventory.QuestDropItem; import com.habitrpg.android.habitica.models.inventory.QuestDropItem;
import com.habitrpg.android.habitica.models.members.Member; import com.habitrpg.android.habitica.models.members.Member;
import com.habitrpg.shared.habitica.models.responses.FeedResponse;
import com.habitrpg.android.habitica.models.social.Challenge; import com.habitrpg.android.habitica.models.social.Challenge;
import com.habitrpg.android.habitica.models.social.ChatMessage; import com.habitrpg.android.habitica.models.social.ChatMessage;
import com.habitrpg.android.habitica.models.social.FindUsernameResult; import com.habitrpg.android.habitica.models.social.FindUsernameResult;
import com.habitrpg.android.habitica.models.social.Group; import com.habitrpg.android.habitica.models.social.Group;
import com.habitrpg.android.habitica.models.tasks.GroupAssignedDetails;
import com.habitrpg.android.habitica.models.tasks.Task; import com.habitrpg.android.habitica.models.tasks.Task;
import com.habitrpg.android.habitica.models.tasks.TaskList; import com.habitrpg.android.habitica.models.tasks.TaskList;
import com.habitrpg.android.habitica.models.user.OwnedItem; import com.habitrpg.android.habitica.models.user.OwnedItem;
@ -33,6 +30,7 @@ import com.habitrpg.android.habitica.models.user.Purchases;
import com.habitrpg.android.habitica.models.user.User; import com.habitrpg.android.habitica.models.user.User;
import com.habitrpg.android.habitica.models.user.auth.SocialAuthentication; import com.habitrpg.android.habitica.models.user.auth.SocialAuthentication;
import com.habitrpg.android.habitica.utils.AchievementListDeserializer; import com.habitrpg.android.habitica.utils.AchievementListDeserializer;
import com.habitrpg.android.habitica.utils.AssignedDetailsDeserializer;
import com.habitrpg.android.habitica.utils.BooleanAsIntAdapter; import com.habitrpg.android.habitica.utils.BooleanAsIntAdapter;
import com.habitrpg.android.habitica.utils.ChallengeDeserializer; import com.habitrpg.android.habitica.utils.ChallengeDeserializer;
import com.habitrpg.android.habitica.utils.ChallengeListDeserializer; import com.habitrpg.android.habitica.utils.ChallengeListDeserializer;
@ -62,6 +60,8 @@ import com.habitrpg.android.habitica.utils.TaskTagDeserializer;
import com.habitrpg.android.habitica.utils.TutorialStepListDeserializer; import com.habitrpg.android.habitica.utils.TutorialStepListDeserializer;
import com.habitrpg.android.habitica.utils.UserDeserializer; import com.habitrpg.android.habitica.utils.UserDeserializer;
import com.habitrpg.android.habitica.utils.WorldStateSerialization; import com.habitrpg.android.habitica.utils.WorldStateSerialization;
import com.habitrpg.common.habitica.models.Notification;
import com.habitrpg.shared.habitica.models.responses.FeedResponse;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.Date; import java.util.Date;
@ -145,6 +145,7 @@ public class GSonFactoryCreator {
.setLenient() .setLenient()
.create(); .create();
} }
public static GsonConverterFactory create() { public static GsonConverterFactory create() {
return GsonConverterFactory.create(createGson()); return GsonConverterFactory.create(createGson());
} }

View file

@ -3,11 +3,11 @@ package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.BaseObject import com.habitrpg.android.habitica.models.BaseObject
interface BaseRepository { interface BaseRepository {
val isClosed: Boolean val isClosed: Boolean
fun close() fun close()
fun <T : BaseObject> getUnmanagedCopy(obj: T): T fun <T : BaseObject> getUnmanagedCopy(obj: T): T
fun <T : BaseObject> getUnmanagedCopy(list: List<T>): List<T> fun <T : BaseObject> getUnmanagedCopy(list: List<T>): List<T>
} }

View file

@ -7,15 +7,25 @@ import com.habitrpg.android.habitica.models.tasks.TaskList
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface ChallengeRepository : BaseRepository { interface ChallengeRepository : BaseRepository {
suspend fun retrieveChallenges(
page: Int = 0,
memberOnly: Boolean,
): List<Challenge>?
suspend fun retrieveChallenges(page: Int = 0, memberOnly: Boolean): List<Challenge>?
fun getChallenges(): Flow<List<Challenge>> fun getChallenges(): Flow<List<Challenge>>
fun getChallenge(challengeId: String): Flow<Challenge> fun getChallenge(challengeId: String): Flow<Challenge>
fun getChallengeTasks(challengeId: String): Flow<List<Task>> fun getChallengeTasks(challengeId: String): Flow<List<Task>>
suspend fun retrieveChallenge(challengeID: String): Challenge? suspend fun retrieveChallenge(challengeID: String): Challenge?
suspend fun retrieveChallengeTasks(challengeID: String): TaskList? suspend fun retrieveChallengeTasks(challengeID: String): TaskList?
suspend fun createChallenge(challenge: Challenge, taskList: List<Task>): Challenge?
suspend fun createChallenge(
challenge: Challenge,
taskList: List<Task>,
): Challenge?
/** /**
* *
@ -31,18 +41,28 @@ interface ChallengeRepository : BaseRepository {
fullTaskList: List<Task>, fullTaskList: List<Task>,
addedTaskList: List<Task>, addedTaskList: List<Task>,
updatedTaskList: List<Task>, updatedTaskList: List<Task>,
removedTaskList: List<String> removedTaskList: List<String>,
): Challenge? ): Challenge?
suspend fun deleteChallenge(challengeId: String): Void? suspend fun deleteChallenge(challengeId: String): Void?
fun getUserChallenges(userId: String? = null): Flow<List<Challenge>> fun getUserChallenges(userId: String? = null): Flow<List<Challenge>>
suspend fun leaveChallenge(challenge: Challenge, keepTasks: String): Void? suspend fun leaveChallenge(
challenge: Challenge,
keepTasks: String,
): Void?
suspend fun joinChallenge(challenge: Challenge): Challenge? suspend fun joinChallenge(challenge: Challenge): Challenge?
fun getChallengepMembership(id: String): Flow<ChallengeMembership> fun getChallengepMembership(id: String): Flow<ChallengeMembership>
fun getChallengeMemberships(): Flow<List<ChallengeMembership>> fun getChallengeMemberships(): Flow<List<ChallengeMembership>>
fun isChallengeMember(challengeID: String): Flow<Boolean> fun isChallengeMember(challengeID: String): Flow<Boolean>
suspend fun reportChallenge(challengeid: String, updateData: Map<String, String>): Void?
suspend fun reportChallenge(
challengeid: String,
updateData: Map<String, String>,
): Void?
} }

View file

@ -8,5 +8,6 @@ interface ContentRepository : BaseRepository {
suspend fun retrieveContent(forced: Boolean = false): ContentResult? suspend fun retrieveContent(forced: Boolean = false): ContentResult?
suspend fun retrieveWorldState(forced: Boolean = false): WorldState? suspend fun retrieveWorldState(forced: Boolean = false): WorldState?
fun getWorldState(): Flow<WorldState> fun getWorldState(): Flow<WorldState>
} }

View file

@ -4,5 +4,9 @@ import com.habitrpg.android.habitica.models.inventory.Customization
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface CustomizationRepository : BaseRepository { interface CustomizationRepository : BaseRepository {
fun getCustomizations(type: String, category: String?, onlyAvailable: Boolean): Flow<List<Customization>> fun getCustomizations(
type: String,
category: String?,
onlyAvailable: Boolean,
): Flow<List<Customization>>
} }

View file

@ -5,5 +5,6 @@ import kotlinx.coroutines.flow.Flow
interface FAQRepository : BaseRepository { interface FAQRepository : BaseRepository {
fun getArticles(): Flow<List<FAQArticle>> fun getArticles(): Flow<List<FAQArticle>>
fun getArticle(position: Int): Flow<FAQArticle> fun getArticle(position: Int): Flow<FAQArticle>
} }

View file

@ -21,10 +21,10 @@ import com.habitrpg.shared.habitica.models.responses.FeedResponse
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface InventoryRepository : BaseRepository { interface InventoryRepository : BaseRepository {
fun getArmoireRemainingCount(): Flow<Int> fun getArmoireRemainingCount(): Flow<Int>
fun getInAppRewards(): Flow<List<ShopItem>> fun getInAppRewards(): Flow<List<ShopItem>>
fun getInAppReward(key: String): Flow<ShopItem> fun getInAppReward(key: String): Flow<ShopItem>
fun getOwnedEquipment(): Flow<List<Equipment>> fun getOwnedEquipment(): Flow<List<Equipment>>
@ -36,16 +36,27 @@ interface InventoryRepository : BaseRepository {
fun getPets(): Flow<List<Pet>> fun getPets(): Flow<List<Pet>>
fun getOwnedPets(): Flow<List<OwnedPet>> fun getOwnedPets(): Flow<List<OwnedPet>>
fun getQuestContent(key: String): Flow<QuestContent?> fun getQuestContent(key: String): Flow<QuestContent?>
fun getQuestContent(keys: List<String>): Flow<List<QuestContent>> fun getQuestContent(keys: List<String>): Flow<List<QuestContent>>
fun getEquipment(searchedKeys: List<String>): Flow<List<Equipment>> fun getEquipment(searchedKeys: List<String>): Flow<List<Equipment>>
suspend fun retrieveInAppRewards(): List<ShopItem>? suspend fun retrieveInAppRewards(): List<ShopItem>?
fun getOwnedEquipment(type: String): Flow<List<Equipment>> fun getOwnedEquipment(type: String): Flow<List<Equipment>>
fun getEquipmentType(type: String, set: String): Flow<List<Equipment>>
fun getOwnedItems(itemType: String, includeZero: Boolean = false): Flow<List<OwnedItem>> fun getEquipmentType(
type: String,
set: String,
): Flow<List<Equipment>>
fun getOwnedItems(
itemType: String,
includeZero: Boolean = false,
): Flow<List<OwnedItem>>
fun getOwnedItems(includeZero: Boolean = false): Flow<Map<String, OwnedItem>> fun getOwnedItems(includeZero: Boolean = false): Flow<Map<String, OwnedItem>>
fun getEquipment(key: String): Flow<Equipment> fun getEquipment(key: String): Flow<Equipment>
@ -53,43 +64,100 @@ interface InventoryRepository : BaseRepository {
suspend fun openMysteryItem(user: User?): Equipment? suspend fun openMysteryItem(user: User?): Equipment?
fun saveEquipment(equipment: Equipment) fun saveEquipment(equipment: Equipment)
fun getMounts(type: String?, group: String?, color: String?): Flow<List<Mount>>
fun getPets(type: String?, group: String?, color: String?): Flow<List<Pet>> fun getMounts(
type: String?,
group: String?,
color: String?,
): Flow<List<Mount>>
fun getPets(
type: String?,
group: String?,
color: String?,
): Flow<List<Pet>>
fun updateOwnedEquipment(user: User) fun updateOwnedEquipment(user: User)
suspend fun changeOwnedCount(type: String, key: String, amountToAdd: Int) suspend fun changeOwnedCount(
type: String,
key: String,
amountToAdd: Int,
)
suspend fun sellItem(
type: String,
key: String,
): User?
suspend fun sellItem(type: String, key: String): User?
suspend fun sellItem(item: OwnedItem): User? suspend fun sellItem(item: OwnedItem): User?
suspend fun equipGear(equipment: String, asCostume: Boolean): Items? suspend fun equipGear(
suspend fun equip(type: String, key: String): Items? equipment: String,
asCostume: Boolean,
): Items?
suspend fun feedPet(pet: Pet, food: Food): FeedResponse? suspend fun equip(
type: String,
key: String,
): Items?
suspend fun hatchPet(egg: Egg, hatchingPotion: HatchingPotion, successFunction: () -> Unit): Items? suspend fun feedPet(
pet: Pet,
food: Food,
): FeedResponse?
suspend fun hatchPet(
egg: Egg,
hatchingPotion: HatchingPotion,
successFunction: () -> Unit,
): Items?
suspend fun inviteToQuest(quest: QuestContent): Quest? suspend fun inviteToQuest(quest: QuestContent): Quest?
suspend fun buyItem(user: User?, id: String, value: Double, purchaseQuantity: Int): BuyResponse? suspend fun buyItem(
user: User?,
id: String,
value: Double,
purchaseQuantity: Int,
): BuyResponse?
suspend fun retrieveShopInventory(identifier: String): Shop? suspend fun retrieveShopInventory(identifier: String): Shop?
suspend fun retrieveMarketGear(): Shop? suspend fun retrieveMarketGear(): Shop?
suspend fun purchaseMysterySet(categoryIdentifier: String): Void? suspend fun purchaseMysterySet(categoryIdentifier: String): Void?
suspend fun purchaseHourglassItem(purchaseType: String, key: String): Void? suspend fun purchaseHourglassItem(
purchaseType: String,
key: String,
): Void?
suspend fun purchaseQuest(key: String): Void? suspend fun purchaseQuest(key: String): Void?
suspend fun purchaseSpecialSpell(key: String): Void? suspend fun purchaseSpecialSpell(key: String): Void?
suspend fun purchaseItem(purchaseType: String, key: String, purchaseQuantity: Int): Void? suspend fun purchaseItem(
purchaseType: String,
key: String,
purchaseQuantity: Int,
): Void?
suspend fun togglePinnedItem(item: ShopItem): List<ShopItem>? suspend fun togglePinnedItem(item: ShopItem): List<ShopItem>?
fun getItems(itemClass: Class<out Item>, keys: Array<String>): Flow<List<Item>>
fun getItems(
itemClass: Class<out Item>,
keys: Array<String>,
): Flow<List<Item>>
fun getItems(itemClass: Class<out Item>): Flow<List<Item>> fun getItems(itemClass: Class<out Item>): Flow<List<Item>>
fun getLatestMysteryItem(): Flow<Equipment> fun getLatestMysteryItem(): Flow<Equipment>
fun getItem(type: String, key: String): Flow<Item>
fun getItem(
type: String,
key: String,
): Flow<Item>
fun getAvailableLimitedItems(): Flow<List<Item>> fun getAvailableLimitedItems(): Flow<List<Item>>
} }

View file

@ -4,9 +4,16 @@ import com.habitrpg.android.habitica.models.SetupCustomization
import com.habitrpg.android.habitica.models.user.User import com.habitrpg.android.habitica.models.user.User
interface SetupCustomizationRepository { interface SetupCustomizationRepository {
fun getCustomizations(
type: String,
user: User,
): List<SetupCustomization>
fun getCustomizations(type: String, user: User): List<SetupCustomization> fun getCustomizations(
fun getCustomizations(type: String, subtype: String?, user: User): List<SetupCustomization> type: String,
subtype: String?,
user: User,
): List<SetupCustomization>
companion object { companion object {
const val CATEGORY_BODY = "body" const val CATEGORY_BODY = "body"

View file

@ -16,7 +16,9 @@ import kotlinx.coroutines.flow.Flow
interface SocialRepository : BaseRepository { interface SocialRepository : BaseRepository {
fun getUserGroups(type: String?): Flow<List<Group>> fun getUserGroups(type: String?): Flow<List<Group>>
suspend fun retrieveGroupChat(groupId: String): List<ChatMessage>? suspend fun retrieveGroupChat(groupId: String): List<ChatMessage>?
fun getGroupChat(groupId: String): Flow<List<ChatMessage>> fun getGroupChat(groupId: String): Flow<List<ChatMessage>>
suspend fun markMessagesSeen(seenGroupId: String) suspend fun markMessagesSeen(seenGroupId: String)
@ -24,10 +26,13 @@ interface SocialRepository : BaseRepository {
suspend fun flagMessage( suspend fun flagMessage(
chatMessageID: String, chatMessageID: String,
additionalInfo: String, additionalInfo: String,
groupID: String? = null groupID: String? = null,
): Void? ): Void?
suspend fun reportMember(memberID: String, data: Map<String, String>): Void? suspend fun reportMember(
memberID: String,
data: Map<String, String>,
): Void?
suspend fun likeMessage(chatMessage: ChatMessage): ChatMessage? suspend fun likeMessage(chatMessage: ChatMessage): ChatMessage?
@ -35,15 +40,22 @@ interface SocialRepository : BaseRepository {
suspend fun postGroupChat( suspend fun postGroupChat(
groupId: String, groupId: String,
messageObject: HashMap<String, String> messageObject: HashMap<String, String>,
): PostChatMessageResult? ): PostChatMessageResult?
suspend fun postGroupChat(groupId: String, message: String): PostChatMessageResult? suspend fun postGroupChat(
groupId: String,
message: String,
): PostChatMessageResult?
suspend fun retrieveGroup(id: String): Group? suspend fun retrieveGroup(id: String): Group?
fun getGroup(id: String?): Flow<Group?> fun getGroup(id: String?): Flow<Group?>
suspend fun leaveGroup(id: String?, keepChallenges: Boolean): Group? suspend fun leaveGroup(
id: String?,
keepChallenges: Boolean,
): Group?
suspend fun joinGroup(id: String?): Group? suspend fun joinGroup(id: String?): Group?
@ -53,7 +65,7 @@ interface SocialRepository : BaseRepository {
leader: String?, leader: String?,
type: String?, type: String?,
privacy: String?, privacy: String?,
leaderCreateChallenge: Boolean? leaderCreateChallenge: Boolean?,
): Group? ): Group?
suspend fun updateGroup( suspend fun updateGroup(
@ -61,43 +73,81 @@ interface SocialRepository : BaseRepository {
name: String?, name: String?,
description: String?, description: String?,
leader: String?, leader: String?,
leaderCreateChallenge: Boolean? leaderCreateChallenge: Boolean?,
): Group? ): Group?
fun getInboxMessages(replyToUserID: String?): Flow<RealmResults<ChatMessage>> fun getInboxMessages(replyToUserID: String?): Flow<RealmResults<ChatMessage>>
suspend fun retrieveInboxMessages(uuid: String, page: Int): List<ChatMessage>?
suspend fun retrieveInboxConversations(): List<InboxConversation>? suspend fun retrieveInboxMessages(
fun getInboxConversations(): Flow<RealmResults<InboxConversation>> uuid: String,
suspend fun postPrivateMessage( page: Int,
recipientId: String,
messageObject: HashMap<String, String>
): List<ChatMessage>? ): List<ChatMessage>?
suspend fun postPrivateMessage(recipientId: String, message: String): List<ChatMessage>? suspend fun retrieveInboxConversations(): List<InboxConversation>?
fun getInboxConversations(): Flow<RealmResults<InboxConversation>>
suspend fun postPrivateMessage(
recipientId: String,
messageObject: HashMap<String, String>,
): List<ChatMessage>?
suspend fun postPrivateMessage(
recipientId: String,
message: String,
): List<ChatMessage>?
suspend fun getPartyMembers(id: String): Flow<List<Member>> suspend fun getPartyMembers(id: String): Flow<List<Member>>
suspend fun getGroupMembers(id: String): Flow<List<Member>> suspend fun getGroupMembers(id: String): Flow<List<Member>>
suspend fun retrievePartyMembers(id: String, includeAllPublicFields: Boolean): List<Member>?
suspend fun inviteToGroup(id: String, inviteData: Map<String, Any>): List<InviteResponse>? suspend fun retrievePartyMembers(
id: String,
includeAllPublicFields: Boolean,
): List<Member>?
suspend fun retrieveMember(userId: String?, fromHall: Boolean = false): Member? suspend fun inviteToGroup(
id: String,
inviteData: Map<String, Any>,
): List<InviteResponse>?
suspend fun retrieveMember(
userId: String?,
fromHall: Boolean = false,
): Member?
suspend fun findUsernames( suspend fun findUsernames(
username: String, username: String,
context: String? = null, context: String? = null,
id: String? = null id: String? = null,
): List<FindUsernameResult>? ): List<FindUsernameResult>?
suspend fun markPrivateMessagesRead(user: User?) suspend fun markPrivateMessagesRead(user: User?)
fun markSomePrivateMessagesAsRead(user: User?, messages: List<ChatMessage>) fun markSomePrivateMessagesAsRead(
user: User?,
messages: List<ChatMessage>,
)
suspend fun transferGroupOwnership(groupID: String, userID: String): Group? suspend fun transferGroupOwnership(
suspend fun removeMemberFromGroup(groupID: String, userID: String): List<Member>? groupID: String,
userID: String,
): Group?
suspend fun acceptQuest(user: User?, partyId: String = "party"): Void? suspend fun removeMemberFromGroup(
suspend fun rejectQuest(user: User?, partyId: String = "party"): Void? groupID: String,
userID: String,
): List<Member>?
suspend fun acceptQuest(
user: User?,
partyId: String = "party",
): Void?
suspend fun rejectQuest(
user: User?,
partyId: String = "party",
): Void?
suspend fun leaveQuest(partyId: String): Void? suspend fun leaveQuest(partyId: String): Void?
@ -111,13 +161,28 @@ interface SocialRepository : BaseRepository {
suspend fun getMemberAchievements(userId: String?): List<Achievement>? suspend fun getMemberAchievements(userId: String?): List<Achievement>?
suspend fun transferGems(giftedID: String, amount: Int): Void? suspend fun transferGems(
giftedID: String,
amount: Int,
): Void?
fun getGroupMembership(id: String): Flow<GroupMembership?> fun getGroupMembership(id: String): Flow<GroupMembership?>
fun getGroupMemberships(): Flow<List<GroupMembership>> fun getGroupMemberships(): Flow<List<GroupMembership>>
suspend fun blockMember(userID: String): List<String>? suspend fun blockMember(userID: String): List<String>?
fun getMember(userID: String?): Flow<Member?> fun getMember(userID: String?): Flow<Member?>
suspend fun updateMember(memberID: String, data: Map<String, Map<String, Boolean>>): Member?
suspend fun updateMember(
memberID: String,
data: Map<String, Map<String, Boolean>>,
): Member?
suspend fun retrievePartySeekingUsers(page: Int = 0): List<Member>? suspend fun retrievePartySeekingUsers(page: Int = 0): List<Member>?
suspend fun retrievegroupInvites(id: String, includeAllPublicFields: Boolean): List<Member>?
suspend fun retrievegroupInvites(
id: String,
includeAllPublicFields: Boolean,
): List<Member>?
} }

View file

@ -4,15 +4,19 @@ import com.habitrpg.android.habitica.models.Tag
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface TagRepository : BaseRepository { interface TagRepository : BaseRepository {
fun getTags(): Flow<List<Tag>> fun getTags(): Flow<List<Tag>>
fun getTags(userId: String): Flow<List<Tag>> fun getTags(userId: String): Flow<List<Tag>>
suspend fun createTag(tag: Tag): Tag? suspend fun createTag(tag: Tag): Tag?
suspend fun updateTag(tag: Tag): Tag? suspend fun updateTag(tag: Tag): Tag?
suspend fun deleteTag(id: String): Void? suspend fun deleteTag(id: String): Void?
suspend fun createTags(tags: Collection<Tag>): List<Tag> suspend fun createTags(tags: Collection<Tag>): List<Tag>
suspend fun updateTags(tags: Collection<Tag>): List<Tag> suspend fun updateTags(tags: Collection<Tag>): List<Tag>
suspend fun deleteTags(tagIds: Collection<String>): List<Void> suspend fun deleteTags(tagIds: Collection<String>): List<Void>
} }

View file

@ -12,59 +12,124 @@ import kotlinx.coroutines.flow.Flow
import java.util.Date import java.util.Date
interface TaskRepository : BaseRepository { interface TaskRepository : BaseRepository {
fun getTasks(taskType: TaskType, userID: String? = null, includedGroupIDs: Array<String>): Flow<List<Task>> fun getTasks(
fun saveTasks(userId: String, order: TasksOrder, tasks: TaskList) taskType: TaskType,
userID: String? = null,
includedGroupIDs: Array<String>,
): Flow<List<Task>>
suspend fun retrieveTasks(userId: String, tasksOrder: TasksOrder): TaskList? fun saveTasks(
suspend fun retrieveTasks(userId: String, tasksOrder: TasksOrder, dueDate: Date): TaskList? userId: String,
order: TasksOrder,
tasks: TaskList,
)
suspend fun retrieveTasks(
userId: String,
tasksOrder: TasksOrder,
): TaskList?
suspend fun retrieveTasks(
userId: String,
tasksOrder: TasksOrder,
dueDate: Date,
): TaskList?
suspend fun taskChecked( suspend fun taskChecked(
user: User?, user: User?,
task: Task, task: Task,
up: Boolean, up: Boolean,
force: Boolean, force: Boolean,
notifyFunc: ((TaskScoringResult) -> Unit)? notifyFunc: ((TaskScoringResult) -> Unit)?,
): TaskScoringResult? ): TaskScoringResult?
suspend fun taskChecked( suspend fun taskChecked(
user: User?, user: User?,
taskId: String, taskId: String,
up: Boolean, up: Boolean,
force: Boolean, force: Boolean,
notifyFunc: ((TaskScoringResult) -> Unit)? notifyFunc: ((TaskScoringResult) -> Unit)?,
): TaskScoringResult? ): TaskScoringResult?
suspend fun scoreChecklistItem(taskId: String, itemId: String): Task?
suspend fun scoreChecklistItem(
taskId: String,
itemId: String,
): Task?
fun getTask(taskId: String): Flow<Task> fun getTask(taskId: String): Flow<Task>
fun getTaskCopy(taskId: String): Flow<Task> fun getTaskCopy(taskId: String): Flow<Task>
suspend fun createTask(task: Task, force: Boolean = false): Task?
suspend fun updateTask(task: Task, force: Boolean = false): Task? suspend fun createTask(
task: Task,
force: Boolean = false,
): Task?
suspend fun updateTask(
task: Task,
force: Boolean = false,
): Task?
suspend fun deleteTask(taskId: String): Void? suspend fun deleteTask(taskId: String): Void?
fun saveTask(task: Task) fun saveTask(task: Task)
suspend fun createTasks(newTasks: List<Task>): List<Task>? suspend fun createTasks(newTasks: List<Task>): List<Task>?
fun markTaskCompleted(taskId: String, isCompleted: Boolean) fun markTaskCompleted(
taskId: String,
isCompleted: Boolean,
)
fun <T : BaseMainObject> modify(obj: T, transaction: (T) -> Unit) fun <T : BaseMainObject> modify(
obj: T,
transaction: (T) -> Unit,
)
fun swapTaskPosition(firstPosition: Int, secondPosition: Int) fun swapTaskPosition(
firstPosition: Int,
secondPosition: Int,
)
suspend fun updateTaskPosition(taskType: TaskType, taskID: String, newPosition: Int): List<String>? suspend fun updateTaskPosition(
taskType: TaskType,
taskID: String,
newPosition: Int,
): List<String>?
fun getUnmanagedTask(taskid: String): Flow<Task> fun getUnmanagedTask(taskid: String): Flow<Task>
fun updateTaskInBackground(task: Task, assignChanges: Map<String, MutableList<String>>) fun updateTaskInBackground(
task: Task,
assignChanges: Map<String, MutableList<String>>,
)
fun createTaskInBackground(task: Task, assignChanges: Map<String, MutableList<String>>) fun createTaskInBackground(
task: Task,
assignChanges: Map<String, MutableList<String>>,
)
fun getTaskCopies(): Flow<List<Task>> fun getTaskCopies(): Flow<List<Task>>
fun getTaskCopies(tasks: List<Task>): List<Task> fun getTaskCopies(tasks: List<Task>): List<Task>
suspend fun retrieveDailiesFromDate(date: Date): TaskList? suspend fun retrieveDailiesFromDate(date: Date): TaskList?
suspend fun retrieveCompletedTodos(userId: String? = null): TaskList? suspend fun retrieveCompletedTodos(userId: String? = null): TaskList?
suspend fun syncErroredTasks(): List<Task>? suspend fun syncErroredTasks(): List<Task>?
suspend fun unlinkAllTasks(challengeID: String?, keepOption: String): Void?
suspend fun unlinkAllTasks(
challengeID: String?,
keepOption: String,
): Void?
fun getTasksForChallenge(challengeID: String?): Flow<List<Task>> fun getTasksForChallenge(challengeID: String?): Flow<List<Task>>
suspend fun bulkScoreTasks(data: List<Map<String, String>>): BulkTaskScoringData? suspend fun bulkScoreTasks(data: List<Map<String, String>>): BulkTaskScoringData?
suspend fun markTaskNeedsWork(task: Task, userID: String)
suspend fun markTaskNeedsWork(
task: Task,
userID: String,
)
} }

View file

@ -4,7 +4,7 @@ import com.habitrpg.android.habitica.models.TutorialStep
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface TutorialRepository : BaseRepository { interface TutorialRepository : BaseRepository {
fun getTutorialStep(key: String): Flow<TutorialStep> fun getTutorialStep(key: String): Flow<TutorialStep>
fun getTutorialSteps(keys: List<String>): Flow<out List<TutorialStep>> fun getTutorialSteps(keys: List<String>): Flow<out List<TutorialStep>>
} }

View file

@ -20,12 +20,21 @@ import kotlinx.coroutines.flow.Flow
interface UserRepository : BaseRepository { interface UserRepository : BaseRepository {
fun getUser(): Flow<User?> fun getUser(): Flow<User?>
fun getUser(userID: String): Flow<User?> fun getUser(userID: String): Flow<User?>
suspend fun updateUser(updateData: Map<String, Any?>): User? suspend fun updateUser(updateData: Map<String, Any?>): User?
suspend fun updateUser(key: String, value: Any?): User?
suspend fun retrieveUser(withTasks: Boolean = false, forced: Boolean = false, overrideExisting: Boolean = false): User? suspend fun updateUser(
key: String,
value: Any?,
): User?
suspend fun retrieveUser(
withTasks: Boolean = false,
forced: Boolean = false,
overrideExisting: Boolean = false,
): User?
suspend fun revive(): Equipment? suspend fun revive(): Equipment?
@ -37,23 +46,40 @@ interface UserRepository : BaseRepository {
fun getSpecialItems(user: User): Flow<List<Skill>> fun getSpecialItems(user: User): Flow<List<Skill>>
suspend fun useSkill(key: String, target: String?, taskId: String): SkillResponse? suspend fun useSkill(
suspend fun useSkill(key: String, target: String?): SkillResponse? key: String,
target: String?,
taskId: String,
): SkillResponse?
suspend fun useSkill(
key: String,
target: String?,
): SkillResponse?
suspend fun disableClasses(): User? suspend fun disableClasses(): User?
suspend fun changeClass(selectedClass: String? = null): User? suspend fun changeClass(selectedClass: String? = null): User?
suspend fun unlockPath(path: String, price: Int): UnlockResponse? suspend fun unlockPath(
path: String,
price: Int,
): UnlockResponse?
suspend fun unlockPath(customization: Customization): UnlockResponse? suspend fun unlockPath(customization: Customization): UnlockResponse?
suspend fun runCron(tasks: MutableList<Task>) suspend fun runCron(tasks: MutableList<Task>)
suspend fun runCron() suspend fun runCron()
suspend fun getNews(): List<Any>? suspend fun getNews(): List<Any>?
suspend fun getNewsNotification(): Notification? suspend fun getNewsNotification(): Notification?
suspend fun readNotification(id: String): List<Any>? suspend fun readNotification(id: String): List<Any>?
suspend fun readNotifications(notificationIds: Map<String, List<String>>): List<Any>? suspend fun readNotifications(notificationIds: Map<String, List<String>>): List<Any>?
suspend fun seeNotifications(notificationIds: Map<String, List<String>>): List<Any>? suspend fun seeNotifications(notificationIds: Map<String, List<String>>): List<Any>?
suspend fun changeCustomDayStart(dayStartTime: Int): User? suspend fun changeCustomDayStart(dayStartTime: Int): User?
@ -61,29 +87,61 @@ interface UserRepository : BaseRepository {
suspend fun updateLanguage(languageCode: String): User? suspend fun updateLanguage(languageCode: String): User?
suspend fun resetAccount(password: String): User? suspend fun resetAccount(password: String): User?
suspend fun deleteAccount(password: String): Void? suspend fun deleteAccount(password: String): Void?
suspend fun sendPasswordResetEmail(email: String): Void? suspend fun sendPasswordResetEmail(email: String): Void?
suspend fun updateLoginName(newLoginName: String, password: String? = null): User? suspend fun updateLoginName(
suspend fun updateEmail(newEmail: String, password: String): Void? newLoginName: String,
suspend fun updatePassword(oldPassword: String, newPassword: String, newPasswordConfirmation: String): Void? password: String? = null,
): User?
suspend fun updateEmail(
newEmail: String,
password: String,
): Void?
suspend fun updatePassword(
oldPassword: String,
newPassword: String,
newPasswordConfirmation: String,
): Void?
suspend fun verifyUsername(username: String): VerifyUsernameResponse? suspend fun verifyUsername(username: String): VerifyUsernameResponse?
suspend fun allocatePoint(stat: Attribute): Stats? suspend fun allocatePoint(stat: Attribute): Stats?
suspend fun bulkAllocatePoints(strength: Int, intelligence: Int, constitution: Int, perception: Int): Stats?
suspend fun useCustomization(type: String, category: String?, identifier: String): User? suspend fun bulkAllocatePoints(
strength: Int,
intelligence: Int,
constitution: Int,
perception: Int,
): Stats?
suspend fun useCustomization(
type: String,
category: String?,
identifier: String,
): User?
suspend fun retrieveAchievements(): List<Achievement>? suspend fun retrieveAchievements(): List<Achievement>?
fun getAchievements(): Flow<List<Achievement>> fun getAchievements(): Flow<List<Achievement>>
fun getQuestAchievements(): Flow<List<QuestAchievement>> fun getQuestAchievements(): Flow<List<QuestAchievement>>
fun getUserQuestStatus(): Flow<UserQuestStatus> fun getUserQuestStatus(): Flow<UserQuestStatus>
suspend fun reroll(): User? suspend fun reroll(): User?
suspend fun retrieveTeamPlans(): List<TeamPlan>? suspend fun retrieveTeamPlans(): List<TeamPlan>?
fun getTeamPlans(): Flow<List<TeamPlan>> fun getTeamPlans(): Flow<List<TeamPlan>>
suspend fun retrieveTeamPlan(teamID: String): Group? suspend fun retrieveTeamPlan(teamID: String): Group?
fun getTeamPlan(teamID: String): Flow<Group?> fun getTeamPlan(teamID: String): Flow<Group?>
suspend fun syncUserStats(): User? suspend fun syncUserStats(): User?
} }

View file

@ -73,9 +73,8 @@ class ApiClientImpl(
private val converter: Converter.Factory, private val converter: Converter.Factory,
override val hostConfig: HostConfig, override val hostConfig: HostConfig,
private val notificationsManager: NotificationsManager, private val notificationsManager: NotificationsManager,
private val context: Context private val context: Context,
) : ApiClient { ) : ApiClient {
private lateinit var retrofitAdapter: Retrofit private lateinit var retrofitAdapter: Retrofit
// I think we don't need the ApiClientImpl anymore we could just use ApiService // I think we don't need the ApiClientImpl anymore we could just use ApiService
@ -114,26 +113,30 @@ class ApiClientImpl(
val calendar = GregorianCalendar() val calendar = GregorianCalendar()
val timeZone = calendar.timeZone val timeZone = calendar.timeZone
val timezoneOffset = -TimeUnit.MINUTES.convert( val timezoneOffset =
-TimeUnit.MINUTES.convert(
timeZone.getOffset(calendar.timeInMillis).toLong(), timeZone.getOffset(calendar.timeInMillis).toLong(),
TimeUnit.MILLISECONDS TimeUnit.MILLISECONDS,
) )
val cacheSize: Long = 10 * 1024 * 1024 // 10 MB val cacheSize: Long = 10 * 1024 * 1024 // 10 MB
val cache = Cache(File(context.cacheDir, "http_cache"), cacheSize) val cache = Cache(File(context.cacheDir, "http_cache"), cacheSize)
val client = OkHttpClient.Builder() val client =
OkHttpClient.Builder()
.cache(cache) .cache(cache)
.addNetworkInterceptor { chain -> .addNetworkInterceptor { chain ->
val original = chain.request() val original = chain.request()
var builder: Request.Builder = original.newBuilder() var builder: Request.Builder = original.newBuilder()
if (this.hostConfig.hasAuthentication()) { if (this.hostConfig.hasAuthentication()) {
builder = builder builder =
builder
.header("x-api-key", this.hostConfig.apiKey) .header("x-api-key", this.hostConfig.apiKey)
.header("x-api-user", this.hostConfig.userID) .header("x-api-user", this.hostConfig.userID)
} }
builder = builder.header("x-client", "habitica-android") builder =
builder.header("x-client", "habitica-android")
.header("x-user-timezoneOffset", timezoneOffset.toString()) .header("x-user-timezoneOffset", timezoneOffset.toString())
if (userAgent != null) { if (userAgent != null) {
builder = builder.header("user-agent", userAgent) builder = builder.header("user-agent", userAgent)
@ -141,7 +144,8 @@ class ApiClientImpl(
if (BuildConfig.STAGING_KEY.isNotEmpty()) { if (BuildConfig.STAGING_KEY.isNotEmpty()) {
builder = builder.header("Authorization", "Basic " + BuildConfig.STAGING_KEY) builder = builder.header("Authorization", "Basic " + BuildConfig.STAGING_KEY)
} }
val request = builder.method(original.method, original.body) val request =
builder.method(original.method, original.body)
.build() .build()
lastAPICallURL = original.url.toString() lastAPICallURL = original.url.toString()
val response = chain.proceed(request) val response = chain.proceed(request)
@ -175,7 +179,8 @@ class ApiClientImpl(
val server = Server(this.hostConfig.address) val server = Server(this.hostConfig.address)
retrofitAdapter = Retrofit.Builder() retrofitAdapter =
Retrofit.Builder()
.client(client) .client(client)
.baseUrl(server.toString()) .baseUrl(server.toString())
.addConverterFactory(converter) .addConverterFactory(converter)
@ -195,7 +200,7 @@ class ApiClientImpl(
username: String, username: String,
email: String, email: String,
password: String, password: String,
confirmPassword: String confirmPassword: String,
): UserAuthResponse? { ): UserAuthResponse? {
val auth = UserAuth() val auth = UserAuth()
auth.username = username auth.username = username
@ -205,7 +210,10 @@ class ApiClientImpl(
return process { this.apiService.registerUser(auth) } return process { this.apiService.registerUser(auth) }
} }
override suspend fun connectUser(username: String, password: String): UserAuthResponse? { override suspend fun connectUser(
username: String,
password: String,
): UserAuthResponse? {
val auth = UserAuth() val auth = UserAuth()
auth.username = username auth.username = username
auth.password = password auth.password = password
@ -215,7 +223,7 @@ class ApiClientImpl(
override suspend fun connectSocial( override suspend fun connectSocial(
network: String, network: String,
userId: String, userId: String,
accessToken: String accessToken: String,
): UserAuthResponse? { ): UserAuthResponse? {
val auth = UserAuthSocial() val auth = UserAuthSocial()
auth.network = network auth.network = network
@ -243,14 +251,14 @@ class ApiClientImpl(
var isUserInputCall = false var isUserInputCall = false
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
if (SocketException::class.java.isAssignableFrom(throwableClass) if (SocketException::class.java.isAssignableFrom(throwableClass) ||
|| SSLException::class.java.isAssignableFrom(throwableClass) SSLException::class.java.isAssignableFrom(throwableClass)
) { ) {
this.showConnectionProblemDialog(R.string.internal_error_api, isUserInputCall) this.showConnectionProblemDialog(R.string.internal_error_api, isUserInputCall)
} else if (throwableClass == SocketTimeoutException::class.java || UnknownHostException::class.java == throwableClass || IOException::class.java == throwableClass) { } else if (throwableClass == SocketTimeoutException::class.java || UnknownHostException::class.java == throwableClass || IOException::class.java == throwableClass) {
this.showConnectionProblemDialog( this.showConnectionProblemDialog(
R.string.network_error_no_network_body, R.string.network_error_no_network_body,
isUserInputCall isUserInputCall,
) )
} else if (HttpException::class.java.isAssignableFrom(throwable.javaClass)) { } else if (HttpException::class.java.isAssignableFrom(throwable.javaClass)) {
val error = throwable as HttpException val error = throwable as HttpException
@ -258,7 +266,8 @@ class ApiClientImpl(
val status = error.code() val status = error.code()
val requestUrl = error.response()?.raw()?.request?.url val requestUrl = error.response()?.raw()?.request?.url
val path = requestUrl?.encodedPath?.removePrefix("/api/v4") ?: "" val path = requestUrl?.encodedPath?.removePrefix("/api/v4") ?: ""
isUserInputCall = when { isUserInputCall =
when {
path.startsWith("/groups") && path.endsWith("invite") -> true path.startsWith("/groups") && path.endsWith("invite") -> true
else -> false else -> false
} }
@ -278,7 +287,7 @@ class ApiClientImpl(
showConnectionProblemDialog( showConnectionProblemDialog(
R.string.authentication_error_title, R.string.authentication_error_title,
R.string.authentication_error_body, R.string.authentication_error_body,
isUserInputCall isUserInputCall,
) )
} }
} else if (status in 500..599) { } else if (status in 500..599) {
@ -295,14 +304,15 @@ class ApiClientImpl(
override suspend fun updateMember( override suspend fun updateMember(
memberID: String, memberID: String,
updateData: Map<String, Map<String, Boolean>> updateData: Map<String, Map<String, Boolean>>,
): Member? { ): Member? {
return process { apiService.updateUser(memberID, updateData) } return process { apiService.updateUser(memberID, updateData) }
} }
override fun getErrorResponse(throwable: HttpException): ErrorResponse { override fun getErrorResponse(throwable: HttpException): ErrorResponse {
val errorResponse = throwable.response()?.errorBody() ?: return ErrorResponse() val errorResponse = throwable.response()?.errorBody() ?: return ErrorResponse()
val errorConverter = converter val errorConverter =
converter
.responseBodyConverter(ErrorResponse::class.java, arrayOfNulls(0), retrofitAdapter) .responseBodyConverter(ErrorResponse::class.java, arrayOfNulls(0), retrofitAdapter)
return try { return try {
errorConverter?.convert(errorResponse) as ErrorResponse errorConverter?.convert(errorResponse) as ErrorResponse
@ -319,7 +329,10 @@ class ApiClientImpl(
return user return user
} }
override suspend fun retrieveInboxMessages(uuid: String, page: Int): List<ChatMessage>? { override suspend fun retrieveInboxMessages(
uuid: String,
page: Int,
): List<ChatMessage>? {
return process { apiService.getInboxMessages(uuid, page) } return process { apiService.getInboxMessages(uuid, page) }
} }
@ -333,7 +346,7 @@ class ApiClientImpl(
private fun showConnectionProblemDialog( private fun showConnectionProblemDialog(
resourceMessageString: Int, resourceMessageString: Int,
isFromUserInput: Boolean isFromUserInput: Boolean,
) { ) {
showConnectionProblemDialog(null, context.getString(resourceMessageString), isFromUserInput) showConnectionProblemDialog(null, context.getString(resourceMessageString), isFromUserInput)
} }
@ -341,37 +354,40 @@ class ApiClientImpl(
private fun showConnectionProblemDialog( private fun showConnectionProblemDialog(
resourceTitleString: Int, resourceTitleString: Int,
resourceMessageString: Int, resourceMessageString: Int,
isFromUserInput: Boolean isFromUserInput: Boolean,
) { ) {
showConnectionProblemDialog( showConnectionProblemDialog(
context.getString(resourceTitleString), context.getString(resourceTitleString),
context.getString(resourceMessageString), context.getString(resourceMessageString),
isFromUserInput isFromUserInput,
) )
} }
private var erroredRequestCount = 0 private var erroredRequestCount = 0
private fun showConnectionProblemDialog( private fun showConnectionProblemDialog(
resourceTitleString: String?, resourceTitleString: String?,
resourceMessageString: String, resourceMessageString: String,
isFromUserInput: Boolean isFromUserInput: Boolean,
) { ) {
erroredRequestCount += 1 erroredRequestCount += 1
val application = (context as? HabiticaBaseApplication) val application =
(context as? HabiticaBaseApplication)
?: (context.applicationContext as? HabiticaBaseApplication) ?: (context.applicationContext as? HabiticaBaseApplication)
application?.currentActivity?.get() application?.currentActivity?.get()
?.showConnectionProblem( ?.showConnectionProblem(
erroredRequestCount, erroredRequestCount,
resourceTitleString, resourceTitleString,
resourceMessageString, resourceMessageString,
isFromUserInput isFromUserInput,
) )
} }
private fun hideConnectionProblemDialog() { private fun hideConnectionProblemDialog() {
if (erroredRequestCount == 0) return if (erroredRequestCount == 0) return
erroredRequestCount = 0 erroredRequestCount = 0
val application = (context as? HabiticaBaseApplication) val application =
(context as? HabiticaBaseApplication)
?: (context.applicationContext as? HabiticaBaseApplication) ?: (context.applicationContext as? HabiticaBaseApplication)
application?.currentActivity?.get() application?.currentActivity?.get()
?.hideConnectionProblem() ?.hideConnectionProblem()
@ -382,7 +398,10 @@ class ApiClientImpl(
See here for more info: http://blog.danlew.net/2015/03/02/dont-break-the-chain/ See here for more info: http://blog.danlew.net/2015/03/02/dont-break-the-chain/
*/ */
override fun updateAuthenticationCredentials(userID: String?, apiToken: String?) { override fun updateAuthenticationCredentials(
userID: String?,
apiToken: String?,
) {
this.hostConfig.userID = userID ?: "" this.hostConfig.userID = userID ?: ""
this.hostConfig.apiKey = apiToken ?: "" this.hostConfig.apiKey = apiToken ?: ""
Analytics.setUserID(hostConfig.userID) Analytics.setUserID(hostConfig.userID)
@ -391,9 +410,10 @@ class ApiClientImpl(
override suspend fun getStatus(): Status? = process { apiService.getStatus() } override suspend fun getStatus(): Status? = process { apiService.getStatus() }
override suspend fun syncUserStats(): User? = process { apiService.syncUserStats() } override suspend fun syncUserStats(): User? = process { apiService.syncUserStats() }
override suspend fun reportChallenge( override suspend fun reportChallenge(
challengeid: String, challengeid: String,
updateData: Map<String, String> updateData: Map<String, String>,
): Void? { ): Void? {
return process { apiService.reportChallenge(challengeid, updateData) } return process { apiService.reportChallenge(challengeid, updateData) }
} }
@ -414,15 +434,24 @@ class ApiClientImpl(
return process { apiService.retrieveInAppRewards() } return process { apiService.retrieveInAppRewards() }
} }
override suspend fun equipItem(type: String, itemKey: String): Items? { override suspend fun equipItem(
type: String,
itemKey: String,
): Items? {
return process { apiService.equipItem(type, itemKey) } return process { apiService.equipItem(type, itemKey) }
} }
override suspend fun buyItem(itemKey: String, purchaseQuantity: Int): BuyResponse? { override suspend fun buyItem(
itemKey: String,
purchaseQuantity: Int,
): BuyResponse? {
return process { apiService.buyItem(itemKey, mapOf(Pair("quantity", purchaseQuantity))) } return process { apiService.buyItem(itemKey, mapOf(Pair("quantity", purchaseQuantity))) }
} }
override suspend fun unlinkAllTasks(challengeID: String?, keepOption: String): Void? { override suspend fun unlinkAllTasks(
challengeID: String?,
keepOption: String,
): Void? {
return process { apiService.unlinkAllTasks(challengeID, keepOption) } return process { apiService.unlinkAllTasks(challengeID, keepOption) }
} }
@ -430,17 +459,22 @@ class ApiClientImpl(
return process { apiService.blockMember(userID) } return process { apiService.blockMember(userID) }
} }
override suspend fun purchaseItem(type: String, itemKey: String, purchaseQuantity: Int): Void? { override suspend fun purchaseItem(
type: String,
itemKey: String,
purchaseQuantity: Int,
): Void? {
return process { return process {
apiService.purchaseItem( apiService.purchaseItem(
type, type,
itemKey, itemKey,
mapOf(Pair("quantity", purchaseQuantity)) mapOf(Pair("quantity", purchaseQuantity)),
) )
} }
} }
val lastSubscribeCall: Date? = null val lastSubscribeCall: Date? = null
override suspend fun validateSubscription(request: PurchaseValidationRequest): Any? { override suspend fun validateSubscription(request: PurchaseValidationRequest): Any? {
return if (lastSubscribeCall == null || Date().time - lastSubscribeCall.time > 60000) { return if (lastSubscribeCall == null || Date().time - lastSubscribeCall.time > 60000) {
process { apiService.validateSubscription(request) } process { apiService.validateSubscription(request) }
@ -461,7 +495,10 @@ class ApiClientImpl(
return processResponse(apiService.cancelSubscription()) return processResponse(apiService.cancelSubscription())
} }
override suspend fun purchaseHourglassItem(type: String, itemKey: String): Void? { override suspend fun purchaseHourglassItem(
type: String,
itemKey: String,
): Void? {
return process { apiService.purchaseHourglassItem(type, itemKey) } return process { apiService.purchaseHourglassItem(type, itemKey) }
} }
@ -477,17 +514,26 @@ class ApiClientImpl(
return process { apiService.purchaseSpecialSpell(key) } return process { apiService.purchaseSpecialSpell(key) }
} }
override suspend fun sellItem(itemType: String, itemKey: String): User? { override suspend fun sellItem(
itemType: String,
itemKey: String,
): User? {
return process { apiService.sellItem(itemType, itemKey) } return process { apiService.sellItem(itemType, itemKey) }
} }
override suspend fun feedPet(petKey: String, foodKey: String): FeedResponse? { override suspend fun feedPet(
petKey: String,
foodKey: String,
): FeedResponse? {
val response = apiService.feedPet(petKey, foodKey) val response = apiService.feedPet(petKey, foodKey)
response.data?.message = response.message response.data?.message = response.message
return process { response } return process { response }
} }
override suspend fun hatchPet(eggKey: String, hatchingPotionKey: String): Items? { override suspend fun hatchPet(
eggKey: String,
hatchingPotionKey: String,
): Items? {
return process { apiService.hatchPet(eggKey, hatchingPotionKey) } return process { apiService.hatchPet(eggKey, hatchingPotionKey) }
} }
@ -497,7 +543,10 @@ class ApiClientImpl(
return process { apiService.getTasks(type) } return process { apiService.getTasks(type) }
} }
override suspend fun getTasks(type: String, dueDate: String): TaskList? { override suspend fun getTasks(
type: String,
dueDate: String,
): TaskList? {
return process { apiService.getTasks(type, dueDate) } return process { apiService.getTasks(type, dueDate) }
} }
@ -513,7 +562,10 @@ class ApiClientImpl(
return process { apiService.getTask(id) } return process { apiService.getTask(id) }
} }
override suspend fun postTaskDirection(id: String, direction: String): TaskDirectionData? { override suspend fun postTaskDirection(
id: String,
direction: String,
): TaskDirectionData? {
return process { apiService.postTaskDirection(id, direction) } return process { apiService.postTaskDirection(id, direction) }
} }
@ -521,11 +573,17 @@ class ApiClientImpl(
return process { apiService.bulkScoreTasks(data) } return process { apiService.bulkScoreTasks(data) }
} }
override suspend fun postTaskNewPosition(id: String, position: Int): List<String>? { override suspend fun postTaskNewPosition(
id: String,
position: Int,
): List<String>? {
return process { apiService.postTaskNewPosition(id, position) } return process { apiService.postTaskNewPosition(id, position) }
} }
override suspend fun scoreChecklistItem(taskId: String, itemId: String): Task? { override suspend fun scoreChecklistItem(
taskId: String,
itemId: String,
): Task? {
return process { apiService.scoreChecklistItem(taskId, itemId) } return process { apiService.scoreChecklistItem(taskId, itemId) }
} }
@ -533,7 +591,10 @@ class ApiClientImpl(
return process { apiService.createTask(item) } return process { apiService.createTask(item) }
} }
override suspend fun createGroupTask(groupId: String, item: Task): Task? { override suspend fun createGroupTask(
groupId: String,
item: Task,
): Task? {
return process { apiService.createGroupTask(groupId, item) } return process { apiService.createGroupTask(groupId, item) }
} }
@ -541,7 +602,10 @@ class ApiClientImpl(
return process { apiService.createTasks(tasks) } return process { apiService.createTasks(tasks) }
} }
override suspend fun updateTask(id: String, item: Task): Task? { override suspend fun updateTask(
id: String,
item: Task,
): Task? {
return process { apiService.updateTask(id, item) } return process { apiService.updateTask(id, item) }
} }
@ -553,7 +617,10 @@ class ApiClientImpl(
return process { apiService.createTag(tag) } return process { apiService.createTag(tag) }
} }
override suspend fun updateTag(id: String, tag: Tag): Tag? { override suspend fun updateTag(
id: String,
tag: Tag,
): Tag? {
return process { apiService.updateTag(id, tag) } return process { apiService.updateTag(id, tag) }
} }
@ -568,12 +635,15 @@ class ApiClientImpl(
override suspend fun useSkill( override suspend fun useSkill(
skillName: String, skillName: String,
targetType: String, targetType: String,
targetId: String targetId: String,
): SkillResponse? { ): SkillResponse? {
return process { apiService.useSkill(skillName, targetType, targetId) } return process { apiService.useSkill(skillName, targetType, targetId) }
} }
override suspend fun useSkill(skillName: String, targetType: String): SkillResponse? { override suspend fun useSkill(
skillName: String,
targetType: String,
): SkillResponse? {
return process { apiService.useSkill(skillName, targetType) } return process { apiService.useSkill(skillName, targetType) }
} }
@ -605,11 +675,17 @@ class ApiClientImpl(
return processResponse(apiService.createGroup(group)) return processResponse(apiService.createGroup(group))
} }
override suspend fun updateGroup(id: String, item: Group): Group? { override suspend fun updateGroup(
id: String,
item: Group,
): Group? {
return processResponse(apiService.updateGroup(id, item)) return processResponse(apiService.updateGroup(id, item))
} }
override suspend fun removeMemberFromGroup(groupID: String, userID: String): Void? { override suspend fun removeMemberFromGroup(
groupID: String,
userID: String,
): Void? {
return processResponse(apiService.removeMemberFromGroup(groupID, userID)) return processResponse(apiService.removeMemberFromGroup(groupID, userID))
} }
@ -621,18 +697,24 @@ class ApiClientImpl(
return processResponse(apiService.joinGroup(groupId)) return processResponse(apiService.joinGroup(groupId))
} }
override suspend fun leaveGroup(groupId: String, keepChallenges: String): Void? { override suspend fun leaveGroup(
groupId: String,
keepChallenges: String,
): Void? {
return processResponse(apiService.leaveGroup(groupId, keepChallenges)) return processResponse(apiService.leaveGroup(groupId, keepChallenges))
} }
override suspend fun postGroupChat( override suspend fun postGroupChat(
groupId: String, groupId: String,
message: Map<String, String> message: Map<String, String>,
): PostChatMessageResult? { ): PostChatMessageResult? {
return process { apiService.postGroupChat(groupId, message) } return process { apiService.postGroupChat(groupId, message) }
} }
override suspend fun deleteMessage(groupId: String, messageId: String): Void? { override suspend fun deleteMessage(
groupId: String,
messageId: String,
): Void? {
return process { apiService.deleteMessage(groupId, messageId) } return process { apiService.deleteMessage(groupId, messageId) }
} }
@ -642,7 +724,7 @@ class ApiClientImpl(
override suspend fun getGroupMembers( override suspend fun getGroupMembers(
groupId: String, groupId: String,
includeAllPublicFields: Boolean? includeAllPublicFields: Boolean?,
): List<Member>? { ): List<Member>? {
return processResponse(apiService.getGroupMembers(groupId, includeAllPublicFields)) return processResponse(apiService.getGroupMembers(groupId, includeAllPublicFields))
} }
@ -650,28 +732,37 @@ class ApiClientImpl(
override suspend fun getGroupMembers( override suspend fun getGroupMembers(
groupId: String, groupId: String,
includeAllPublicFields: Boolean?, includeAllPublicFields: Boolean?,
lastId: String lastId: String,
): List<Member>? { ): List<Member>? {
return processResponse(apiService.getGroupMembers(groupId, includeAllPublicFields, lastId)) return processResponse(apiService.getGroupMembers(groupId, includeAllPublicFields, lastId))
} }
override suspend fun likeMessage(groupId: String, mid: String): ChatMessage? { override suspend fun likeMessage(
groupId: String,
mid: String,
): ChatMessage? {
return process { apiService.likeMessage(groupId, mid) } return process { apiService.likeMessage(groupId, mid) }
} }
override suspend fun reportMember(mid: String, data: Map<String, String>): Void? { override suspend fun reportMember(
mid: String,
data: Map<String, String>,
): Void? {
return process { apiService.reportMember(mid, data) } return process { apiService.reportMember(mid, data) }
} }
override suspend fun flagMessage( override suspend fun flagMessage(
groupId: String, groupId: String,
mid: String, mid: String,
data: MutableMap<String, String> data: MutableMap<String, String>,
): Void? { ): Void? {
return process { apiService.flagMessage(groupId, mid, data) } return process { apiService.flagMessage(groupId, mid, data) }
} }
override suspend fun flagInboxMessage(mid: String, data: MutableMap<String, String>): Void? { override suspend fun flagInboxMessage(
mid: String,
data: MutableMap<String, String>,
): Void? {
return process { apiService.flagInboxMessage(mid, data) } return process { apiService.flagInboxMessage(mid, data) }
} }
@ -681,7 +772,7 @@ class ApiClientImpl(
override suspend fun inviteToGroup( override suspend fun inviteToGroup(
groupId: String, groupId: String,
inviteData: Map<String, Any> inviteData: Map<String, Any>,
): List<InviteResponse>? { ): List<InviteResponse>? {
return process { apiService.inviteToGroup(groupId, inviteData) } return process { apiService.inviteToGroup(groupId, inviteData) }
} }
@ -692,7 +783,7 @@ class ApiClientImpl(
override suspend fun getGroupInvites( override suspend fun getGroupInvites(
groupId: String, groupId: String,
includeAllPublicFields: Boolean? includeAllPublicFields: Boolean?,
): List<Member>? { ): List<Member>? {
return process { apiService.getGroupInvites(groupId, includeAllPublicFields) } return process { apiService.getGroupInvites(groupId, includeAllPublicFields) }
} }
@ -709,11 +800,17 @@ class ApiClientImpl(
return process { apiService.cancelQuest(groupId) } return process { apiService.cancelQuest(groupId) }
} }
override suspend fun forceStartQuest(groupId: String, group: Group): Quest? { override suspend fun forceStartQuest(
groupId: String,
group: Group,
): Quest? {
return process { apiService.forceStartQuest(groupId, group) } return process { apiService.forceStartQuest(groupId, group) }
} }
override suspend fun inviteToQuest(groupId: String, questKey: String): Quest? { override suspend fun inviteToQuest(
groupId: String,
questKey: String,
): Quest? {
return process { apiService.inviteToQuest(groupId, questKey) } return process { apiService.inviteToQuest(groupId, questKey) }
} }
@ -726,6 +823,7 @@ class ApiClientImpl(
} }
private val lastPurchaseValidation: Date? = null private val lastPurchaseValidation: Date? = null
override suspend fun validatePurchase(request: PurchaseValidationRequest): PurchaseValidationResult? { override suspend fun validatePurchase(request: PurchaseValidationRequest): PurchaseValidationResult? {
// make sure a purchase attempt doesn't happen // make sure a purchase attempt doesn't happen
return if (lastPurchaseValidation == null || Date().time - lastPurchaseValidation.time > 5000) { return if (lastPurchaseValidation == null || Date().time - lastPurchaseValidation.time > 5000) {
@ -739,7 +837,10 @@ class ApiClientImpl(
return process { apiService.changeCustomDayStart(updateObject) } return process { apiService.changeCustomDayStart(updateObject) }
} }
override suspend fun markTaskNeedsWork(taskID: String, userID: String): Task? { override suspend fun markTaskNeedsWork(
taskID: String,
userID: String,
): Task? {
return process { apiService.markTaskNeedsWork(taskID, userID) } return process { apiService.markTaskNeedsWork(taskID, userID) }
} }
@ -760,7 +861,7 @@ class ApiClientImpl(
override suspend fun findUsernames( override suspend fun findUsernames(
username: String, username: String,
context: String?, context: String?,
id: String? id: String?,
): List<FindUsernameResult>? { ): List<FindUsernameResult>? {
return process { apiService.findUsernames(username, context, id) } return process { apiService.findUsernames(username, context, id) }
} }
@ -781,7 +882,10 @@ class ApiClientImpl(
return process { apiService.deletePushDevice(regId) } return process { apiService.deletePushDevice(regId) }
} }
override suspend fun getUserChallenges(page: Int, memberOnly: Boolean): List<Challenge>? { override suspend fun getUserChallenges(
page: Int,
memberOnly: Boolean,
): List<Challenge>? {
return if (memberOnly) { return if (memberOnly) {
process { apiService.getUserChallenges(page, memberOnly) } process { apiService.getUserChallenges(page, memberOnly) }
} else { } else {
@ -801,7 +905,10 @@ class ApiClientImpl(
return process { apiService.joinChallenge(challengeId) } return process { apiService.joinChallenge(challengeId) }
} }
override suspend fun leaveChallenge(challengeId: String, body: LeaveChallengeBody): Void? { override suspend fun leaveChallenge(
challengeId: String,
body: LeaveChallengeBody,
): Void? {
return process { apiService.leaveChallenge(challengeId, body) } return process { apiService.leaveChallenge(challengeId, body) }
} }
@ -809,11 +916,17 @@ class ApiClientImpl(
return process { apiService.createChallenge(challenge) } return process { apiService.createChallenge(challenge) }
} }
override suspend fun createChallengeTasks(challengeId: String, tasks: List<Task>): List<Task>? { override suspend fun createChallengeTasks(
challengeId: String,
tasks: List<Task>,
): List<Task>? {
return process { apiService.createChallengeTasks(challengeId, tasks) } return process { apiService.createChallengeTasks(challengeId, tasks) }
} }
override suspend fun createChallengeTask(challengeId: String, task: Task): Task? { override suspend fun createChallengeTask(
challengeId: String,
task: Task,
): Task? {
return process { apiService.createChallengeTask(challengeId, task) } return process { apiService.createChallengeTask(challengeId, task) }
} }
@ -867,7 +980,10 @@ class ApiClientImpl(
return process { apiService.deleteAccount(updateObject) } return process { apiService.deleteAccount(updateObject) }
} }
override suspend fun togglePinnedItem(pinType: String, path: String): Void? { override suspend fun togglePinnedItem(
pinType: String,
path: String,
): Void? {
return process { apiService.togglePinnedItem(pinType, path) } return process { apiService.togglePinnedItem(pinType, path) }
} }
@ -877,7 +993,10 @@ class ApiClientImpl(
return process { apiService.sendPasswordResetEmail(data) } return process { apiService.sendPasswordResetEmail(data) }
} }
override suspend fun updateLoginName(newLoginName: String, password: String): Void? { override suspend fun updateLoginName(
newLoginName: String,
password: String,
): Void? {
val updateObject = HashMap<String, String>() val updateObject = HashMap<String, String>()
updateObject["username"] = newLoginName updateObject["username"] = newLoginName
updateObject["password"] = password updateObject["password"] = password
@ -896,7 +1015,10 @@ class ApiClientImpl(
return process { this.apiService.verifyUsername(updateObject) } return process { this.apiService.verifyUsername(updateObject) }
} }
override suspend fun updateEmail(newEmail: String, password: String): Void? { override suspend fun updateEmail(
newEmail: String,
password: String,
): Void? {
val updateObject = HashMap<String, String>() val updateObject = HashMap<String, String>()
updateObject["newEmail"] = newEmail updateObject["newEmail"] = newEmail
if (password.isNotBlank()) { if (password.isNotBlank()) {
@ -908,7 +1030,7 @@ class ApiClientImpl(
override suspend fun updatePassword( override suspend fun updatePassword(
oldPassword: String, oldPassword: String,
newPassword: String, newPassword: String,
newPasswordConfirmation: String newPasswordConfirmation: String,
): Void? { ): Void? {
val updateObject = HashMap<String, String>() val updateObject = HashMap<String, String>()
updateObject["password"] = oldPassword updateObject["password"] = oldPassword
@ -921,13 +1043,16 @@ class ApiClientImpl(
return process { apiService.allocatePoint(stat) } return process { apiService.allocatePoint(stat) }
} }
override suspend fun transferGems(giftedID: String, amount: Int): Void? { override suspend fun transferGems(
giftedID: String,
amount: Int,
): Void? {
return process { return process {
apiService.transferGems( apiService.transferGems(
mapOf( mapOf(
Pair("toUserId", giftedID), Pair("toUserId", giftedID),
Pair("gemAmount", amount) Pair("gemAmount", amount),
) ),
) )
} }
} }
@ -940,11 +1065,17 @@ class ApiClientImpl(
return processResponse(apiService.getTeamPlanTasks(teamID)) return processResponse(apiService.getTeamPlanTasks(teamID))
} }
override suspend fun assignToTask(taskId: String, ids: List<String>): Task? { override suspend fun assignToTask(
taskId: String,
ids: List<String>,
): Task? {
return process { apiService.assignToTask(taskId, ids) } return process { apiService.assignToTask(taskId, ids) }
} }
override suspend fun unassignFromTask(taskId: String, userID: String): Task? { override suspend fun unassignFromTask(
taskId: String,
userID: String,
): Task? {
return process { apiService.unassignFromTask(taskId, userID) } return process { apiService.unassignFromTask(taskId, userID) }
} }
@ -952,7 +1083,7 @@ class ApiClientImpl(
strength: Int, strength: Int,
intelligence: Int, intelligence: Int,
constitution: Int, constitution: Int,
perception: Int perception: Int,
): Stats? { ): Stats? {
val body = HashMap<String, Map<String, Int>>() val body = HashMap<String, Map<String, Int>>()
val stats = HashMap<String, Int>() val stats = HashMap<String, Int>()

View file

@ -9,9 +9,8 @@ import com.habitrpg.android.habitica.modules.AuthenticationHandler
abstract class BaseRepositoryImpl<T : BaseLocalRepository>( abstract class BaseRepositoryImpl<T : BaseLocalRepository>(
protected val localRepository: T, protected val localRepository: T,
protected val apiClient: ApiClient, protected val apiClient: ApiClient,
protected val authenticationHandler: AuthenticationHandler protected val authenticationHandler: AuthenticationHandler,
) : BaseRepository { ) : BaseRepository {
val currentUserID: String val currentUserID: String
get() = authenticationHandler.currentUserID ?: "" get() = authenticationHandler.currentUserID ?: ""

View file

@ -16,16 +16,16 @@ import kotlinx.coroutines.flow.Flow
class ChallengeRepositoryImpl( class ChallengeRepositoryImpl(
localRepository: ChallengeLocalRepository, localRepository: ChallengeLocalRepository,
apiClient: ApiClient, apiClient: ApiClient,
authenticationHandler: AuthenticationHandler authenticationHandler: AuthenticationHandler,
) : BaseRepositoryImpl<ChallengeLocalRepository>(localRepository, apiClient, authenticationHandler), ChallengeRepository { ) : BaseRepositoryImpl<ChallengeLocalRepository>(localRepository, apiClient, authenticationHandler),
ChallengeRepository {
override fun isChallengeMember(challengeID: String): Flow<Boolean> { override fun isChallengeMember(challengeID: String): Flow<Boolean> {
return localRepository.isChallengeMember(currentUserID, challengeID) return localRepository.isChallengeMember(currentUserID, challengeID)
} }
override suspend fun reportChallenge( override suspend fun reportChallenge(
challengeid: String, challengeid: String,
updateData: Map<String, String> updateData: Map<String, String>,
): Void? { ): Void? {
return apiClient.reportChallenge(challengeid, updateData) return apiClient.reportChallenge(challengeid, updateData)
} }
@ -83,14 +83,29 @@ class ChallengeRepositoryImpl(
return tasksOrder return tasksOrder
} }
private suspend fun addChallengeTasks(challenge: Challenge, addedTaskList: List<Task>) { private suspend fun addChallengeTasks(
challenge: Challenge,
addedTaskList: List<Task>,
) {
when { when {
addedTaskList.count() == 1 -> apiClient.createChallengeTask(challenge.id ?: "", addedTaskList[0]) addedTaskList.count() == 1 ->
addedTaskList.count() > 1 -> apiClient.createChallengeTasks(challenge.id ?: "", addedTaskList) apiClient.createChallengeTask(
challenge.id ?: "",
addedTaskList[0],
)
addedTaskList.count() > 1 ->
apiClient.createChallengeTasks(
challenge.id ?: "",
addedTaskList,
)
} }
} }
override suspend fun createChallenge(challenge: Challenge, taskList: List<Task>): Challenge? { override suspend fun createChallenge(
challenge: Challenge,
taskList: List<Task>,
): Challenge? {
challenge.tasksOrder = getTaskOrders(taskList) challenge.tasksOrder = getTaskOrders(taskList)
val createdChallenge = apiClient.createChallenge(challenge) val createdChallenge = apiClient.createChallenge(challenge)
@ -105,7 +120,7 @@ class ChallengeRepositoryImpl(
fullTaskList: List<Task>, fullTaskList: List<Task>,
addedTaskList: List<Task>, addedTaskList: List<Task>,
updatedTaskList: List<Task>, updatedTaskList: List<Task>,
removedTaskList: List<String> removedTaskList: List<String>,
): Challenge? { ): Challenge? {
updatedTaskList updatedTaskList
.map { localRepository.getUnmanagedCopy(it) } .map { localRepository.getUnmanagedCopy(it) }
@ -141,7 +156,10 @@ class ChallengeRepositoryImpl(
return localRepository.getUserChallenges(userId ?: currentUserID) return localRepository.getUserChallenges(userId ?: currentUserID)
} }
override suspend fun retrieveChallenges(page: Int, memberOnly: Boolean): List<Challenge>? { override suspend fun retrieveChallenges(
page: Int,
memberOnly: Boolean,
): List<Challenge>? {
val challenges = apiClient.getUserChallenges(page, memberOnly) val challenges = apiClient.getUserChallenges(page, memberOnly)
if (challenges != null) { if (challenges != null) {
localRepository.saveChallenges(challenges, page == 0, memberOnly, currentUserID) localRepository.saveChallenges(challenges, page == 0, memberOnly, currentUserID)
@ -149,7 +167,10 @@ class ChallengeRepositoryImpl(
return challenges return challenges
} }
override suspend fun leaveChallenge(challenge: Challenge, keepTasks: String): Void? { override suspend fun leaveChallenge(
challenge: Challenge,
keepTasks: String,
): Void? {
apiClient.leaveChallenge(challenge.id ?: "", LeaveChallengeBody(keepTasks)) apiClient.leaveChallenge(challenge.id ?: "", LeaveChallengeBody(keepTasks))
localRepository.setParticipating(currentUserID, challenge.id ?: "", false) localRepository.setParticipating(currentUserID, challenge.id ?: "", false)
return null return null

View file

@ -17,9 +17,8 @@ class ContentRepositoryImpl<T : ContentLocalRepository>(
localRepository: T, localRepository: T,
apiClient: ApiClient, apiClient: ApiClient,
context: Context, context: Context,
authenticationHandler: AuthenticationHandler authenticationHandler: AuthenticationHandler,
) : BaseRepositoryImpl<T>(localRepository, apiClient, authenticationHandler), ContentRepository { ) : BaseRepositoryImpl<T>(localRepository, apiClient, authenticationHandler), ContentRepository {
private val mysteryItem = SpecialItem.makeMysteryItem(context) private val mysteryItem = SpecialItem.makeMysteryItem(context)
private var lastContentSync = 0L private var lastContentSync = 0L

View file

@ -10,10 +10,18 @@ import kotlinx.coroutines.flow.Flow
class CustomizationRepositoryImpl( class CustomizationRepositoryImpl(
localRepository: CustomizationLocalRepository, localRepository: CustomizationLocalRepository,
apiClient: ApiClient, apiClient: ApiClient,
authenticationHandler: AuthenticationHandler authenticationHandler: AuthenticationHandler,
) : BaseRepositoryImpl<CustomizationLocalRepository>(localRepository, apiClient, authenticationHandler), CustomizationRepository { ) : BaseRepositoryImpl<CustomizationLocalRepository>(
localRepository,
override fun getCustomizations(type: String, category: String?, onlyAvailable: Boolean): Flow<List<Customization>> { apiClient,
authenticationHandler,
),
CustomizationRepository {
override fun getCustomizations(
type: String,
category: String?,
onlyAvailable: Boolean,
): Flow<List<Customization>> {
return localRepository.getCustomizations(type, category, onlyAvailable) return localRepository.getCustomizations(type, category, onlyAvailable)
} }
} }

View file

@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.Flow
class FAQRepositoryImpl( class FAQRepositoryImpl(
localRepository: FAQLocalRepository, localRepository: FAQLocalRepository,
apiClient: ApiClient, apiClient: ApiClient,
authenticationHandler: AuthenticationHandler authenticationHandler: AuthenticationHandler,
) : BaseRepositoryImpl<FAQLocalRepository>(localRepository, apiClient, authenticationHandler), ) : BaseRepositoryImpl<FAQLocalRepository>(localRepository, apiClient, authenticationHandler),
FAQRepository { FAQRepository {
override fun getArticle(position: Int): Flow<FAQArticle> { override fun getArticle(position: Int): Flow<FAQArticle> {

View file

@ -34,8 +34,9 @@ class InventoryRepositoryImpl(
localRepository: InventoryLocalRepository, localRepository: InventoryLocalRepository,
apiClient: ApiClient, apiClient: ApiClient,
authenticationHandler: AuthenticationHandler, authenticationHandler: AuthenticationHandler,
var appConfigManager: AppConfigManager var appConfigManager: AppConfigManager,
) : BaseRepositoryImpl<InventoryLocalRepository>(localRepository, apiClient, authenticationHandler), InventoryRepository { ) : BaseRepositoryImpl<InventoryLocalRepository>(localRepository, apiClient, authenticationHandler),
InventoryRepository {
override fun getQuestContent(keys: List<String>) = localRepository.getQuestContent(keys) override fun getQuestContent(keys: List<String>) = localRepository.getQuestContent(keys)
override fun getQuestContent(key: String) = localRepository.getQuestContent(key) override fun getQuestContent(key: String) = localRepository.getQuestContent(key)
@ -72,19 +73,39 @@ class InventoryRepositoryImpl(
return localRepository.getOwnedEquipment() return localRepository.getOwnedEquipment()
} }
override fun getEquipmentType(type: String, set: String): Flow<List<Equipment>> { override fun getEquipmentType(
type: String,
set: String,
): Flow<List<Equipment>> {
return localRepository.getEquipmentType(type, set) return localRepository.getEquipmentType(type, set)
} }
override fun getOwnedItems(itemType: String, includeZero: Boolean): Flow<List<OwnedItem>> { override fun getOwnedItems(
return authenticationHandler.userIDFlow.flatMapLatest { localRepository.getOwnedItems(itemType, it, includeZero) } itemType: String,
includeZero: Boolean,
): Flow<List<OwnedItem>> {
return authenticationHandler.userIDFlow.flatMapLatest {
localRepository.getOwnedItems(
itemType,
it,
includeZero,
)
}
} }
override fun getOwnedItems(includeZero: Boolean): Flow<Map<String, OwnedItem>> { override fun getOwnedItems(includeZero: Boolean): Flow<Map<String, OwnedItem>> {
return authenticationHandler.userIDFlow.flatMapLatest { localRepository.getOwnedItems(it, includeZero) } return authenticationHandler.userIDFlow.flatMapLatest {
localRepository.getOwnedItems(
it,
includeZero,
)
}
} }
override fun getItems(itemClass: Class<out Item>, keys: Array<String>): Flow<List<Item>> { override fun getItems(
itemClass: Class<out Item>,
keys: Array<String>,
): Flow<List<Item>> {
return localRepository.getItems(itemClass, keys) return localRepository.getItems(itemClass, keys)
} }
@ -115,7 +136,11 @@ class InventoryRepositoryImpl(
return localRepository.getMounts() return localRepository.getMounts()
} }
override fun getMounts(type: String?, group: String?, color: String?): Flow<List<Mount>> { override fun getMounts(
type: String?,
group: String?,
color: String?,
): Flow<List<Mount>> {
return localRepository.getMounts(type, group, color) return localRepository.getMounts(type, group, color)
} }
@ -127,7 +152,11 @@ class InventoryRepositoryImpl(
return localRepository.getPets() return localRepository.getPets()
} }
override fun getPets(type: String?, group: String?, color: String?): Flow<List<Pet>> { override fun getPets(
type: String?,
group: String?,
color: String?,
): Flow<List<Pet>> {
return localRepository.getPets(type, group, color) return localRepository.getPets(type, group, color)
} }
@ -139,17 +168,28 @@ class InventoryRepositoryImpl(
localRepository.updateOwnedEquipment(user) localRepository.updateOwnedEquipment(user)
} }
override suspend fun changeOwnedCount(type: String, key: String, amountToAdd: Int) { override suspend fun changeOwnedCount(
type: String,
key: String,
amountToAdd: Int,
) {
localRepository.changeOwnedCount(type, key, currentUserID, amountToAdd) localRepository.changeOwnedCount(type, key, currentUserID, amountToAdd)
} }
override suspend fun sellItem(type: String, key: String): User? { override suspend fun sellItem(
val item = localRepository.getOwnedItem(currentUserID, type, key, true).firstOrNull() ?: return null type: String,
key: String,
): User? {
val item =
localRepository.getOwnedItem(currentUserID, type, key, true).firstOrNull()
?: return null
return sellItem(item) return sellItem(item)
} }
override suspend fun sellItem(item: OwnedItem): User? { override suspend fun sellItem(item: OwnedItem): User? {
val itemData = localRepository.getItem(item.itemType ?: "", item.key ?: "").firstOrNull() ?: return null val itemData =
localRepository.getItem(item.itemType ?: "", item.key ?: "").firstOrNull()
?: return null
return sellItem(itemData, item) return sellItem(itemData, item)
} }
@ -157,11 +197,17 @@ class InventoryRepositoryImpl(
return localRepository.getLatestMysteryItem() return localRepository.getLatestMysteryItem()
} }
override fun getItem(type: String, key: String): Flow<Item> { override fun getItem(
type: String,
key: String,
): Flow<Item> {
return localRepository.getItem(type, key) return localRepository.getItem(type, key)
} }
private suspend fun sellItem(item: Item, ownedItem: OwnedItem): User? { private suspend fun sellItem(
item: Item,
ownedItem: OwnedItem,
): User? {
localRepository.executeTransaction { localRepository.executeTransaction {
val liveItem = localRepository.getLiveObject(ownedItem) val liveItem = localRepository.getLiveObject(ownedItem)
liveItem?.numberOwned = (liveItem?.numberOwned ?: 0) - 1 liveItem?.numberOwned = (liveItem?.numberOwned ?: 0) - 1
@ -170,11 +216,17 @@ class InventoryRepositoryImpl(
return localRepository.soldItem(currentUserID, user) return localRepository.soldItem(currentUserID, user)
} }
override suspend fun equipGear(equipment: String, asCostume: Boolean): Items? { override suspend fun equipGear(
equipment: String,
asCostume: Boolean,
): Items? {
return equip(if (asCostume) "costume" else "equipped", equipment) return equip(if (asCostume) "costume" else "equipped", equipment)
} }
override suspend fun equip(type: String, key: String): Items? { override suspend fun equip(
type: String,
key: String,
): Items? {
val liveUser = localRepository.getLiveUser(currentUserID) val liveUser = localRepository.getLiveUser(currentUserID)
if (liveUser != null) { if (liveUser != null) {
@ -184,7 +236,8 @@ class InventoryRepositoryImpl(
} else if (type == "pet") { } else if (type == "pet") {
user.items?.currentPet = key user.items?.currentPet = key
} }
val outfit = if (type == "costume") { val outfit =
if (type == "costume") {
user.items?.gear?.costume user.items?.gear?.costume
} else { } else {
user.items?.gear?.equipped user.items?.gear?.equipped
@ -217,13 +270,20 @@ class InventoryRepositoryImpl(
return items return items
} }
override suspend fun feedPet(pet: Pet, food: Food): FeedResponse? { override suspend fun feedPet(
pet: Pet,
food: Food,
): FeedResponse? {
val feedResponse = apiClient.feedPet(pet.key, food.key) ?: return null val feedResponse = apiClient.feedPet(pet.key, food.key) ?: return null
localRepository.feedPet(food.key, pet.key, feedResponse.value ?: 0, currentUserID) localRepository.feedPet(food.key, pet.key, feedResponse.value ?: 0, currentUserID)
return feedResponse return feedResponse
} }
override suspend fun hatchPet(egg: Egg, hatchingPotion: HatchingPotion, successFunction: () -> Unit): Items? { override suspend fun hatchPet(
egg: Egg,
hatchingPotion: HatchingPotion,
successFunction: () -> Unit,
): Items? {
if (appConfigManager.enableLocalChanges()) { if (appConfigManager.enableLocalChanges()) {
localRepository.hatchPet(egg.key, hatchingPotion.key, currentUserID) localRepository.hatchPet(egg.key, hatchingPotion.key, currentUserID)
successFunction() successFunction()
@ -242,7 +302,12 @@ class InventoryRepositoryImpl(
return newQuest return newQuest
} }
override suspend fun buyItem(user: User?, id: String, value: Double, purchaseQuantity: Int): BuyResponse? { override suspend fun buyItem(
user: User?,
id: String,
value: Double,
purchaseQuantity: Int,
): BuyResponse? {
val buyResponse = apiClient.buyItem(id, purchaseQuantity) ?: return null val buyResponse = apiClient.buyItem(id, purchaseQuantity) ?: return null
val foundUser = user ?: localRepository.getLiveUser(currentUserID) ?: return buyResponse val foundUser = user ?: localRepository.getLiveUser(currentUserID) ?: return buyResponse
val copiedUser = localRepository.getUnmanagedCopy(foundUser) val copiedUser = localRepository.getUnmanagedCopy(foundUser)
@ -286,7 +351,10 @@ class InventoryRepositoryImpl(
return apiClient.purchaseMysterySet(categoryIdentifier) return apiClient.purchaseMysterySet(categoryIdentifier)
} }
override suspend fun purchaseHourglassItem(purchaseType: String, key: String): Void? { override suspend fun purchaseHourglassItem(
purchaseType: String,
key: String,
): Void? {
return apiClient.purchaseHourglassItem(purchaseType, key) return apiClient.purchaseHourglassItem(purchaseType, key)
} }
@ -298,12 +366,17 @@ class InventoryRepositoryImpl(
return apiClient.purchaseSpecialSpell(key) return apiClient.purchaseSpecialSpell(key)
} }
override suspend fun purchaseItem(purchaseType: String, key: String, purchaseQuantity: Int): Void? { override suspend fun purchaseItem(
purchaseType: String,
key: String,
purchaseQuantity: Int,
): Void? {
val response = apiClient.purchaseItem(purchaseType, key, purchaseQuantity) val response = apiClient.purchaseItem(purchaseType, key, purchaseQuantity)
if (key == "gem") { if (key == "gem") {
val user = localRepository.getLiveUser(currentUserID) val user = localRepository.getLiveUser(currentUserID)
localRepository.executeTransaction { localRepository.executeTransaction {
user?.purchased?.plan?.gemsBought = purchaseQuantity + (user?.purchased?.plan?.gemsBought ?: 0) user?.purchased?.plan?.gemsBought =
purchaseQuantity + (user?.purchased?.plan?.gemsBought ?: 0)
} }
} }
return response return response

View file

@ -8,49 +8,148 @@ import com.habitrpg.android.habitica.models.user.User
import javax.inject.Inject import javax.inject.Inject
@Suppress("StringLiteralDuplication") @Suppress("StringLiteralDuplication")
class SetupCustomizationRepositoryImpl @Inject class SetupCustomizationRepositoryImpl
@Inject
constructor(private val context: Context) : SetupCustomizationRepository { constructor(private val context: Context) : SetupCustomizationRepository {
private val wheelchairs: List<SetupCustomization> private val wheelchairs: List<SetupCustomization>
get() = listOf(SetupCustomization.createWheelchair("none", 0), SetupCustomization.createWheelchair("black", R.drawable.creator_chair_black), SetupCustomization.createWheelchair("blue", R.drawable.creator_chair_blue), SetupCustomization.createWheelchair("green", R.drawable.creator_chair_green), SetupCustomization.createWheelchair("pink", R.drawable.creator_chair_pink), SetupCustomization.createWheelchair("red", R.drawable.creator_chair_red), SetupCustomization.createWheelchair("yellow", R.drawable.creator_chair_yellow)) get() =
listOf(
SetupCustomization.createWheelchair("none", 0),
SetupCustomization.createWheelchair("black", R.drawable.creator_chair_black),
SetupCustomization.createWheelchair("blue", R.drawable.creator_chair_blue),
SetupCustomization.createWheelchair("green", R.drawable.creator_chair_green),
SetupCustomization.createWheelchair("pink", R.drawable.creator_chair_pink),
SetupCustomization.createWheelchair("red", R.drawable.creator_chair_red),
SetupCustomization.createWheelchair("yellow", R.drawable.creator_chair_yellow),
)
private val glasses: List<SetupCustomization> private val glasses: List<SetupCustomization>
get() = listOf(SetupCustomization.createGlasses("", R.drawable.creator_blank_face), SetupCustomization.createGlasses("eyewear_special_blackTopFrame", R.drawable.creator_eyewear_special_blacktopframe), SetupCustomization.createGlasses("eyewear_special_blueTopFrame", R.drawable.creator_eyewear_special_bluetopframe), SetupCustomization.createGlasses("eyewear_special_greenTopFrame", R.drawable.creator_eyewear_special_greentopframe), SetupCustomization.createGlasses("eyewear_special_pinkTopFrame", R.drawable.creator_eyewear_special_pinktopframe), SetupCustomization.createGlasses("eyewear_special_redTopFrame", R.drawable.creator_eyewear_special_redtopframe), SetupCustomization.createGlasses("eyewear_special_yellowTopFrame", R.drawable.creator_eyewear_special_yellowtopframe), SetupCustomization.createGlasses("eyewear_special_whiteTopFrame", R.drawable.creator_eyewear_special_whitetopframe)) get() =
listOf(
SetupCustomization.createGlasses("", R.drawable.creator_blank_face),
SetupCustomization.createGlasses(
"eyewear_special_blackTopFrame",
R.drawable.creator_eyewear_special_blacktopframe,
),
SetupCustomization.createGlasses(
"eyewear_special_blueTopFrame",
R.drawable.creator_eyewear_special_bluetopframe,
),
SetupCustomization.createGlasses(
"eyewear_special_greenTopFrame",
R.drawable.creator_eyewear_special_greentopframe,
),
SetupCustomization.createGlasses(
"eyewear_special_pinkTopFrame",
R.drawable.creator_eyewear_special_pinktopframe,
),
SetupCustomization.createGlasses(
"eyewear_special_redTopFrame",
R.drawable.creator_eyewear_special_redtopframe,
),
SetupCustomization.createGlasses(
"eyewear_special_yellowTopFrame",
R.drawable.creator_eyewear_special_yellowtopframe,
),
SetupCustomization.createGlasses(
"eyewear_special_whiteTopFrame",
R.drawable.creator_eyewear_special_whitetopframe,
),
)
private val flowers: List<SetupCustomization> private val flowers: List<SetupCustomization>
get() = listOf(SetupCustomization.createFlower("0", R.drawable.creator_blank_face), SetupCustomization.createFlower("1", R.drawable.creator_hair_flower_1), SetupCustomization.createFlower("2", R.drawable.creator_hair_flower_2), SetupCustomization.createFlower("3", R.drawable.creator_hair_flower_3), SetupCustomization.createFlower("4", R.drawable.creator_hair_flower_4), SetupCustomization.createFlower("5", R.drawable.creator_hair_flower_5), SetupCustomization.createFlower("6", R.drawable.creator_hair_flower_6)) get() =
listOf(
SetupCustomization.createFlower("0", R.drawable.creator_blank_face),
SetupCustomization.createFlower("1", R.drawable.creator_hair_flower_1),
SetupCustomization.createFlower("2", R.drawable.creator_hair_flower_2),
SetupCustomization.createFlower("3", R.drawable.creator_hair_flower_3),
SetupCustomization.createFlower("4", R.drawable.creator_hair_flower_4),
SetupCustomization.createFlower("5", R.drawable.creator_hair_flower_5),
SetupCustomization.createFlower("6", R.drawable.creator_hair_flower_6),
)
private val hairColors: List<SetupCustomization> private val hairColors: List<SetupCustomization>
get() = listOf(SetupCustomization.createHairColor("white", R.color.hair_white), SetupCustomization.createHairColor("brown", R.color.hair_brown), SetupCustomization.createHairColor("blond", R.color.hair_blond), SetupCustomization.createHairColor("red", R.color.hair_red), SetupCustomization.createHairColor("black", R.color.hair_black)) get() =
listOf(
SetupCustomization.createHairColor("white", R.color.hair_white),
SetupCustomization.createHairColor("brown", R.color.hair_brown),
SetupCustomization.createHairColor("blond", R.color.hair_blond),
SetupCustomization.createHairColor("red", R.color.hair_red),
SetupCustomization.createHairColor("black", R.color.hair_black),
)
private val sizes: List<SetupCustomization> private val sizes: List<SetupCustomization>
get() = listOf(SetupCustomization.createSize("slim", R.drawable.creator_slim_shirt_black, context.getString(R.string.avatar_size_slim)), SetupCustomization.createSize("broad", R.drawable.creator_broad_shirt_black, context.getString(R.string.avatar_size_broad))) get() =
listOf(
SetupCustomization.createSize(
"slim",
R.drawable.creator_slim_shirt_black,
context.getString(R.string.avatar_size_slim),
),
SetupCustomization.createSize(
"broad",
R.drawable.creator_broad_shirt_black,
context.getString(R.string.avatar_size_broad),
),
)
private val skins: List<SetupCustomization> private val skins: List<SetupCustomization>
get() = listOf(SetupCustomization.createSkin("ddc994", R.color.skin_ddc994), SetupCustomization.createSkin("f5a76e", R.color.skin_f5a76e), SetupCustomization.createSkin("ea8349", R.color.skin_ea8349), SetupCustomization.createSkin("c06534", R.color.skin_c06534), SetupCustomization.createSkin("98461a", R.color.skin_98461a), SetupCustomization.createSkin("915533", R.color.skin_915533), SetupCustomization.createSkin("c3e1dc", R.color.skin_c3e1dc), SetupCustomization.createSkin("6bd049", R.color.skin_6bd049)) get() =
listOf(
SetupCustomization.createSkin("ddc994", R.color.skin_ddc994),
SetupCustomization.createSkin("f5a76e", R.color.skin_f5a76e),
SetupCustomization.createSkin("ea8349", R.color.skin_ea8349),
SetupCustomization.createSkin("c06534", R.color.skin_c06534),
SetupCustomization.createSkin("98461a", R.color.skin_98461a),
SetupCustomization.createSkin("915533", R.color.skin_915533),
SetupCustomization.createSkin("c3e1dc", R.color.skin_c3e1dc),
SetupCustomization.createSkin("6bd049", R.color.skin_6bd049),
)
override fun getCustomizations(type: String, user: User): List<SetupCustomization> { override fun getCustomizations(
type: String,
user: User,
): List<SetupCustomization> {
return getCustomizations(type, null, user) return getCustomizations(type, null, user)
} }
override fun getCustomizations(type: String, subtype: String?, user: User): List<SetupCustomization> { override fun getCustomizations(
type: String,
subtype: String?,
user: User,
): List<SetupCustomization> {
return when (type) { return when (type) {
SetupCustomizationRepository.CATEGORY_BODY -> { SetupCustomizationRepository.CATEGORY_BODY -> {
when (subtype) { when (subtype) {
SetupCustomizationRepository.SUBCATEGORY_SIZE -> sizes SetupCustomizationRepository.SUBCATEGORY_SIZE -> sizes
SetupCustomizationRepository.SUBCATEGORY_SHIRT -> getShirts(user.preferences?.size ?: "slim") SetupCustomizationRepository.SUBCATEGORY_SHIRT ->
getShirts(
user.preferences?.size ?: "slim",
)
else -> emptyList() else -> emptyList()
} }
} }
SetupCustomizationRepository.CATEGORY_SKIN -> skins SetupCustomizationRepository.CATEGORY_SKIN -> skins
SetupCustomizationRepository.CATEGORY_HAIR -> { SetupCustomizationRepository.CATEGORY_HAIR -> {
when (subtype) { when (subtype) {
SetupCustomizationRepository.SUBCATEGORY_BANGS -> getBangs(user.preferences?.hair?.color ?: "") SetupCustomizationRepository.SUBCATEGORY_BANGS ->
SetupCustomizationRepository.SUBCATEGORY_PONYTAIL -> getHairBases(user.preferences?.hair?.color ?: "") getBangs(
user.preferences?.hair?.color ?: "",
)
SetupCustomizationRepository.SUBCATEGORY_PONYTAIL ->
getHairBases(
user.preferences?.hair?.color ?: "",
)
SetupCustomizationRepository.SUBCATEGORY_COLOR -> hairColors SetupCustomizationRepository.SUBCATEGORY_COLOR -> hairColors
else -> emptyList() else -> emptyList()
} }
} }
SetupCustomizationRepository.CATEGORY_EXTRAS -> { SetupCustomizationRepository.CATEGORY_EXTRAS -> {
when (subtype) { when (subtype) {
SetupCustomizationRepository.SUBCATEGORY_FLOWER -> flowers SetupCustomizationRepository.SUBCATEGORY_FLOWER -> flowers
@ -59,23 +158,47 @@ constructor(private val context: Context) : SetupCustomizationRepository {
else -> emptyList() else -> emptyList()
} }
} }
else -> emptyList() else -> emptyList()
} }
} }
private fun getHairBases(color: String): List<SetupCustomization> { private fun getHairBases(color: String): List<SetupCustomization> {
return listOf(SetupCustomization.createHairPonytail("0", R.drawable.creator_blank_face), SetupCustomization.createHairPonytail("1", getResId("creator_hair_base_1_$color")), SetupCustomization.createHairPonytail("3", getResId("creator_hair_base_3_$color"))) return listOf(
SetupCustomization.createHairPonytail("0", R.drawable.creator_blank_face),
SetupCustomization.createHairPonytail("1", getResId("creator_hair_base_1_$color")),
SetupCustomization.createHairPonytail("3", getResId("creator_hair_base_3_$color")),
)
} }
private fun getBangs(color: String): List<SetupCustomization> { private fun getBangs(color: String): List<SetupCustomization> {
return listOf(SetupCustomization.createHairBangs("0", R.drawable.creator_blank_face), SetupCustomization.createHairBangs("1", getResId("creator_hair_bangs_1_$color")), SetupCustomization.createHairBangs("2", getResId("creator_hair_bangs_2_$color")), SetupCustomization.createHairBangs("3", getResId("creator_hair_bangs_3_$color"))) return listOf(
SetupCustomization.createHairBangs("0", R.drawable.creator_blank_face),
SetupCustomization.createHairBangs("1", getResId("creator_hair_bangs_1_$color")),
SetupCustomization.createHairBangs("2", getResId("creator_hair_bangs_2_$color")),
SetupCustomization.createHairBangs("3", getResId("creator_hair_bangs_3_$color")),
)
} }
private fun getShirts(size: String): List<SetupCustomization> { private fun getShirts(size: String): List<SetupCustomization> {
return if (size == "broad") { return if (size == "broad") {
listOf(SetupCustomization.createShirt("black", R.drawable.creator_broad_shirt_black), SetupCustomization.createShirt("blue", R.drawable.creator_broad_shirt_blue), SetupCustomization.createShirt("green", R.drawable.creator_broad_shirt_green), SetupCustomization.createShirt("pink", R.drawable.creator_broad_shirt_pink), SetupCustomization.createShirt("white", R.drawable.creator_broad_shirt_white), SetupCustomization.createShirt("yellow", R.drawable.creator_broad_shirt_yellow)) listOf(
SetupCustomization.createShirt("black", R.drawable.creator_broad_shirt_black),
SetupCustomization.createShirt("blue", R.drawable.creator_broad_shirt_blue),
SetupCustomization.createShirt("green", R.drawable.creator_broad_shirt_green),
SetupCustomization.createShirt("pink", R.drawable.creator_broad_shirt_pink),
SetupCustomization.createShirt("white", R.drawable.creator_broad_shirt_white),
SetupCustomization.createShirt("yellow", R.drawable.creator_broad_shirt_yellow),
)
} else { } else {
listOf(SetupCustomization.createShirt("black", R.drawable.creator_slim_shirt_black), SetupCustomization.createShirt("blue", R.drawable.creator_slim_shirt_blue), SetupCustomization.createShirt("green", R.drawable.creator_slim_shirt_green), SetupCustomization.createShirt("pink", R.drawable.creator_slim_shirt_pink), SetupCustomization.createShirt("white", R.drawable.creator_slim_shirt_white), SetupCustomization.createShirt("yellow", R.drawable.creator_slim_shirt_yellow)) listOf(
SetupCustomization.createShirt("black", R.drawable.creator_slim_shirt_black),
SetupCustomization.createShirt("blue", R.drawable.creator_slim_shirt_blue),
SetupCustomization.createShirt("green", R.drawable.creator_slim_shirt_green),
SetupCustomization.createShirt("pink", R.drawable.creator_slim_shirt_pink),
SetupCustomization.createShirt("white", R.drawable.creator_slim_shirt_white),
SetupCustomization.createShirt("yellow", R.drawable.creator_slim_shirt_yellow),
)
} }
} }

View file

@ -28,15 +28,23 @@ import java.util.UUID
class SocialRepositoryImpl( class SocialRepositoryImpl(
localRepository: SocialLocalRepository, localRepository: SocialLocalRepository,
apiClient: ApiClient, apiClient: ApiClient,
authenticationHandler: AuthenticationHandler authenticationHandler: AuthenticationHandler,
) : BaseRepositoryImpl<SocialLocalRepository>(localRepository, apiClient, authenticationHandler), SocialRepository { ) : BaseRepositoryImpl<SocialLocalRepository>(localRepository, apiClient, authenticationHandler),
override suspend fun transferGroupOwnership(groupID: String, userID: String): Group? { SocialRepository {
val group = localRepository.getGroup(groupID).first()?.let { localRepository.getUnmanagedCopy(it) } override suspend fun transferGroupOwnership(
groupID: String,
userID: String,
): Group? {
val group =
localRepository.getGroup(groupID).first()?.let { localRepository.getUnmanagedCopy(it) }
group?.leaderID = userID group?.leaderID = userID
return group?.let { apiClient.updateGroup(groupID, it) } return group?.let { apiClient.updateGroup(groupID, it) }
} }
override suspend fun removeMemberFromGroup(groupID: String, userID: String): List<Member>? { override suspend fun removeMemberFromGroup(
groupID: String,
userID: String,
): List<Member>? {
apiClient.removeMemberFromGroup(groupID, userID) apiClient.removeMemberFromGroup(groupID, userID)
return retrievePartyMembers(groupID, true) return retrievePartyMembers(groupID, true)
} }
@ -51,7 +59,7 @@ class SocialRepositoryImpl(
override suspend fun updateMember( override suspend fun updateMember(
memberID: String, memberID: String,
data: Map<String, Map<String, Boolean>> data: Map<String, Map<String, Boolean>>,
): Member? { ): Member? {
return apiClient.updateMember(memberID, data) return apiClient.updateMember(memberID, data)
} }
@ -60,10 +68,20 @@ class SocialRepositoryImpl(
return apiClient.retrievePartySeekingUsers(page) return apiClient.retrievePartySeekingUsers(page)
} }
override fun getGroupMembership(id: String) = authenticationHandler.userIDFlow.flatMapLatest { localRepository.getGroupMembership(it, id) } override fun getGroupMembership(id: String) =
authenticationHandler.userIDFlow.flatMapLatest {
localRepository.getGroupMembership(
it,
id,
)
}
override fun getGroupMemberships(): Flow<List<GroupMembership>> { override fun getGroupMemberships(): Flow<List<GroupMembership>> {
return authenticationHandler.userIDFlow.flatMapLatest { localRepository.getGroupMemberships(it) } return authenticationHandler.userIDFlow.flatMapLatest {
localRepository.getGroupMemberships(
it,
)
}
} }
override suspend fun retrieveGroupChat(groupId: String): List<ChatMessage>? { override suspend fun retrieveGroupChat(groupId: String): List<ChatMessage>? {
@ -80,7 +98,11 @@ class SocialRepositoryImpl(
apiClient.seenMessages(seenGroupId) apiClient.seenMessages(seenGroupId)
} }
override suspend fun flagMessage(chatMessageID: String, additionalInfo: String, groupID: String?): Void? { override suspend fun flagMessage(
chatMessageID: String,
additionalInfo: String,
groupID: String?,
): Void? {
return when { return when {
chatMessageID.isBlank() -> return null chatMessageID.isBlank() -> return null
currentUserID == BuildConfig.ANDROID_TESTING_UUID -> return null currentUserID == BuildConfig.ANDROID_TESTING_UUID -> return null
@ -96,7 +118,10 @@ class SocialRepositoryImpl(
} }
} }
override suspend fun reportMember(memberID: String, data: Map<String, String>): Void? { override suspend fun reportMember(
memberID: String,
data: Map<String, String>,
): Void? {
return apiClient.reportMember(memberID, data) return apiClient.reportMember(memberID, data)
} }
@ -120,13 +145,19 @@ class SocialRepositoryImpl(
return null return null
} }
override suspend fun postGroupChat(groupId: String, messageObject: HashMap<String, String>): PostChatMessageResult? { override suspend fun postGroupChat(
groupId: String,
messageObject: HashMap<String, String>,
): PostChatMessageResult? {
val result = apiClient.postGroupChat(groupId, messageObject) val result = apiClient.postGroupChat(groupId, messageObject)
result?.message?.groupId = groupId result?.message?.groupId = groupId
return result return result
} }
override suspend fun postGroupChat(groupId: String, message: String): PostChatMessageResult? { override suspend fun postGroupChat(
groupId: String,
message: String,
): PostChatMessageResult? {
val messageObject = HashMap<String, String>() val messageObject = HashMap<String, String>()
messageObject["message"] = message messageObject["message"] = message
return postGroupChat(groupId, messageObject) return postGroupChat(groupId, messageObject)
@ -146,7 +177,10 @@ class SocialRepositoryImpl(
return localRepository.getGroup(id) return localRepository.getGroup(id)
} }
override suspend fun leaveGroup(id: String?, keepChallenges: Boolean): Group? { override suspend fun leaveGroup(
id: String?,
keepChallenges: Boolean,
): Group? {
if (id?.isNotBlank() != true) { if (id?.isNotBlank() != true) {
return null return null
} }
@ -174,7 +208,7 @@ class SocialRepositoryImpl(
leader: String?, leader: String?,
type: String?, type: String?,
privacy: String?, privacy: String?,
leaderCreateChallenge: Boolean? leaderCreateChallenge: Boolean?,
): Group? { ): Group? {
val group = Group() val group = Group()
group.name = name group.name = name
@ -192,7 +226,7 @@ class SocialRepositoryImpl(
name: String?, name: String?,
description: String?, description: String?,
leader: String?, leader: String?,
leaderCreateChallenge: Boolean? leaderCreateChallenge: Boolean?,
): Group? { ): Group? {
if (group == null) { if (group == null) {
return null return null
@ -206,11 +240,21 @@ class SocialRepositoryImpl(
return apiClient.updateGroup(copiedGroup.id, copiedGroup) return apiClient.updateGroup(copiedGroup.id, copiedGroup)
} }
override fun getInboxConversations() = authenticationHandler.userIDFlow.flatMapLatest { localRepository.getInboxConversation(it) } override fun getInboxConversations() =
authenticationHandler.userIDFlow.flatMapLatest { localRepository.getInboxConversation(it) }
override fun getInboxMessages(replyToUserID: String?) = authenticationHandler.userIDFlow.flatMapLatest { localRepository.getInboxMessages(it, replyToUserID) } override fun getInboxMessages(replyToUserID: String?) =
authenticationHandler.userIDFlow.flatMapLatest {
localRepository.getInboxMessages(
it,
replyToUserID,
)
}
override suspend fun retrieveInboxMessages(uuid: String, page: Int): List<ChatMessage>? { override suspend fun retrieveInboxMessages(
uuid: String,
page: Int,
): List<ChatMessage>? {
val messages = apiClient.retrieveInboxMessages(uuid, page) ?: return null val messages = apiClient.retrieveInboxMessages(uuid, page) ?: return null
messages.forEach { messages.forEach {
it.isInboxMessage = true it.isInboxMessage = true
@ -225,12 +269,18 @@ class SocialRepositoryImpl(
return conversations return conversations
} }
override suspend fun postPrivateMessage(recipientId: String, messageObject: HashMap<String, String>): List<ChatMessage>? { override suspend fun postPrivateMessage(
recipientId: String,
messageObject: HashMap<String, String>,
): List<ChatMessage>? {
apiClient.postPrivateMessage(messageObject) apiClient.postPrivateMessage(messageObject)
return retrieveInboxMessages(recipientId, 0) return retrieveInboxMessages(recipientId, 0)
} }
override suspend fun postPrivateMessage(recipientId: String, message: String): List<ChatMessage>? { override suspend fun postPrivateMessage(
recipientId: String,
message: String,
): List<ChatMessage>? {
val messageObject = HashMap<String, String>() val messageObject = HashMap<String, String>()
messageObject["message"] = message messageObject["message"] = message
messageObject["toUserId"] = recipientId messageObject["toUserId"] = recipientId
@ -238,17 +288,28 @@ class SocialRepositoryImpl(
} }
override suspend fun getPartyMembers(id: String) = localRepository.getPartyMembers(id) override suspend fun getPartyMembers(id: String) = localRepository.getPartyMembers(id)
override suspend fun getGroupMembers(id: String) = localRepository.getGroupMembers(id) override suspend fun getGroupMembers(id: String) = localRepository.getGroupMembers(id)
override suspend fun retrievePartyMembers(id: String, includeAllPublicFields: Boolean): List<Member>? { override suspend fun retrievePartyMembers(
id: String,
includeAllPublicFields: Boolean,
): List<Member>? {
val members = apiClient.getGroupMembers(id, includeAllPublicFields) val members = apiClient.getGroupMembers(id, includeAllPublicFields)
members?.let { localRepository.savePartyMembers(id, it) } members?.let { localRepository.savePartyMembers(id, it) }
return members return members
} }
override suspend fun inviteToGroup(id: String, inviteData: Map<String, Any>) = apiClient.inviteToGroup(id, inviteData) override suspend fun inviteToGroup(
id: String,
inviteData: Map<String, Any>,
) =
apiClient.inviteToGroup(id, inviteData)
override suspend fun retrieveMember(userId: String?, fromHall: Boolean): Member? { override suspend fun retrieveMember(
userId: String?,
fromHall: Boolean,
): Member? {
return if (userId == null) { return if (userId == null) {
null null
} else { } else {
@ -265,9 +326,17 @@ class SocialRepositoryImpl(
} }
} }
override suspend fun retrievegroupInvites(id: String, includeAllPublicFields: Boolean) = apiClient.getGroupInvites(id, includeAllPublicFields) override suspend fun retrievegroupInvites(
id: String,
includeAllPublicFields: Boolean,
) =
apiClient.getGroupInvites(id, includeAllPublicFields)
override suspend fun findUsernames(username: String, context: String?, id: String?): List<FindUsernameResult>? { override suspend fun findUsernames(
username: String,
context: String?,
id: String?,
): List<FindUsernameResult>? {
return apiClient.findUsernames(username, context, id) return apiClient.findUsernames(username, context, id)
} }
@ -280,7 +349,10 @@ class SocialRepositoryImpl(
return apiClient.markPrivateMessagesRead() return apiClient.markPrivateMessagesRead()
} }
override fun markSomePrivateMessagesAsRead(user: User?, messages: List<ChatMessage>) { override fun markSomePrivateMessagesAsRead(
user: User?,
messages: List<ChatMessage>,
) {
if (user?.isManaged == true) { if (user?.isManaged == true) {
val numOfUnseenMessages = messages.count { !it.isSeen } val numOfUnseenMessages = messages.count { !it.isSeen }
localRepository.modify(user) { localRepository.modify(user) {
@ -299,9 +371,13 @@ class SocialRepositoryImpl(
} }
} }
override fun getUserGroups(type: String?) = authenticationHandler.userIDFlow.flatMapLatest { localRepository.getUserGroups(it, type) } override fun getUserGroups(type: String?) =
authenticationHandler.userIDFlow.flatMapLatest { localRepository.getUserGroups(it, type) }
override suspend fun acceptQuest(user: User?, partyId: String): Void? { override suspend fun acceptQuest(
user: User?,
partyId: String,
): Void? {
apiClient.acceptQuest(partyId) apiClient.acceptQuest(partyId)
user?.let { user?.let {
localRepository.updateRSVPNeeded(it, false) localRepository.updateRSVPNeeded(it, false)
@ -309,7 +385,10 @@ class SocialRepositoryImpl(
return null return null
} }
override suspend fun rejectQuest(user: User?, partyId: String): Void? { override suspend fun rejectQuest(
user: User?,
partyId: String,
): Void? {
apiClient.rejectQuest(partyId) apiClient.rejectQuest(partyId)
user?.let { user?.let {
localRepository.updateRSVPNeeded(it, false) localRepository.updateRSVPNeeded(it, false)
@ -353,7 +432,10 @@ class SocialRepositoryImpl(
} }
} }
override suspend fun transferGems(giftedID: String, amount: Int): Void? { override suspend fun transferGems(
giftedID: String,
amount: Int,
): Void? {
return apiClient.transferGems(giftedID, amount) return apiClient.transferGems(giftedID, amount)
} }
} }

View file

@ -14,10 +14,9 @@ import kotlinx.coroutines.flow.flatMapLatest
class TagRepositoryImpl( class TagRepositoryImpl(
localRepository: TagLocalRepository, localRepository: TagLocalRepository,
apiClient: ApiClient, apiClient: ApiClient,
authenticationHandler: AuthenticationHandler authenticationHandler: AuthenticationHandler,
) : BaseRepositoryImpl<TagLocalRepository>(localRepository, apiClient, authenticationHandler), ) : BaseRepositoryImpl<TagLocalRepository>(localRepository, apiClient, authenticationHandler),
TagRepository { TagRepository {
override fun getTags() = authenticationHandler.userIDFlow.flatMapLatest { getTags(it) } override fun getTags() = authenticationHandler.userIDFlow.flatMapLatest { getTags(it) }
override fun getTags(userId: String): Flow<List<Tag>> { override fun getTags(userId: String): Flow<List<Tag>> {

View file

@ -39,17 +39,33 @@ class TaskRepositoryImpl(
apiClient: ApiClient, apiClient: ApiClient,
authenticationHandler: AuthenticationHandler, authenticationHandler: AuthenticationHandler,
val appConfigManager: AppConfigManager, val appConfigManager: AppConfigManager,
) : BaseRepositoryImpl<TaskLocalRepository>(localRepository, apiClient, authenticationHandler), TaskRepository { ) : BaseRepositoryImpl<TaskLocalRepository>(localRepository, apiClient, authenticationHandler),
TaskRepository {
private var lastTaskAction: Long = 0 private var lastTaskAction: Long = 0
override fun getTasks(taskType: TaskType, userID: String?, includedGroupIDs: Array<String>): Flow<List<Task>> = override fun getTasks(
this.localRepository.getTasks(taskType, userID ?: authenticationHandler.currentUserID ?: "", includedGroupIDs) taskType: TaskType,
userID: String?,
includedGroupIDs: Array<String>,
): Flow<List<Task>> =
this.localRepository.getTasks(
taskType,
userID ?: authenticationHandler.currentUserID ?: "",
includedGroupIDs,
)
override fun saveTasks(userId: String, order: TasksOrder, tasks: TaskList) { override fun saveTasks(
userId: String,
order: TasksOrder,
tasks: TaskList,
) {
localRepository.saveTasks(userId, order, tasks) localRepository.saveTasks(userId, order, tasks)
} }
override suspend fun retrieveTasks(userId: String, tasksOrder: TasksOrder): TaskList? { override suspend fun retrieveTasks(
userId: String,
tasksOrder: TasksOrder,
): TaskList? {
val tasks = apiClient.getTasks() ?: return null val tasks = apiClient.getTasks() ?: return null
this.localRepository.saveTasks(userId, tasksOrder, tasks) this.localRepository.saveTasks(userId, tasksOrder, tasks)
return tasks return tasks
@ -58,11 +74,18 @@ class TaskRepositoryImpl(
override suspend fun retrieveCompletedTodos(userId: String?): TaskList? { override suspend fun retrieveCompletedTodos(userId: String?): TaskList? {
val taskList = this.apiClient.getTasks("completedTodos") ?: return null val taskList = this.apiClient.getTasks("completedTodos") ?: return null
val tasks = taskList.tasks val tasks = taskList.tasks
this.localRepository.saveCompletedTodos(userId ?: authenticationHandler.currentUserID ?: "", tasks.values) this.localRepository.saveCompletedTodos(
userId ?: authenticationHandler.currentUserID ?: "",
tasks.values,
)
return taskList return taskList
} }
override suspend fun retrieveTasks(userId: String, tasksOrder: TasksOrder, dueDate: Date): TaskList? { override suspend fun retrieveTasks(
userId: String,
tasksOrder: TasksOrder,
dueDate: Date,
): TaskList? {
val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZZZZZ", Locale.US) val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZZZZZ", Locale.US)
val taskList = this.apiClient.getTasks("dailys", formatter.format(dueDate)) ?: return null val taskList = this.apiClient.getTasks("dailys", formatter.format(dueDate)) ?: return null
this.localRepository.saveTasks(userId, tasksOrder, taskList) this.localRepository.saveTasks(userId, tasksOrder, taskList)
@ -75,10 +98,15 @@ class TaskRepositoryImpl(
task: Task, task: Task,
up: Boolean, up: Boolean,
force: Boolean, force: Boolean,
notifyFunc: ((TaskScoringResult) -> Unit)? notifyFunc: ((TaskScoringResult) -> Unit)?,
): TaskScoringResult? { ): TaskScoringResult? {
val localData = if (user != null && appConfigManager.enableLocalTaskScoring()) { val localData =
ScoreTaskLocallyInteractor.score(user, task, if (up) TaskDirection.UP else TaskDirection.DOWN) if (user != null && appConfigManager.enableLocalTaskScoring()) {
ScoreTaskLocallyInteractor.score(
user,
task,
if (up) TaskDirection.UP else TaskDirection.DOWN,
)
} else { } else {
null null
} }
@ -96,9 +124,15 @@ class TaskRepositoryImpl(
} }
lastTaskAction = now lastTaskAction = now
val res = this.apiClient.postTaskDirection(id, (if (up) TaskDirection.UP else TaskDirection.DOWN).text) ?: return null val res =
this.apiClient.postTaskDirection(
id,
(if (up) TaskDirection.UP else TaskDirection.DOWN).text,
) ?: return null
// There are cases where the user object is not set correctly. So the app refetches it as a fallback // There are cases where the user object is not set correctly. So the app refetches it as a fallback
val thisUser = user ?: localRepository.getUser(authenticationHandler.currentUserID ?: "").firstOrNull() ?: return null val thisUser =
user ?: localRepository.getUser(authenticationHandler.currentUserID ?: "").firstOrNull()
?: return null
// save local task changes // save local task changes
Analytics.sendEvent( Analytics.sendEvent(
@ -108,8 +142,8 @@ class TaskRepositoryImpl(
mapOf( mapOf(
"type" to (task.type ?: ""), "type" to (task.type ?: ""),
"scored_up" to up, "scored_up" to up,
"value" to task.value "value" to task.value,
) ),
) )
if (res.lvl == 0) { if (res.lvl == 0) {
// Team tasks that require approval have weird data that we should just ignore. // Team tasks that require approval have weird data that we should just ignore.
@ -132,7 +166,7 @@ class TaskRepositoryImpl(
res: TaskDirectionData, res: TaskDirectionData,
task: Task, task: Task,
up: Boolean, up: Boolean,
localDelta: Float localDelta: Float,
) { ) {
this.localRepository.executeTransaction { this.localRepository.executeTransaction {
val bgTask = localRepository.getLiveObject(task) ?: return@executeTransaction val bgTask = localRepository.getLiveObject(task) ?: return@executeTransaction
@ -157,7 +191,8 @@ class TaskRepositoryImpl(
} }
if (bgTask.isGroupTask) { if (bgTask.isGroupTask) {
val entry = bgTask.group?.assignedUsersDetail?.firstOrNull { it.assignedUserID == user.id } val entry =
bgTask.group?.assignedUsersDetail?.firstOrNull { it.assignedUserID == user.id }
entry?.completed = up entry?.completed = up
if (up) { if (up) {
entry?.completedDate = Date() entry?.completedDate = Date()
@ -167,12 +202,15 @@ class TaskRepositoryImpl(
} }
} }
res._tmp?.drop?.key?.let { key -> res._tmp?.drop?.key?.let { key ->
val type = when (res._tmp?.drop?.type?.lowercase(Locale.US)) { val type =
when (res._tmp?.drop?.type?.lowercase(Locale.US)) {
"hatchingpotion" -> "hatchingPotions" "hatchingpotion" -> "hatchingPotions"
"egg" -> "eggs" "egg" -> "eggs"
else -> res._tmp?.drop?.type?.lowercase(Locale.US) else -> res._tmp?.drop?.type?.lowercase(Locale.US)
} }
var item = it.where(OwnedItem::class.java).equalTo("itemType", type).equalTo("key", key).findFirst() var item =
it.where(OwnedItem::class.java).equalTo("itemType", type).equalTo("key", key)
.findFirst()
if (item == null) { if (item == null) {
item = OwnedItem() item = OwnedItem()
item.key = key item.key = key
@ -201,7 +239,10 @@ class TaskRepositoryImpl(
} }
} }
override suspend fun markTaskNeedsWork(task: Task, userID: String) { override suspend fun markTaskNeedsWork(
task: Task,
userID: String,
) {
val savedTask = apiClient.markTaskNeedsWork(task.id ?: "", userID) val savedTask = apiClient.markTaskNeedsWork(task.id ?: "", userID)
if (savedTask != null) { if (savedTask != null) {
savedTask.id = task.id savedTask.id = task.id
@ -219,13 +260,16 @@ class TaskRepositoryImpl(
taskId: String, taskId: String,
up: Boolean, up: Boolean,
force: Boolean, force: Boolean,
notifyFunc: ((TaskScoringResult) -> Unit)? notifyFunc: ((TaskScoringResult) -> Unit)?,
): TaskScoringResult? { ): TaskScoringResult? {
val task = localRepository.getTask(taskId).firstOrNull() ?: return null val task = localRepository.getTask(taskId).firstOrNull() ?: return null
return taskChecked(user, task, up, force, notifyFunc) return taskChecked(user, task, up, force, notifyFunc)
} }
override suspend fun scoreChecklistItem(taskId: String, itemId: String): Task? { override suspend fun scoreChecklistItem(
taskId: String,
itemId: String,
): Task? {
val task = apiClient.scoreChecklistItem(taskId, itemId) val task = apiClient.scoreChecklistItem(taskId, itemId)
val updatedItem: ChecklistItem? = task?.checklist?.lastOrNull { itemId == it.id } val updatedItem: ChecklistItem? = task?.checklist?.lastOrNull { itemId == it.id }
if (updatedItem != null) { if (updatedItem != null) {
@ -238,7 +282,10 @@ class TaskRepositoryImpl(
override fun getTaskCopy(taskId: String) = localRepository.getTaskCopy(taskId) override fun getTaskCopy(taskId: String) = localRepository.getTaskCopy(taskId)
override suspend fun createTask(task: Task, force: Boolean): Task? { override suspend fun createTask(
task: Task,
force: Boolean,
): Task? {
val now = Date().time val now = Date().time
if (lastTaskAction > now - 500 && !force) { if (lastTaskAction > now - 500 && !force) {
return null return null
@ -248,7 +295,8 @@ class TaskRepositoryImpl(
task.isSaving = true task.isSaving = true
task.isCreating = true task.isCreating = true
task.hasErrored = false task.hasErrored = false
task.ownerID = if (task.isGroupTask) { task.ownerID =
if (task.isGroupTask) {
task.group?.groupID ?: "" task.group?.groupID ?: ""
} else { } else {
authenticationHandler.currentUserID ?: "" authenticationHandler.currentUserID ?: ""
@ -258,7 +306,8 @@ class TaskRepositoryImpl(
} }
localRepository.save(task) localRepository.save(task)
val savedTask = if (task.isGroupTask) { val savedTask =
if (task.isGroupTask) {
apiClient.createGroupTask(task.group?.groupID ?: "", task) apiClient.createGroupTask(task.group?.groupID ?: "", task)
} else { } else {
apiClient.createTask(task) apiClient.createTask(task)
@ -276,7 +325,10 @@ class TaskRepositoryImpl(
} }
@Suppress("ReturnCount") @Suppress("ReturnCount")
override suspend fun updateTask(task: Task, force: Boolean): Task? { override suspend fun updateTask(
task: Task,
force: Boolean,
): Task? {
val now = Date().time val now = Date().time
if ((lastTaskAction > now - 500 && !force) || !task.isValid) { if ((lastTaskAction > now - 500 && !force) || !task.isValid) {
return task return task
@ -314,41 +366,64 @@ class TaskRepositoryImpl(
override suspend fun createTasks(newTasks: List<Task>) = apiClient.createTasks(newTasks) override suspend fun createTasks(newTasks: List<Task>) = apiClient.createTasks(newTasks)
override fun markTaskCompleted(taskId: String, isCompleted: Boolean) { override fun markTaskCompleted(
taskId: String,
isCompleted: Boolean,
) {
localRepository.markTaskCompleted(taskId, isCompleted) localRepository.markTaskCompleted(taskId, isCompleted)
} }
override fun <T : BaseMainObject> modify(obj: T, transaction: (T) -> Unit) { override fun <T : BaseMainObject> modify(
obj: T,
transaction: (T) -> Unit,
) {
localRepository.modify(obj, transaction) localRepository.modify(obj, transaction)
} }
override fun swapTaskPosition(firstPosition: Int, secondPosition: Int) { override fun swapTaskPosition(
firstPosition: Int,
secondPosition: Int,
) {
localRepository.swapTaskPosition(firstPosition, secondPosition) localRepository.swapTaskPosition(firstPosition, secondPosition)
} }
override suspend fun updateTaskPosition(taskType: TaskType, taskID: String, newPosition: Int): List<String>? { override suspend fun updateTaskPosition(
taskType: TaskType,
taskID: String,
newPosition: Int,
): List<String>? {
val positions = apiClient.postTaskNewPosition(taskID, newPosition) ?: return null val positions = apiClient.postTaskNewPosition(taskID, newPosition) ?: return null
localRepository.updateTaskPositions(positions) localRepository.updateTaskPositions(positions)
return positions return positions
} }
override fun getUnmanagedTask(taskid: String) = getTask(taskid).map { localRepository.getUnmanagedCopy(it) } override fun getUnmanagedTask(taskid: String) =
getTask(taskid).map { localRepository.getUnmanagedCopy(it) }
override fun updateTaskInBackground(task: Task, assignChanges: Map<String, MutableList<String>>) { override fun updateTaskInBackground(
task: Task,
assignChanges: Map<String, MutableList<String>>,
) {
MainScope().launchCatching { MainScope().launchCatching {
val updatedTask = updateTask(task) ?: return@launchCatching val updatedTask = updateTask(task) ?: return@launchCatching
handleAssignmentChanges(updatedTask, assignChanges) handleAssignmentChanges(updatedTask, assignChanges)
} }
} }
override fun createTaskInBackground(task: Task, assignChanges: Map<String, MutableList<String>>) { override fun createTaskInBackground(
task: Task,
assignChanges: Map<String, MutableList<String>>,
) {
MainScope().launchCatching { MainScope().launchCatching {
val createdTask = createTask(task) ?: return@launchCatching val createdTask = createTask(task) ?: return@launchCatching
handleAssignmentChanges(createdTask, assignChanges) handleAssignmentChanges(createdTask, assignChanges)
} }
} }
private suspend fun handleAssignmentChanges(task: Task, assignChanges: Map<String, MutableList<String>>) { private suspend fun handleAssignmentChanges(
task: Task,
assignChanges: Map<String, MutableList<String>>,
) {
val taskID = task.id ?: return val taskID = task.id ?: return
assignChanges["assign"]?.let { assignments -> assignChanges["assign"]?.let { assignments ->
if (assignments.isEmpty()) return@let if (assignments.isEmpty()) return@let
@ -373,11 +448,13 @@ class TaskRepositoryImpl(
} }
} }
override fun getTaskCopies(): Flow<List<Task>> = authenticationHandler.userIDFlow.flatMapLatest { override fun getTaskCopies(): Flow<List<Task>> =
authenticationHandler.userIDFlow.flatMapLatest {
localRepository.getTasks(it) localRepository.getTasks(it)
}.map { localRepository.getUnmanagedCopy(it) } }.map { localRepository.getUnmanagedCopy(it) }
override fun getTaskCopies(tasks: List<Task>): List<Task> = localRepository.getUnmanagedCopy(tasks) override fun getTaskCopies(tasks: List<Task>): List<Task> =
localRepository.getUnmanagedCopy(tasks)
override suspend fun retrieveDailiesFromDate(date: Date): TaskList? { override suspend fun retrieveDailiesFromDate(date: Date): TaskList? {
val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZZZZZ", Locale.US) val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZZZZZ", Locale.US)
@ -385,7 +462,7 @@ class TaskRepositoryImpl(
} }
override suspend fun syncErroredTasks(): List<Task>? { override suspend fun syncErroredTasks(): List<Task>? {
val tasks = localRepository.getErroredTasks(currentUserID ?: "").firstOrNull() val tasks = localRepository.getErroredTasks(currentUserID).firstOrNull()
return tasks?.map { localRepository.getUnmanagedCopy(it) }?.mapNotNull { return tasks?.map { localRepository.getUnmanagedCopy(it) }?.mapNotNull {
if (it.isCreating) { if (it.isCreating) {
createTask(it, true) createTask(it, true)
@ -395,11 +472,14 @@ class TaskRepositoryImpl(
} }
} }
override suspend fun unlinkAllTasks(challengeID: String?, keepOption: String): Void? { override suspend fun unlinkAllTasks(
challengeID: String?,
keepOption: String,
): Void? {
return apiClient.unlinkAllTasks(challengeID, keepOption) return apiClient.unlinkAllTasks(challengeID, keepOption)
} }
override fun getTasksForChallenge(challengeID: String?): Flow<List<Task>> { override fun getTasksForChallenge(challengeID: String?): Flow<List<Task>> {
return localRepository.getTasksForChallenge(challengeID, currentUserID ?: "") return localRepository.getTasksForChallenge(challengeID, currentUserID)
} }
} }

View file

@ -10,9 +10,9 @@ import kotlinx.coroutines.flow.Flow
class TutorialRepositoryImpl( class TutorialRepositoryImpl(
localRepository: TutorialLocalRepository, localRepository: TutorialLocalRepository,
apiClient: ApiClient, apiClient: ApiClient,
authenticationHandler: AuthenticationHandler authenticationHandler: AuthenticationHandler,
) : BaseRepositoryImpl<TutorialLocalRepository>(localRepository, apiClient, authenticationHandler), TutorialRepository { ) : BaseRepositoryImpl<TutorialLocalRepository>(localRepository, apiClient, authenticationHandler),
TutorialRepository {
override fun getTutorialStep(key: String): Flow<TutorialStep> = override fun getTutorialStep(key: String): Flow<TutorialStep> =
localRepository.getTutorialStep(key) localRepository.getTutorialStep(key)

View file

@ -6,21 +6,28 @@ import com.habitrpg.android.habitica.models.user.User
import io.realm.Realm import io.realm.Realm
interface BaseLocalRepository { interface BaseLocalRepository {
val isClosed: Boolean val isClosed: Boolean
var realm: Realm var realm: Realm
fun close() fun close()
fun executeTransaction(transaction: (Realm) -> Unit) fun executeTransaction(transaction: (Realm) -> Unit)
fun <T : BaseMainObject> modify(obj: T, transaction: (T) -> Unit)
fun <T : BaseMainObject> modify(
obj: T,
transaction: (T) -> Unit,
)
fun <T : BaseObject> getLiveObject(obj: T): T? fun <T : BaseObject> getLiveObject(obj: T): T?
fun <T : BaseObject> getUnmanagedCopy(managedObject: T): T fun <T : BaseObject> getUnmanagedCopy(managedObject: T): T
fun <T : BaseObject> getUnmanagedCopy(list: List<T>): List<T> fun <T : BaseObject> getUnmanagedCopy(list: List<T>): List<T>
fun <T : BaseObject> save(objects: List<T>) fun <T : BaseObject> save(objects: List<T>)
fun <T : BaseObject> save(`object`: T) fun <T : BaseObject> save(`object`: T)
fun <T : BaseMainObject> delete(obj: T) fun <T : BaseMainObject> delete(obj: T)
fun getLiveUser(id: String): User? fun getLiveUser(id: String): User?

View file

@ -6,22 +6,36 @@ import com.habitrpg.android.habitica.models.tasks.Task
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface ChallengeLocalRepository : BaseLocalRepository { interface ChallengeLocalRepository : BaseLocalRepository {
val challenges: Flow<List<Challenge>> val challenges: Flow<List<Challenge>>
fun getChallenge(id: String): Flow<Challenge> fun getChallenge(id: String): Flow<Challenge>
fun getTasks(challengeID: String): Flow<List<Task>> fun getTasks(challengeID: String): Flow<List<Task>>
fun getUserChallenges(userId: String): Flow<List<Challenge>> fun getUserChallenges(userId: String): Flow<List<Challenge>>
fun setParticipating(userID: String, challengeID: String, isParticipating: Boolean) fun setParticipating(
userID: String,
challengeID: String,
isParticipating: Boolean,
)
fun saveChallenges( fun saveChallenges(
challenges: List<Challenge>, challenges: List<Challenge>,
clearChallenges: Boolean, clearChallenges: Boolean,
memberOnly: Boolean, memberOnly: Boolean,
userID: String userID: String,
) )
fun getChallengeMembership(userId: String, id: String): Flow<ChallengeMembership>
fun getChallengeMembership(
userId: String,
id: String,
): Flow<ChallengeMembership>
fun getChallengeMemberships(userId: String): Flow<List<ChallengeMembership>> fun getChallengeMemberships(userId: String): Flow<List<ChallengeMembership>>
fun isChallengeMember(userID: String, challengeID: String): Flow<Boolean>
fun isChallengeMember(
userID: String,
challengeID: String,
): Flow<Boolean>
} }

View file

@ -6,6 +6,8 @@ import kotlinx.coroutines.flow.Flow
interface ContentLocalRepository : BaseLocalRepository { interface ContentLocalRepository : BaseLocalRepository {
fun saveContent(contentResult: ContentResult) fun saveContent(contentResult: ContentResult)
fun saveWorldState(worldState: WorldState) fun saveWorldState(worldState: WorldState)
fun getWorldState(): Flow<WorldState> fun getWorldState(): Flow<WorldState>
} }

View file

@ -4,5 +4,9 @@ import com.habitrpg.android.habitica.models.inventory.Customization
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface CustomizationLocalRepository : ContentLocalRepository { interface CustomizationLocalRepository : ContentLocalRepository {
fun getCustomizations(type: String, category: String?, onlyAvailable: Boolean): Flow<List<Customization>> fun getCustomizations(
type: String,
category: String?,
onlyAvailable: Boolean,
): Flow<List<Customization>>
} }

View file

@ -14,8 +14,8 @@ import com.habitrpg.android.habitica.models.user.User
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface InventoryLocalRepository : ContentLocalRepository { interface InventoryLocalRepository : ContentLocalRepository {
fun getArmoireRemainingCount(): Flow<Int> fun getArmoireRemainingCount(): Flow<Int>
fun getOwnedEquipment(): Flow<List<Equipment>> fun getOwnedEquipment(): Flow<List<Equipment>>
fun getMounts(): Flow<List<Mount>> fun getMounts(): Flow<List<Mount>>
@ -27,44 +27,116 @@ interface InventoryLocalRepository : ContentLocalRepository {
fun getOwnedPets(userID: String): Flow<List<OwnedPet>> fun getOwnedPets(userID: String): Flow<List<OwnedPet>>
fun getInAppRewards(): Flow<List<ShopItem>> fun getInAppRewards(): Flow<List<ShopItem>>
fun getInAppReward(key: String): Flow<ShopItem> fun getInAppReward(key: String): Flow<ShopItem>
fun getQuestContent(key: String): Flow<QuestContent?> fun getQuestContent(key: String): Flow<QuestContent?>
fun getQuestContent(keys: List<String>): Flow<List<QuestContent>> fun getQuestContent(keys: List<String>): Flow<List<QuestContent>>
fun getEquipment(searchedKeys: List<String>): Flow<List<Equipment>> fun getEquipment(searchedKeys: List<String>): Flow<List<Equipment>>
fun getOwnedEquipment(type: String): Flow<List<Equipment>> fun getOwnedEquipment(type: String): Flow<List<Equipment>>
fun getOwnedItems(itemType: String, userID: String, includeZero: Boolean): Flow<List<OwnedItem>> fun getOwnedItems(
fun getOwnedItems(userID: String, includeZero: Boolean): Flow<Map<String, OwnedItem>> itemType: String,
fun getEquipmentType(type: String, set: String): Flow<List<Equipment>> userID: String,
includeZero: Boolean,
): Flow<List<OwnedItem>>
fun getOwnedItems(
userID: String,
includeZero: Boolean,
): Flow<Map<String, OwnedItem>>
fun getEquipmentType(
type: String,
set: String,
): Flow<List<Equipment>>
fun getEquipment(key: String): Flow<Equipment> fun getEquipment(key: String): Flow<Equipment>
fun getMounts(type: String?, group: String?, color: String?): Flow<List<Mount>>
fun getPets(type: String?, group: String?, color: String?): Flow<List<Pet>> fun getMounts(
type: String?,
group: String?,
color: String?,
): Flow<List<Mount>>
fun getPets(
type: String?,
group: String?,
color: String?,
): Flow<List<Pet>>
fun updateOwnedEquipment(user: User) fun updateOwnedEquipment(user: User)
suspend fun changeOwnedCount(type: String, key: String, userID: String, amountToAdd: Int) suspend fun changeOwnedCount(
fun changeOwnedCount(item: OwnedItem, amountToAdd: Int?) type: String,
key: String,
userID: String,
amountToAdd: Int,
)
fun getItem(type: String, key: String): Flow<Item> fun changeOwnedCount(
fun getOwnedItem(userID: String, type: String, key: String, includeZero: Boolean): Flow<OwnedItem> item: OwnedItem,
amountToAdd: Int?,
)
fun getItem(
type: String,
key: String,
): Flow<Item>
fun getOwnedItem(
userID: String,
type: String,
key: String,
includeZero: Boolean,
): Flow<OwnedItem>
fun decrementMysteryItemCount(user: User?) fun decrementMysteryItemCount(user: User?)
fun saveInAppRewards(onlineItems: List<ShopItem>) fun saveInAppRewards(onlineItems: List<ShopItem>)
fun hatchPet(eggKey: String, potionKey: String, userID: String) fun hatchPet(
fun unhatchPet(eggKey: String, potionKey: String, userID: String) eggKey: String,
fun feedPet(foodKey: String, petKey: String, feedValue: Int, userID: String) potionKey: String,
userID: String,
)
fun unhatchPet(
eggKey: String,
potionKey: String,
userID: String,
)
fun feedPet(
foodKey: String,
petKey: String,
feedValue: Int,
userID: String,
)
fun getLatestMysteryItem(): Flow<Equipment> fun getLatestMysteryItem(): Flow<Equipment>
fun soldItem(userID: String, updatedUser: User): User
fun soldItem(
userID: String,
updatedUser: User,
): User
fun getAvailableLimitedItems(): Flow<List<Item>> fun getAvailableLimitedItems(): Flow<List<Item>>
fun save(items: Items, userID: String) fun save(
items: Items,
userID: String,
)
fun getLiveObject(obj: OwnedItem): OwnedItem? fun getLiveObject(obj: OwnedItem): OwnedItem?
fun getItems(itemClass: Class<out Item>): Flow<List<Item>> fun getItems(itemClass: Class<out Item>): Flow<List<Item>>
fun getItems(itemClass: Class<out Item>, keys: Array<String>): Flow<List<Item>>
fun getItems(
itemClass: Class<out Item>,
keys: Array<String>,
): Flow<List<Item>>
} }

View file

@ -10,9 +10,13 @@ import io.realm.RealmResults
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface SocialLocalRepository : BaseLocalRepository { interface SocialLocalRepository : BaseLocalRepository {
fun getUserGroups(userID: String, type: String?): Flow<List<Group>> fun getUserGroups(
userID: String,
type: String?,
): Flow<List<Group>>
fun getGroup(id: String): Flow<Group?> fun getGroup(id: String): Flow<Group?>
fun saveGroup(group: Group) fun saveGroup(group: Group)
fun getGroupChat(groupId: String): Flow<List<ChatMessage>> fun getGroupChat(groupId: String): Flow<List<ChatMessage>>
@ -20,36 +24,80 @@ interface SocialLocalRepository : BaseLocalRepository {
fun deleteMessage(id: String) fun deleteMessage(id: String)
fun getPartyMembers(partyId: String): Flow<List<Member>> fun getPartyMembers(partyId: String): Flow<List<Member>>
fun getGroupMembers(groupID: String): Flow<List<Member>> fun getGroupMembers(groupID: String): Flow<List<Member>>
fun updateRSVPNeeded(user: User?, newValue: Boolean) fun updateRSVPNeeded(
user: User?,
newValue: Boolean,
)
fun likeMessage(chatMessage: ChatMessage, userId: String, liked: Boolean) fun likeMessage(
chatMessage: ChatMessage,
userId: String,
liked: Boolean,
)
fun savePartyMembers(groupId: String?, members: List<Member>) fun savePartyMembers(
groupId: String?,
members: List<Member>,
)
fun removeQuest(partyId: String) fun removeQuest(partyId: String)
fun setQuestActivity(party: Group?, active: Boolean) fun setQuestActivity(
party: Group?,
active: Boolean,
)
fun saveChatMessages(groupId: String?, chatMessages: List<ChatMessage>) fun saveChatMessages(
groupId: String?,
chatMessages: List<ChatMessage>,
)
fun doesGroupExist(id: String): Boolean fun doesGroupExist(id: String): Boolean
fun updateMembership(userId: String, id: String, isMember: Boolean)
fun getGroupMembership(userId: String, id: String): Flow<GroupMembership?>
fun getGroupMemberships(userId: String): Flow<List<GroupMembership>>
fun rejectGroupInvitation(userID: String, groupID: String)
fun getInboxMessages(userId: String, replyToUserID: String?): Flow<RealmResults<ChatMessage>> fun updateMembership(
userId: String,
id: String,
isMember: Boolean,
)
fun getGroupMembership(
userId: String,
id: String,
): Flow<GroupMembership?>
fun getGroupMemberships(userId: String): Flow<List<GroupMembership>>
fun rejectGroupInvitation(
userID: String,
groupID: String,
)
fun getInboxMessages(
userId: String,
replyToUserID: String?,
): Flow<RealmResults<ChatMessage>>
fun getInboxConversation(userId: String): Flow<RealmResults<InboxConversation>> fun getInboxConversation(userId: String): Flow<RealmResults<InboxConversation>>
fun saveGroupMemberships(userID: String?, memberships: List<GroupMembership>)
fun saveGroupMemberships(
userID: String?,
memberships: List<GroupMembership>,
)
fun saveInboxMessages( fun saveInboxMessages(
userID: String, userID: String,
recipientID: String, recipientID: String,
messages: List<ChatMessage>, messages: List<ChatMessage>,
page: Int page: Int,
) )
fun saveInboxConversations(userID: String, conversations: List<InboxConversation>)
fun saveInboxConversations(
userID: String,
conversations: List<InboxConversation>,
)
fun getMember(userID: String?): Flow<Member?> fun getMember(userID: String?): Flow<Member?>
} }

View file

@ -8,28 +8,56 @@ import com.habitrpg.shared.habitica.models.tasks.TasksOrder
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface TaskLocalRepository : BaseLocalRepository { interface TaskLocalRepository : BaseLocalRepository {
fun getTasks(
taskType: TaskType,
userID: String,
includedGroupIDs: Array<String>,
): Flow<List<Task>>
fun getTasks(taskType: TaskType, userID: String, includedGroupIDs: Array<String>): Flow<List<Task>>
fun getTasks(userId: String): Flow<List<Task>> fun getTasks(userId: String): Flow<List<Task>>
fun saveTasks(ownerID: String, tasksOrder: TasksOrder, tasks: TaskList) fun saveTasks(
ownerID: String,
tasksOrder: TasksOrder,
tasks: TaskList,
)
fun deleteTask(taskID: String) fun deleteTask(taskID: String)
fun getTask(taskId: String): Flow<Task> fun getTask(taskId: String): Flow<Task>
fun getTaskCopy(taskId: String): Flow<Task> fun getTaskCopy(taskId: String): Flow<Task>
fun markTaskCompleted(taskId: String, isCompleted: Boolean) fun markTaskCompleted(
taskId: String,
isCompleted: Boolean,
)
fun swapTaskPosition(firstPosition: Int, secondPosition: Int) fun swapTaskPosition(
firstPosition: Int,
secondPosition: Int,
)
fun getTaskAtPosition(taskType: String, position: Int): Flow<Task> fun getTaskAtPosition(
taskType: String,
position: Int,
): Flow<Task>
fun updateIsdue(daily: TaskList): TaskList fun updateIsdue(daily: TaskList): TaskList
fun updateTaskPositions(taskOrder: List<String>) fun updateTaskPositions(taskOrder: List<String>)
fun saveCompletedTodos(userId: String, tasks: MutableCollection<Task>)
fun saveCompletedTodos(
userId: String,
tasks: MutableCollection<Task>,
)
fun getErroredTasks(userID: String): Flow<List<Task>> fun getErroredTasks(userID: String): Flow<List<Task>>
fun getUser(userID: String): Flow<User> fun getUser(userID: String): Flow<User>
fun getTasksForChallenge(challengeID: String?, userID: String?): Flow<List<Task>>
fun getTasksForChallenge(
challengeID: String?,
userID: String?,
): Flow<List<Task>>
} }

View file

@ -4,7 +4,7 @@ import com.habitrpg.android.habitica.models.TutorialStep
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface TutorialLocalRepository : BaseLocalRepository { interface TutorialLocalRepository : BaseLocalRepository {
fun getTutorialStep(key: String): Flow<TutorialStep> fun getTutorialStep(key: String): Flow<TutorialStep>
fun getTutorialSteps(keys: List<String>): Flow<List<TutorialStep>> fun getTutorialSteps(keys: List<String>): Flow<List<TutorialStep>>
} }

View file

@ -13,20 +13,28 @@ import io.realm.RealmResults
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface UserLocalRepository : BaseLocalRepository { interface UserLocalRepository : BaseLocalRepository {
suspend fun getTutorialSteps(): Flow<RealmResults<TutorialStep>> suspend fun getTutorialSteps(): Flow<RealmResults<TutorialStep>>
fun getUser(userID: String): Flow<User?> fun getUser(userID: String): Flow<User?>
fun saveUser(user: User, overrideExisting: Boolean = true)
fun saveUser(
user: User,
overrideExisting: Boolean = true,
)
fun saveMessages(messages: List<ChatMessage>) fun saveMessages(messages: List<ChatMessage>)
fun getSkills(user: User): Flow<List<Skill>> fun getSkills(user: User): Flow<List<Skill>>
fun getSpecialItems(user: User): Flow<List<Skill>> fun getSpecialItems(user: User): Flow<List<Skill>>
fun getAchievements(): Flow<List<Achievement>> fun getAchievements(): Flow<List<Achievement>>
fun getQuestAchievements(userID: String): Flow<List<QuestAchievement>> fun getQuestAchievements(userID: String): Flow<List<QuestAchievement>>
fun getUserQuestStatus(userID: String): Flow<UserQuestStatus> fun getUserQuestStatus(userID: String): Flow<UserQuestStatus>
fun getTeamPlans(userID: String): Flow<List<TeamPlan>> fun getTeamPlans(userID: String): Flow<List<TeamPlan>>
fun getTeamPlan(teamID: String): Flow<Group?> fun getTeamPlan(teamID: String): Flow<Group?>
} }

View file

@ -14,8 +14,8 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
abstract class RealmBaseLocalRepository internal constructor(override var realm: Realm) : BaseLocalRepository { abstract class RealmBaseLocalRepository internal constructor(override var realm: Realm) :
BaseLocalRepository {
override val isClosed: Boolean override val isClosed: Boolean
get() = realm.isClosed get() = realm.isClosed
@ -39,7 +39,9 @@ abstract class RealmBaseLocalRepository internal constructor(override var realm:
} }
override fun <T : BaseObject> getUnmanagedCopy(list: List<T>): List<T> { override fun <T : BaseObject> getUnmanagedCopy(list: List<T>): List<T> {
if (isClosed) { return emptyList() } if (isClosed) {
return emptyList()
}
return realm.copyFromRealm(list) return realm.copyFromRealm(list)
} }
@ -48,7 +50,10 @@ abstract class RealmBaseLocalRepository internal constructor(override var realm:
private var pendingSaves = mutableListOf<Any>() private var pendingSaves = mutableListOf<Any>()
} }
private fun <T : RealmModel> copy(realm: Realm, obj: T) { private fun <T : RealmModel> copy(
realm: Realm,
obj: T,
) {
try { try {
realm.insertOrUpdate(obj) realm.insertOrUpdate(obj)
} catch (_: java.lang.IllegalArgumentException) { } catch (_: java.lang.IllegalArgumentException) {
@ -56,7 +61,9 @@ abstract class RealmBaseLocalRepository internal constructor(override var realm:
} }
private fun process() { private fun process() {
if (isClosed) { return } if (isClosed) {
return
}
realm.executeTransaction { realm.executeTransaction {
while (pendingSaves.isNotEmpty()) { while (pendingSaves.isNotEmpty()) {
val pending = pendingSaves.removeFirst() val pending = pendingSaves.removeFirst()
@ -89,8 +96,13 @@ abstract class RealmBaseLocalRepository internal constructor(override var realm:
} }
} }
override fun <T : BaseMainObject> modify(obj: T, transaction: (T) -> Unit) { override fun <T : BaseMainObject> modify(
if (isClosed) { return } obj: T,
transaction: (T) -> Unit,
) {
if (isClosed) {
return
}
val liveObject = getLiveObject(obj) ?: return val liveObject = getLiveObject(obj) ?: return
executeTransaction { executeTransaction {
transaction(liveObject) transaction(liveObject)
@ -98,7 +110,9 @@ abstract class RealmBaseLocalRepository internal constructor(override var realm:
} }
override fun <T : BaseMainObject> delete(obj: T) { override fun <T : BaseMainObject> delete(obj: T) {
if (isClosed) { return } if (isClosed) {
return
}
val liveObject = getLiveObject(obj) ?: return val liveObject = getLiveObject(obj) ?: return
executeTransaction { executeTransaction {
liveObject.deleteFromRealm() liveObject.deleteFromRealm()
@ -114,7 +128,9 @@ abstract class RealmBaseLocalRepository internal constructor(override var realm:
if (obj !is RealmObject || !obj.isManaged) return obj if (obj !is RealmObject || !obj.isManaged) return obj
val baseObject = obj as? BaseMainObject ?: return null val baseObject = obj as? BaseMainObject ?: return null
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
return realm.where(baseObject.realmClass).equalTo(baseObject.primaryIdentifierName, baseObject.primaryIdentifier).findFirst() as? T return realm.where(baseObject.realmClass)
.equalTo(baseObject.primaryIdentifierName, baseObject.primaryIdentifier)
.findFirst() as? T
} }
fun queryUser(userID: String): Flow<User?> { fun queryUser(userID: String): Flow<User?> {

View file

@ -15,9 +15,14 @@ import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
class RealmChallengeLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm), ChallengeLocalRepository { class RealmChallengeLocalRepository(realm: Realm) :
RealmBaseLocalRepository(realm),
override fun isChallengeMember(userID: String, challengeID: String): Flow<Boolean> = realm.where(ChallengeMembership::class.java) ChallengeLocalRepository {
override fun isChallengeMember(
userID: String,
challengeID: String,
): Flow<Boolean> =
realm.where(ChallengeMembership::class.java)
.equalTo("userID", userID) .equalTo("userID", userID)
.equalTo("challengeID", challengeID) .equalTo("challengeID", challengeID)
.findAll() .findAll()
@ -25,7 +30,11 @@ class RealmChallengeLocalRepository(realm: Realm) : RealmBaseLocalRepository(rea
.filter { it.isLoaded } .filter { it.isLoaded }
.map { it.count() > 0 } .map { it.count() > 0 }
override fun getChallengeMembership(userId: String, id: String) = realm.where(ChallengeMembership::class.java) override fun getChallengeMembership(
userId: String,
id: String,
) =
realm.where(ChallengeMembership::class.java)
.equalTo("userID", userId) .equalTo("userID", userId)
.equalTo("challengeID", id) .equalTo("challengeID", id)
.findAll() .findAll()
@ -34,7 +43,8 @@ class RealmChallengeLocalRepository(realm: Realm) : RealmBaseLocalRepository(rea
.map { it.first() } .map { it.first() }
.filterNotNull() .filterNotNull()
override fun getChallengeMemberships(userId: String) = realm.where(ChallengeMembership::class.java) override fun getChallengeMemberships(userId: String) =
realm.where(ChallengeMembership::class.java)
.equalTo("userID", userId) .equalTo("userID", userId)
.findAll() .findAll()
.toFlow() .toFlow()
@ -59,7 +69,8 @@ class RealmChallengeLocalRepository(realm: Realm) : RealmBaseLocalRepository(rea
} }
override val challenges: Flow<List<Challenge>> override val challenges: Flow<List<Challenge>>
get() = realm.where(Challenge::class.java) get() =
realm.where(Challenge::class.java)
.isNotNull("name") .isNotNull("name")
.sort("official", Sort.DESCENDING, "createdAt", Sort.DESCENDING) .sort("official", Sort.DESCENDING, "createdAt", Sort.DESCENDING)
.findAll() .findAll()
@ -74,7 +85,8 @@ class RealmChallengeLocalRepository(realm: Realm) : RealmBaseLocalRepository(rea
.toFlow() .toFlow()
.filter { it.isLoaded } .filter { it.isLoaded }
.flatMapLatest { it -> .flatMapLatest { it ->
val ids = it.map { val ids =
it.map {
return@map it.challengeID return@map it.challengeID
}.toTypedArray() }.toTypedArray()
realm.where(Challenge::class.java) realm.where(Challenge::class.java)
@ -91,13 +103,19 @@ class RealmChallengeLocalRepository(realm: Realm) : RealmBaseLocalRepository(rea
} }
} }
override fun setParticipating(userID: String, challengeID: String, isParticipating: Boolean) { override fun setParticipating(
userID: String,
challengeID: String,
isParticipating: Boolean,
) {
val user = realm.where(User::class.java).equalTo("id", userID).findFirst() ?: return val user = realm.where(User::class.java).equalTo("id", userID).findFirst() ?: return
executeTransaction { executeTransaction {
if (isParticipating) { if (isParticipating) {
user.challenges?.add(ChallengeMembership(userID, challengeID)) user.challenges?.add(ChallengeMembership(userID, challengeID))
} else { } else {
val membership = user.challenges?.firstOrNull { it.challengeID == challengeID } ?: return@executeTransaction val membership =
user.challenges?.firstOrNull { it.challengeID == challengeID }
?: return@executeTransaction
user.challenges?.remove(membership) user.challenges?.remove(membership)
} }
} }
@ -107,7 +125,7 @@ class RealmChallengeLocalRepository(realm: Realm) : RealmBaseLocalRepository(rea
challenges: List<Challenge>, challenges: List<Challenge>,
clearChallenges: Boolean, clearChallenges: Boolean,
memberOnly: Boolean, memberOnly: Boolean,
userID: String userID: String,
) { ) {
if (clearChallenges || memberOnly) { if (clearChallenges || memberOnly) {
val localChallenges = realm.where(Challenge::class.java).findAll().createSnapshot() val localChallenges = realm.where(Challenge::class.java).findAll().createSnapshot()

View file

@ -10,8 +10,9 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
open class RealmContentLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm), ContentLocalRepository { open class RealmContentLocalRepository(realm: Realm) :
RealmBaseLocalRepository(realm),
ContentLocalRepository {
override fun saveContent(contentResult: ContentResult) { override fun saveContent(contentResult: ContentResult) {
executeTransaction { realm1 -> executeTransaction { realm1 ->
contentResult.potion?.let { realm1.insertOrUpdate(it) } contentResult.potion?.let { realm1.insertOrUpdate(it) }

View file

@ -8,15 +8,22 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import java.util.Date import java.util.Date
class RealmCustomizationLocalRepository(realm: Realm) : RealmContentLocalRepository(realm), CustomizationLocalRepository { class RealmCustomizationLocalRepository(realm: Realm) :
RealmContentLocalRepository(realm),
override fun getCustomizations(type: String, category: String?, onlyAvailable: Boolean): Flow<List<Customization>> { CustomizationLocalRepository {
var query = realm.where(Customization::class.java) override fun getCustomizations(
type: String,
category: String?,
onlyAvailable: Boolean,
): Flow<List<Customization>> {
var query =
realm.where(Customization::class.java)
.equalTo("type", type) .equalTo("type", type)
.equalTo("category", category) .equalTo("category", category)
if (onlyAvailable) { if (onlyAvailable) {
val today = Date() val today = Date()
query = query query =
query
.beginGroup() .beginGroup()
.beginGroup() .beginGroup()
.lessThanOrEqualTo("availableFrom", today) .lessThanOrEqualTo("availableFrom", today)

View file

@ -9,7 +9,9 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
class RealmFAQLocalRepository(realm: Realm) : RealmContentLocalRepository(realm), FAQLocalRepository { class RealmFAQLocalRepository(realm: Realm) :
RealmContentLocalRepository(realm),
FAQLocalRepository {
override fun getArticle(position: Int): Flow<FAQArticle> { override fun getArticle(position: Int): Flow<FAQArticle> {
return realm.where(FAQArticle::class.java) return realm.where(FAQArticle::class.java)
.equalTo("position", position) .equalTo("position", position)
@ -21,7 +23,8 @@ class RealmFAQLocalRepository(realm: Realm) : RealmContentLocalRepository(realm)
} }
override val articles: Flow<List<FAQArticle>> override val articles: Flow<List<FAQArticle>>
get() = realm.where(FAQArticle::class.java) get() =
realm.where(FAQArticle::class.java)
.findAll() .findAll()
.toFlow() .toFlow()
.filter { it.isLoaded } .filter { it.isLoaded }

View file

@ -92,7 +92,10 @@ class RealmInventoryLocalRepository(realm: Realm) :
.filter { it.isLoaded } .filter { it.isLoaded }
} }
override fun getEquipmentType(type: String, set: String): Flow<out List<Equipment>> { override fun getEquipmentType(
type: String,
set: String,
): Flow<out List<Equipment>> {
return realm.where(Equipment::class.java) return realm.where(Equipment::class.java)
.equalTo("type", type) .equalTo("type", type)
.equalTo("gearSet", set) .equalTo("gearSet", set)
@ -104,10 +107,11 @@ class RealmInventoryLocalRepository(realm: Realm) :
override fun getOwnedItems( override fun getOwnedItems(
itemType: String, itemType: String,
userID: String, userID: String,
includeZero: Boolean includeZero: Boolean,
): Flow<List<OwnedItem>> { ): Flow<List<OwnedItem>> {
return queryUser(userID).map { return queryUser(userID).map {
val items = when (itemType) { val items =
when (itemType) {
"eggs" -> it?.items?.eggs "eggs" -> it?.items?.eggs
"hatchingPotions" -> it?.items?.hatchingPotions "hatchingPotions" -> it?.items?.hatchingPotions
"food" -> it?.items?.food "food" -> it?.items?.food
@ -128,12 +132,18 @@ class RealmInventoryLocalRepository(realm: Realm) :
.filter { it.isLoaded } .filter { it.isLoaded }
} }
override fun getItems(itemClass: Class<out Item>, keys: Array<String>): Flow<List<Item>> { override fun getItems(
itemClass: Class<out Item>,
keys: Array<String>,
): Flow<List<Item>> {
return realm.where(itemClass).`in`("key", keys).findAll().toFlow() return realm.where(itemClass).`in`("key", keys).findAll().toFlow()
.filter { it.isLoaded } .filter { it.isLoaded }
} }
override fun getOwnedItems(userID: String, includeZero: Boolean): Flow<Map<String, OwnedItem>> { override fun getOwnedItems(
userID: String,
includeZero: Boolean,
): Flow<Map<String, OwnedItem>> {
return queryUser(userID) return queryUser(userID)
.filterNotNull() .filterNotNull()
.map { .map {
@ -168,8 +178,13 @@ class RealmInventoryLocalRepository(realm: Realm) :
.filter { it.isLoaded } .filter { it.isLoaded }
} }
override fun getMounts(type: String?, group: String?, color: String?): Flow<List<Mount>> { override fun getMounts(
var query = realm.where(Mount::class.java) type: String?,
group: String?,
color: String?,
): Flow<List<Mount>> {
var query =
realm.where(Mount::class.java)
.sort("type", Sort.ASCENDING, if (color == null) "color" else "animal", Sort.ASCENDING) .sort("type", Sort.ASCENDING, if (color == null) "color" else "animal", Sort.ASCENDING)
if (type != null) { if (type != null) {
query = query.equalTo("animal", type) query = query.equalTo("animal", type)
@ -202,8 +217,13 @@ class RealmInventoryLocalRepository(realm: Realm) :
.filter { it.isLoaded } .filter { it.isLoaded }
} }
override fun getPets(type: String?, group: String?, color: String?): Flow<List<Pet>> { override fun getPets(
var query = realm.where(Pet::class.java) type: String?,
group: String?,
color: String?,
): Flow<List<Pet>> {
var query =
realm.where(Pet::class.java)
.sort("type", Sort.ASCENDING, if (color == null) "color" else "animal", Sort.ASCENDING) .sort("type", Sort.ASCENDING, if (color == null) "color" else "animal", Sort.ASCENDING)
if (type != null) { if (type != null) {
query = query.equalTo("animal", type) query = query.equalTo("animal", type)
@ -239,7 +259,7 @@ class RealmInventoryLocalRepository(realm: Realm) :
type: String, type: String,
key: String, key: String,
userID: String, userID: String,
amountToAdd: Int amountToAdd: Int,
) { ) {
val item = getOwnedItem(userID, type, key, true).firstOrNull() val item = getOwnedItem(userID, type, key, true).firstOrNull()
if (item != null) { if (item != null) {
@ -247,7 +267,10 @@ class RealmInventoryLocalRepository(realm: Realm) :
} }
} }
override fun changeOwnedCount(item: OwnedItem, amountToAdd: Int?) { override fun changeOwnedCount(
item: OwnedItem,
amountToAdd: Int?,
) {
val liveItem = getLiveObject(item) ?: return val liveItem = getLiveObject(item) ?: return
amountToAdd?.let { amount -> amountToAdd?.let { amount ->
executeTransaction { liveItem.numberOwned = liveItem.numberOwned + amount } executeTransaction { liveItem.numberOwned = liveItem.numberOwned + amount }
@ -258,7 +281,7 @@ class RealmInventoryLocalRepository(realm: Realm) :
userID: String, userID: String,
type: String, type: String,
key: String, key: String,
includeZero: Boolean includeZero: Boolean,
): Flow<OwnedItem> { ): Flow<OwnedItem> {
return queryUser(userID) return queryUser(userID)
.filterNotNull() .filterNotNull()
@ -283,8 +306,12 @@ class RealmInventoryLocalRepository(realm: Realm) :
.map { it.first() } .map { it.first() }
} }
override fun getItem(type: String, key: String): Flow<Item> { override fun getItem(
val itemClass: Class<out RealmObject> = when (type) { type: String,
key: String,
): Flow<Item> {
val itemClass: Class<out RealmObject> =
when (type) {
"eggs" -> Egg::class.java "eggs" -> Egg::class.java
"hatchingPotions" -> HatchingPotion::class.java "hatchingPotions" -> HatchingPotion::class.java
"food" -> Food::class.java "food" -> Food::class.java
@ -347,7 +374,11 @@ class RealmInventoryLocalRepository(realm: Realm) :
} }
} }
override fun hatchPet(eggKey: String, potionKey: String, userID: String) { override fun hatchPet(
eggKey: String,
potionKey: String,
userID: String,
) {
val newPet = OwnedPet() val newPet = OwnedPet()
newPet.key = "$eggKey-$potionKey" newPet.key = "$eggKey-$potionKey"
newPet.trained = 5 newPet.trained = 5
@ -369,7 +400,10 @@ class RealmInventoryLocalRepository(realm: Realm) :
.equalTo("itemType", obj.itemType).findFirst() .equalTo("itemType", obj.itemType).findFirst()
} }
override fun save(items: Items, userID: String) { override fun save(
items: Items,
userID: String,
) {
val user = realm.where(User::class.java).equalTo("id", userID).findFirst() ?: return val user = realm.where(User::class.java).equalTo("id", userID).findFirst() ?: return
items.setItemTypes() items.setItemTypes()
executeTransaction { executeTransaction {
@ -377,7 +411,11 @@ class RealmInventoryLocalRepository(realm: Realm) :
} }
} }
override fun unhatchPet(eggKey: String, potionKey: String, userID: String) { override fun unhatchPet(
eggKey: String,
potionKey: String,
userID: String,
) {
val pet = realm.where(OwnedPet::class.java).equalTo("key", "$eggKey-$potionKey").findFirst() val pet = realm.where(OwnedPet::class.java).equalTo("key", "$eggKey-$potionKey").findFirst()
val user = realm.where(User::class.java).equalTo("id", userID).findFirst() ?: return val user = realm.where(User::class.java).equalTo("id", userID).findFirst() ?: return
val egg = user.items?.eggs?.firstOrNull { it.key == eggKey } ?: return val egg = user.items?.eggs?.firstOrNull { it.key == eggKey } ?: return
@ -390,7 +428,12 @@ class RealmInventoryLocalRepository(realm: Realm) :
} }
} }
override fun feedPet(foodKey: String, petKey: String, feedValue: Int, userID: String) { override fun feedPet(
foodKey: String,
petKey: String,
feedValue: Int,
userID: String,
) {
val user = realm.where(User::class.java).equalTo("id", userID).findFirst() ?: return val user = realm.where(User::class.java).equalTo("id", userID).findFirst() ?: return
val pet = user.items?.pets?.firstOrNull { it.key == petKey } ?: return val pet = user.items?.pets?.firstOrNull { it.key == petKey } ?: return
val food = user.items?.food?.firstOrNull { it.key == foodKey } ?: return val food = user.items?.food?.firstOrNull { it.key == foodKey } ?: return
@ -422,8 +465,12 @@ class RealmInventoryLocalRepository(realm: Realm) :
} }
} }
override fun soldItem(userID: String, updatedUser: User): User { override fun soldItem(
val user = realm.where(User::class.java) userID: String,
updatedUser: User,
): User {
val user =
realm.where(User::class.java)
.equalTo("id", userID) .equalTo("id", userID)
.findFirst() ?: return updatedUser .findFirst() ?: return updatedUser
executeTransaction { executeTransaction {
@ -453,7 +500,7 @@ class RealmInventoryLocalRepository(realm: Realm) :
realm.where(Food::class.java) realm.where(Food::class.java)
.lessThan("event.start", Date()) .lessThan("event.start", Date())
.greaterThan("event.end", Date()) .greaterThan("event.end", Date())
.findAll().toFlow() .findAll().toFlow(),
) { items, food -> ) { items, food ->
items.addAll(food) items.addAll(food)
items items
@ -462,7 +509,7 @@ class RealmInventoryLocalRepository(realm: Realm) :
realm.where(HatchingPotion::class.java) realm.where(HatchingPotion::class.java)
.lessThan("event.start", Date()) .lessThan("event.start", Date())
.greaterThan("event.end", Date()) .greaterThan("event.end", Date())
.findAll().toFlow() .findAll().toFlow(),
) { items, food -> ) { items, food ->
items.addAll(food) items.addAll(food)
items items
@ -471,7 +518,7 @@ class RealmInventoryLocalRepository(realm: Realm) :
realm.where(QuestContent::class.java) realm.where(QuestContent::class.java)
.lessThan("event.start", Date()) .lessThan("event.start", Date())
.greaterThan("event.end", Date()) .greaterThan("event.end", Date())
.findAll().toFlow() .findAll().toFlow(),
) { items, food -> ) { items, food ->
items.addAll(food) items.addAll(food)
items items

View file

@ -18,9 +18,14 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm), SocialLocalRepository { class RealmSocialLocalRepository(realm: Realm) :
RealmBaseLocalRepository(realm),
override fun getGroupMembership(userId: String, id: String) = realm.where(GroupMembership::class.java) SocialLocalRepository {
override fun getGroupMembership(
userId: String,
id: String,
) =
realm.where(GroupMembership::class.java)
.equalTo("userID", userId) .equalTo("userID", userId)
.equalTo("groupID", id) .equalTo("groupID", id)
.findAll() .findAll()
@ -28,17 +33,24 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
.filter { it.isLoaded && it.isNotEmpty() } .filter { it.isLoaded && it.isNotEmpty() }
.map { it.first() } .map { it.first() }
override fun getGroupMemberships(userId: String): Flow<List<GroupMembership>> = realm.where(GroupMembership::class.java) override fun getGroupMemberships(userId: String): Flow<List<GroupMembership>> =
realm.where(GroupMembership::class.java)
.equalTo("userID", userId) .equalTo("userID", userId)
.findAll() .findAll()
.toFlow() .toFlow()
.filter { it.isLoaded } .filter { it.isLoaded }
override fun updateMembership(userId: String, id: String, isMember: Boolean) { override fun updateMembership(
userId: String,
id: String,
isMember: Boolean,
) {
if (isMember) { if (isMember) {
save(GroupMembership(userId, id)) save(GroupMembership(userId, id))
} else { } else {
val membership = realm.where(GroupMembership::class.java).equalTo("userID", userId).equalTo("groupID", id).findFirst() val membership =
realm.where(GroupMembership::class.java).equalTo("userID", userId)
.equalTo("groupID", id).findFirst()
if (membership != null) { if (membership != null) {
executeTransaction { executeTransaction {
membership.deleteFromRealm() membership.deleteFromRealm()
@ -61,11 +73,12 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
userID: String, userID: String,
recipientID: String, recipientID: String,
messages: List<ChatMessage>, messages: List<ChatMessage>,
page: Int page: Int,
) { ) {
messages.forEach { it.userID = userID } messages.forEach { it.userID = userID }
for (message in messages) { for (message in messages) {
val existingMessage = realm.where(ChatMessage::class.java) val existingMessage =
realm.where(ChatMessage::class.java)
.equalTo("id", message.id) .equalTo("id", message.id)
.findAll() .findAll()
.firstOrNull() .firstOrNull()
@ -73,7 +86,9 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
} }
save(messages) save(messages)
if (page != 0) return if (page != 0) return
val existingMessages = realm.where(ChatMessage::class.java).equalTo("isInboxMessage", true).equalTo("uuid", recipientID).findAll() val existingMessages =
realm.where(ChatMessage::class.java).equalTo("isInboxMessage", true)
.equalTo("uuid", recipientID).findAll()
val messagesToRemove = ArrayList<ChatMessage>() val messagesToRemove = ArrayList<ChatMessage>()
for (existingMessage in existingMessages) { for (existingMessage in existingMessages) {
val isStillMember = messages.any { existingMessage.id == it.id } val isStillMember = messages.any { existingMessage.id == it.id }
@ -86,7 +101,10 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
} }
} }
override fun saveInboxConversations(userID: String, conversations: List<InboxConversation>) { override fun saveInboxConversations(
userID: String,
conversations: List<InboxConversation>,
) {
conversations.forEach { it.userID = userID } conversations.forEach { it.userID = userID }
save(conversations) save(conversations)
val existingConversations = realm.where(InboxConversation::class.java).findAll() val existingConversations = realm.where(InboxConversation::class.java).findAll()
@ -111,10 +129,14 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
.map { member -> member.firstOrNull() } .map { member -> member.firstOrNull() }
} }
override fun saveGroupMemberships(userID: String?, memberships: List<GroupMembership>) { override fun saveGroupMemberships(
userID: String?,
memberships: List<GroupMembership>,
) {
save(memberships) save(memberships)
if (userID != null) { if (userID != null) {
val existingMemberships = realm.where(GroupMembership::class.java).equalTo("userID", userID).findAll() val existingMemberships =
realm.where(GroupMembership::class.java).equalTo("userID", userID).findAll()
val membersToRemove = ArrayList<GroupMembership>() val membersToRemove = ArrayList<GroupMembership>()
for (existingMembership in existingMemberships) { for (existingMembership in existingMemberships) {
val isStillMember = memberships.any { existingMembership.groupID == it.groupID } val isStillMember = memberships.any { existingMembership.groupID == it.groupID }
@ -129,7 +151,11 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
} }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override fun getUserGroups(userID: String, type: String?) = realm.where(GroupMembership::class.java) override fun getUserGroups(
userID: String,
type: String?,
) =
realm.where(GroupMembership::class.java)
.equalTo("userID", userID) .equalTo("userID", userID)
.findAll() .findAll()
.toFlow() .toFlow()
@ -141,7 +167,7 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
"id", "id",
memberships.map { memberships.map {
return@map it.groupID return@map it.groupID
}.toTypedArray() }.toTypedArray(),
) )
.sort("memberCount", Sort.DESCENDING) .sort("memberCount", Sort.DESCENDING)
.findAll() .findAll()
@ -171,23 +197,32 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
executeTransaction { chatMessage?.deleteFromRealm() } executeTransaction { chatMessage?.deleteFromRealm() }
} }
override fun getPartyMembers(partyId: String) = realm.where(Member::class.java) override fun getPartyMembers(partyId: String) =
realm.where(Member::class.java)
.equalTo("party.id", partyId) .equalTo("party.id", partyId)
.findAll() .findAll()
.toFlow() .toFlow()
override fun getGroupMembers(groupID: String) = realm.where(GroupMembership::class.java) override fun getGroupMembers(groupID: String) =
realm.where(GroupMembership::class.java)
.equalTo("groupID", groupID) .equalTo("groupID", groupID)
.findAll() .findAll()
.toFlow() .toFlow()
.map { memberships -> memberships.map { it.userID }.toTypedArray() } .map { memberships -> memberships.map { it.userID }.toTypedArray() }
.flatMapLatest { realm.where(Member::class.java).`in`("id", it).findAll().toFlow() } .flatMapLatest { realm.where(Member::class.java).`in`("id", it).findAll().toFlow() }
override fun updateRSVPNeeded(user: User?, newValue: Boolean) { override fun updateRSVPNeeded(
user: User?,
newValue: Boolean,
) {
executeTransaction { user?.party?.quest?.RSVPNeeded = newValue } executeTransaction { user?.party?.quest?.RSVPNeeded = newValue }
} }
override fun likeMessage(chatMessage: ChatMessage, userId: String, liked: Boolean) { override fun likeMessage(
chatMessage: ChatMessage,
userId: String,
liked: Boolean,
) {
val liveMessage = getLiveObject(chatMessage) val liveMessage = getLiveObject(chatMessage)
if (liveMessage == null) { if (liveMessage == null) {
executeTransaction { executeTransaction {
@ -216,13 +251,18 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
} }
} }
override fun savePartyMembers(groupId: String?, members: List<Member>) { override fun savePartyMembers(
groupId: String?,
members: List<Member>,
) {
save(members) save(members)
if (groupId != null) { if (groupId != null) {
val existingMembers = realm.where(Member::class.java).equalTo("party.id", groupId).findAll() val existingMembers =
realm.where(Member::class.java).equalTo("party.id", groupId).findAll()
val membersToRemove = ArrayList<Member>() val membersToRemove = ArrayList<Member>()
for (existingMember in existingMembers) { for (existingMember in existingMembers) {
val isStillMember = members.any { existingMember.id != null && existingMember.id == it.id } val isStillMember =
members.any { existingMember.id != null && existingMember.id == it.id }
if (!isStillMember) { if (!isStillMember) {
membersToRemove.add(existingMember) membersToRemove.add(existingMember)
} }
@ -233,7 +273,10 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
} }
} }
override fun rejectGroupInvitation(userID: String, groupID: String) { override fun rejectGroupInvitation(
userID: String,
groupID: String,
) {
val user = realm.where(User::class.java).equalTo("id", userID).findFirst() val user = realm.where(User::class.java).equalTo("id", userID).findFirst()
executeTransaction { executeTransaction {
user?.invitations?.removeInvitation(groupID) user?.invitations?.removeInvitation(groupID)
@ -247,7 +290,10 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
} }
} }
override fun setQuestActivity(party: Group?, active: Boolean) { override fun setQuestActivity(
party: Group?,
active: Boolean,
) {
if (party == null) return if (party == null) return
val liveParty = getLiveObject(party) val liveParty = getLiveObject(party)
executeTransaction { executeTransaction {
@ -255,10 +301,14 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
} }
} }
override fun saveChatMessages(groupId: String?, chatMessages: List<ChatMessage>) { override fun saveChatMessages(
groupId: String?,
chatMessages: List<ChatMessage>,
) {
save(chatMessages) save(chatMessages)
if (groupId != null) { if (groupId != null) {
val existingMessages = realm.where(ChatMessage::class.java).equalTo("groupId", groupId).findAll() val existingMessages =
realm.where(ChatMessage::class.java).equalTo("groupId", groupId).findAll()
val messagesToRemove = ArrayList<ChatMessage>() val messagesToRemove = ArrayList<ChatMessage>()
for (existingMessage in existingMessages) { for (existingMessage in existingMessages) {
val isStillMember = chatMessages.any { existingMessage.id == it.id } val isStillMember = chatMessages.any { existingMessage.id == it.id }
@ -279,7 +329,11 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
return party != null && party.isValid return party != null && party.isValid
} }
override fun getInboxMessages(userId: String, replyToUserID: String?) = realm.where(ChatMessage::class.java) override fun getInboxMessages(
userId: String,
replyToUserID: String?,
) =
realm.where(ChatMessage::class.java)
.equalTo("isInboxMessage", true) .equalTo("isInboxMessage", true)
.equalTo("uuid", replyToUserID) .equalTo("uuid", replyToUserID)
.equalTo("userID", userId) .equalTo("userID", userId)
@ -288,7 +342,8 @@ class RealmSocialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm)
.toFlow() .toFlow()
.filter { it.isLoaded } .filter { it.isLoaded }
override fun getInboxConversation(userId: String) = realm.where(InboxConversation::class.java) override fun getInboxConversation(userId: String) =
realm.where(InboxConversation::class.java)
.equalTo("userID", userId) .equalTo("userID", userId)
.sort("timestamp", Sort.DESCENDING) .sort("timestamp", Sort.DESCENDING)
.findAll() .findAll()

View file

@ -18,9 +18,14 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
class RealmTaskLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm), TaskLocalRepository { class RealmTaskLocalRepository(realm: Realm) :
RealmBaseLocalRepository(realm),
override fun getTasks(taskType: TaskType, userID: String, includedGroupIDs: Array<String>): Flow<List<Task>> { TaskLocalRepository {
override fun getTasks(
taskType: TaskType,
userID: String,
includedGroupIDs: Array<String>,
): Flow<List<Task>> {
if (realm.isClosed) return emptyFlow() if (realm.isClosed) return emptyFlow()
return findTasks(taskType, userID) return findTasks(taskType, userID)
.toFlow() .toFlow()
@ -29,7 +34,7 @@ class RealmTaskLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm),
private fun findTasks( private fun findTasks(
taskType: TaskType, taskType: TaskType,
ownerID: String ownerID: String,
): RealmResults<Task> { ): RealmResults<Task> {
return realm.where(Task::class.java) return realm.where(Task::class.java)
.equalTo("typeValue", taskType.value) .equalTo("typeValue", taskType.value)
@ -47,7 +52,11 @@ class RealmTaskLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm),
.filter { it.isLoaded } .filter { it.isLoaded }
} }
override fun saveTasks(ownerID: String, tasksOrder: TasksOrder, tasks: TaskList) { override fun saveTasks(
ownerID: String,
tasksOrder: TasksOrder,
tasks: TaskList,
) {
val sortedTasks = mutableListOf<Task>() val sortedTasks = mutableListOf<Task>()
sortedTasks.addAll(sortTasks(tasks.tasks, tasksOrder.habits)) sortedTasks.addAll(sortTasks(tasks.tasks, tasksOrder.habits))
sortedTasks.addAll(sortTasks(tasks.tasks, tasksOrder.dailys)) sortedTasks.addAll(sortTasks(tasks.tasks, tasksOrder.dailys))
@ -74,7 +83,10 @@ class RealmTaskLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm),
executeTransaction { realm1 -> realm1.insertOrUpdate(sortedTasks) } executeTransaction { realm1 -> realm1.insertOrUpdate(sortedTasks) }
} }
override fun saveCompletedTodos(userId: String, tasks: MutableCollection<Task>) { override fun saveCompletedTodos(
userId: String,
tasks: MutableCollection<Task>,
) {
removeCompletedTodos(userId, tasks) removeCompletedTodos(userId, tasks)
executeTransaction { realm1 -> realm1.insertOrUpdate(tasks) } executeTransaction { realm1 -> realm1.insertOrUpdate(tasks) }
} }
@ -99,7 +111,10 @@ class RealmTaskLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm),
} }
} }
private fun sortTasks(taskMap: MutableMap<String, Task>, taskOrder: List<String>): List<Task> { private fun sortTasks(
taskMap: MutableMap<String, Task>,
taskOrder: List<String>,
): List<Task> {
val taskList = ArrayList<Task>() val taskList = ArrayList<Task>()
var position = 0 var position = 0
for (taskId in taskOrder) { for (taskId in taskOrder) {
@ -114,9 +129,13 @@ class RealmTaskLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm),
return taskList return taskList
} }
private fun removeOldTasks(ownerID: String, onlineTaskList: List<Task>) { private fun removeOldTasks(
ownerID: String,
onlineTaskList: List<Task>,
) {
if (realm.isClosed) return if (realm.isClosed) return
val localTasks = realm.where(Task::class.java) val localTasks =
realm.where(Task::class.java)
.equalTo("ownerID", ownerID) .equalTo("ownerID", ownerID)
.beginGroup() .beginGroup()
.beginGroup() .beginGroup()
@ -136,8 +155,12 @@ class RealmTaskLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm),
} }
} }
private fun removeCompletedTodos(userID: String, onlineTaskList: MutableCollection<Task>) { private fun removeCompletedTodos(
val localTasks = realm.where(Task::class.java) userID: String,
onlineTaskList: MutableCollection<Task>,
) {
val localTasks =
realm.where(Task::class.java)
.equalTo("ownerID", userID) .equalTo("ownerID", userID)
.equalTo("typeValue", TaskType.TODO.value) .equalTo("typeValue", TaskType.TODO.value)
.equalTo("completed", true) .equalTo("completed", true)
@ -181,14 +204,21 @@ class RealmTaskLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm),
} }
} }
override fun markTaskCompleted(taskId: String, isCompleted: Boolean) { override fun markTaskCompleted(
taskId: String,
isCompleted: Boolean,
) {
val task = realm.where(Task::class.java).equalTo("id", taskId).findFirst() val task = realm.where(Task::class.java).equalTo("id", taskId).findFirst()
executeTransaction { task?.completed = true } executeTransaction { task?.completed = true }
} }
override fun swapTaskPosition(firstPosition: Int, secondPosition: Int) { override fun swapTaskPosition(
firstPosition: Int,
secondPosition: Int,
) {
val firstTask = realm.where(Task::class.java).equalTo("position", firstPosition).findFirst() val firstTask = realm.where(Task::class.java).equalTo("position", firstPosition).findFirst()
val secondTask = realm.where(Task::class.java).equalTo("position", secondPosition).findFirst() val secondTask =
realm.where(Task::class.java).equalTo("position", secondPosition).findFirst()
if (firstTask != null && secondTask != null && firstTask.isValid && secondTask.isValid) { if (firstTask != null && secondTask != null && firstTask.isValid && secondTask.isValid) {
executeTransaction { executeTransaction {
firstTask.position = secondPosition firstTask.position = secondPosition
@ -197,8 +227,12 @@ class RealmTaskLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm),
} }
} }
override fun getTaskAtPosition(taskType: String, position: Int): Flow<Task> { override fun getTaskAtPosition(
return realm.where(Task::class.java).equalTo("typeValue", taskType).equalTo("position", position) taskType: String,
position: Int,
): Flow<Task> {
return realm.where(Task::class.java).equalTo("typeValue", taskType)
.equalTo("position", position)
.findAll() .findAll()
.toFlow() .toFlow()
.filter { realmObject -> realmObject.isLoaded && realmObject.isNotEmpty() } .filter { realmObject -> realmObject.isLoaded && realmObject.isNotEmpty() }
@ -207,9 +241,11 @@ class RealmTaskLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm),
} }
override fun updateIsdue(daily: TaskList): TaskList { override fun updateIsdue(daily: TaskList): TaskList {
val tasks = realm.where(Task::class.java).equalTo("typeValue", TaskType.DAILY.value).findAll() val tasks =
realm.where(Task::class.java).equalTo("typeValue", TaskType.DAILY.value).findAll()
realm.beginTransaction() realm.beginTransaction()
tasks.filter { daily.tasks.containsKey(it.id) }.forEach { it.isDue = daily.tasks[it.id]?.isDue } tasks.filter { daily.tasks.containsKey(it.id) }
.forEach { it.isDue = daily.tasks[it.id]?.isDue }
realm.commitTransaction() realm.commitTransaction()
return daily return daily
} }
@ -218,7 +254,8 @@ class RealmTaskLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm),
if (taskOrder.isNotEmpty()) { if (taskOrder.isNotEmpty()) {
val tasks = realm.where(Task::class.java).`in`("id", taskOrder.toTypedArray()).findAll() val tasks = realm.where(Task::class.java).`in`("id", taskOrder.toTypedArray()).findAll()
executeTransaction { _ -> executeTransaction { _ ->
tasks.filter { taskOrder.contains(it.id) }.forEach { it.position = taskOrder.indexOf(it.id) } tasks.filter { taskOrder.contains(it.id) }
.forEach { it.position = taskOrder.indexOf(it.id) }
} }
} }
} }
@ -243,7 +280,10 @@ class RealmTaskLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm),
.filterNotNull() .filterNotNull()
} }
override fun getTasksForChallenge(challengeID: String?, userID: String?): Flow<List<Task>> { override fun getTasksForChallenge(
challengeID: String?,
userID: String?,
): Flow<List<Task>> {
return realm.where(Task::class.java) return realm.where(Task::class.java)
.equalTo("challengeID", challengeID) .equalTo("challengeID", challengeID)
.equalTo("ownerID", userID) .equalTo("ownerID", userID)

View file

@ -10,8 +10,9 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
class RealmTutorialLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm), TutorialLocalRepository { class RealmTutorialLocalRepository(realm: Realm) :
RealmBaseLocalRepository(realm),
TutorialLocalRepository {
override fun getTutorialStep(key: String): Flow<TutorialStep> { override fun getTutorialStep(key: String): Flow<TutorialStep> {
if (realm.isClosed) return emptyFlow() if (realm.isClosed) return emptyFlow()
return realm.where(TutorialStep::class.java).equalTo("identifier", key) return realm.where(TutorialStep::class.java).equalTo("identifier", key)

View file

@ -44,6 +44,7 @@ class RealmUserLocalRepository(realm: Realm) :
it.quest?.members?.find { questMember -> questMember.key == userID } === null -> UserQuestStatus.NO_QUEST it.quest?.members?.find { questMember -> questMember.key == userID } === null -> UserQuestStatus.NO_QUEST
it.quest?.progress?.collect?.isNotEmpty() it.quest?.progress?.collect?.isNotEmpty()
?: false -> UserQuestStatus.QUEST_COLLECT ?: false -> UserQuestStatus.QUEST_COLLECT
(it.quest?.progress?.hp ?: 0.0) > 0.0 -> UserQuestStatus.QUEST_BOSS (it.quest?.progress?.hp ?: 0.0) > 0.0 -> UserQuestStatus.QUEST_BOSS
else -> UserQuestStatus.QUEST_UNKNOWN else -> UserQuestStatus.QUEST_UNKNOWN
} }
@ -81,9 +82,13 @@ class RealmUserLocalRepository(realm: Realm) :
.map { users -> users.first() } .map { users -> users.first() }
} }
override fun saveUser(user: User, overrideExisting: Boolean) { override fun saveUser(
user: User,
overrideExisting: Boolean,
) {
if (realm.isClosed) return if (realm.isClosed) return
val oldUser = realm.where(User::class.java) val oldUser =
realm.where(User::class.java)
.equalTo("id", user.id) .equalTo("id", user.id)
.findFirst() .findFirst()
if (oldUser != null && oldUser.isValid) { if (oldUser != null && oldUser.isValid) {
@ -101,7 +106,10 @@ class RealmUserLocalRepository(realm: Realm) :
removeOldTags(user.id ?: "", user.tags) removeOldTags(user.id ?: "", user.tags)
} }
private fun removeOldTags(userId: String, onlineTags: List<Tag>) { private fun removeOldTags(
userId: String,
onlineTags: List<Tag>,
) {
val tags = realm.where(Tag::class.java).equalTo("userId", userId).findAll().createSnapshot() val tags = realm.where(Tag::class.java).equalTo("userId", userId).findAll().createSnapshot()
val tagsToDelete = tags.filterNot { onlineTags.contains(it) } val tagsToDelete = tags.filterNot { onlineTags.contains(it) }
executeTransaction { executeTransaction {

View file

@ -5,21 +5,21 @@ import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog
fun HabiticaAlertDialog.addOkButton( fun HabiticaAlertDialog.addOkButton(
isPrimary: Boolean = true, isPrimary: Boolean = true,
listener: ((HabiticaAlertDialog, Int) -> Unit)? = null listener: ((HabiticaAlertDialog, Int) -> Unit)? = null,
) { ) {
this.addButton(R.string.ok, isPrimary, false, true, listener) this.addButton(R.string.ok, isPrimary, false, true, listener)
} }
fun HabiticaAlertDialog.addCloseButton( fun HabiticaAlertDialog.addCloseButton(
isPrimary: Boolean = false, isPrimary: Boolean = false,
listener: ((HabiticaAlertDialog, Int) -> Unit)? = null listener: ((HabiticaAlertDialog, Int) -> Unit)? = null,
) { ) {
this.addButton(R.string.close, isPrimary, false, true, listener) this.addButton(R.string.close, isPrimary, false, true, listener)
} }
fun HabiticaAlertDialog.addCancelButton( fun HabiticaAlertDialog.addCancelButton(
isPrimary: Boolean = false, isPrimary: Boolean = false,
listener: ((HabiticaAlertDialog, Int) -> Unit)? = null listener: ((HabiticaAlertDialog, Int) -> Unit)? = null,
) { ) {
this.addButton(R.string.cancel, isPrimary, false, true, listener) this.addButton(R.string.cancel, isPrimary, false, true, listener)
} }

View file

@ -8,7 +8,10 @@ fun Animal.getTranslatedType(c: Context?): String? {
return getTranslatedAnimalType(c, type) return getTranslatedAnimalType(c, type)
} }
fun getTranslatedAnimalType(c: Context?, type: String?): String? { fun getTranslatedAnimalType(
c: Context?,
type: String?,
): String? {
if (c == null) { if (c == null) {
return type return type
} }

View file

@ -4,5 +4,8 @@ import android.content.Context
import android.content.res.TypedArray import android.content.res.TypedArray
import android.util.AttributeSet import android.util.AttributeSet
fun AttributeSet.styledAttributes(context: Context?, style: IntArray): TypedArray? = fun AttributeSet.styledAttributes(
context: Context?,
style: IntArray,
): TypedArray? =
context?.theme?.obtainStyledAttributes(this, style, 0, 0) context?.theme?.obtainStyledAttributes(this, style, 0, 0)

View file

@ -13,7 +13,11 @@ import kotlin.time.toDuration
class DateUtils { class DateUtils {
companion object { companion object {
fun createDate(year: Int, month: Int, day: Int): Date { fun createDate(
year: Int,
month: Int,
day: Int,
): Date {
val cal = Calendar.getInstance() val cal = Calendar.getInstance()
cal.set(Calendar.YEAR, year) cal.set(Calendar.YEAR, year)
cal.set(Calendar.MONTH, month) cal.set(Calendar.MONTH, month)
@ -25,7 +29,10 @@ class DateUtils {
return cal.time return cal.time
} }
fun isSameDay(date1 : Date, date2 : Date) : Boolean { fun isSameDay(
date1: Date,
date2: Date,
): Boolean {
val cal1 = Calendar.getInstance() val cal1 = Calendar.getInstance()
val cal2 = Calendar.getInstance() val cal2 = Calendar.getInstance()
cal1.time = date1 cal1.time = date1
@ -50,26 +57,34 @@ fun Long.getAgoString(res: Resources): String {
val diffMonths = diffDays / 30 val diffMonths = diffDays / 30
return when { return when {
diffMonths != 0L -> if (diffMonths == 1L) { diffMonths != 0L ->
if (diffMonths == 1L) {
res.getString(R.string.ago_1month) res.getString(R.string.ago_1month)
} else { } else {
res.getString(R.string.ago_months, diffMonths) res.getString(R.string.ago_months, diffMonths)
} }
diffWeeks != 0L -> if (diffWeeks == 1L) {
diffWeeks != 0L ->
if (diffWeeks == 1L) {
res.getString(R.string.ago_1week) res.getString(R.string.ago_1week)
} else { } else {
res.getString(R.string.ago_weeks, diffWeeks) res.getString(R.string.ago_weeks, diffWeeks)
} }
diffDays != 0L -> if (diffDays == 1L) {
diffDays != 0L ->
if (diffDays == 1L) {
res.getString(R.string.ago_1day) res.getString(R.string.ago_1day)
} else { } else {
res.getString(R.string.ago_days, diffDays) res.getString(R.string.ago_days, diffDays)
} }
diffHours != 0L -> if (diffHours == 1L) {
diffHours != 0L ->
if (diffHours == 1L) {
res.getString(R.string.ago_1hour) res.getString(R.string.ago_1hour)
} else { } else {
res.getString(R.string.ago_hours, diffHours) res.getString(R.string.ago_hours, diffHours)
} }
diffMinutes == 1L -> res.getString(R.string.ago_1Minute) diffMinutes == 1L -> res.getString(R.string.ago_1Minute)
else -> res.getString(R.string.ago_minutes, diffMinutes) else -> res.getString(R.string.ago_minutes, diffMinutes)
} }
@ -89,26 +104,34 @@ fun Long.getRemainingString(res: Resources): String {
val diffMonths = diffDays / 30 val diffMonths = diffDays / 30
return when { return when {
diffMonths != 0L -> if (diffMonths == 1L) { diffMonths != 0L ->
if (diffMonths == 1L) {
res.getString(R.string.remaining_1month) res.getString(R.string.remaining_1month)
} else { } else {
res.getString(R.string.remaining_months, diffMonths) res.getString(R.string.remaining_months, diffMonths)
} }
diffWeeks != 0L -> if (diffWeeks == 1L) {
diffWeeks != 0L ->
if (diffWeeks == 1L) {
res.getString(R.string.remaining_1week) res.getString(R.string.remaining_1week)
} else { } else {
res.getString(R.string.remaining_weeks, diffWeeks) res.getString(R.string.remaining_weeks, diffWeeks)
} }
diffDays != 0L -> if (diffDays == 1L) {
diffDays != 0L ->
if (diffDays == 1L) {
res.getString(R.string.remaining_1day) res.getString(R.string.remaining_1day)
} else { } else {
res.getString(R.string.remaining_days, diffDays) res.getString(R.string.remaining_days, diffDays)
} }
diffHours != 0L -> if (diffHours == 1L) {
diffHours != 0L ->
if (diffHours == 1L) {
res.getString(R.string.remaining_1hour) res.getString(R.string.remaining_1hour)
} else { } else {
res.getString(R.string.remaining_hours, diffHours) res.getString(R.string.remaining_hours, diffHours)
} }
diffMinutes == 1L -> res.getString(R.string.remaining_1Minute) diffMinutes == 1L -> res.getString(R.string.remaining_1Minute)
else -> res.getString(R.string.remaining_minutes, diffMinutes) else -> res.getString(R.string.remaining_minutes, diffMinutes)
} }
@ -151,7 +174,8 @@ fun Duration.getMinuteOrSeconds(): DurationUnit {
fun Date.formatForLocale(): String { fun Date.formatForLocale(): String {
val locale = Locale.getDefault() val locale = Locale.getDefault()
val dateFormatter: DateFormat = if (locale == Locale.US || locale == Locale.ENGLISH) { val dateFormatter: DateFormat =
if (locale == Locale.US || locale == Locale.ENGLISH) {
SimpleDateFormat("M/d/yy", locale) SimpleDateFormat("M/d/yy", locale)
} else { } else {
SimpleDateFormat.getDateInstance(DateFormat.LONG, locale) SimpleDateFormat.getDateInstance(DateFormat.LONG, locale)

View file

@ -8,7 +8,10 @@ import com.google.firebase.ktx.Firebase
import com.habitrpg.android.habitica.ui.activities.BaseActivity import com.habitrpg.android.habitica.ui.activities.BaseActivity
import java.util.Locale import java.util.Locale
fun Resources.forceLocale(activity: BaseActivity, locale: Locale) { fun Resources.forceLocale(
activity: BaseActivity,
locale: Locale,
) {
Locale.setDefault(locale) Locale.setDefault(locale)
val configuration = Configuration() val configuration = Configuration()
configuration.setLocale(locale) configuration.setLocale(locale)

View file

@ -3,24 +3,50 @@ package com.habitrpg.android.habitica.extensions
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
class OnChangeTextWatcher(private var function: (CharSequence?, Int, Int, Int) -> Unit) : TextWatcher { class OnChangeTextWatcher(private var function: (CharSequence?, Int, Int, Int) -> Unit) :
override fun afterTextChanged(s: Editable?) { /* no-on */ } TextWatcher {
override fun afterTextChanged(s: Editable?) { // no-on
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { /* no-on */ } override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int,
) { // no-on
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { override fun onTextChanged(
s: CharSequence?,
start: Int,
before: Int,
count: Int,
) {
function(s, start, before, count) function(s, start, before, count)
} }
} }
class BeforeChangeTextWatcher(private var function: (CharSequence?, Int, Int, Int) -> Unit) : TextWatcher { class BeforeChangeTextWatcher(private var function: (CharSequence?, Int, Int, Int) -> Unit) :
override fun afterTextChanged(s: Editable?) { /* no-on */ } TextWatcher {
override fun afterTextChanged(s: Editable?) { // no-on
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int,
) {
function(s, start, count, after) function(s, start, count, after)
} }
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { /* no-on */ } override fun onTextChanged(
s: CharSequence?,
start: Int,
before: Int,
count: Int,
) { // no-on
}
} }
class AfterChangeTextWatcher(private var function: (Editable?) -> Unit) : TextWatcher { class AfterChangeTextWatcher(private var function: (Editable?) -> Unit) : TextWatcher {
@ -28,7 +54,19 @@ class AfterChangeTextWatcher(private var function: (Editable?) -> Unit) : TextWa
function(s) function(s)
} }
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { /* no-on */ } override fun beforeTextChanged(
s: CharSequence?,
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { /* no-on */ } start: Int,
count: Int,
after: Int,
) { // no-on
}
override fun onTextChanged(
s: CharSequence?,
start: Int,
before: Int,
count: Int,
) { // no-on
}
} }

View file

@ -6,11 +6,15 @@ import android.view.Window
import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.R
import com.habitrpg.common.habitica.extensions.getThemeColor import com.habitrpg.common.habitica.extensions.getThemeColor
fun Window.updateStatusBarColor(color: Int, isLight: Boolean) { fun Window.updateStatusBarColor(
color: Int,
isLight: Boolean,
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
statusBarColor = color statusBarColor = color
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
decorView.systemUiVisibility = if (isLight) View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR else View.SYSTEM_UI_FLAG_VISIBLE decorView.systemUiVisibility =
if (isLight) View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR else View.SYSTEM_UI_FLAG_VISIBLE
} else { } else {
statusBarColor = context.getThemeColor(R.attr.colorPrimaryDark) statusBarColor = context.getThemeColor(R.attr.colorPrimaryDark)
} }

View file

@ -1,7 +1,6 @@
package com.habitrpg.android.habitica.extensions package com.habitrpg.android.habitica.extensions
import com.habitrpg.android.habitica.models.tasks.Days import com.habitrpg.android.habitica.models.tasks.Days
import com.habitrpg.shared.habitica.models.tasks.Frequency
import java.time.DayOfWeek import java.time.DayOfWeek
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneId import java.time.ZoneId
@ -9,16 +8,16 @@ import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.format.DateTimeFormatterBuilder import java.time.format.DateTimeFormatterBuilder
import java.time.format.TextStyle import java.time.format.TextStyle
import java.time.temporal.ChronoUnit
import java.time.temporal.TemporalAccessor import java.time.temporal.TemporalAccessor
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
fun String.parseToZonedDateTime(): ZonedDateTime? { fun String.parseToZonedDateTime(): ZonedDateTime? {
val parsed: TemporalAccessor = formatter().parseBest( val parsed: TemporalAccessor =
formatter().parseBest(
this, this,
ZonedDateTime::from, ZonedDateTime::from,
LocalDateTime::from LocalDateTime::from,
) )
return if (parsed is ZonedDateTime) { return if (parsed is ZonedDateTime) {
parsed parsed
@ -46,7 +45,6 @@ fun formatter(): DateTimeFormatter =
.appendPattern("[XX]") .appendPattern("[XX]")
.toFormatter() .toFormatter()
fun ZonedDateTime.matchesRepeatDays(repeatDays: Days?): Boolean { fun ZonedDateTime.matchesRepeatDays(repeatDays: Days?): Boolean {
repeatDays ?: return true // If no repeatDays specified, assume it matches repeatDays ?: return true // If no repeatDays specified, assume it matches
@ -61,7 +59,3 @@ fun ZonedDateTime.matchesRepeatDays(repeatDays: Days?): Boolean {
else -> false else -> false
} }
} }

View file

@ -23,7 +23,8 @@ import kotlin.time.toDuration
enum class AdType { enum class AdType {
ARMOIRE, ARMOIRE,
SPELL, SPELL,
FAINT; FAINT,
;
val adUnitID: String val adUnitID: String
get() { get() {
@ -67,7 +68,7 @@ class AdHandler(val activity: Activity, val type: AdType, val rewardAction: (Boo
UNINITIALIZED, UNINITIALIZED,
INITIALIZING, INITIALIZING,
READY, READY,
DISABLED DISABLED,
} }
private lateinit var sharedPreferences: SharedPreferences private lateinit var sharedPreferences: SharedPreferences
@ -100,7 +101,10 @@ class AdHandler(val activity: Activity, val type: AdType, val rewardAction: (Boo
} }
} }
fun initialize(context: Context, onComplete: () -> Unit) { fun initialize(
context: Context,
onComplete: () -> Unit,
) {
if (currentAdStatus != AdStatus.UNINITIALIZED) return if (currentAdStatus != AdStatus.UNINITIALIZED) return
if (BuildConfig.DEBUG || BuildConfig.TESTING_LEVEL == "staff" || BuildConfig.TESTING_LEVEL == "alpha") { if (BuildConfig.DEBUG || BuildConfig.TESTING_LEVEL == "staff" || BuildConfig.TESTING_LEVEL == "alpha") {
@ -119,19 +123,25 @@ class AdHandler(val activity: Activity, val type: AdType, val rewardAction: (Boo
}*/ }*/
} }
fun whenAdsInitialized(context: Context, onComplete: () -> Unit) { fun whenAdsInitialized(
context: Context,
onComplete: () -> Unit,
) {
when (currentAdStatus) { when (currentAdStatus) {
AdStatus.READY -> { AdStatus.READY -> {
onComplete() onComplete()
} }
AdStatus.DISABLED -> { AdStatus.DISABLED -> {
return return
} }
AdStatus.UNINITIALIZED -> { AdStatus.UNINITIALIZED -> {
initialize(context) { initialize(context) {
onComplete() onComplete()
} }
} }
AdStatus.INITIALIZING -> { AdStatus.INITIALIZING -> {
return return
} }
@ -189,15 +199,18 @@ class AdHandler(val activity: Activity, val type: AdType, val rewardAction: (Boo
AdStatus.READY -> { AdStatus.READY -> {
showRewardedAd() showRewardedAd()
} }
AdStatus.DISABLED -> { AdStatus.DISABLED -> {
rewardAction(false) rewardAction(false)
return return
} }
AdStatus.UNINITIALIZED -> { AdStatus.UNINITIALIZED -> {
initialize(activity) { initialize(activity) {
showRewardedAd() showRewardedAd()
} }
} }
AdStatus.INITIALIZING -> { AdStatus.INITIALIZING -> {
return return
} }

View file

@ -13,12 +13,12 @@ import com.habitrpg.android.habitica.R
enum class AnalyticsTarget { enum class AnalyticsTarget {
AMPLITUDE, AMPLITUDE,
FIREBASE FIREBASE,
} }
enum class EventCategory(val key: String) { enum class EventCategory(val key: String) {
BEHAVIOUR("behaviour"), BEHAVIOUR("behaviour"),
NAVIGATION("navigation") NAVIGATION("navigation"),
} }
enum class HitType(val key: String) { enum class HitType(val key: String) {
@ -26,7 +26,7 @@ enum class HitType(val key: String) {
PAGEVIEW("pageview"), PAGEVIEW("pageview"),
CREATE_WIDGET("create"), CREATE_WIDGET("create"),
REMOVE_WIDGET("remove"), REMOVE_WIDGET("remove"),
UPDATE_WIDGET("update") UPDATE_WIDGET("update"),
} }
object Analytics { object Analytics {
@ -39,16 +39,17 @@ object Analytics {
category: EventCategory?, category: EventCategory?,
hitType: HitType?, hitType: HitType?,
additionalData: Map<String, Any>? = null, additionalData: Map<String, Any>? = null,
target: AnalyticsTarget? = null target: AnalyticsTarget? = null,
) { ) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
return return
} }
val data = mutableMapOf<String, Any?>( val data =
mutableMapOf<String, Any?>(
"eventAction" to eventAction, "eventAction" to eventAction,
"eventCategory" to category?.key, "eventCategory" to category?.key,
"hitType" to hitType?.key, "hitType" to hitType?.key,
"status" to "displayed" "status" to "displayed",
) )
if (additionalData != null) { if (additionalData != null) {
data.putAll(additionalData) data.putAll(additionalData)
@ -74,17 +75,19 @@ object Analytics {
} }
fun initialize(context: Context) { fun initialize(context: Context) {
amplitude = Amplitude( amplitude =
Amplitude(
Configuration( Configuration(
context.getString(R.string.amplitude_app_id), context.getString(R.string.amplitude_app_id),
context context,
) ),
) )
firebase = FirebaseAnalytics.getInstance(context) firebase = FirebaseAnalytics.getInstance(context)
} }
fun identify(sharedPrefs: SharedPreferences) { fun identify(sharedPrefs: SharedPreferences) {
val identify = Identify() val identify =
Identify()
.setOnce("androidStore", BuildConfig.STORE) .setOnce("androidStore", BuildConfig.STORE)
sharedPrefs.getString("launch_screen", "")?.let { sharedPrefs.getString("launch_screen", "")?.let {
identify.set("launch_screen", it) identify.set("launch_screen", it)
@ -104,7 +107,10 @@ object Analytics {
} }
} }
fun setUserProperty(identifier: String, value: Any?) { fun setUserProperty(
identifier: String,
value: Any?,
) {
if (this::amplitude.isInitialized) { if (this::amplitude.isInitialized) {
amplitude.identify(mapOf(identifier to value)) amplitude.identify(mapOf(identifier to value))
} }

View file

@ -17,8 +17,8 @@ import com.habitrpg.common.habitica.helpers.launchCatching
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import java.util.Date import java.util.Date
class AppConfigManager(contentRepository: ContentRepository?) : com.habitrpg.common.habitica.helpers.AppConfigManager() { class AppConfigManager(contentRepository: ContentRepository?) :
com.habitrpg.common.habitica.helpers.AppConfigManager() {
private var worldState: WorldState? = null private var worldState: WorldState? = null
init { init {
@ -122,7 +122,12 @@ class AppConfigManager(contentRepository: ContentRepository?) : com.habitrpg.com
if (worldState?.isValid == true) { if (worldState?.isValid == true) {
for (event in worldState?.events ?: listOf(worldState?.currentEvent)) { for (event in worldState?.events ?: listOf(worldState?.currentEvent)) {
if (event == null) return null if (event == null) return null
val thisPromo = getHabiticaPromotionFromKey(event.promo ?: event.eventKey ?: "", event.start, event.end) val thisPromo =
getHabiticaPromotionFromKey(
event.promo ?: event.eventKey ?: "",
event.start,
event.end,
)
if (thisPromo != null) { if (thisPromo != null) {
promo = thisPromo promo = thisPromo
} }
@ -178,7 +183,8 @@ class AppConfigManager(contentRepository: ContentRepository?) : com.habitrpg.com
} }
fun getBirthdayEvent(): WorldStateEvent? { fun getBirthdayEvent(): WorldStateEvent? {
val events = ((worldState?.events as? List<WorldStateEvent>) ?: listOf(worldState?.currentEvent)) val events =
((worldState?.events as? List<WorldStateEvent>) ?: listOf(worldState?.currentEvent))
return events.firstOrNull { it?.eventKey == "birthday10" && it.end?.after(Date()) == true } return events.firstOrNull { it?.eventKey == "birthday10" && it.end?.after(Date()) == true }
} }

View file

@ -3,11 +3,13 @@ package com.habitrpg.android.habitica.helpers
import java.util.Date import java.util.Date
class AprilFoolsHandler { class AprilFoolsHandler {
companion object { companion object {
private var eventEnd: Date? = null private var eventEnd: Date? = null
fun handle(name: String?, endDate: Date?) { fun handle(
name: String?,
endDate: Date?,
) {
if (endDate != null) { if (endDate != null) {
this.eventEnd = endDate this.eventEnd = endDate
} }

View file

@ -4,8 +4,14 @@ import android.content.res.Resources
import com.habitrpg.android.habitica.models.tasks.Task import com.habitrpg.android.habitica.models.tasks.Task
interface GroupPlanInfoProvider { interface GroupPlanInfoProvider {
fun assignedTextForTask(resources: Resources, assignedUsers: List<String>): String fun assignedTextForTask(
resources: Resources,
assignedUsers: List<String>,
): String
fun canScoreTask(task: Task): Boolean fun canScoreTask(task: Task): Boolean
suspend fun canEditTask(task: Task): Boolean suspend fun canEditTask(task: Task): Boolean
suspend fun canAddTasks(): Boolean suspend fun canAddTasks(): Boolean
} }

View file

@ -15,15 +15,18 @@ import kotlin.coroutines.EmptyCoroutineContext
@Composable @Composable
fun <T> rememberFlow( fun <T> rememberFlow(
flow: Flow<T>, flow: Flow<T>,
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
): Flow<T> { ): Flow<T> {
return remember(key1 = flow, key2 = lifecycleOwner) { flow.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED) } return remember(
key1 = flow,
key2 = lifecycleOwner,
) { flow.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED) }
} }
@Composable @Composable
fun <T : R, R> Flow<T>.collectAsStateLifecycleAware( fun <T : R, R> Flow<T>.collectAsStateLifecycleAware(
initial: R, initial: R,
context: CoroutineContext = EmptyCoroutineContext context: CoroutineContext = EmptyCoroutineContext,
): State<R> { ): State<R> {
val lifecycleAwareFlow = rememberFlow(flow = this) val lifecycleAwareFlow = rememberFlow(flow = this)
return lifecycleAwareFlow.collectAsState(initial = initial, context = context) return lifecycleAwareFlow.collectAsState(initial = initial, context = context)

View file

@ -10,21 +10,41 @@ import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class NotificationOpenHandler { class NotificationOpenHandler {
companion object { companion object {
fun handleOpenedByNotification(
fun handleOpenedByNotification(identifier: String, intent: Intent) { identifier: String,
intent: Intent,
) {
MainScope().launch(context = Dispatchers.Main) { MainScope().launch(context = Dispatchers.Main) {
when (identifier) { when (identifier) {
PushNotificationManager.PARTY_INVITE_PUSH_NOTIFICATION_KEY -> openNoPartyScreen() PushNotificationManager.PARTY_INVITE_PUSH_NOTIFICATION_KEY -> openNoPartyScreen()
PushNotificationManager.QUEST_BEGUN_PUSH_NOTIFICATION_KEY -> openPartyScreen() PushNotificationManager.QUEST_BEGUN_PUSH_NOTIFICATION_KEY -> openPartyScreen()
PushNotificationManager.QUEST_INVITE_PUSH_NOTIFICATION_KEY -> openPartyScreen() PushNotificationManager.QUEST_INVITE_PUSH_NOTIFICATION_KEY -> openPartyScreen()
PushNotificationManager.GUILD_INVITE_PUSH_NOTIFICATION_KEY -> openGuildDetailScreen(intent.getStringExtra("groupID")) PushNotificationManager.GUILD_INVITE_PUSH_NOTIFICATION_KEY ->
PushNotificationManager.RECEIVED_PRIVATE_MESSAGE_PUSH_NOTIFICATION_KEY -> openPrivateMessageScreen(intent.getStringExtra("replyToUUID"), intent.getStringExtra("replyToUsername")) openGuildDetailScreen(
intent.getStringExtra("groupID"),
)
PushNotificationManager.RECEIVED_PRIVATE_MESSAGE_PUSH_NOTIFICATION_KEY ->
openPrivateMessageScreen(
intent.getStringExtra("replyToUUID"),
intent.getStringExtra("replyToUsername"),
)
PushNotificationManager.CHANGE_USERNAME_PUSH_NOTIFICATION_KEY -> openSettingsScreen() PushNotificationManager.CHANGE_USERNAME_PUSH_NOTIFICATION_KEY -> openSettingsScreen()
PushNotificationManager.GIFT_ONE_GET_ONE_PUSH_NOTIFICATION_KEY -> openSubscriptionScreen() PushNotificationManager.GIFT_ONE_GET_ONE_PUSH_NOTIFICATION_KEY -> openSubscriptionScreen()
PushNotificationManager.CHAT_MENTION_NOTIFICATION_KEY -> handleChatMessage(intent.getStringExtra("type"), intent.getStringExtra("groupID")) PushNotificationManager.CHAT_MENTION_NOTIFICATION_KEY ->
PushNotificationManager.GROUP_ACTIVITY_NOTIFICATION_KEY -> handleChatMessage(intent.getStringExtra("type"), intent.getStringExtra("groupID")) handleChatMessage(
intent.getStringExtra("type"),
intent.getStringExtra("groupID"),
)
PushNotificationManager.GROUP_ACTIVITY_NOTIFICATION_KEY ->
handleChatMessage(
intent.getStringExtra("type"),
intent.getStringExtra("groupID"),
)
PushNotificationManager.G1G1_PROMO_KEY -> openGiftOneGetOneInfoScreen() PushNotificationManager.G1G1_PROMO_KEY -> openGiftOneGetOneInfoScreen()
else -> { else -> {
intent.getStringExtra("openURL")?.let { intent.getStringExtra("openURL")?.let {
@ -36,12 +56,21 @@ class NotificationOpenHandler {
} }
private fun openSubscriptionScreen() { private fun openSubscriptionScreen() {
MainNavigationController.navigate(R.id.gemPurchaseActivity, bundleOf(Pair("openSubscription", true))) MainNavigationController.navigate(
R.id.gemPurchaseActivity,
bundleOf(Pair("openSubscription", true)),
)
} }
private fun openPrivateMessageScreen(userID: String?, userName: String?) { private fun openPrivateMessageScreen(
userID: String?,
userName: String?,
) {
if (userID != null && userName != null) { if (userID != null && userName != null) {
MainNavigationController.navigate(R.id.inboxMessageListFragment, bundleOf("userID" to userID, "username" to userName)) MainNavigationController.navigate(
R.id.inboxMessageListFragment,
bundleOf("userID" to userID, "username" to userName),
)
} else { } else {
MainNavigationController.navigate(R.id.inboxFragment) MainNavigationController.navigate(R.id.inboxFragment)
} }
@ -49,7 +78,10 @@ class NotificationOpenHandler {
private fun openPartyScreen(isChatNotification: Boolean = false) { private fun openPartyScreen(isChatNotification: Boolean = false) {
val tabToOpen = if (isChatNotification) 1 else 0 val tabToOpen = if (isChatNotification) 1 else 0
MainNavigationController.navigate(R.id.partyFragment, bundleOf("tabToOpen" to tabToOpen)) MainNavigationController.navigate(
R.id.partyFragment,
bundleOf("tabToOpen" to tabToOpen),
)
} }
private fun openNoPartyScreen() { private fun openNoPartyScreen() {
@ -72,7 +104,10 @@ class NotificationOpenHandler {
MainNavigationController.navigate(R.id.prefsActivity) MainNavigationController.navigate(R.id.prefsActivity)
} }
private fun handleChatMessage(type: String?, groupID: String?) { private fun handleChatMessage(
type: String?,
groupID: String?,
) {
when (type) { when (type) {
"party" -> openPartyScreen() "party" -> openPartyScreen()
"guild" -> openGuildDetailScreen(groupID) "guild" -> openGuildDetailScreen(groupID)

View file

@ -20,20 +20,26 @@ interface NotificationsManager {
var apiClient: WeakReference<ApiClient>? var apiClient: WeakReference<ApiClient>?
fun setNotifications(current: List<Notification>) fun setNotifications(current: List<Notification>)
fun getNotifications(): Flow<List<Notification>> fun getNotifications(): Flow<List<Notification>>
fun getNotification(id: String): Notification? fun getNotification(id: String): Notification?
fun dismissTaskNotification(context: Context, task: Task)
fun dismissTaskNotification(
context: Context,
task: Task,
)
} }
class MainNotificationsManager : NotificationsManager { class MainNotificationsManager : NotificationsManager {
private val seenNotifications: MutableMap<String, Boolean> private val seenNotifications: MutableMap<String, Boolean>
override var apiClient: WeakReference<ApiClient>? = null override var apiClient: WeakReference<ApiClient>? = null
private var lastNotificationHandling: Date? = null private var lastNotificationHandling: Date? = null
private val notificationsFlow = MutableStateFlow<List<Notification>?>(null) private val notificationsFlow = MutableStateFlow<List<Notification>?>(null)
private val displayedNotificationEvents = Channel<Notification>() private val displayedNotificationEvents = Channel<Notification>()
override val displayNotificationEvents: Flow<Notification> = displayedNotificationEvents.receiveAsFlow().filterNotNull() override val displayNotificationEvents: Flow<Notification> =
displayedNotificationEvents.receiveAsFlow().filterNotNull()
init { init {
this.seenNotifications = HashMap() this.seenNotifications = HashMap()
@ -52,7 +58,10 @@ class MainNotificationsManager : NotificationsManager {
return notificationsFlow.value?.find { it.id == id } return notificationsFlow.value?.find { it.id == id }
} }
override fun dismissTaskNotification(context: Context, task: Task) { override fun dismissTaskNotification(
context: Context,
task: Task,
) {
NotificationManagerCompat.from(context).cancel(task.id.hashCode()) NotificationManagerCompat.from(context).cancel(task.id.hashCode())
} }
@ -65,7 +74,8 @@ class MainNotificationsManager : NotificationsManager {
notifications notifications
.filter { !this.seenNotifications.containsKey(it.id) } .filter { !this.seenNotifications.containsKey(it.id) }
.map { .map {
val notificationDisplayed = when (it.type) { val notificationDisplayed =
when (it.type) {
Notification.Type.ACHIEVEMENT_PARTY_UP.type -> true Notification.Type.ACHIEVEMENT_PARTY_UP.type -> true
Notification.Type.ACHIEVEMENT_PARTY_ON.type -> true Notification.Type.ACHIEVEMENT_PARTY_ON.type -> true
Notification.Type.ACHIEVEMENT_BEAST_MASTER.type -> true Notification.Type.ACHIEVEMENT_BEAST_MASTER.type -> true

View file

@ -54,23 +54,29 @@ import kotlin.time.toDuration
class PurchaseHandler( class PurchaseHandler(
private val context: Context, private val context: Context,
private val apiClient: ApiClient, private val apiClient: ApiClient,
private val userViewModel: MainUserViewModel private val userViewModel: MainUserViewModel,
) : PurchasesUpdatedListener, PurchasesResponseListener { ) : PurchasesUpdatedListener, PurchasesResponseListener {
private val billingClient = private val billingClient =
BillingClient.newBuilder(context).setListener(this).enablePendingPurchases().build() BillingClient.newBuilder(context).setListener(this).enablePendingPurchases().build()
override fun onPurchasesUpdated(result: BillingResult, purchases: MutableList<Purchase>?) { override fun onPurchasesUpdated(
result: BillingResult,
purchases: MutableList<Purchase>?,
) {
purchases?.let { processPurchases(result, it) } purchases?.let { processPurchases(result, it) }
} }
override fun onQueryPurchasesResponse( override fun onQueryPurchasesResponse(
result: BillingResult, result: BillingResult,
purchases: MutableList<Purchase> purchases: MutableList<Purchase>,
) { ) {
processPurchases(result, purchases) processPurchases(result, purchases)
} }
private fun processPurchases(result: BillingResult, purchases: List<Purchase>) { private fun processPurchases(
result: BillingResult,
purchases: List<Purchase>,
) {
when (result.responseCode) { when (result.responseCode) {
BillingClient.BillingResponseCode.OK -> { BillingClient.BillingResponseCode.OK -> {
val mostRecentSub = findMostRecentSubscription(purchases) val mostRecentSub = findMostRecentSubscription(purchases)
@ -79,8 +85,9 @@ class PurchaseHandler(
.filterNotNull().take(1).collect { .filterNotNull().take(1).collect {
val plan = it.purchased!!.plan val plan = it.purchased!!.plan
for (purchase in purchases) { for (purchase in purchases) {
if (plan?.isActive == true && PurchaseTypes.allSubscriptionTypes.contains( if (plan?.isActive == true &&
purchase.products.firstOrNull() PurchaseTypes.allSubscriptionTypes.contains(
purchase.products.firstOrNull(),
) )
) { ) {
if (((plan.dateTerminated != null) == purchase.isAutoRenewing) || if (((plan.dateTerminated != null) == purchase.isAutoRenewing) ||
@ -125,7 +132,12 @@ class PurchaseHandler(
private var billingClientState: BillingClientState = BillingClientState.UNINITIALIZED private var billingClientState: BillingClientState = BillingClientState.UNINITIALIZED
private enum class BillingClientState { private enum class BillingClientState {
UNINITIALIZED, READY, UNAVAILABLE, DISCONNECTED, CONNECTING; UNINITIALIZED,
READY,
UNAVAILABLE,
DISCONNECTED,
CONNECTING,
;
val canMaybePurchase: Boolean val canMaybePurchase: Boolean
get() { get() {
@ -134,6 +146,7 @@ class PurchaseHandler(
} }
private var listeningRetryCount = 0 private var listeningRetryCount = 0
fun startListening() { fun startListening() {
if (billingClient.connectionState == BillingClient.ConnectionState.CONNECTING || if (billingClient.connectionState == BillingClient.ConnectionState.CONNECTING ||
billingClient.connectionState == BillingClient.ConnectionState.CONNECTED || billingClient.connectionState == BillingClient.ConnectionState.CONNECTED ||
@ -147,7 +160,8 @@ class PurchaseHandler(
return return
} }
billingClientState = BillingClientState.CONNECTING billingClientState = BillingClientState.CONNECTING
billingClient.startConnection(object : BillingClientStateListener { billingClient.startConnection(
object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) { override fun onBillingSetupFinished(billingResult: BillingResult) {
when (billingResult.responseCode) { when (billingResult.responseCode) {
BillingClient.BillingResponseCode.OK -> { BillingClient.BillingResponseCode.OK -> {
@ -156,12 +170,15 @@ class PurchaseHandler(
queryPurchases() queryPurchases()
} }
} }
BillingClient.BillingResponseCode.SERVICE_DISCONNECTED -> { BillingClient.BillingResponseCode.SERVICE_DISCONNECTED -> {
retryListening() retryListening()
} }
BillingClient.BillingResponseCode.SERVICE_TIMEOUT -> { BillingClient.BillingResponseCode.SERVICE_TIMEOUT -> {
retryListening() retryListening()
} }
else -> { else -> {
billingClientState = BillingClientState.UNAVAILABLE billingClientState = BillingClientState.UNAVAILABLE
} }
@ -172,7 +189,8 @@ class PurchaseHandler(
billingClientState = BillingClientState.DISCONNECTED billingClientState = BillingClientState.DISCONNECTED
retryListening() retryListening()
} }
}) },
)
} }
private fun retryListening() { private fun retryListening() {
@ -195,20 +213,22 @@ class PurchaseHandler(
} }
billingClientState.canMaybePurchase && billingClient.isReady billingClientState.canMaybePurchase && billingClient.isReady
} }
val subResponse = billingClient.queryPurchasesAsync( val subResponse =
billingClient.queryPurchasesAsync(
QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS) QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS)
.build() .build(),
) )
processPurchases(subResponse.billingResult, subResponse.purchasesList) processPurchases(subResponse.billingResult, subResponse.purchasesList)
val iapResponse = billingClient.queryPurchasesAsync( val iapResponse =
billingClient.queryPurchasesAsync(
QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.INAPP) QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.INAPP)
.build() .build(),
) )
processPurchases(iapResponse.billingResult, iapResponse.purchasesList) processPurchases(iapResponse.billingResult, iapResponse.purchasesList)
} }
suspend fun getGryphatriceSKU() = suspend fun getGryphatriceSKU() =
getSKU(BillingClient.ProductType.INAPP, PurchaseTypes.JubilantGrphatrice) getSKU(BillingClient.ProductType.INAPP, PurchaseTypes.JUBILANT_GRYPHATRICE)
suspend fun getAllGemSKUs() = suspend fun getAllGemSKUs() =
getSKUs(BillingClient.ProductType.INAPP, PurchaseTypes.allGemTypes) getSKUs(BillingClient.ProductType.INAPP, PurchaseTypes.allGemTypes)
@ -222,27 +242,38 @@ class PurchaseHandler(
suspend fun getInAppPurchaseSKU(identifier: String) = suspend fun getInAppPurchaseSKU(identifier: String) =
getSKU(BillingClient.ProductType.INAPP, identifier) getSKU(BillingClient.ProductType.INAPP, identifier)
private suspend fun getSKUs(type: String, identifiers: List<String>) = private suspend fun getSKUs(
type: String,
identifiers: List<String>,
) =
loadInventory(type, identifiers) ?: emptyList() loadInventory(type, identifiers) ?: emptyList()
private suspend fun getSKU(type: String, identifier: String): ProductDetails? { private suspend fun getSKU(
type: String,
identifier: String,
): ProductDetails? {
val inventory = loadInventory(type, listOf(identifier)) val inventory = loadInventory(type, listOf(identifier))
return inventory?.firstOrNull() return inventory?.firstOrNull()
} }
private suspend fun loadInventory(type: String, skus: List<String>): List<ProductDetails>? { private suspend fun loadInventory(
type: String,
skus: List<String>,
): List<ProductDetails>? {
retryUntil { retryUntil {
if (billingClientState == BillingClientState.DISCONNECTED) { if (billingClientState == BillingClientState.DISCONNECTED) {
startListening() startListening()
} }
billingClientState.canMaybePurchase && billingClient.isReady billingClientState.canMaybePurchase && billingClient.isReady
} }
val params = QueryProductDetailsParams.newBuilder().setProductList( val params =
QueryProductDetailsParams.newBuilder().setProductList(
skus.map { skus.map {
Product.newBuilder().setProductId(it).setProductType(type).build() Product.newBuilder().setProductId(it).setProductType(type).build()
} },
).build() ).build()
val skuDetailsResult = withContext(Dispatchers.IO) { val skuDetailsResult =
withContext(Dispatchers.IO) {
billingClient.queryProductDetails(params) billingClient.queryProductDetails(params)
} }
return skuDetailsResult.productDetailsList return skuDetailsResult.productDetailsList
@ -253,7 +284,7 @@ class PurchaseHandler(
skuDetails: ProductDetails, skuDetails: ProductDetails,
recipient: String? = null, recipient: String? = null,
recipientUsername: String? = null, recipientUsername: String? = null,
isSaleGemPurchase: Boolean = false isSaleGemPurchase: Boolean = false,
) { ) {
this.isSaleGemPurchase = isSaleGemPurchase this.isSaleGemPurchase = isSaleGemPurchase
recipient?.let { recipient?.let {
@ -264,14 +295,17 @@ class PurchaseHandler(
listOf(skuDetails).map { listOf(skuDetails).map {
BillingFlowParams.ProductDetailsParams.newBuilder() BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(skuDetails).setOfferToken( .setProductDetails(skuDetails).setOfferToken(
skuDetails.subscriptionOfferDetails?.first()?.offerToken ?: "" skuDetails.subscriptionOfferDetails?.first()?.offerToken ?: "",
).build() ).build()
} },
).build() ).build()
billingClient.launchBillingFlow(activity, flowParams) billingClient.launchBillingFlow(activity, flowParams)
} }
private suspend fun consume(purchase: Purchase, retries: Int = 4) { private suspend fun consume(
purchase: Purchase,
retries: Int = 4,
) {
retryUntil { billingClientState.canMaybePurchase && billingClient.isReady } retryUntil { billingClientState.canMaybePurchase && billingClient.isReady }
val params = ConsumeParams.newBuilder().setPurchaseToken(purchase.purchaseToken).build() val params = ConsumeParams.newBuilder().setPurchaseToken(purchase.purchaseToken).build()
val result = billingClient.consumePurchase(params) val result = billingClient.consumePurchase(params)
@ -287,14 +321,19 @@ class PurchaseHandler(
} }
private var processedPurchases = mutableSetOf<String>() private var processedPurchases = mutableSetOf<String>()
private fun handle(purchase: Purchase) { private fun handle(purchase: Purchase) {
if (purchase.purchaseState != Purchase.PurchaseState.PURCHASED || processedPurchases.contains(purchase.orderId)) { if (purchase.purchaseState != Purchase.PurchaseState.PURCHASED ||
processedPurchases.contains(
purchase.orderId,
)
) {
return return
} }
purchase.orderId?.let { processedPurchases.add(it) } purchase.orderId?.let { processedPurchases.add(it) }
val sku = purchase.products.firstOrNull() val sku = purchase.products.firstOrNull()
when { when {
sku == PurchaseTypes.JubilantGrphatrice -> { sku == PurchaseTypes.JUBILANT_GRYPHATRICE -> {
val validationRequest = buildValidationRequest(purchase) val validationRequest = buildValidationRequest(purchase)
MainScope().launchCatching { MainScope().launchCatching {
try { try {
@ -363,7 +402,10 @@ class PurchaseHandler(
} }
} }
private suspend fun acknowledgePurchase(purchase: Purchase, retries: Int = 4) { private suspend fun acknowledgePurchase(
purchase: Purchase,
retries: Int = 4,
) {
val params = val params =
AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchase.purchaseToken).build() AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchase.purchaseToken).build()
val response = billingClient.acknowledgePurchase(params) val response = billingClient.acknowledgePurchase(params)
@ -397,7 +439,10 @@ class PurchaseHandler(
return validationRequest return validationRequest
} }
private fun handleError(throwable: Throwable, purchase: Purchase) { private fun handleError(
throwable: Throwable,
purchase: Purchase,
) {
when (throwable) { when (throwable) {
is HttpException -> { is HttpException -> {
if (throwable.code() == 401) { if (throwable.code() == 401) {
@ -412,6 +457,7 @@ class PurchaseHandler(
} }
} }
} }
else -> { else -> {
// Handles other potential errors such as IOException or an exception // Handles other potential errors such as IOException or an exception
// thrown by billingClient.consumePurchase method that is not handled // thrown by billingClient.consumePurchase method that is not handled
@ -425,7 +471,8 @@ class PurchaseHandler(
} }
suspend fun checkForSubscription(): Purchase? { suspend fun checkForSubscription(): Purchase? {
val result = withContext(Dispatchers.IO) { val result =
withContext(Dispatchers.IO) {
val params = val params =
QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS) QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS)
.build() .build()
@ -454,6 +501,7 @@ class PurchaseHandler(
} }
private var alreadyTriedCancellation = false private var alreadyTriedCancellation = false
suspend fun cancelSubscription(): User? { suspend fun cancelSubscription(): User? {
if (alreadyTriedCancellation) return null if (alreadyTriedCancellation) return null
alreadyTriedCancellation = true alreadyTriedCancellation = true
@ -463,10 +511,10 @@ class PurchaseHandler(
private fun durationString(sku: String): String { private fun durationString(sku: String): String {
return when (sku) { return when (sku) {
PurchaseTypes.Subscription1MonthNoRenew, PurchaseTypes.Subscription1Month -> "1" PurchaseTypes.SUBSCRIPTION_1_MONTH_NORENEW, PurchaseTypes.SUBSCRIPTION_1_MONTH -> "1"
PurchaseTypes.Subscription3MonthNoRenew, PurchaseTypes.Subscription3Month -> "3" PurchaseTypes.SUBSCRIPTION_3_MONTH_NORENEW, PurchaseTypes.SUBSCRIPTION_3_MONTH -> "3"
PurchaseTypes.Subscription6MonthNoRenew, PurchaseTypes.Subscription6Month -> "6" PurchaseTypes.SUBSCRIPTION_6_MONTH_NORENEW, PurchaseTypes.SUBSCRIPTION_6_MONTH -> "6"
PurchaseTypes.Subscription12MonthNoRenew, PurchaseTypes.Subscription12Month -> "12" PurchaseTypes.SUBSCRIPTION_12_MONTH_NORENEW, PurchaseTypes.SUBSCRIPTION_12_MONTH -> "12"
else -> "" else -> ""
} }
} }
@ -477,18 +525,18 @@ class PurchaseHandler(
if (isSaleGemPurchase) { if (isSaleGemPurchase) {
isSaleGemPurchase = false isSaleGemPurchase = false
return when (sku) { return when (sku) {
PurchaseTypes.Purchase4Gems -> "5" PurchaseTypes.PURCHASE_4_GEMS -> "5"
PurchaseTypes.Purchase21Gems -> "30" PurchaseTypes.PURCHASE_21_GEMS -> "30"
PurchaseTypes.Purchase42Gems -> "60" PurchaseTypes.PURCHASE_42_GEMS -> "60"
PurchaseTypes.Purchase84Gems -> "125" PurchaseTypes.PURCHASE_84_GEMS -> "125"
else -> "" else -> ""
} }
} else { } else {
return when (sku) { return when (sku) {
PurchaseTypes.Purchase4Gems -> "4" PurchaseTypes.PURCHASE_4_GEMS -> "4"
PurchaseTypes.Purchase21Gems -> "21" PurchaseTypes.PURCHASE_21_GEMS -> "21"
PurchaseTypes.Purchase42Gems -> "42" PurchaseTypes.PURCHASE_42_GEMS -> "42"
PurchaseTypes.Purchase84Gems -> "84" PurchaseTypes.PURCHASE_84_GEMS -> "84"
else -> "" else -> ""
} }
} }
@ -496,33 +544,38 @@ class PurchaseHandler(
private val displayedConfirmations = mutableListOf<String>() private val displayedConfirmations = mutableListOf<String>()
private fun displayConfirmationDialog(purchase: Purchase, giftedTo: String? = null) { private fun displayConfirmationDialog(
purchase: Purchase,
giftedTo: String? = null,
) {
if (displayedConfirmations.contains(purchase.orderId)) { if (displayedConfirmations.contains(purchase.orderId)) {
return return
} }
purchase.orderId?.let { displayedConfirmations.add(it) } purchase.orderId?.let { displayedConfirmations.add(it) }
CoroutineScope(Dispatchers.Main).launchCatching { CoroutineScope(Dispatchers.Main).launchCatching {
val application = (context as? HabiticaBaseApplication) val application =
(context as? HabiticaBaseApplication)
?: (context.applicationContext as? HabiticaBaseApplication) ?: return@launchCatching ?: (context.applicationContext as? HabiticaBaseApplication) ?: return@launchCatching
val sku = purchase.products.firstOrNull() ?: return@launchCatching val sku = purchase.products.firstOrNull() ?: return@launchCatching
var title = context.getString(R.string.successful_purchase_generic) var title = context.getString(R.string.successful_purchase_generic)
val message = when { val message =
when {
PurchaseTypes.allSubscriptionNoRenewTypes.contains(sku) -> { PurchaseTypes.allSubscriptionNoRenewTypes.contains(sku) -> {
title = context.getString(R.string.gift_confirmation_title) title = context.getString(R.string.gift_confirmation_title)
context.getString( context.getString(
R.string.gift_confirmation_text_sub, R.string.gift_confirmation_text_sub,
giftedTo, giftedTo,
durationString(sku) durationString(sku),
) )
} }
PurchaseTypes.allSubscriptionTypes.contains(sku) -> { PurchaseTypes.allSubscriptionTypes.contains(sku) -> {
if (sku == PurchaseTypes.Subscription1Month) { if (sku == PurchaseTypes.SUBSCRIPTION_1_MONTH) {
context.getString(R.string.subscription_confirmation) context.getString(R.string.subscription_confirmation)
} else { } else {
context.getString( context.getString(
R.string.subscription_confirmation_multiple, R.string.subscription_confirmation_multiple,
durationString(sku) durationString(sku),
) )
} }
} }
@ -532,7 +585,7 @@ class PurchaseHandler(
context.getString( context.getString(
R.string.gift_confirmation_text_gems_new, R.string.gift_confirmation_text_gems_new,
giftedTo, giftedTo,
gemAmountString(sku) gemAmountString(sku),
) )
} }
@ -559,13 +612,15 @@ class PurchaseHandler(
private fun displayGryphatriceConfirmationDialog( private fun displayGryphatriceConfirmationDialog(
purchase: Purchase, purchase: Purchase,
giftedTo: String? = null giftedTo: String? = null,
) { ) {
MainScope().launch(ExceptionHandler.coroutine()) { MainScope().launch(ExceptionHandler.coroutine()) {
val application = (context as? HabiticaBaseApplication) val application =
(context as? HabiticaBaseApplication)
?: (context.applicationContext as? HabiticaBaseApplication) ?: return@launch ?: (context.applicationContext as? HabiticaBaseApplication) ?: return@launch
val title = context.getString(R.string.successful_purchase_generic) val title = context.getString(R.string.successful_purchase_generic)
val message = if (giftedTo != null) { val message =
if (giftedTo != null) {
context.getString(R.string.jubilant_gryphatrice_confirmation_gift) context.getString(R.string.jubilant_gryphatrice_confirmation_gift)
} else { } else {
context.getString(R.string.jubilant_gryphatrice_confirmation) context.getString(R.string.jubilant_gryphatrice_confirmation)
@ -590,7 +645,11 @@ class PurchaseHandler(
private var pendingGifts: MutableMap<String, Triple<Date, String, String>> = HashMap() private var pendingGifts: MutableMap<String, Triple<Date, String, String>> = HashMap()
private var preferences: SharedPreferences? = null private var preferences: SharedPreferences? = null
fun addGift(sku: String, userID: String, username: String) { fun addGift(
sku: String,
userID: String,
username: String,
) {
pendingGifts[sku] = Triple(Date(), userID, username) pendingGifts[sku] = Triple(Date(), userID, username)
savePendingGifts() savePendingGifts()
} }
@ -616,7 +675,7 @@ suspend fun retryUntil(
initialDelay: Long = 100, // 0.1 second initialDelay: Long = 100, // 0.1 second
maxDelay: Long = 1000, // 1 second maxDelay: Long = 1000, // 1 second
factor: Double = 2.0, factor: Double = 2.0,
block: suspend () -> Boolean block: suspend () -> Boolean,
) { ) {
var currentDelay = initialDelay var currentDelay = initialDelay
repeat(times - 1) { repeat(times - 1) {

View file

@ -1,30 +1,36 @@
package com.habitrpg.android.habitica.helpers package com.habitrpg.android.habitica.helpers
object PurchaseTypes { object PurchaseTypes {
const val JubilantGrphatrice = "com.habitrpg.android.habitica.iap.pets.gryphatrice_jubilant" const val JUBILANT_GRYPHATRICE = "com.habitrpg.android.habitica.iap.pets.gryphatrice_jubilant"
const val Purchase4Gems = "com.habitrpg.android.habitica.iap.4gems" const val PURCHASE_4_GEMS = "com.habitrpg.android.habitica.iap.4gems"
const val Purchase21Gems = "com.habitrpg.android.habitica.iap.21gems" const val PURCHASE_21_GEMS = "com.habitrpg.android.habitica.iap.21gems"
const val Purchase42Gems = "com.habitrpg.android.habitica.iap.42gems" const val PURCHASE_42_GEMS = "com.habitrpg.android.habitica.iap.42gems"
const val Purchase84Gems = "com.habitrpg.android.habitica.iap.84gems" const val PURCHASE_84_GEMS = "com.habitrpg.android.habitica.iap.84gems"
val allGemTypes = listOf(Purchase4Gems, Purchase21Gems, Purchase42Gems, Purchase84Gems) val allGemTypes = listOf(PURCHASE_4_GEMS, PURCHASE_21_GEMS, PURCHASE_42_GEMS, PURCHASE_84_GEMS)
const val Subscription1Month = "com.habitrpg.android.habitica.subscription.1month" const val SUBSCRIPTION_1_MONTH = "com.habitrpg.android.habitica.subscription.1month"
const val Subscription3Month = "com.habitrpg.android.habitica.subscription.3month" const val SUBSCRIPTION_3_MONTH = "com.habitrpg.android.habitica.subscription.3month"
const val Subscription6Month = "com.habitrpg.android.habitica.subscription.6month" const val SUBSCRIPTION_6_MONTH = "com.habitrpg.android.habitica.subscription.6month"
const val Subscription12Month = "com.habitrpg.android.habitica.subscription.12month" const val SUBSCRIPTION_12_MONTH = "com.habitrpg.android.habitica.subscription.12month"
val allSubscriptionTypes = mutableListOf( val allSubscriptionTypes =
Subscription1Month, mutableListOf(
Subscription3Month, SUBSCRIPTION_1_MONTH,
Subscription6Month, SUBSCRIPTION_3_MONTH,
Subscription12Month SUBSCRIPTION_6_MONTH,
SUBSCRIPTION_12_MONTH,
) )
const val Subscription1MonthNoRenew = "com.habitrpg.android.habitica.norenew_subscription.1month" const val SUBSCRIPTION_1_MONTH_NORENEW =
const val Subscription3MonthNoRenew = "com.habitrpg.android.habitica.norenew_subscription.3month" "com.habitrpg.android.habitica.norenew_subscription.1month"
const val Subscription6MonthNoRenew = "com.habitrpg.android.habitica.norenew_subscription.6month" const val SUBSCRIPTION_3_MONTH_NORENEW =
const val Subscription12MonthNoRenew = "com.habitrpg.android.habitica.norenew_subscription.12month" "com.habitrpg.android.habitica.norenew_subscription.3month"
var allSubscriptionNoRenewTypes = listOf( const val SUBSCRIPTION_6_MONTH_NORENEW =
Subscription1MonthNoRenew, "com.habitrpg.android.habitica.norenew_subscription.6month"
Subscription3MonthNoRenew, const val SUBSCRIPTION_12_MONTH_NORENEW =
Subscription6MonthNoRenew, "com.habitrpg.android.habitica.norenew_subscription.12month"
Subscription12MonthNoRenew var allSubscriptionNoRenewTypes =
listOf(
SUBSCRIPTION_1_MONTH_NORENEW,
SUBSCRIPTION_3_MONTH_NORENEW,
SUBSCRIPTION_6_MONTH_NORENEW,
SUBSCRIPTION_12_MONTH_NORENEW,
) )
} }

View file

@ -6,7 +6,6 @@ import androidx.core.content.edit
import com.google.android.play.core.review.ReviewManagerFactory import com.google.android.play.core.review.ReviewManagerFactory
class ReviewManager(context: Context, private val configManager: AppConfigManager) { class ReviewManager(context: Context, private val configManager: AppConfigManager) {
private val reviewManager = ReviewManagerFactory.create(context) private val reviewManager = ReviewManagerFactory.create(context)
private val sharedPref = context.getSharedPreferences("ReviewPrefs", Context.MODE_PRIVATE) private val sharedPref = context.getSharedPreferences("ReviewPrefs", Context.MODE_PRIVATE)
@ -59,7 +58,10 @@ class ReviewManager(context: Context, private val configManager: AppConfigManage
return !(lastReviewCheckin != -1 && currentCheckins - lastReviewCheckin < 5) return !(lastReviewCheckin != -1 && currentCheckins - lastReviewCheckin < 5)
} }
fun requestReview(activity: AppCompatActivity, currentCheckins: Int) { fun requestReview(
activity: AppCompatActivity,
currentCheckins: Int,
) {
if (!canRequestReview(currentCheckins)) return if (!canRequestReview(currentCheckins)) return
val request = reviewManager.requestReviewFlow() val request = reviewManager.requestReviewFlow()

View file

@ -36,7 +36,8 @@ class SoundFile(val theme: String, private val fileName: String) {
try { try {
player?.setDataSource(file?.path) player?.setDataSource(file?.path)
val attributes = AudioAttributes.Builder() val attributes =
AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setLegacyStreamType(AudioManager.STREAM_MUSIC) .setLegacyStreamType(AudioManager.STREAM_MUSIC)
.build() .build()

View file

@ -20,7 +20,9 @@ class SoundFileLoader(private val context: Context) {
private val externalCacheDir: String? private val externalCacheDir: String?
get() { get() {
val cacheDir = HabiticaBaseApplication.getInstance(context)?.getExternalFilesDir(Environment.DIRECTORY_NOTIFICATIONS) val cacheDir =
HabiticaBaseApplication.getInstance(context)
?.getExternalFilesDir(Environment.DIRECTORY_NOTIFICATIONS)
return cacheDir?.path return cacheDir?.path
} }

View file

@ -6,35 +6,37 @@ import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class SoundManager @Inject constructor(var soundFileLoader: SoundFileLoader) { class SoundManager
var soundTheme: String = SoundThemeOff @Inject
constructor(var soundFileLoader: SoundFileLoader) {
var soundTheme: String = SOUND_THEME_OFF
private val loadedSoundFiles: MutableMap<String, SoundFile> = HashMap() private val loadedSoundFiles: MutableMap<String, SoundFile> = HashMap()
fun preloadAllFiles() { fun preloadAllFiles() {
loadedSoundFiles.clear() loadedSoundFiles.clear()
if (soundTheme == SoundThemeOff) { if (soundTheme == SOUND_THEME_OFF) {
return return
} }
val soundFiles = ArrayList<SoundFile>() val soundFiles = ArrayList<SoundFile>()
soundFiles.add(SoundFile(soundTheme, SoundAchievementUnlocked)) soundFiles.add(SoundFile(soundTheme, SOUND_ACHIEVEMENT_UNLOCKED))
soundFiles.add(SoundFile(soundTheme, SoundChat)) soundFiles.add(SoundFile(soundTheme, SOUND_CHAT))
soundFiles.add(SoundFile(soundTheme, SoundDaily)) soundFiles.add(SoundFile(soundTheme, SOUND_DAILY))
soundFiles.add(SoundFile(soundTheme, SoundDeath)) soundFiles.add(SoundFile(soundTheme, SOUND_DEATH))
soundFiles.add(SoundFile(soundTheme, SoundItemDrop)) soundFiles.add(SoundFile(soundTheme, SOUND_ITEM_DROP))
soundFiles.add(SoundFile(soundTheme, SoundLevelUp)) soundFiles.add(SoundFile(soundTheme, SOUND_LEVEL_UP))
soundFiles.add(SoundFile(soundTheme, SoundMinusHabit)) soundFiles.add(SoundFile(soundTheme, SOUND_MINUS_HABIT))
soundFiles.add(SoundFile(soundTheme, SoundPlusHabit)) soundFiles.add(SoundFile(soundTheme, SOUND_PLUS_HABIT))
soundFiles.add(SoundFile(soundTheme, SoundReward)) soundFiles.add(SoundFile(soundTheme, SOUND_REWARD))
soundFiles.add(SoundFile(soundTheme, SoundTodo)) soundFiles.add(SoundFile(soundTheme, SOUND_TODO))
MainScope().launchCatching { MainScope().launchCatching {
soundFileLoader.download(soundFiles) soundFileLoader.download(soundFiles)
} }
} }
fun loadAndPlayAudio(type: String) { fun loadAndPlayAudio(type: String) {
if (soundTheme == SoundThemeOff) { if (soundTheme == SOUND_THEME_OFF) {
return return
} }
@ -54,16 +56,16 @@ class SoundManager @Inject constructor(var soundFileLoader: SoundFileLoader) {
} }
companion object { companion object {
const val SoundAchievementUnlocked = "Achievement_Unlocked" const val SOUND_ACHIEVEMENT_UNLOCKED = "Achievement_Unlocked"
const val SoundChat = "Chat" const val SOUND_CHAT = "Chat"
const val SoundDaily = "Daily" const val SOUND_DAILY = "Daily"
const val SoundDeath = "Death" const val SOUND_DEATH = "Death"
const val SoundItemDrop = "Item_Drop" const val SOUND_ITEM_DROP = "Item_Drop"
const val SoundLevelUp = "Level_Up" const val SOUND_LEVEL_UP = "Level_Up"
const val SoundMinusHabit = "Minus_Habit" const val SOUND_MINUS_HABIT = "Minus_Habit"
const val SoundPlusHabit = "Plus_Habit" const val SOUND_PLUS_HABIT = "Plus_Habit"
const val SoundReward = "Reward" const val SOUND_REWARD = "Reward"
const val SoundTodo = "Todo" const val SOUND_TODO = "Todo"
const val SoundThemeOff = "off" const val SOUND_THEME_OFF = "off"
} }
} }

View file

@ -35,12 +35,11 @@ import java.util.Date
class TaskAlarmManager( class TaskAlarmManager(
private var context: Context, private var context: Context,
private var taskRepository: TaskRepository, private var taskRepository: TaskRepository,
private var authenticationHandler: AuthenticationHandler private var authenticationHandler: AuthenticationHandler,
) { ) {
private val am: AlarmManager? = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager private val am: AlarmManager? = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
private val upcomingReminderOccurrencesToSchedule = 3 private val upcomingReminderOccurrencesToSchedule = 3
/** /**
* Schedules multiple alarms for each reminder associated with a given task. * Schedules multiple alarms for each reminder associated with a given task.
* *
@ -60,7 +59,10 @@ class TaskAlarmManager(
*/ */
private fun setAlarmsForTask(task: Task) { private fun setAlarmsForTask(task: Task) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val reminderOccurencesToSchedule = if (task.type == TaskType.TODO) { 1 } else { val reminderOccurencesToSchedule =
if (task.type == TaskType.TODO) {
1
} else {
// For dailies, we schedule multiple reminders in advance // For dailies, we schedule multiple reminders in advance
upcomingReminderOccurrencesToSchedule upcomingReminderOccurrencesToSchedule
} }
@ -82,7 +84,6 @@ class TaskAlarmManager(
} }
} }
fun removeAlarmsForTask(task: Task) { fun removeAlarmsForTask(task: Task) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
task.reminders?.let { reminders -> task.reminders?.let { reminders ->
@ -99,7 +100,8 @@ class TaskAlarmManager(
// We may be able to use repeating alarms instead of this in the future // We may be able to use repeating alarms instead of this in the future
fun addAlarmForTaskId(taskId: String) { fun addAlarmForTaskId(taskId: String) {
MainScope().launch(ExceptionHandler.coroutine()) { MainScope().launch(ExceptionHandler.coroutine()) {
val task = taskRepository.getTaskCopy(taskId) val task =
taskRepository.getTaskCopy(taskId)
.filter { task -> task.isValid && task.isManaged && TaskType.DAILY == task.type } .filter { task -> task.isValid && task.isManaged && TaskType.DAILY == task.type }
.first() .first()
setAlarmsForTask(task) setAlarmsForTask(task)
@ -137,7 +139,11 @@ class TaskAlarmManager(
* @param remindersItem The reminder item containing details like ID and the time for the reminder. * @param remindersItem The reminder item containing details like ID and the time for the reminder.
* If this is null, the method returns immediately without scheduling an alarm. * If this is null, the method returns immediately without scheduling an alarm.
*/ */
private fun setAlarmForRemindersItem(reminderItemTask: Task, remindersItem: RemindersItem?, occurrenceIndex: Int) { private fun setAlarmForRemindersItem(
reminderItemTask: Task,
remindersItem: RemindersItem?,
occurrenceIndex: Int,
) {
if (remindersItem == null) return if (remindersItem == null) return
val now = ZonedDateTime.now().withZoneSameLocal(ZoneId.systemDefault())?.toInstant() val now = ZonedDateTime.now().withZoneSameLocal(ZoneId.systemDefault())?.toInstant()
@ -147,7 +153,6 @@ class TaskAlarmManager(
return return
} }
val intent = Intent(context, TaskReceiver::class.java) val intent = Intent(context, TaskReceiver::class.java)
intent.action = remindersItem.id intent.action = remindersItem.id
intent.putExtra(TASK_NAME_INTENT_KEY, reminderItemTask.text) intent.putExtra(TASK_NAME_INTENT_KEY, reminderItemTask.text)
@ -157,39 +162,55 @@ class TaskAlarmManager(
val intentId = (remindersItem.id?.hashCode() ?: 0) + occurrenceIndex val intentId = (remindersItem.id?.hashCode() ?: 0) + occurrenceIndex
// Cancel alarm if already exists // Cancel alarm if already exists
val previousSender = PendingIntent.getBroadcast( val previousSender =
PendingIntent.getBroadcast(
context, context,
intentId, intentId,
intent, intent,
withImmutableFlag(PendingIntent.FLAG_NO_CREATE) withImmutableFlag(PendingIntent.FLAG_NO_CREATE),
) )
if (previousSender != null) { if (previousSender != null) {
previousSender.cancel() previousSender.cancel()
am?.cancel(previousSender) am?.cancel(previousSender)
} }
val sender = PendingIntent.getBroadcast( val sender =
PendingIntent.getBroadcast(
context, context,
intentId, intentId,
intent, intent,
withImmutableFlag(PendingIntent.FLAG_CANCEL_CURRENT) withImmutableFlag(PendingIntent.FLAG_CANCEL_CURRENT),
) )
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
setAlarm(context, reminderZonedTime.toEpochMilli(), sender) setAlarm(context, reminderZonedTime.toEpochMilli(), sender)
} }
} }
private fun removeAlarmForRemindersItem(remindersItem: RemindersItem, occurrenceIndex: Int? = null) { private fun removeAlarmForRemindersItem(
remindersItem: RemindersItem,
occurrenceIndex: Int? = null,
) {
val intent = Intent(context, TaskReceiver::class.java) val intent = Intent(context, TaskReceiver::class.java)
intent.action = remindersItem.id intent.action = remindersItem.id
val intentId = if (occurrenceIndex != null) (remindersItem.id?.hashCode() ?: (0 and 0xfffffff)) + occurrenceIndex else (remindersItem.id?.hashCode() ?: (0 and 0xfffffff)) val intentId =
val sender = PendingIntent.getBroadcast( if (occurrenceIndex != null) {
(
remindersItem.id?.hashCode()
?: (0 and 0xfffffff)
) + occurrenceIndex
} else {
(
remindersItem.id?.hashCode()
?: (0 and 0xfffffff)
)
}
val sender =
PendingIntent.getBroadcast(
context, context,
intentId, intentId,
intent, intent,
withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT) withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT),
) )
val am = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager val am = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
sender.cancel() sender.cancel()
@ -225,22 +246,24 @@ class TaskAlarmManager(
notificationIntent.putExtra(NotificationPublisher.CHECK_DAILIES, false) notificationIntent.putExtra(NotificationPublisher.CHECK_DAILIES, false)
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
val previousSender = PendingIntent.getBroadcast( val previousSender =
PendingIntent.getBroadcast(
context, context,
0, 0,
notificationIntent, notificationIntent,
withImmutableFlag(PendingIntent.FLAG_NO_CREATE) withImmutableFlag(PendingIntent.FLAG_NO_CREATE),
) )
if (previousSender != null) { if (previousSender != null) {
previousSender.cancel() previousSender.cancel()
alarmManager?.cancel(previousSender) alarmManager?.cancel(previousSender)
} }
val pendingIntent = PendingIntent.getBroadcast( val pendingIntent =
PendingIntent.getBroadcast(
context, context,
0, 0,
notificationIntent, notificationIntent,
withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT) withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT),
) )
setAlarm(context, triggerTime, pendingIntent) setAlarm(context, triggerTime, pendingIntent)
@ -255,7 +278,11 @@ class TaskAlarmManager(
alarmManager?.cancel(displayIntent) alarmManager?.cancel(displayIntent)
} }
private fun setAlarm(context: Context, time: Long, pendingIntent: PendingIntent?) { private fun setAlarm(
context: Context,
time: Long,
pendingIntent: PendingIntent?,
) {
HLogger.log(LogLevel.INFO, "TaskAlarmManager", "Scheduling for $time") HLogger.log(LogLevel.INFO, "TaskAlarmManager", "Scheduling for $time")
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
@ -267,16 +294,24 @@ class TaskAlarmManager(
// For SDK >= Android 12, allows batching of reminders // For SDK >= Android 12, allows batching of reminders
try { try {
alarmManager?.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pendingIntent) alarmManager?.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pendingIntent)
Log.d("TaskAlarmManager", "setAlarm: Scheduling for $time using setAndAllowWhileIdle") Log.d(
"TaskAlarmManager",
"setAlarm: Scheduling for $time using setAndAllowWhileIdle",
)
} catch (ex: Exception) { } catch (ex: Exception) {
when (ex) { when (ex) {
is IllegalStateException, is SecurityException -> { is IllegalStateException, is SecurityException -> {
alarmManager?.setWindow(AlarmManager.RTC_WAKEUP, time, 600000, pendingIntent) alarmManager?.setWindow(
AlarmManager.RTC_WAKEUP,
time,
600000,
pendingIntent,
)
} }
else -> { else -> {
throw ex throw ex
} }
} }
} }
} else { } else {

View file

@ -15,34 +15,38 @@ import java.util.Date
import java.util.Locale import java.util.Locale
class TaskDescriptionBuilder(private val context: Context) { class TaskDescriptionBuilder(private val context: Context) {
fun describe(task: Task): String { fun describe(task: Task): String {
return when (task.type) { return when (task.type) {
TaskType.HABIT -> context.getString( TaskType.HABIT ->
context.getString(
R.string.habit_summary_description, R.string.habit_summary_description,
describeHabitDirections(task.up ?: false, task.down ?: false), describeHabitDirections(task.up ?: false, task.down ?: false),
describeDifficulty(task.priority) describeDifficulty(task.priority),
) )
TaskType.TODO -> { TaskType.TODO -> {
if (task.dueDate != null) { if (task.dueDate != null) {
context.getString( context.getString(
R.string.todo_summary_description_duedate, R.string.todo_summary_description_duedate,
describeDifficulty(task.priority), describeDifficulty(task.priority),
describeDate(task.dueDate!!) describeDate(task.dueDate!!),
) )
} else { } else {
context.getString( context.getString(
R.string.todo_summary_description, R.string.todo_summary_description,
describeDifficulty(task.priority) describeDifficulty(task.priority),
) )
} }
} }
TaskType.DAILY -> context.getString(
TaskType.DAILY ->
context.getString(
R.string.daily_summary_description, R.string.daily_summary_description,
describeDifficulty(task.priority), describeDifficulty(task.priority),
describeRepeatInterval(task.frequency, task.everyX ?: 1), describeRepeatInterval(task.frequency, task.everyX ?: 1),
describeRepeatDays(task) describeRepeatDays(task),
) )
else -> "" else -> ""
} }
} }
@ -59,7 +63,8 @@ class TaskDescriptionBuilder(private val context: Context) {
} }
return when (task.frequency) { return when (task.frequency) {
Frequency.WEEKLY -> { Frequency.WEEKLY -> {
" " + if (task.repeat?.isEveryDay == true) { " " +
if (task.repeat?.isEveryDay == true) {
context.getString(R.string.on_every_day_of_week) context.getString(R.string.on_every_day_of_week)
} else { } else {
if (task.repeat?.isOnlyWeekdays == true) { if (task.repeat?.isOnlyWeekdays == true) {
@ -72,14 +77,18 @@ class TaskDescriptionBuilder(private val context: Context) {
} }
} }
} }
Frequency.MONTHLY -> { Frequency.MONTHLY -> {
" " + if (task.getDaysOfMonth()?.isNotEmpty() == true) { " " +
val dayList = task.getDaysOfMonth()?.map { if (task.getDaysOfMonth()?.isNotEmpty() == true) {
val dayList =
task.getDaysOfMonth()?.map {
withOrdinal(it) withOrdinal(it)
} }
context.getString(R.string.on_the_x, joinToCount(dayList)) context.getString(R.string.on_the_x, joinToCount(dayList))
} else if (task.getWeeksOfMonth()?.isNotEmpty() == true) { } else if (task.getWeeksOfMonth()?.isNotEmpty() == true) {
val occurrence = when (task.getWeeksOfMonth()?.first()) { val occurrence =
when (task.getWeeksOfMonth()?.first()) {
0 -> context.getString(R.string.first) 0 -> context.getString(R.string.first)
1 -> context.getString(R.string.second) 1 -> context.getString(R.string.second)
2 -> context.getString(R.string.third) 2 -> context.getString(R.string.third)
@ -89,18 +98,26 @@ class TaskDescriptionBuilder(private val context: Context) {
} }
val dayStrings = task.repeat?.dayStrings(context) ?: listOf() val dayStrings = task.repeat?.dayStrings(context) ?: listOf()
context.getString(R.string.on_the_x_of_month, occurrence, joinToCount(dayStrings)) context.getString(
R.string.on_the_x_of_month,
occurrence,
joinToCount(dayStrings),
)
} else { } else {
"" ""
} }
} }
Frequency.YEARLY -> " " + context.getString(
Frequency.YEARLY ->
" " +
context.getString(
R.string.on_x, R.string.on_x,
task.startDate?.let { task.startDate?.let {
val flags = DateUtils.FORMAT_SHOW_DATE + DateUtils.FORMAT_NO_YEAR val flags = DateUtils.FORMAT_SHOW_DATE + DateUtils.FORMAT_NO_YEAR
DateUtils.formatDateTime(context, it.time, flags) DateUtils.formatDateTime(context, it.time, flags)
} ?: "" } ?: "",
) )
else -> "" else -> ""
} }
} }
@ -121,24 +138,50 @@ class TaskDescriptionBuilder(private val context: Context) {
} }
} }
private fun describeRepeatInterval(interval: Frequency?, everyX: Int): String { private fun describeRepeatInterval(
interval: Frequency?,
everyX: Int,
): String {
if (everyX == 0) { if (everyX == 0) {
return context.getString(R.string.never) return context.getString(R.string.never)
} }
return when (interval) { return when (interval) {
Frequency.DAILY -> context.resources.getQuantityString(R.plurals.repeat_daily, everyX, everyX) Frequency.DAILY ->
Frequency.WEEKLY -> context.resources.getQuantityString(R.plurals.repeat_weekly, everyX, everyX) context.resources.getQuantityString(
Frequency.MONTHLY -> context.resources.getQuantityString( R.plurals.repeat_daily,
everyX,
everyX,
)
Frequency.WEEKLY ->
context.resources.getQuantityString(
R.plurals.repeat_weekly,
everyX,
everyX,
)
Frequency.MONTHLY ->
context.resources.getQuantityString(
R.plurals.repeat_monthly, R.plurals.repeat_monthly,
everyX, everyX,
everyX everyX,
) )
Frequency.YEARLY -> context.resources.getQuantityString(R.plurals.repeat_yearly, everyX, everyX)
Frequency.YEARLY ->
context.resources.getQuantityString(
R.plurals.repeat_yearly,
everyX,
everyX,
)
null -> "" null -> ""
} }
} }
private fun describeHabitDirections(up: Boolean, down: Boolean): String { private fun describeHabitDirections(
up: Boolean,
down: Boolean,
): String {
return if (up && down) { return if (up && down) {
context.getString(R.string.positive_and_negative) context.getString(R.string.positive_and_negative)
} else if (up) { } else if (up) {

View file

@ -6,7 +6,6 @@ import com.habitrpg.android.habitica.models.user.Stats
import com.habitrpg.shared.habitica.models.Avatar import com.habitrpg.shared.habitica.models.Avatar
class UserStatComputer { class UserStatComputer {
interface StatsRow interface StatsRow
inner class AttributeRow : StatsRow { inner class AttributeRow : StatsRow {
@ -25,7 +24,10 @@ class UserStatComputer {
var stats: String? = null var stats: String? = null
} }
fun computeClassBonus(equipmentList: List<Equipment>?, user: Avatar): List<StatsRow> { fun computeClassBonus(
equipmentList: List<Equipment>?,
user: Avatar,
): List<StatsRow> {
val skillRows = ArrayList<StatsRow>() val skillRows = ArrayList<StatsRow>()
var strAttributes = 0f var strAttributes = 0f
@ -41,7 +43,7 @@ class UserStatComputer {
// Summarize stats and fill equipment table // Summarize stats and fill equipment table
for (i in equipmentList ?: emptyList()) { for (i in equipmentList ?: emptyList()) {
val strength = i.str val strength = i.str
val intelligence = i._int val intelligence = i.intelligence
val constitution = i.con val constitution = i.con
val perception = i.per val perception = i.per
@ -88,8 +90,10 @@ class UserStatComputer {
} }
var classBonus = 0.5f var classBonus = 0.5f
val userClassMatchesGearClass = !classDoesNotExist && itemClass == user.stats?.habitClass val userClassMatchesGearClass =
val userClassMatchesGearSpecialClass = !specialClassDoesNotExist && itemSpecialClass == user.stats?.habitClass !classDoesNotExist && itemClass == user.stats?.habitClass
val userClassMatchesGearSpecialClass =
!specialClassDoesNotExist && itemSpecialClass == user.stats?.habitClass
if (!userClassMatchesGearClass && !userClassMatchesGearSpecialClass) classBonus = 0f if (!userClassMatchesGearClass && !userClassMatchesGearSpecialClass) classBonus = 0f
@ -102,14 +106,17 @@ class UserStatComputer {
strClassBonus += strength * classBonus strClassBonus += strength * classBonus
perClassBonus += perception * classBonus perClassBonus += perception * classBonus
} }
Stats.HEALER -> { Stats.HEALER -> {
conClassBonus += constitution * classBonus conClassBonus += constitution * classBonus
intClassBonus += intelligence * classBonus intClassBonus += intelligence * classBonus
} }
Stats.WARRIOR -> { Stats.WARRIOR -> {
strClassBonus += strength * classBonus strClassBonus += strength * classBonus
conClassBonus += constitution * classBonus conClassBonus += constitution * classBonus
} }
Stats.MAGE -> { Stats.MAGE -> {
intClassBonus += intelligence * classBonus intClassBonus += intelligence * classBonus
perClassBonus += perception * classBonus perClassBonus += perception * classBonus

View file

@ -2,4 +2,5 @@ package com.habitrpg.android.habitica.helpers.notifications
import android.content.Context import android.content.Context
class ChangeUsernameLocalNotification(context: Context, identifier: String?) : HabiticaLocalNotification(context, identifier) class ChangeUsernameLocalNotification(context: Context, identifier: String?) :
HabiticaLocalNotification(context, identifier)

View file

@ -4,10 +4,11 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
class ChatMentionNotification(context: Context, identifier: String?) : HabiticaLocalNotification(context, identifier) { class ChatMentionNotification(context: Context, identifier: String?) :
HabiticaLocalNotification(context, identifier) {
override fun configureNotificationBuilder(data: MutableMap<String, String>): NotificationCompat.Builder { override fun configureNotificationBuilder(data: MutableMap<String, String>): NotificationCompat.Builder {
val style = NotificationCompat.BigTextStyle() val style =
NotificationCompat.BigTextStyle()
.setBigContentTitle(title) .setBigContentTitle(title)
.bigText(message) .bigText(message)
return super.configureNotificationBuilder(data) return super.configureNotificationBuilder(data)

View file

@ -2,4 +2,5 @@ package com.habitrpg.android.habitica.helpers.notifications
import android.content.Context import android.content.Context
class GenericLocalNotification(context: Context, identifier: String?) : HabiticaLocalNotification(context, identifier) class GenericLocalNotification(context: Context, identifier: String?) :
HabiticaLocalNotification(context, identifier)

View file

@ -2,4 +2,5 @@ package com.habitrpg.android.habitica.helpers.notifications
import android.content.Context import android.content.Context
class GiftOneGetOneLocalNotification(context: Context, identifier: String?) : HabiticaLocalNotification(context, identifier) class GiftOneGetOneLocalNotification(context: Context, identifier: String?) :
HabiticaLocalNotification(context, identifier)

View file

@ -18,8 +18,8 @@ import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
class GroupActivityNotification(context: Context, identifier: String?) : HabiticaLocalNotification(context, identifier) { class GroupActivityNotification(context: Context, identifier: String?) :
HabiticaLocalNotification(context, identifier) {
override fun getNotificationID(data: MutableMap<String, String>): Int { override fun getNotificationID(data: MutableMap<String, String>): Int {
return data["groupID"].hashCode() return data["groupID"].hashCode()
} }
@ -27,17 +27,22 @@ class GroupActivityNotification(context: Context, identifier: String?) : Habitic
override fun configureNotificationBuilder(data: MutableMap<String, String>): NotificationCompat.Builder { override fun configureNotificationBuilder(data: MutableMap<String, String>): NotificationCompat.Builder {
val user = Person.Builder().setName("You").build() val user = Person.Builder().setName("You").build()
val message = makeMessageFromData(data) val message = makeMessageFromData(data)
var style = NotificationCompat.MessagingStyle(user) var style =
NotificationCompat.MessagingStyle(user)
.setGroupConversation(true) .setGroupConversation(true)
.setConversationTitle(data["groupName"]) .setConversationTitle(data["groupName"])
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager val notificationManager =
val existingNotifications = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
val existingNotifications =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
notificationManager?.activeNotifications?.filter { it.id == getNotificationID(data) } notificationManager?.activeNotifications?.filter { it.id == getNotificationID(data) }
} else { } else {
null null
} }
val oldMessages = existingNotifications?.firstOrNull()?.notification?.extras?.getBundle("messages")?.get("messages") as? ArrayList<Map<String, String>> ?: arrayListOf() val oldMessages =
existingNotifications?.firstOrNull()?.notification?.extras?.getBundle("messages")
?.get("messages") as? ArrayList<Map<String, String>> ?: arrayListOf()
for (oldMessage in oldMessages) { for (oldMessage in oldMessages) {
style = style.addMessage(makeMessageFromData(oldMessage)) style = style.addMessage(makeMessageFromData(oldMessage))
} }
@ -57,17 +62,21 @@ class GroupActivityNotification(context: Context, identifier: String?) : Habitic
return NotificationCompat.MessagingStyle.Message( return NotificationCompat.MessagingStyle.Message(
messageText, messageText,
timestamp.time, timestamp.time,
sender sender,
) )
} }
override fun setNotificationActions(notificationId: Int, data: Map<String, String>) { override fun setNotificationActions(
notificationId: Int,
data: Map<String, String>,
) {
super.setNotificationActions(notificationId, data) super.setNotificationActions(notificationId, data)
val groupID = data["groupID"] ?: return val groupID = data["groupID"] ?: return
val actionName = context.getString(R.string.group_message_reply) val actionName = context.getString(R.string.group_message_reply)
val replyLabel: String = context.getString(R.string.reply) val replyLabel: String = context.getString(R.string.reply)
val remoteInput: RemoteInput = RemoteInput.Builder(actionName).run { val remoteInput: RemoteInput =
RemoteInput.Builder(actionName).run {
setLabel(replyLabel) setLabel(replyLabel)
build() build()
} }
@ -80,14 +89,14 @@ class GroupActivityNotification(context: Context, identifier: String?) : Habitic
context, context,
groupID.hashCode(), groupID.hashCode(),
intent, intent,
withMutableFlag(PendingIntent.FLAG_UPDATE_CURRENT) withMutableFlag(PendingIntent.FLAG_UPDATE_CURRENT),
) )
val action: NotificationCompat.Action = val action: NotificationCompat.Action =
NotificationCompat.Action.Builder( NotificationCompat.Action.Builder(
R.drawable.ic_send_grey_600_24dp, R.drawable.ic_send_grey_600_24dp,
context.getString(R.string.reply), context.getString(R.string.reply),
replyPendingIntent replyPendingIntent,
) )
.addRemoteInput(remoteInput) .addRemoteInput(remoteInput)
.build() .build()

View file

@ -10,14 +10,17 @@ import com.habitrpg.android.habitica.receivers.LocalNotificationActionReceiver
/** /**
* Created by keithholliday on 7/1/16. * Created by keithholliday on 7/1/16.
*/ */
class GuildInviteLocalNotification(context: Context, identifier: String?) : HabiticaLocalNotification(context, identifier) { class GuildInviteLocalNotification(context: Context, identifier: String?) :
HabiticaLocalNotification(context, identifier) {
override fun configureMainIntent(intent: Intent) { override fun configureMainIntent(intent: Intent) {
super.configureMainIntent(intent) super.configureMainIntent(intent)
intent.putExtra("groupID", data?.get("groupID")) intent.putExtra("groupID", data?.get("groupID"))
} }
override fun setNotificationActions(notificationId: Int, data: Map<String, String>) { override fun setNotificationActions(
notificationId: Int,
data: Map<String, String>,
) {
super.setNotificationActions(notificationId, data) super.setNotificationActions(notificationId, data)
val res = context.resources val res = context.resources
@ -26,11 +29,12 @@ class GuildInviteLocalNotification(context: Context, identifier: String?) : Habi
val groupID = data["groupID"] val groupID = data["groupID"]
acceptInviteIntent.putExtra("groupID", groupID) acceptInviteIntent.putExtra("groupID", groupID)
acceptInviteIntent.putExtra("NOTIFICATION_ID", notificationId) acceptInviteIntent.putExtra("NOTIFICATION_ID", notificationId)
val pendingIntentAccept = PendingIntent.getBroadcast( val pendingIntentAccept =
PendingIntent.getBroadcast(
context, context,
groupID.hashCode(), groupID.hashCode(),
acceptInviteIntent, acceptInviteIntent,
withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT) withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT),
) )
notificationBuilder.addAction(0, "Accept", pendingIntentAccept) notificationBuilder.addAction(0, "Accept", pendingIntentAccept)
@ -38,11 +42,12 @@ class GuildInviteLocalNotification(context: Context, identifier: String?) : Habi
rejectInviteIntent.action = res.getString(R.string.reject_guild_invite) rejectInviteIntent.action = res.getString(R.string.reject_guild_invite)
rejectInviteIntent.putExtra("groupID", groupID) rejectInviteIntent.putExtra("groupID", groupID)
acceptInviteIntent.putExtra("NOTIFICATION_ID", notificationId) acceptInviteIntent.putExtra("NOTIFICATION_ID", notificationId)
val pendingIntentReject = PendingIntent.getBroadcast( val pendingIntentReject =
PendingIntent.getBroadcast(
context, context,
groupID.hashCode() + 1, groupID.hashCode() + 1,
rejectInviteIntent, rejectInviteIntent,
withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT) withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT),
) )
notificationBuilder.addAction(0, "Reject", pendingIntentReject) notificationBuilder.addAction(0, "Reject", pendingIntentReject)
} }

View file

@ -8,7 +8,6 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class HabiticaFirebaseMessagingService : FirebaseMessagingService() { class HabiticaFirebaseMessagingService : FirebaseMessagingService() {
@Inject @Inject
internal lateinit var pushNotificationManager: PushNotificationManager internal lateinit var pushNotificationManager: PushNotificationManager

View file

@ -18,14 +18,14 @@ import java.util.Date
*/ */
abstract class HabiticaLocalNotification( abstract class HabiticaLocalNotification(
protected var context: Context, protected var context: Context,
protected var identifier: String? protected var identifier: String?,
) { ) {
protected var data: Map<String, String>? = null protected var data: Map<String, String>? = null
protected var title: String? = null protected var title: String? = null
protected var message: String? = null protected var message: String? = null
protected var notificationBuilder = NotificationCompat.Builder(context, "default") protected var notificationBuilder =
NotificationCompat.Builder(context, "default")
.setSmallIcon(R.drawable.ic_gryphon_white) .setSmallIcon(R.drawable.ic_gryphon_white)
.setAutoCancel(true) .setAutoCancel(true)
@ -37,7 +37,11 @@ abstract class HabiticaLocalNotification(
} }
@CallSuper @CallSuper
open fun notifyLocally(title: String?, message: String?, data: MutableMap<String, String>) { open fun notifyLocally(
title: String?,
message: String?,
data: MutableMap<String, String>,
) {
this.title = title this.title = title
this.message = message this.message = message
@ -65,15 +69,19 @@ abstract class HabiticaLocalNotification(
this.data = data this.data = data
} }
protected open fun setNotificationActions(notificationId: Int, data: Map<String, String>) { protected open fun setNotificationActions(
notificationId: Int,
data: Map<String, String>,
) {
val intent = Intent(context, MainActivity::class.java) val intent = Intent(context, MainActivity::class.java)
configureMainIntent(intent) configureMainIntent(intent)
intent.putExtra("NOTIFICATION_ID", notificationId) intent.putExtra("NOTIFICATION_ID", notificationId)
val pendingIntent = PendingIntent.getActivity( val pendingIntent =
PendingIntent.getActivity(
context, context,
3000, 3000,
intent, intent,
withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT) withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT),
) )
notificationBuilder.setContentIntent(pendingIntent) notificationBuilder.setContentIntent(pendingIntent)
} }

View file

@ -4,44 +4,95 @@ import android.content.Context
class HabiticaLocalNotificationFactory { class HabiticaLocalNotificationFactory {
// use getShape method to get object of type shape // use getShape method to get object of type shape
fun build(notificationType: String?, context: Context?): HabiticaLocalNotification { fun build(
notificationType: String?,
context: Context?,
): HabiticaLocalNotification {
return when { return when {
PushNotificationManager.PARTY_INVITE_PUSH_NOTIFICATION_KEY.equals(notificationType, true) -> { PushNotificationManager.PARTY_INVITE_PUSH_NOTIFICATION_KEY.equals(
notificationType,
true,
) -> {
PartyInviteLocalNotification(context!!, notificationType) PartyInviteLocalNotification(context!!, notificationType)
} }
PushNotificationManager.RECEIVED_PRIVATE_MESSAGE_PUSH_NOTIFICATION_KEY.equals(notificationType, true) -> {
PushNotificationManager.RECEIVED_PRIVATE_MESSAGE_PUSH_NOTIFICATION_KEY.equals(
notificationType,
true,
) -> {
ReceivedPrivateMessageLocalNotification(context!!, notificationType) ReceivedPrivateMessageLocalNotification(context!!, notificationType)
} }
PushNotificationManager.RECEIVED_GEMS_PUSH_NOTIFICATION_KEY.equals(notificationType, true) -> {
PushNotificationManager.RECEIVED_GEMS_PUSH_NOTIFICATION_KEY.equals(
notificationType,
true,
) -> {
ReceivedGemsGiftLocalNotification(context!!, notificationType) ReceivedGemsGiftLocalNotification(context!!, notificationType)
} }
PushNotificationManager.RECEIVED_SUBSCRIPTION_GIFT_PUSH_NOTIFICATION_KEY.equals(notificationType, true) -> {
PushNotificationManager.RECEIVED_SUBSCRIPTION_GIFT_PUSH_NOTIFICATION_KEY.equals(
notificationType,
true,
) -> {
ReceivedSubscriptionGiftLocalNotification(context!!, notificationType) ReceivedSubscriptionGiftLocalNotification(context!!, notificationType)
} }
PushNotificationManager.GUILD_INVITE_PUSH_NOTIFICATION_KEY.equals(notificationType, true) -> {
PushNotificationManager.GUILD_INVITE_PUSH_NOTIFICATION_KEY.equals(
notificationType,
true,
) -> {
GuildInviteLocalNotification(context!!, notificationType) GuildInviteLocalNotification(context!!, notificationType)
} }
PushNotificationManager.QUEST_INVITE_PUSH_NOTIFICATION_KEY.equals(notificationType, true) -> {
PushNotificationManager.QUEST_INVITE_PUSH_NOTIFICATION_KEY.equals(
notificationType,
true,
) -> {
QuestInviteLocalNotification(context!!, notificationType) QuestInviteLocalNotification(context!!, notificationType)
} }
PushNotificationManager.QUEST_BEGUN_PUSH_NOTIFICATION_KEY.equals(notificationType, true) -> {
PushNotificationManager.QUEST_BEGUN_PUSH_NOTIFICATION_KEY.equals(
notificationType,
true,
) -> {
QuestBegunLocalNotification(context!!, notificationType) QuestBegunLocalNotification(context!!, notificationType)
} }
PushNotificationManager.WON_CHALLENGE_PUSH_NOTIFICATION_KEY.equals(notificationType, true) -> {
PushNotificationManager.WON_CHALLENGE_PUSH_NOTIFICATION_KEY.equals(
notificationType,
true,
) -> {
WonChallengeLocalNotification(context!!, notificationType) WonChallengeLocalNotification(context!!, notificationType)
} }
PushNotificationManager.CHANGE_USERNAME_PUSH_NOTIFICATION_KEY.equals(notificationType, true) -> {
PushNotificationManager.CHANGE_USERNAME_PUSH_NOTIFICATION_KEY.equals(
notificationType,
true,
) -> {
ChangeUsernameLocalNotification(context!!, notificationType) ChangeUsernameLocalNotification(context!!, notificationType)
} }
PushNotificationManager.GIFT_ONE_GET_ONE_PUSH_NOTIFICATION_KEY.equals(notificationType, true) -> {
PushNotificationManager.GIFT_ONE_GET_ONE_PUSH_NOTIFICATION_KEY.equals(
notificationType,
true,
) -> {
GiftOneGetOneLocalNotification(context!!, notificationType) GiftOneGetOneLocalNotification(context!!, notificationType)
} }
PushNotificationManager.CHAT_MENTION_NOTIFICATION_KEY.equals(notificationType, true) -> {
PushNotificationManager.CHAT_MENTION_NOTIFICATION_KEY.equals(
notificationType,
true,
) -> {
ChatMentionNotification(context!!, notificationType) ChatMentionNotification(context!!, notificationType)
} }
PushNotificationManager.GROUP_ACTIVITY_NOTIFICATION_KEY.equals(notificationType, true) -> {
PushNotificationManager.GROUP_ACTIVITY_NOTIFICATION_KEY.equals(
notificationType,
true,
) -> {
GroupActivityNotification(context!!, notificationType) GroupActivityNotification(context!!, notificationType)
} }
else -> { else -> {
GenericLocalNotification(context!!, notificationType) GenericLocalNotification(context!!, notificationType)
} }

View file

@ -10,9 +10,12 @@ import com.habitrpg.android.habitica.receivers.LocalNotificationActionReceiver
/** /**
* Created by keithholliday on 6/28/16. * Created by keithholliday on 6/28/16.
*/ */
class PartyInviteLocalNotification(context: Context, identifier: String?) : HabiticaLocalNotification(context, identifier) { class PartyInviteLocalNotification(context: Context, identifier: String?) :
HabiticaLocalNotification(context, identifier) {
override fun setNotificationActions(notificationId: Int, data: Map<String, String>) { override fun setNotificationActions(
notificationId: Int,
data: Map<String, String>,
) {
super.setNotificationActions(notificationId, data) super.setNotificationActions(notificationId, data)
val res = context.resources val res = context.resources
@ -21,11 +24,12 @@ class PartyInviteLocalNotification(context: Context, identifier: String?) : Habi
val groupID = data["groupID"] val groupID = data["groupID"]
acceptInviteIntent.putExtra("groupID", groupID) acceptInviteIntent.putExtra("groupID", groupID)
acceptInviteIntent.putExtra("NOTIFICATION_ID", notificationId) acceptInviteIntent.putExtra("NOTIFICATION_ID", notificationId)
val pendingIntentAccept = PendingIntent.getBroadcast( val pendingIntentAccept =
PendingIntent.getBroadcast(
context, context,
groupID.hashCode(), groupID.hashCode(),
acceptInviteIntent, acceptInviteIntent,
withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT) withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT),
) )
notificationBuilder.addAction(0, context.getString(R.string.accept), pendingIntentAccept) notificationBuilder.addAction(0, context.getString(R.string.accept), pendingIntentAccept)
@ -33,11 +37,12 @@ class PartyInviteLocalNotification(context: Context, identifier: String?) : Habi
rejectInviteIntent.action = res.getString(R.string.reject_party_invite) rejectInviteIntent.action = res.getString(R.string.reject_party_invite)
rejectInviteIntent.putExtra("groupID", groupID) rejectInviteIntent.putExtra("groupID", groupID)
rejectInviteIntent.putExtra("NOTIFICATION_ID", notificationId) rejectInviteIntent.putExtra("NOTIFICATION_ID", notificationId)
val pendingIntentReject = PendingIntent.getBroadcast( val pendingIntentReject =
PendingIntent.getBroadcast(
context, context,
groupID.hashCode() + 1, groupID.hashCode() + 1,
rejectInviteIntent, rejectInviteIntent,
withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT) withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT),
) )
notificationBuilder.addAction(0, context.getString(R.string.reject), pendingIntentReject) notificationBuilder.addAction(0, context.getString(R.string.reject), pendingIntentReject)
} }

View file

@ -18,9 +18,8 @@ import java.io.IOException
class PushNotificationManager( class PushNotificationManager(
var apiClient: ApiClient, var apiClient: ApiClient,
private val sharedPreferences: SharedPreferences, private val sharedPreferences: SharedPreferences,
private val context: Context private val context: Context,
) { ) {
var refreshedToken: String = "" var refreshedToken: String = ""
set(value) { set(value) {
if (value.isEmpty()) { if (value.isEmpty()) {
@ -100,7 +99,8 @@ class PushNotificationManager(
} }
private fun userIsSubscribedToNotificationType(type: String?): Boolean { private fun userIsSubscribedToNotificationType(type: String?): Boolean {
val key = when { val key =
when {
type == PARTY_INVITE_PUSH_NOTIFICATION_KEY -> "preference_push_invited_to_party" type == PARTY_INVITE_PUSH_NOTIFICATION_KEY -> "preference_push_invited_to_party"
type?.contains(RECEIVED_PRIVATE_MESSAGE_PUSH_NOTIFICATION_KEY) == true -> "preference_push_received_a_private_message" type?.contains(RECEIVED_PRIVATE_MESSAGE_PUSH_NOTIFICATION_KEY) == true -> "preference_push_received_a_private_message"
type?.contains(RECEIVED_GEMS_PUSH_NOTIFICATION_KEY) == true -> "preference_push_gifted_gems" type?.contains(RECEIVED_GEMS_PUSH_NOTIFICATION_KEY) == true -> "preference_push_gifted_gems"
@ -133,7 +133,11 @@ class PushNotificationManager(
const val G1G1_PROMO_KEY = "g1g1Promo" const val G1G1_PROMO_KEY = "g1g1Promo"
private const val DEVICE_TOKEN_PREFERENCE_KEY = "device-token-preference" private const val DEVICE_TOKEN_PREFERENCE_KEY = "device-token-preference"
fun displayNotification(remoteMessage: RemoteMessage, context: Context, pushNotificationManager: PushNotificationManager? = null) { fun displayNotification(
remoteMessage: RemoteMessage,
context: Context,
pushNotificationManager: PushNotificationManager? = null,
) {
val remoteMessageIdentifier = remoteMessage.data["identifier"] val remoteMessageIdentifier = remoteMessage.data["identifier"]
if (pushNotificationManager?.userIsSubscribedToNotificationType(remoteMessageIdentifier) != false) { if (pushNotificationManager?.userIsSubscribedToNotificationType(remoteMessageIdentifier) != false) {
@ -144,14 +148,15 @@ class PushNotificationManager(
"receive notification", "receive notification",
EventCategory.BEHAVIOUR, EventCategory.BEHAVIOUR,
HitType.EVENT, HitType.EVENT,
additionalData additionalData,
) )
} }
val notificationFactory = HabiticaLocalNotificationFactory() val notificationFactory = HabiticaLocalNotificationFactory()
val localNotification = notificationFactory.build( val localNotification =
notificationFactory.build(
remoteMessageIdentifier, remoteMessageIdentifier,
context context,
) )
localNotification.setExtras(remoteMessage.data) localNotification.setExtras(remoteMessage.data)
val notification = remoteMessage.notification val notification = remoteMessage.notification
@ -159,13 +164,13 @@ class PushNotificationManager(
localNotification.notifyLocally( localNotification.notifyLocally(
notification.title ?: remoteMessage.data["title"], notification.title ?: remoteMessage.data["title"],
notification.body ?: remoteMessage.data["body"], notification.body ?: remoteMessage.data["body"],
remoteMessage.data remoteMessage.data,
) )
} else { } else {
localNotification.notifyLocally( localNotification.notifyLocally(
remoteMessage.data["title"], remoteMessage.data["title"],
remoteMessage.data["body"], remoteMessage.data["body"],
remoteMessage.data remoteMessage.data,
) )
} }
} }

View file

@ -5,4 +5,5 @@ import android.content.Context
/** /**
* Created by keithholliday on 7/1/16. * Created by keithholliday on 7/1/16.
*/ */
class QuestBegunLocalNotification(context: Context, identifier: String?) : HabiticaLocalNotification(context, identifier) class QuestBegunLocalNotification(context: Context, identifier: String?) :
HabiticaLocalNotification(context, identifier)

View file

@ -10,40 +10,46 @@ import com.habitrpg.android.habitica.receivers.LocalNotificationActionReceiver
/** /**
* Created by keithholliday on 7/1/16. * Created by keithholliday on 7/1/16.
*/ */
class QuestInviteLocalNotification(context: Context, identifier: String?) : HabiticaLocalNotification(context, identifier) { class QuestInviteLocalNotification(context: Context, identifier: String?) :
HabiticaLocalNotification(context, identifier) {
override fun getNotificationID(data: MutableMap<String, String>): Int { override fun getNotificationID(data: MutableMap<String, String>): Int {
return 1000 return 1000
} }
override fun setNotificationActions(notificationId: Int, data: Map<String, String>) { override fun setNotificationActions(
notificationId: Int,
data: Map<String, String>,
) {
super.setNotificationActions(notificationId, data) super.setNotificationActions(notificationId, data)
val res = context.resources val res = context.resources
val acceptInviteIntent = Intent(context, LocalNotificationActionReceiver::class.java) val acceptInviteIntent = Intent(context, LocalNotificationActionReceiver::class.java)
acceptInviteIntent.action = res.getString(R.string.accept_quest_invite) acceptInviteIntent.action = res.getString(R.string.accept_quest_invite)
acceptInviteIntent.putExtra("NOTIFICATION_ID", notificationId) acceptInviteIntent.putExtra("NOTIFICATION_ID", notificationId)
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val flags =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_UPDATE_CURRENT + PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_UPDATE_CURRENT + PendingIntent.FLAG_IMMUTABLE
} else { } else {
PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_UPDATE_CURRENT
} }
val pendingIntentAccept = PendingIntent.getBroadcast( val pendingIntentAccept =
PendingIntent.getBroadcast(
context, context,
3001, 3001,
acceptInviteIntent, acceptInviteIntent,
flags flags,
) )
notificationBuilder.addAction(0, "Accept", pendingIntentAccept) notificationBuilder.addAction(0, "Accept", pendingIntentAccept)
val rejectInviteIntent = Intent(context, LocalNotificationActionReceiver::class.java) val rejectInviteIntent = Intent(context, LocalNotificationActionReceiver::class.java)
rejectInviteIntent.action = res.getString(R.string.reject_quest_invite) rejectInviteIntent.action = res.getString(R.string.reject_quest_invite)
rejectInviteIntent.putExtra("NOTIFICATION_ID", notificationId) rejectInviteIntent.putExtra("NOTIFICATION_ID", notificationId)
val pendingIntentReject = PendingIntent.getBroadcast( val pendingIntentReject =
PendingIntent.getBroadcast(
context, context,
2001, 2001,
rejectInviteIntent, rejectInviteIntent,
flags flags,
) )
notificationBuilder.addAction(0, "Reject", pendingIntentReject) notificationBuilder.addAction(0, "Reject", pendingIntentReject)
} }

Some files were not shown because too many files have changed in this diff Show more