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>
@ -541,7 +541,7 @@
<style name="SubscriptionListTitle" parent="GemPurchaseListItem"> <style name="SubscriptionListTitle" parent="GemPurchaseListItem">
<item name="android:layout_gravity">left|center_vertical</item> <item name="android:layout_gravity">left|center_vertical</item>
<item name="android:fontFamily" >@string/font_family_medium</item> <item name="android:fontFamily">@string/font_family_medium</item>
<item name="android:textSize">14sp</item> <item name="android:textSize">14sp</item>
<item name="paddingEnd">32dp</item> <item name="paddingEnd">32dp</item>
</style> </style>
@ -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>
@ -618,7 +619,7 @@
<style name="subscriptionBoxText.Title"> <style name="subscriptionBoxText.Title">
<item name="android:textSize">14sp</item> <item name="android:textSize">14sp</item>
<item name="android:fontFamily" > <item name="android:fontFamily">
@string/font_family_medium @string/font_family_medium
</item> </item>
<item name="android:textColor">@color/text_primary</item> <item name="android:textColor">@color/text_primary</item>
@ -627,7 +628,7 @@
<style name="subscriptionBoxText.Subtitle"> <style name="subscriptionBoxText.Subtitle">
<item name="android:textSize">14sp</item> <item name="android:textSize">14sp</item>
<item name="android:fontFamily" > <item name="android:fontFamily">
@string/font_family_regular @string/font_family_regular
</item> </item>
<item name="android:textColor">@color/text_primary</item> <item name="android:textColor">@color/text_primary</item>
@ -635,7 +636,7 @@
<style name="SubscriptionListDescription" parent="GemPurchaseListItemDescription"> <style name="SubscriptionListDescription" parent="GemPurchaseListItemDescription">
<item name="android:layout_marginBottom">24dp</item> <item name="android:layout_marginBottom">24dp</item>
<item name="android:fontFamily" > <item name="android:fontFamily">
@string/font_family_condensed @string/font_family_condensed
</item> </item>
<item name="android:textSize">14sp</item> <item name="android:textSize">14sp</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,9 +23,10 @@ class MainItem(parent: Matcher<View>) : KRecyclerItem<MainItem>(parent) {
} }
class NavigationDrawerScreen : Screen<NavigationDrawerScreen>() { class NavigationDrawerScreen : Screen<NavigationDrawerScreen>() {
val recycler: KRecyclerView = KRecyclerView({ val recycler: KRecyclerView =
withId(R.id.recyclerView) KRecyclerView({
}, itemTypeBuilder = { withId(R.id.recyclerView)
}, itemTypeBuilder = {
itemType(::SectionHeaderItem) itemType(::SectionHeaderItem)
itemType(::MainItem) itemType(::MainItem)
}) })
@ -38,9 +39,10 @@ internal class NavigationDrawerFragmentTest : FragmentTestCase<NavigationDrawerF
} }
override fun launchFragment(args: Bundle?) { override fun launchFragment(args: Bundle?) {
scenario = launchFragmentInContainer(args, R.style.MainAppTheme) { scenario =
return@launchFragmentInContainer fragment launchFragmentInContainer(args, R.style.MainAppTheme) {
} return@launchFragmentInContainer fragment
}
} }
override val screen = NavigationDrawerScreen() override val screen = NavigationDrawerScreen()

View file

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

View file

@ -21,32 +21,35 @@ 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 =
withId(R.id.allocateButton) KButton {
isDescendantOfA { withId(R.id.strengthStatsView) } withId(R.id.allocateButton)
} isDescendantOfA { withId(R.id.strengthStatsView) }
}
val intelligenceStatsView = KView { withId(R.id.intelligenceStatsView) } val intelligenceStatsView = KView { withId(R.id.intelligenceStatsView) }
val intelligenceAllocateButton = KButton { val intelligenceAllocateButton =
withId(R.id.allocateButton) KButton {
isDescendantOfA { withId(R.id.intelligenceStatsView) } withId(R.id.allocateButton)
} isDescendantOfA { withId(R.id.intelligenceStatsView) }
}
val constitutionStatsView = KView { withId(R.id.constitutionStatsView) } val constitutionStatsView = KView { withId(R.id.constitutionStatsView) }
val constitutionAllocateButton = KButton { val constitutionAllocateButton =
withId(R.id.allocateButton) KButton {
isDescendantOfA { withId(R.id.constitutionStatsView) } withId(R.id.allocateButton)
} isDescendantOfA { withId(R.id.constitutionStatsView) }
}
val perceptionStatsView = KView { withId(R.id.perceptionStatsView) } val perceptionStatsView = KView { withId(R.id.perceptionStatsView) }
val perceptionAllocateButton = KButton { val perceptionAllocateButton =
withId(R.id.allocateButton) KButton {
isDescendantOfA { withId(R.id.perceptionStatsView) } withId(R.id.allocateButton)
} isDescendantOfA { withId(R.id.perceptionStatsView) }
}
val bulkAllocateButton = KView { withId(R.id.statsAllocationButton) } val bulkAllocateButton = KView { withId(R.id.statsAllocationButton) }
} }
@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,9 +58,10 @@ class StatsFragmentTest : FragmentTestCase<StatsFragment, FragmentStatsBinding,
} }
override fun launchFragment(args: Bundle?) { override fun launchFragment(args: Bundle?) {
scenario = launchFragmentInContainer(args, R.style.MainAppTheme) { scenario =
return@launchFragmentInContainer fragment launchFragmentInContainer(args, R.style.MainAppTheme) {
} return@launchFragmentInContainer fragment
}
} }
@Before @Before

View file

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

View file

@ -21,9 +21,10 @@ 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 =
withId(R.id.recyclerView) KRecyclerView({
}, itemTypeBuilder = { withId(R.id.recyclerView)
}, itemTypeBuilder = {
itemType(::SectionItem) itemType(::SectionItem)
itemType(::PetItem) itemType(::PetItem)
}) })
@ -37,23 +38,25 @@ 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
mapOf( flowOf(
Pair( mapOf(
"Saddle-food", Pair(
saddle "Saddle-food",
) saddle,
),
),
) )
)
fragment = spyk() fragment = spyk()
fragment.shouldInitializeComponent = false fragment.shouldInitializeComponent = false
} }
override fun launchFragment(args: Bundle?) { override fun launchFragment(args: Bundle?) {
scenario = launchFragmentInContainer(args, R.style.MainAppTheme) { scenario =
return@launchFragmentInContainer fragment launchFragmentInContainer(args, R.style.MainAppTheme) {
} return@launchFragmentInContainer fragment
}
} }
override val screen = PetDetailScreen() override val screen = PetDetailScreen()
@ -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,9 +30,10 @@ class SectionItem(parent: Matcher<View>) : KRecyclerItem<PetItem>(parent) {
} }
class StableScreen : Screen<StableScreen>() { class StableScreen : Screen<StableScreen>() {
val recycler: KRecyclerView = KRecyclerView({ val recycler: KRecyclerView =
withId(R.id.recyclerView) KRecyclerView({
}, itemTypeBuilder = { withId(R.id.recyclerView)
}, itemTypeBuilder = {
itemType(::SectionItem) itemType(::SectionItem)
itemType(::PetItem) itemType(::PetItem)
}) })
@ -48,9 +49,10 @@ internal class StableRecyclerFragmentTest : FragmentTestCase<StableRecyclerFragm
} }
override fun launchFragment(args: Bundle?) { override fun launchFragment(args: Bundle?) {
scenario = launchFragmentInContainer(args, R.style.MainAppTheme) { scenario =
return@launchFragmentInContainer fragment launchFragmentInContainer(args, R.style.MainAppTheme) {
} return@launchFragmentInContainer fragment
}
} }
override val screen = StableScreen() override val screen = StableScreen()

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,15 +28,15 @@ 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 =
withId(R.id.recyclerView) KRecyclerView({
}, itemTypeBuilder = { withId(R.id.recyclerView)
}, itemTypeBuilder = {
itemType(::TaskItem) itemType(::TaskItem)
}) })
} }
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,9 +46,10 @@ internal class TaskRecyclerViewFragmentTest : FragmentTestCase<TaskRecyclerViewF
} }
override fun launchFragment(args: Bundle?) { override fun launchFragment(args: Bundle?) {
scenario = launchFragmentInContainer(args, R.style.MainAppTheme) { scenario =
return@launchFragmentInContainer fragment launchFragmentInContainer(args, R.style.MainAppTheme) {
} return@launchFragmentInContainer fragment
}
} }
override val screen = TaskListScreen() override val screen = TaskListScreen()

View file

@ -1,328 +1,342 @@
package com.habitrpg.android.habitica package com.habitrpg.android.habitica
import android.app.Activity import android.app.Activity
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources
import android.database.DatabaseErrorHandler import android.database.DatabaseErrorHandler
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import android.os.Build import android.os.Build
import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION.SDK_INT
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit import androidx.core.content.edit
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.gms.wearable.Wearable import com.google.android.gms.wearable.Wearable
import com.google.firebase.installations.FirebaseInstallations import com.google.firebase.installations.FirebaseInstallations
import com.google.firebase.remoteconfig.FirebaseRemoteConfig import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings
import com.gu.toolargetool.TooLargeTool import com.gu.toolargetool.TooLargeTool
import com.habitrpg.android.habitica.data.ApiClient import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.extensions.DateUtils import com.habitrpg.android.habitica.extensions.DateUtils
import com.habitrpg.android.habitica.helpers.AdHandler import com.habitrpg.android.habitica.helpers.AdHandler
import com.habitrpg.android.habitica.helpers.Analytics import com.habitrpg.android.habitica.helpers.Analytics
import com.habitrpg.android.habitica.helpers.notifications.PushNotificationManager import com.habitrpg.android.habitica.helpers.notifications.PushNotificationManager
import com.habitrpg.android.habitica.modules.AuthenticationHandler import com.habitrpg.android.habitica.modules.AuthenticationHandler
import com.habitrpg.android.habitica.ui.activities.BaseActivity import com.habitrpg.android.habitica.ui.activities.BaseActivity
import com.habitrpg.android.habitica.ui.activities.LoginActivity import com.habitrpg.android.habitica.ui.activities.LoginActivity
import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper
import com.habitrpg.common.habitica.extensions.setupCoil import com.habitrpg.common.habitica.extensions.setupCoil
import com.habitrpg.common.habitica.helpers.ExceptionHandler import com.habitrpg.common.habitica.helpers.ExceptionHandler
import com.habitrpg.common.habitica.helpers.LanguageHelper import com.habitrpg.common.habitica.helpers.LanguageHelper
import com.habitrpg.common.habitica.helpers.MarkdownParser import com.habitrpg.common.habitica.helpers.MarkdownParser
import com.habitrpg.common.habitica.helpers.launchCatching import com.habitrpg.common.habitica.helpers.launchCatching
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import java.lang.ref.WeakReference 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) :
private var lastResumeTime = 0L DefaultLifecycleObserver {
override fun onResume(owner : LifecycleOwner) { private var lastResumeTime = 0L
super.onResume(owner)
lastResumeTime = Date().time override fun onResume(owner: LifecycleOwner) {
} super.onResume(owner)
lastResumeTime = Date().time
override fun onPause(owner : LifecycleOwner) { }
super.onPause(owner)
val duration = Date().time - lastResumeTime override fun onPause(owner: LifecycleOwner) {
addDurationToDay(duration / 1000) super.onPause(owner)
} val duration = Date().time - lastResumeTime
addDurationToDay(duration / 1000)
private fun addDurationToDay(duration: Long) { }
var currentTotal = sharedPreferences.getLong("usage_time_total", 0L)
currentTotal += duration private fun addDurationToDay(duration: Long) {
var currentDay = Date() var currentTotal = sharedPreferences.getLong("usage_time_total", 0L)
if (sharedPreferences.contains("usage_time_day")) { currentTotal += duration
currentDay = Date(sharedPreferences.getLong("usage_time_day", 0L)) var currentDay = Date()
} if (sharedPreferences.contains("usage_time_day")) {
var current = sharedPreferences.getLong("usage_time_current", 0L) currentDay = Date(sharedPreferences.getLong("usage_time_day", 0L))
if (!DateUtils.isSameDay(currentDay, Date())) { }
var average = sharedPreferences.getLong("usage_time_daily_average", 0L) var current = sharedPreferences.getLong("usage_time_current", 0L)
var observedDays = sharedPreferences.getInt("usage_time_day_count", 0) if (!DateUtils.isSameDay(currentDay, Date())) {
average = ((average * observedDays) + current) / (observedDays + 1) var average = sharedPreferences.getLong("usage_time_daily_average", 0L)
sharedPreferences.edit { var observedDays = sharedPreferences.getInt("usage_time_day_count", 0)
putInt("usage_time_day_count", ++observedDays) average = ((average * observedDays) + current) / (observedDays + 1)
putLong("usage_time_daily_average", average) sharedPreferences.edit {
} putInt("usage_time_day_count", ++observedDays)
Analytics.setUserProperty("usage_time_daily_average", average) putLong("usage_time_daily_average", average)
Analytics.setUserProperty("usage_time_total", currentTotal) }
current = 0 Analytics.setUserProperty("usage_time_daily_average", average)
currentDay = Date() Analytics.setUserProperty("usage_time_total", currentTotal)
} current = 0
current += duration currentDay = Date()
sharedPreferences.edit { }
putLong("usage_time_current", current) current += duration
putLong("usage_time_total", currentTotal) sharedPreferences.edit {
putLong("usage_time_day", currentDay.time) putLong("usage_time_current", current)
} putLong("usage_time_total", currentTotal)
} putLong("usage_time_day", currentDay.time)
} }
}
@HiltAndroidApp }
abstract class HabiticaBaseApplication : Application(), Application.ActivityLifecycleCallbacks {
@Inject @HiltAndroidApp
internal lateinit var lazyApiHelper: ApiClient abstract class HabiticaBaseApplication : Application(), Application.ActivityLifecycleCallbacks {
@Inject
@Inject internal lateinit var lazyApiHelper: ApiClient
internal lateinit var sharedPrefs: SharedPreferences
@Inject
@Inject internal lateinit var sharedPrefs: SharedPreferences
internal lateinit var pushNotificationManager: PushNotificationManager
@Inject
@Inject internal lateinit var pushNotificationManager: PushNotificationManager
internal lateinit var authenticationHandler: AuthenticationHandler
@Inject
private lateinit var lifecycleTracker: ApplicationLifecycleTracker internal lateinit var authenticationHandler: AuthenticationHandler
/** private lateinit var lifecycleTracker: ApplicationLifecycleTracker
* For better performance billing class should be used as singleton
*/ // endregion
// endregion
override fun onCreate() {
override fun onCreate() { super.onCreate()
super.onCreate()
lifecycleTracker = ApplicationLifecycleTracker(sharedPrefs)
lifecycleTracker = ApplicationLifecycleTracker(sharedPrefs) ProcessLifecycleOwner.get().lifecycle.addObserver(lifecycleTracker)
ProcessLifecycleOwner.get().lifecycle.addObserver(lifecycleTracker)
if (!BuildConfig.DEBUG) {
if (!BuildConfig.DEBUG) { TooLargeTool.startLogging(this)
TooLargeTool.startLogging(this) try {
try { Analytics.initialize(this)
Analytics.initialize(this) } catch (ignored: Resources.NotFoundException) {
} catch (ignored: Resources.NotFoundException) { }
} Analytics.identify(sharedPrefs)
Analytics.identify(sharedPrefs) Analytics.setUserID(lazyApiHelper.hostConfig.userID)
Analytics.setUserID(lazyApiHelper.hostConfig.userID) }
} registerActivityLifecycleCallbacks(this)
registerActivityLifecycleCallbacks(this) setupRealm()
setupRealm() setLocale()
setLocale() setupRemoteConfig()
setupRemoteConfig() setupNotifications()
setupNotifications() setupAdHandler()
setupAdHandler() HabiticaIconsHelper.init(this)
HabiticaIconsHelper.init(this) MarkdownParser.setup(this)
MarkdownParser.setup(this) AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
setupCoil()
setupCoil()
ExceptionHandler.init {
ExceptionHandler.init { Analytics.logException(it)
Analytics.logException(it) }
}
Analytics.setUserProperty("app_testing_level", BuildConfig.TESTING_LEVEL)
Analytics.setUserProperty("app_testing_level", BuildConfig.TESTING_LEVEL)
checkIfNewVersion()
checkIfNewVersion() }
}
private fun setupAdHandler() {
AdHandler.setup(sharedPrefs)
private fun setupAdHandler() { }
AdHandler.setup(sharedPrefs)
} private fun setLocale() {
val resources = resources
private fun setLocale() { val configuration: Configuration = resources.configuration
val resources = resources val languageHelper = LanguageHelper(sharedPrefs.getString("language", "en"))
val configuration: Configuration = resources.configuration if (if (SDK_INT >= Build.VERSION_CODES.N) {
val languageHelper = LanguageHelper(sharedPrefs.getString("language", "en")) configuration.locales.isEmpty || configuration.locales[0] != languageHelper.locale
if (if (SDK_INT >= Build.VERSION_CODES.N) { } else {
configuration.locales.isEmpty || configuration.locales[0] != languageHelper.locale @Suppress("DEPRECATION")
} else { configuration.locale != languageHelper.locale
@Suppress("DEPRECATION") }
configuration.locale != languageHelper.locale ) {
} configuration.setLocale(languageHelper.locale)
) { resources.updateConfiguration(configuration, null)
configuration.setLocale(languageHelper.locale) }
resources.updateConfiguration(configuration, null) }
}
} protected open fun setupRealm() {
Realm.init(this)
protected open fun setupRealm() { val builder =
Realm.init(this) RealmConfiguration.Builder()
val builder = RealmConfiguration.Builder() .schemaVersion(1)
.schemaVersion(1) .deleteRealmIfMigrationNeeded()
.deleteRealmIfMigrationNeeded() .allowWritesOnUiThread(true)
.allowWritesOnUiThread(true) .compactOnLaunch { totalBytes, usedBytes ->
.compactOnLaunch { totalBytes, usedBytes -> // Compact if the file is over 100MB in size and less than 50% 'used'
// Compact if the file is over 100MB in size and less than 50% 'used' val oneHundredMB = 50 * 1024 * 1024
val oneHundredMB = 50 * 1024 * 1024 (totalBytes > oneHundredMB) && (usedBytes / totalBytes) < 0.5
(totalBytes > oneHundredMB) && (usedBytes / totalBytes) < 0.5 }
} try {
try { Realm.setDefaultConfiguration(builder.build())
Realm.setDefaultConfiguration(builder.build()) } catch (ignored: UnsatisfiedLinkError) {
} catch (ignored: UnsatisfiedLinkError) { // Catch crash in tests
// Catch crash in tests }
} }
}
private fun checkIfNewVersion() {
private fun checkIfNewVersion() { var info: PackageInfo? = null
var info: PackageInfo? = null try {
try { info = packageManager.getPackageInfo(packageName, 0)
info = packageManager.getPackageInfo(packageName, 0) } catch (e: PackageManager.NameNotFoundException) {
} catch (e: PackageManager.NameNotFoundException) { Log.e("MyApplication", "couldn't get package info!")
Log.e("MyApplication", "couldn't get package info!") }
}
if (info == null) {
if (info == null) { return
return }
}
val lastInstalledVersion = sharedPrefs.getInt("last_installed_version", 0)
val lastInstalledVersion = sharedPrefs.getInt("last_installed_version", 0) @Suppress("DEPRECATION")
@Suppress("DEPRECATION") if (lastInstalledVersion < info.versionCode) {
if (lastInstalledVersion < info.versionCode) { @Suppress("DEPRECATION")
@Suppress("DEPRECATION") sharedPrefs.edit {
sharedPrefs.edit { putInt("last_installed_version", info.versionCode)
putInt("last_installed_version", info.versionCode) }
} }
} }
}
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) }
}
override fun openOrCreateDatabase(
override fun openOrCreateDatabase( name: String,
name: String, mode: Int,
mode: Int, factory: SQLiteDatabase.CursorFactory?,
factory: SQLiteDatabase.CursorFactory?, errorHandler: DatabaseErrorHandler?,
errorHandler: DatabaseErrorHandler? ): SQLiteDatabase {
): SQLiteDatabase { return super.openOrCreateDatabase(
return super.openOrCreateDatabase(getDatabasePath(name).absolutePath, mode, factory, errorHandler) getDatabasePath(name).absolutePath,
} mode,
factory,
// endregion errorHandler,
)
// region IAP - Specific }
override fun deleteDatabase(name: String): Boolean { // endregion
val realm = Realm.getDefaultInstance()
realm.executeTransaction { realm1 -> // region IAP - Specific
realm1.deleteAll()
realm1.close() override fun deleteDatabase(name: String): Boolean {
} val realm = Realm.getDefaultInstance()
return true realm.executeTransaction { realm1 ->
} realm1.deleteAll()
realm1.close()
private fun setupRemoteConfig() { }
val remoteConfig = FirebaseRemoteConfig.getInstance() return true
val configSettings = FirebaseRemoteConfigSettings.Builder() }
.setMinimumFetchIntervalInSeconds(if (BuildConfig.DEBUG) 0 else 3600)
.build() private fun setupRemoteConfig() {
remoteConfig.setConfigSettingsAsync(configSettings) val remoteConfig = FirebaseRemoteConfig.getInstance()
remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults) val configSettings =
remoteConfig.fetchAndActivate() FirebaseRemoteConfigSettings.Builder()
} .setMinimumFetchIntervalInSeconds(if (BuildConfig.DEBUG) 0 else 3600)
.build()
private fun setupNotifications() { remoteConfig.setConfigSettingsAsync(configSettings)
FirebaseInstallations.getInstance().id.addOnCompleteListener { task -> remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults)
if (!task.isSuccessful) { remoteConfig.fetchAndActivate()
Log.w("Token", "getInstanceId failed", task.exception) }
return@addOnCompleteListener
} private fun setupNotifications() {
val token = task.result FirebaseInstallations.getInstance().id.addOnCompleteListener { task ->
if (BuildConfig.DEBUG) { if (!task.isSuccessful) {
Log.d("Token", "Firebase Notification Token: $token") Log.w("Token", "getInstanceId failed", task.exception)
} return@addOnCompleteListener
} }
} val token = task.result
if (BuildConfig.DEBUG) {
var currentActivity: WeakReference<BaseActivity>? = null Log.d("Token", "Firebase Notification Token: $token")
}
override fun onActivityResumed(activity: Activity) { }
currentActivity = WeakReference(activity as? BaseActivity) }
}
var currentActivity: WeakReference<BaseActivity>? = null
override fun onActivityStarted(activity: Activity) {
currentActivity = WeakReference(activity as? BaseActivity) override fun onActivityResumed(activity: Activity) {
} currentActivity = WeakReference(activity as? BaseActivity)
}
override fun onActivityPaused(activity: Activity) {
if (currentActivity?.get() == activity) { override fun onActivityStarted(activity: Activity) {
currentActivity = null currentActivity = WeakReference(activity as? BaseActivity)
} }
}
override fun onActivityPaused(activity: Activity) {
override fun onActivityCreated(p0: Activity, p1: Bundle?) { if (currentActivity?.get() == activity) {
} currentActivity = null
}
override fun onActivityDestroyed(p0: Activity) { }
}
override fun onActivityCreated(
override fun onActivitySaveInstanceState(p0: Activity, p1: Bundle) { p0: Activity,
} p1: Bundle?,
) {
override fun onActivityStopped(p0: Activity) { }
}
override fun onActivityDestroyed(p0: Activity) {
companion object { }
fun getInstance(context: Context): HabiticaBaseApplication? {
return context.applicationContext as? HabiticaBaseApplication override fun onActivitySaveInstanceState(
} p0: Activity,
p1: Bundle,
fun logout(context: Context) { ) {
MainScope().launchCatching { }
getInstance(context)?.pushNotificationManager?.removePushDeviceUsingStoredToken()
val realm = Realm.getDefaultInstance() override fun onActivityStopped(p0: Activity) {
getInstance(context)?.deleteDatabase(realm.path) }
realm.close()
val preferences = PreferenceManager.getDefaultSharedPreferences(context) companion object {
val useReminder = preferences.getBoolean("use_reminder", false) fun getInstance(context: Context): HabiticaBaseApplication? {
val reminderTime = preferences.getString("reminder_time", "19:00") return context.applicationContext as? HabiticaBaseApplication
val lightMode = preferences.getString("theme_mode", "system") }
val launchScreen = preferences.getString("launch_screen", "")
preferences.edit { fun logout(context: Context) {
clear() MainScope().launchCatching {
putBoolean("use_reminder", useReminder) getInstance(context)?.pushNotificationManager?.removePushDeviceUsingStoredToken()
putString("reminder_time", reminderTime) val realm = Realm.getDefaultInstance()
putString("theme_mode", lightMode) getInstance(context)?.deleteDatabase(realm.path)
putString("launch_screen", launchScreen) realm.close()
} val preferences = PreferenceManager.getDefaultSharedPreferences(context)
getInstance(context)?.lazyApiHelper?.updateAuthenticationCredentials(null, null) val useReminder = preferences.getBoolean("use_reminder", false)
Wearable.getCapabilityClient(context).removeLocalCapability("provide_auth") val reminderTime = preferences.getString("reminder_time", "19:00")
startActivity(LoginActivity::class.java, context) val lightMode = preferences.getString("theme_mode", "system")
} val launchScreen = preferences.getString("launch_screen", "")
} preferences.edit {
clear()
private fun startActivity(activityClass: Class<*>, context: Context) { putBoolean("use_reminder", useReminder)
val intent = Intent(context, activityClass) putString("reminder_time", reminderTime)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) putString("theme_mode", lightMode)
context.startActivity(intent) putString("launch_screen", launchScreen)
} }
} getInstance(context)?.lazyApiHelper?.updateAuthenticationCredentials(null, null)
} Wearable.getCapabilityClient(context).removeLocalCapability("provide_auth")
startActivity(LoginActivity::class.java, context)
}
}
private fun startActivity(
activityClass: Class<*>,
context: Context,
) {
val intent = Intent(context, activityClass)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
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,7 +145,8 @@ public class GSonFactoryCreator {
.setLenient() .setLenient()
.create(); .create();
} }
public static GsonConverterFactory create() {
return GsonConverterFactory.create(createGson()); public static GsonConverterFactory create() {
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

@ -1,12 +1,13 @@
package com.habitrpg.android.habitica.data package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.ContentResult import com.habitrpg.android.habitica.models.ContentResult
import com.habitrpg.android.habitica.models.WorldState import com.habitrpg.android.habitica.models.WorldState
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface ContentRepository : BaseRepository { 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

@ -1,8 +1,12 @@
package com.habitrpg.android.habitica.data package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.inventory.Customization 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

@ -1,9 +1,10 @@
package com.habitrpg.android.habitica.data package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.FAQArticle import com.habitrpg.android.habitica.models.FAQArticle
import kotlinx.coroutines.flow.Flow 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

@ -1,95 +1,163 @@
package com.habitrpg.android.habitica.data package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.inventory.Egg import com.habitrpg.android.habitica.models.inventory.Egg
import com.habitrpg.android.habitica.models.inventory.Equipment import com.habitrpg.android.habitica.models.inventory.Equipment
import com.habitrpg.android.habitica.models.inventory.Food import com.habitrpg.android.habitica.models.inventory.Food
import com.habitrpg.android.habitica.models.inventory.HatchingPotion import com.habitrpg.android.habitica.models.inventory.HatchingPotion
import com.habitrpg.android.habitica.models.inventory.Item import com.habitrpg.android.habitica.models.inventory.Item
import com.habitrpg.android.habitica.models.inventory.Mount import com.habitrpg.android.habitica.models.inventory.Mount
import com.habitrpg.android.habitica.models.inventory.Pet import com.habitrpg.android.habitica.models.inventory.Pet
import com.habitrpg.android.habitica.models.inventory.Quest import com.habitrpg.android.habitica.models.inventory.Quest
import com.habitrpg.android.habitica.models.inventory.QuestContent import com.habitrpg.android.habitica.models.inventory.QuestContent
import com.habitrpg.android.habitica.models.responses.BuyResponse import com.habitrpg.android.habitica.models.responses.BuyResponse
import com.habitrpg.android.habitica.models.shops.Shop import com.habitrpg.android.habitica.models.shops.Shop
import com.habitrpg.android.habitica.models.shops.ShopItem import com.habitrpg.android.habitica.models.shops.ShopItem
import com.habitrpg.android.habitica.models.user.Items import com.habitrpg.android.habitica.models.user.Items
import com.habitrpg.android.habitica.models.user.OwnedItem import com.habitrpg.android.habitica.models.user.OwnedItem
import com.habitrpg.android.habitica.models.user.OwnedMount import com.habitrpg.android.habitica.models.user.OwnedMount
import com.habitrpg.android.habitica.models.user.OwnedPet import com.habitrpg.android.habitica.models.user.OwnedPet
import com.habitrpg.android.habitica.models.user.User import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.shared.habitica.models.responses.FeedResponse 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>>
fun getMounts(): Flow<List<Mount>> fun getMounts(): Flow<List<Mount>>
fun getOwnedMounts(): Flow<List<OwnedMount>> fun getOwnedMounts(): Flow<List<OwnedMount>>
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(keys: List<String>): Flow<List<QuestContent>> fun getQuestContent(key: String): Flow<QuestContent?>
fun getEquipment(searchedKeys: List<String>): Flow<List<Equipment>> fun getQuestContent(keys: List<String>): Flow<List<QuestContent>>
suspend fun retrieveInAppRewards(): List<ShopItem>?
fun getEquipment(searchedKeys: List<String>): Flow<List<Equipment>>
fun getOwnedEquipment(type: String): Flow<List<Equipment>>
fun getEquipmentType(type: String, set: String): Flow<List<Equipment>> suspend fun retrieveInAppRewards(): List<ShopItem>?
fun getOwnedItems(itemType: String, includeZero: Boolean = false): Flow<List<OwnedItem>> fun getOwnedEquipment(type: String): Flow<List<Equipment>>
fun getOwnedItems(includeZero: Boolean = false): Flow<Map<String, OwnedItem>>
fun getEquipmentType(
fun getEquipment(key: String): Flow<Equipment> type: String,
set: String,
suspend fun openMysteryItem(user: User?): Equipment? ): Flow<List<Equipment>>
fun saveEquipment(equipment: Equipment) fun getOwnedItems(
fun getMounts(type: String?, group: String?, color: String?): Flow<List<Mount>> itemType: String,
fun getPets(type: String?, group: String?, color: String?): Flow<List<Pet>> includeZero: Boolean = false,
): Flow<List<OwnedItem>>
fun updateOwnedEquipment(user: User)
fun getOwnedItems(includeZero: Boolean = false): Flow<Map<String, OwnedItem>>
suspend fun changeOwnedCount(type: String, key: String, amountToAdd: Int)
fun getEquipment(key: String): Flow<Equipment>
suspend fun sellItem(type: String, key: String): User?
suspend fun sellItem(item: OwnedItem): User? suspend fun openMysteryItem(user: User?): Equipment?
suspend fun equipGear(equipment: String, asCostume: Boolean): Items? fun saveEquipment(equipment: Equipment)
suspend fun equip(type: String, key: String): Items?
fun getMounts(
suspend fun feedPet(pet: Pet, food: Food): FeedResponse? type: String?,
group: String?,
suspend fun hatchPet(egg: Egg, hatchingPotion: HatchingPotion, successFunction: () -> Unit): Items? color: String?,
): Flow<List<Mount>>
suspend fun inviteToQuest(quest: QuestContent): Quest?
fun getPets(
suspend fun buyItem(user: User?, id: String, value: Double, purchaseQuantity: Int): BuyResponse? type: String?,
group: String?,
suspend fun retrieveShopInventory(identifier: String): Shop? color: String?,
suspend fun retrieveMarketGear(): Shop? ): Flow<List<Pet>>
suspend fun purchaseMysterySet(categoryIdentifier: String): Void? fun updateOwnedEquipment(user: User)
suspend fun purchaseHourglassItem(purchaseType: String, key: String): Void? suspend fun changeOwnedCount(
type: String,
suspend fun purchaseQuest(key: String): Void? key: String,
suspend fun purchaseSpecialSpell(key: String): Void? amountToAdd: Int,
)
suspend fun purchaseItem(purchaseType: String, key: String, purchaseQuantity: Int): Void?
suspend fun sellItem(
suspend fun togglePinnedItem(item: ShopItem): List<ShopItem>? type: String,
fun getItems(itemClass: Class<out Item>, keys: Array<String>): Flow<List<Item>> key: String,
fun getItems(itemClass: Class<out Item>): Flow<List<Item>> ): User?
fun getLatestMysteryItem(): Flow<Equipment>
fun getItem(type: String, key: String): Flow<Item> suspend fun sellItem(item: OwnedItem): User?
fun getAvailableLimitedItems(): Flow<List<Item>>
} suspend fun equipGear(
equipment: String,
asCostume: Boolean,
): Items?
suspend fun equip(
type: String,
key: String,
): 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 buyItem(
user: User?,
id: String,
value: Double,
purchaseQuantity: Int,
): BuyResponse?
suspend fun retrieveShopInventory(identifier: String): Shop?
suspend fun retrieveMarketGear(): Shop?
suspend fun purchaseMysterySet(categoryIdentifier: String): Void?
suspend fun purchaseHourglassItem(
purchaseType: String,
key: String,
): Void?
suspend fun purchaseQuest(key: String): Void?
suspend fun purchaseSpecialSpell(key: String): Void?
suspend fun purchaseItem(
purchaseType: String,
key: String,
purchaseQuantity: Int,
): Void?
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>): Flow<List<Item>>
fun getLatestMysteryItem(): Flow<Equipment>
fun getItem(
type: String,
key: String,
): Flow<Item>
fun getAvailableLimitedItems(): Flow<List<Item>>
}

View file

@ -1,26 +1,33 @@
package com.habitrpg.android.habitica.data package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.SetupCustomization 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(
fun getCustomizations(type: String, user: User): List<SetupCustomization> type: String,
fun getCustomizations(type: String, subtype: String?, user: User): List<SetupCustomization> user: User,
): List<SetupCustomization>
companion object {
const val CATEGORY_BODY = "body" fun getCustomizations(
const val CATEGORY_SKIN = "skin" type: String,
const val CATEGORY_HAIR = "hair" subtype: String?,
const val CATEGORY_EXTRAS = "extras" user: User,
): List<SetupCustomization>
const val SUBCATEGORY_SIZE = "size"
const val SUBCATEGORY_SHIRT = "shirt" companion object {
const val SUBCATEGORY_COLOR = "color" const val CATEGORY_BODY = "body"
const val SUBCATEGORY_PONYTAIL = "ponytail" const val CATEGORY_SKIN = "skin"
const val SUBCATEGORY_BANGS = "bangs" const val CATEGORY_HAIR = "hair"
const val SUBCATEGORY_FLOWER = "flower" const val CATEGORY_EXTRAS = "extras"
const val SUBCATEGORY_WHEELCHAIR = "wheelchair"
const val SUBCATEGORY_GLASSES = "glasses" const val SUBCATEGORY_SIZE = "size"
} const val SUBCATEGORY_SHIRT = "shirt"
} const val SUBCATEGORY_COLOR = "color"
const val SUBCATEGORY_PONYTAIL = "ponytail"
const val SUBCATEGORY_BANGS = "bangs"
const val SUBCATEGORY_FLOWER = "flower"
const val SUBCATEGORY_WHEELCHAIR = "wheelchair"
const val SUBCATEGORY_GLASSES = "glasses"
}
}

View file

@ -1,123 +1,188 @@
package com.habitrpg.android.habitica.data package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.Achievement import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.models.inventory.Quest import com.habitrpg.android.habitica.models.inventory.Quest
import com.habitrpg.android.habitica.models.invitations.InviteResponse import com.habitrpg.android.habitica.models.invitations.InviteResponse
import com.habitrpg.android.habitica.models.members.Member import com.habitrpg.android.habitica.models.members.Member
import com.habitrpg.android.habitica.models.responses.PostChatMessageResult import com.habitrpg.android.habitica.models.responses.PostChatMessageResult
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.social.GroupMembership import com.habitrpg.android.habitica.models.social.GroupMembership
import com.habitrpg.android.habitica.models.social.InboxConversation import com.habitrpg.android.habitica.models.social.InboxConversation
import com.habitrpg.android.habitica.models.user.User import com.habitrpg.android.habitica.models.user.User
import io.realm.RealmResults import io.realm.RealmResults
import kotlinx.coroutines.flow.Flow 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>?
fun getGroupChat(groupId: String): Flow<List<ChatMessage>> suspend fun retrieveGroupChat(groupId: String): List<ChatMessage>?
suspend fun markMessagesSeen(seenGroupId: String) fun getGroupChat(groupId: String): Flow<List<ChatMessage>>
suspend fun flagMessage( suspend fun markMessagesSeen(seenGroupId: String)
chatMessageID: String,
additionalInfo: String, suspend fun flagMessage(
groupID: String? = null chatMessageID: String,
): Void? additionalInfo: String,
groupID: String? = null,
suspend fun reportMember(memberID: String, data: Map<String, String>): Void? ): Void?
suspend fun likeMessage(chatMessage: ChatMessage): ChatMessage? suspend fun reportMember(
memberID: String,
suspend fun deleteMessage(chatMessage: ChatMessage): Void? data: Map<String, String>,
): Void?
suspend fun postGroupChat(
groupId: String, suspend fun likeMessage(chatMessage: ChatMessage): ChatMessage?
messageObject: HashMap<String, String>
): PostChatMessageResult? suspend fun deleteMessage(chatMessage: ChatMessage): Void?
suspend fun postGroupChat(groupId: String, message: String): PostChatMessageResult? suspend fun postGroupChat(
groupId: String,
suspend fun retrieveGroup(id: String): Group? messageObject: HashMap<String, String>,
fun getGroup(id: String?): Flow<Group?> ): PostChatMessageResult?
suspend fun leaveGroup(id: String?, keepChallenges: Boolean): Group? suspend fun postGroupChat(
groupId: String,
suspend fun joinGroup(id: String?): Group? message: String,
): PostChatMessageResult?
suspend fun createGroup(
name: String?, suspend fun retrieveGroup(id: String): Group?
description: String?,
leader: String?, fun getGroup(id: String?): Flow<Group?>
type: String?,
privacy: String?, suspend fun leaveGroup(
leaderCreateChallenge: Boolean? id: String?,
): Group? keepChallenges: Boolean,
): Group?
suspend fun updateGroup(
group: Group?, suspend fun joinGroup(id: String?): Group?
name: String?,
description: String?, suspend fun createGroup(
leader: String?, name: String?,
leaderCreateChallenge: Boolean? description: String?,
): Group? leader: String?,
type: String?,
fun getInboxMessages(replyToUserID: String?): Flow<RealmResults<ChatMessage>> privacy: String?,
suspend fun retrieveInboxMessages(uuid: String, page: Int): List<ChatMessage>? leaderCreateChallenge: Boolean?,
suspend fun retrieveInboxConversations(): List<InboxConversation>? ): Group?
fun getInboxConversations(): Flow<RealmResults<InboxConversation>>
suspend fun postPrivateMessage( suspend fun updateGroup(
recipientId: String, group: Group?,
messageObject: HashMap<String, String> name: String?,
): List<ChatMessage>? description: String?,
leader: String?,
suspend fun postPrivateMessage(recipientId: String, message: String): List<ChatMessage>? leaderCreateChallenge: Boolean?,
): Group?
suspend fun getPartyMembers(id: String): Flow<List<Member>>
suspend fun getGroupMembers(id: String): Flow<List<Member>> fun getInboxMessages(replyToUserID: String?): Flow<RealmResults<ChatMessage>>
suspend fun retrievePartyMembers(id: String, includeAllPublicFields: Boolean): List<Member>?
suspend fun retrieveInboxMessages(
suspend fun inviteToGroup(id: String, inviteData: Map<String, Any>): List<InviteResponse>? uuid: String,
page: Int,
suspend fun retrieveMember(userId: String?, fromHall: Boolean = false): Member? ): List<ChatMessage>?
suspend fun findUsernames( suspend fun retrieveInboxConversations(): List<InboxConversation>?
username: String,
context: String? = null, fun getInboxConversations(): Flow<RealmResults<InboxConversation>>
id: String? = null
): List<FindUsernameResult>? suspend fun postPrivateMessage(
recipientId: String,
suspend fun markPrivateMessagesRead(user: User?) messageObject: HashMap<String, String>,
): List<ChatMessage>?
fun markSomePrivateMessagesAsRead(user: User?, messages: List<ChatMessage>)
suspend fun postPrivateMessage(
suspend fun transferGroupOwnership(groupID: String, userID: String): Group? recipientId: String,
suspend fun removeMemberFromGroup(groupID: String, userID: String): List<Member>? message: String,
): List<ChatMessage>?
suspend fun acceptQuest(user: User?, partyId: String = "party"): Void?
suspend fun rejectQuest(user: User?, partyId: String = "party"): Void? suspend fun getPartyMembers(id: String): Flow<List<Member>>
suspend fun leaveQuest(partyId: String): Void? suspend fun getGroupMembers(id: String): Flow<List<Member>>
suspend fun cancelQuest(partyId: String): Void? suspend fun retrievePartyMembers(
id: String,
suspend fun abortQuest(partyId: String): Quest? includeAllPublicFields: Boolean,
): List<Member>?
suspend fun rejectGroupInvite(groupId: String): Void?
suspend fun inviteToGroup(
suspend fun forceStartQuest(party: Group): Quest? id: String,
inviteData: Map<String, Any>,
suspend fun getMemberAchievements(userId: String?): List<Achievement>? ): List<InviteResponse>?
suspend fun transferGems(giftedID: String, amount: Int): Void? suspend fun retrieveMember(
userId: String?,
fun getGroupMembership(id: String): Flow<GroupMembership?> fromHall: Boolean = false,
fun getGroupMemberships(): Flow<List<GroupMembership>> ): Member?
suspend fun blockMember(userID: String): List<String>?
fun getMember(userID: String?): Flow<Member?> suspend fun findUsernames(
suspend fun updateMember(memberID: String, data: Map<String, Map<String, Boolean>>): Member? username: String,
suspend fun retrievePartySeekingUsers(page: Int = 0): List<Member>? context: String? = null,
suspend fun retrievegroupInvites(id: String, includeAllPublicFields: Boolean): List<Member>? id: String? = null,
} ): List<FindUsernameResult>?
suspend fun markPrivateMessagesRead(user: User?)
fun markSomePrivateMessagesAsRead(
user: User?,
messages: List<ChatMessage>,
)
suspend fun transferGroupOwnership(
groupID: String,
userID: String,
): Group?
suspend fun removeMemberFromGroup(
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 cancelQuest(partyId: String): Void?
suspend fun abortQuest(partyId: String): Quest?
suspend fun rejectGroupInvite(groupId: String): Void?
suspend fun forceStartQuest(party: Group): Quest?
suspend fun getMemberAchievements(userId: String?): List<Achievement>?
suspend fun transferGems(
giftedID: String,
amount: Int,
): Void?
fun getGroupMembership(id: String): Flow<GroupMembership?>
fun getGroupMemberships(): Flow<List<GroupMembership>>
suspend fun blockMember(userID: String): List<String>?
fun getMember(userID: String?): Flow<Member?>
suspend fun updateMember(
memberID: String,
data: Map<String, Map<String, Boolean>>,
): Member?
suspend fun retrievePartySeekingUsers(page: Int = 0): List<Member>?
suspend fun retrievegroupInvites(
id: String,
includeAllPublicFields: Boolean,
): List<Member>?
}

View file

@ -1,18 +1,22 @@
package com.habitrpg.android.habitica.data package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.Tag 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 deleteTag(id: String): Void? suspend fun updateTag(tag: Tag): Tag?
suspend fun createTags(tags: Collection<Tag>): List<Tag> suspend fun deleteTag(id: String): Void?
suspend fun updateTags(tags: Collection<Tag>): List<Tag>
suspend fun deleteTags(tagIds: Collection<String>): List<Void> suspend fun createTags(tags: Collection<Tag>): List<Tag>
}
suspend fun updateTags(tags: Collection<Tag>): List<Tag>
suspend fun deleteTags(tagIds: Collection<String>): List<Void>
}

View file

@ -1,70 +1,135 @@
package com.habitrpg.android.habitica.data package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.BaseMainObject import com.habitrpg.android.habitica.models.BaseMainObject
import com.habitrpg.android.habitica.models.responses.BulkTaskScoringData import com.habitrpg.android.habitica.models.responses.BulkTaskScoringData
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.User import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.shared.habitica.models.responses.TaskScoringResult import com.habitrpg.shared.habitica.models.responses.TaskScoringResult
import com.habitrpg.shared.habitica.models.tasks.TaskType import com.habitrpg.shared.habitica.models.tasks.TaskType
import com.habitrpg.shared.habitica.models.tasks.TasksOrder import com.habitrpg.shared.habitica.models.tasks.TasksOrder
import kotlinx.coroutines.flow.Flow 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,
suspend fun retrieveTasks(userId: String, tasksOrder: TasksOrder): TaskList? includedGroupIDs: Array<String>,
suspend fun retrieveTasks(userId: String, tasksOrder: TasksOrder, dueDate: Date): TaskList? ): Flow<List<Task>>
suspend fun taskChecked( fun saveTasks(
user: User?, userId: String,
task: Task, order: TasksOrder,
up: Boolean, tasks: TaskList,
force: Boolean, )
notifyFunc: ((TaskScoringResult) -> Unit)?
): TaskScoringResult? suspend fun retrieveTasks(
suspend fun taskChecked( userId: String,
user: User?, tasksOrder: TasksOrder,
taskId: String, ): TaskList?
up: Boolean,
force: Boolean, suspend fun retrieveTasks(
notifyFunc: ((TaskScoringResult) -> Unit)? userId: String,
): TaskScoringResult? tasksOrder: TasksOrder,
suspend fun scoreChecklistItem(taskId: String, itemId: String): Task? dueDate: Date,
): TaskList?
fun getTask(taskId: String): Flow<Task>
fun getTaskCopy(taskId: String): Flow<Task> suspend fun taskChecked(
suspend fun createTask(task: Task, force: Boolean = false): Task? user: User?,
suspend fun updateTask(task: Task, force: Boolean = false): Task? task: Task,
suspend fun deleteTask(taskId: String): Void? up: Boolean,
fun saveTask(task: Task) force: Boolean,
notifyFunc: ((TaskScoringResult) -> Unit)?,
suspend fun createTasks(newTasks: List<Task>): List<Task>? ): TaskScoringResult?
fun markTaskCompleted(taskId: String, isCompleted: Boolean) suspend fun taskChecked(
user: User?,
fun <T : BaseMainObject> modify(obj: T, transaction: (T) -> Unit) taskId: String,
up: Boolean,
fun swapTaskPosition(firstPosition: Int, secondPosition: Int) force: Boolean,
notifyFunc: ((TaskScoringResult) -> Unit)?,
suspend fun updateTaskPosition(taskType: TaskType, taskID: String, newPosition: Int): List<String>? ): TaskScoringResult?
fun getUnmanagedTask(taskid: String): Flow<Task> suspend fun scoreChecklistItem(
taskId: String,
fun updateTaskInBackground(task: Task, assignChanges: Map<String, MutableList<String>>) itemId: String,
): Task?
fun createTaskInBackground(task: Task, assignChanges: Map<String, MutableList<String>>)
fun getTask(taskId: String): Flow<Task>
fun getTaskCopies(): Flow<List<Task>>
fun getTaskCopies(tasks: List<Task>): List<Task> fun getTaskCopy(taskId: String): Flow<Task>
suspend fun retrieveDailiesFromDate(date: Date): TaskList? suspend fun createTask(
suspend fun retrieveCompletedTodos(userId: String? = null): TaskList? task: Task,
suspend fun syncErroredTasks(): List<Task>? force: Boolean = false,
suspend fun unlinkAllTasks(challengeID: String?, keepOption: String): Void? ): Task?
fun getTasksForChallenge(challengeID: String?): Flow<List<Task>>
suspend fun bulkScoreTasks(data: List<Map<String, String>>): BulkTaskScoringData? suspend fun updateTask(
suspend fun markTaskNeedsWork(task: Task, userID: String) task: Task,
} force: Boolean = false,
): Task?
suspend fun deleteTask(taskId: String): Void?
fun saveTask(task: Task)
suspend fun createTasks(newTasks: List<Task>): List<Task>?
fun markTaskCompleted(
taskId: String,
isCompleted: Boolean,
)
fun <T : BaseMainObject> modify(
obj: T,
transaction: (T) -> Unit,
)
fun swapTaskPosition(
firstPosition: Int,
secondPosition: Int,
)
suspend fun updateTaskPosition(
taskType: TaskType,
taskID: String,
newPosition: Int,
): List<String>?
fun getUnmanagedTask(taskid: String): Flow<Task>
fun updateTaskInBackground(
task: Task,
assignChanges: Map<String, MutableList<String>>,
)
fun createTaskInBackground(
task: Task,
assignChanges: Map<String, MutableList<String>>,
)
fun getTaskCopies(): Flow<List<Task>>
fun getTaskCopies(tasks: List<Task>): List<Task>
suspend fun retrieveDailiesFromDate(date: Date): TaskList?
suspend fun retrieveCompletedTodos(userId: String? = null): TaskList?
suspend fun syncErroredTasks(): List<Task>?
suspend fun unlinkAllTasks(
challengeID: String?,
keepOption: String,
): Void?
fun getTasksForChallenge(challengeID: String?): Flow<List<Task>>
suspend fun bulkScoreTasks(data: List<Map<String, String>>): BulkTaskScoringData?
suspend fun markTaskNeedsWork(
task: Task,
userID: String,
)
}

View file

@ -1,10 +1,10 @@
package com.habitrpg.android.habitica.data package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.TutorialStep 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

@ -1,89 +1,147 @@
package com.habitrpg.android.habitica.data package com.habitrpg.android.habitica.data
import com.habitrpg.android.habitica.models.Achievement import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.models.QuestAchievement import com.habitrpg.android.habitica.models.QuestAchievement
import com.habitrpg.android.habitica.models.Skill import com.habitrpg.android.habitica.models.Skill
import com.habitrpg.android.habitica.models.TeamPlan import com.habitrpg.android.habitica.models.TeamPlan
import com.habitrpg.android.habitica.models.inventory.Customization import com.habitrpg.android.habitica.models.inventory.Customization
import com.habitrpg.android.habitica.models.inventory.Equipment import com.habitrpg.android.habitica.models.inventory.Equipment
import com.habitrpg.android.habitica.models.responses.SkillResponse import com.habitrpg.android.habitica.models.responses.SkillResponse
import com.habitrpg.android.habitica.models.responses.UnlockResponse import com.habitrpg.android.habitica.models.responses.UnlockResponse
import com.habitrpg.android.habitica.models.social.Group import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.models.tasks.Task import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.models.user.Stats import com.habitrpg.android.habitica.models.user.Stats
import com.habitrpg.android.habitica.models.user.User import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.models.user.UserQuestStatus import com.habitrpg.android.habitica.models.user.UserQuestStatus
import com.habitrpg.common.habitica.models.Notification import com.habitrpg.common.habitica.models.Notification
import com.habitrpg.shared.habitica.models.responses.VerifyUsernameResponse import com.habitrpg.shared.habitica.models.responses.VerifyUsernameResponse
import com.habitrpg.shared.habitica.models.tasks.Attribute import com.habitrpg.shared.habitica.models.tasks.Attribute
import kotlinx.coroutines.flow.Flow 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(key: String, value: Any?): User? suspend fun updateUser(updateData: Map<String, Any?>): User?
suspend fun retrieveUser(withTasks: Boolean = false, forced: Boolean = false, overrideExisting: Boolean = false): User? suspend fun updateUser(
key: String,
suspend fun revive(): Equipment? value: Any?,
): User?
suspend fun resetTutorial(): User?
suspend fun retrieveUser(
suspend fun sleep(user: User): User? withTasks: Boolean = false,
forced: Boolean = false,
fun getSkills(user: User): Flow<List<Skill>> overrideExisting: Boolean = false,
): User?
fun getSpecialItems(user: User): Flow<List<Skill>>
suspend fun revive(): Equipment?
suspend fun useSkill(key: String, target: String?, taskId: String): SkillResponse?
suspend fun useSkill(key: String, target: String?): SkillResponse? suspend fun resetTutorial(): User?
suspend fun disableClasses(): User? suspend fun sleep(user: User): User?
suspend fun changeClass(selectedClass: String? = null): User?
fun getSkills(user: User): Flow<List<Skill>>
suspend fun unlockPath(path: String, price: Int): UnlockResponse?
suspend fun unlockPath(customization: Customization): UnlockResponse? fun getSpecialItems(user: User): Flow<List<Skill>>
suspend fun runCron(tasks: MutableList<Task>) suspend fun useSkill(
suspend fun runCron() key: String,
target: String?,
suspend fun getNews(): List<Any>? taskId: String,
suspend fun getNewsNotification(): Notification? ): SkillResponse?
suspend fun readNotification(id: String): List<Any>? suspend fun useSkill(
suspend fun readNotifications(notificationIds: Map<String, List<String>>): List<Any>? key: String,
suspend fun seeNotifications(notificationIds: Map<String, List<String>>): List<Any>? target: String?,
): SkillResponse?
suspend fun changeCustomDayStart(dayStartTime: Int): User?
suspend fun disableClasses(): User?
suspend fun updateLanguage(languageCode: String): User?
suspend fun changeClass(selectedClass: String? = null): User?
suspend fun resetAccount(password: String): User?
suspend fun deleteAccount(password: String): Void? suspend fun unlockPath(
path: String,
suspend fun sendPasswordResetEmail(email: String): Void? price: Int,
): UnlockResponse?
suspend fun updateLoginName(newLoginName: String, password: String? = null): User?
suspend fun updateEmail(newEmail: String, password: String): Void? suspend fun unlockPath(customization: Customization): UnlockResponse?
suspend fun updatePassword(oldPassword: String, newPassword: String, newPasswordConfirmation: String): Void?
suspend fun verifyUsername(username: String): VerifyUsernameResponse? suspend fun runCron(tasks: MutableList<Task>)
suspend fun allocatePoint(stat: Attribute): Stats? suspend fun runCron()
suspend fun bulkAllocatePoints(strength: Int, intelligence: Int, constitution: Int, perception: Int): Stats?
suspend fun getNews(): List<Any>?
suspend fun useCustomization(type: String, category: String?, identifier: String): User?
suspend fun retrieveAchievements(): List<Achievement>? suspend fun getNewsNotification(): Notification?
fun getAchievements(): Flow<List<Achievement>>
fun getQuestAchievements(): Flow<List<QuestAchievement>> suspend fun readNotification(id: String): List<Any>?
fun getUserQuestStatus(): Flow<UserQuestStatus> suspend fun readNotifications(notificationIds: Map<String, List<String>>): List<Any>?
suspend fun reroll(): User? suspend fun seeNotifications(notificationIds: Map<String, List<String>>): List<Any>?
suspend fun retrieveTeamPlans(): List<TeamPlan>?
fun getTeamPlans(): Flow<List<TeamPlan>> suspend fun changeCustomDayStart(dayStartTime: Int): User?
suspend fun retrieveTeamPlan(teamID: String): Group?
fun getTeamPlan(teamID: String): Flow<Group?> suspend fun updateLanguage(languageCode: String): User?
suspend fun syncUserStats(): User?
} suspend fun resetAccount(password: String): User?
suspend fun deleteAccount(password: String): Void?
suspend fun sendPasswordResetEmail(email: String): Void?
suspend fun updateLoginName(
newLoginName: String,
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 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 retrieveAchievements(): List<Achievement>?
fun getAchievements(): Flow<List<Achievement>>
fun getQuestAchievements(): Flow<List<QuestAchievement>>
fun getUserQuestStatus(): Flow<UserQuestStatus>
suspend fun reroll(): User?
suspend fun retrieveTeamPlans(): List<TeamPlan>?
fun getTeamPlans(): Flow<List<TeamPlan>>
suspend fun retrieveTeamPlan(teamID: String): Group?
fun getTeamPlan(teamID: String): Flow<Group?>
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,72 +113,78 @@ class ApiClientImpl(
val calendar = GregorianCalendar() val calendar = GregorianCalendar()
val timeZone = calendar.timeZone val timeZone = calendar.timeZone
val timezoneOffset = -TimeUnit.MINUTES.convert( val timezoneOffset =
timeZone.getOffset(calendar.timeInMillis).toLong(), -TimeUnit.MINUTES.convert(
TimeUnit.MILLISECONDS timeZone.getOffset(calendar.timeInMillis).toLong(),
) 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 =
.cache(cache) OkHttpClient.Builder()
.addNetworkInterceptor { chain -> .cache(cache)
val original = chain.request() .addNetworkInterceptor { chain ->
var builder: Request.Builder = original.newBuilder() val original = chain.request()
if (this.hostConfig.hasAuthentication()) { var builder: Request.Builder = original.newBuilder()
builder = builder if (this.hostConfig.hasAuthentication()) {
.header("x-api-key", this.hostConfig.apiKey) builder =
.header("x-api-user", this.hostConfig.userID) builder
} .header("x-api-key", this.hostConfig.apiKey)
builder = builder.header("x-client", "habitica-android") .header("x-api-user", this.hostConfig.userID)
.header("x-user-timezoneOffset", timezoneOffset.toString()) }
if (userAgent != null) { builder =
builder = builder.header("user-agent", userAgent) builder.header("x-client", "habitica-android")
} .header("x-user-timezoneOffset", timezoneOffset.toString())
if (BuildConfig.STAGING_KEY.isNotEmpty()) { if (userAgent != null) {
builder = builder.header("Authorization", "Basic " + BuildConfig.STAGING_KEY) builder = builder.header("user-agent", userAgent)
} }
val request = builder.method(original.method, original.body) if (BuildConfig.STAGING_KEY.isNotEmpty()) {
.build() builder = builder.header("Authorization", "Basic " + BuildConfig.STAGING_KEY)
lastAPICallURL = original.url.toString() }
val response = chain.proceed(request) val request =
if (response.isSuccessful) { builder.method(original.method, original.body)
hideConnectionProblemDialog() .build()
return@addNetworkInterceptor response lastAPICallURL = original.url.toString()
} else { val response = chain.proceed(request)
// Modify cache control for 4xx or 5xx range - effectively "do not cache", preventing caching of 4xx and 5xx responses if (response.isSuccessful) {
if (response.code in 400..599) { hideConnectionProblemDialog()
when (response.code) {
404 -> {
// The server is returning a 404 error, which means the requested resource was not found.
// In this case - we want to actually cache the response, and handle it in the app
// to prevent a niche HttpException/potential network crash
return@addNetworkInterceptor response
}
else -> {
return@addNetworkInterceptor response.newBuilder()
.header("Cache-Control", "no-store").build()
}
}
} else {
return@addNetworkInterceptor response return@addNetworkInterceptor response
} else {
// Modify cache control for 4xx or 5xx range - effectively "do not cache", preventing caching of 4xx and 5xx responses
if (response.code in 400..599) {
when (response.code) {
404 -> {
// The server is returning a 404 error, which means the requested resource was not found.
// In this case - we want to actually cache the response, and handle it in the app
// to prevent a niche HttpException/potential network crash
return@addNetworkInterceptor response
}
else -> {
return@addNetworkInterceptor response.newBuilder()
.header("Cache-Control", "no-store").build()
}
}
} else {
return@addNetworkInterceptor response
}
} }
} }
} .addInterceptor(logging)
.addInterceptor(logging) .readTimeout(2400, TimeUnit.SECONDS)
.readTimeout(2400, TimeUnit.SECONDS) .build()
.build()
val server = Server(this.hostConfig.address) val server = Server(this.hostConfig.address)
retrofitAdapter = Retrofit.Builder() retrofitAdapter =
.client(client) Retrofit.Builder()
.baseUrl(server.toString()) .client(client)
.addConverterFactory(converter) .baseUrl(server.toString())
.build() .addConverterFactory(converter)
.build()
this.apiService = retrofitAdapter.create(ApiService::class.java) this.apiService = retrofitAdapter.create(ApiService::class.java)
} }
@ -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,10 +266,11 @@ 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 =
path.startsWith("/groups") && path.endsWith("invite") -> true when {
else -> false path.startsWith("/groups") && path.endsWith("invite") -> true
} else -> false
}
if (res.message != null && res.message == "RECEIPT_ALREADY_USED") { if (res.message != null && res.message == "RECEIPT_ALREADY_USED") {
return return
@ -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,15 +304,16 @@ 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 =
.responseBodyConverter(ErrorResponse::class.java, arrayOfNulls(0), retrofitAdapter) converter
.responseBodyConverter(ErrorResponse::class.java, arrayOfNulls(0), retrofitAdapter)
return try { return try {
errorConverter?.convert(errorResponse) as ErrorResponse errorConverter?.convert(errorResponse) as ErrorResponse
} catch (e: IOException) { } catch (e: IOException) {
@ -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,38 +354,41 @@ 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.applicationContext as? HabiticaBaseApplication) (context 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.applicationContext as? HabiticaBaseApplication) (context 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

@ -1,60 +1,59 @@
package com.habitrpg.android.habitica.data.implementation package com.habitrpg.android.habitica.data.implementation
import android.content.Context import android.content.Context
import com.habitrpg.android.habitica.data.ApiClient import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.data.ContentRepository import com.habitrpg.android.habitica.data.ContentRepository
import com.habitrpg.android.habitica.data.local.ContentLocalRepository import com.habitrpg.android.habitica.data.local.ContentLocalRepository
import com.habitrpg.android.habitica.helpers.AprilFoolsHandler import com.habitrpg.android.habitica.helpers.AprilFoolsHandler
import com.habitrpg.android.habitica.models.ContentResult import com.habitrpg.android.habitica.models.ContentResult
import com.habitrpg.android.habitica.models.WorldState import com.habitrpg.android.habitica.models.WorldState
import com.habitrpg.android.habitica.models.inventory.SpecialItem import com.habitrpg.android.habitica.models.inventory.SpecialItem
import com.habitrpg.android.habitica.modules.AuthenticationHandler import com.habitrpg.android.habitica.modules.AuthenticationHandler
import io.realm.RealmList import io.realm.RealmList
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import java.util.Date import java.util.Date
class ContentRepositoryImpl<T : ContentLocalRepository>( 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 private var lastWorldStateSync = 0L
private var lastWorldStateSync = 0L
override suspend fun retrieveContent(forced: Boolean): ContentResult? {
override suspend fun retrieveContent(forced: Boolean): ContentResult? { val now = Date().time
val now = Date().time if (forced || now - this.lastContentSync > 300000) {
if (forced || now - this.lastContentSync > 300000) { val content = apiClient.getContent() ?: return null
val content = apiClient.getContent() ?: return null lastContentSync = now
lastContentSync = now content.special = RealmList()
content.special = RealmList() content.special.add(mysteryItem)
content.special.add(mysteryItem) localRepository.saveContent(content)
localRepository.saveContent(content) return content
return content }
} return null
return null }
}
override suspend fun retrieveWorldState(forced: Boolean): WorldState? {
override suspend fun retrieveWorldState(forced: Boolean): WorldState? { val now = Date().time
val now = Date().time if (forced || now - this.lastWorldStateSync > 3600000) {
if (forced || now - this.lastWorldStateSync > 3600000) { val state = apiClient.getWorldState() ?: return null
val state = apiClient.getWorldState() ?: return null lastWorldStateSync = now
lastWorldStateSync = now localRepository.save(state)
localRepository.save(state) for (event in state.events) {
for (event in state.events) { if (event.aprilFools != null && event.isCurrentlyActive) {
if (event.aprilFools != null && event.isCurrentlyActive) { AprilFoolsHandler.handle(event.aprilFools, event.end)
AprilFoolsHandler.handle(event.aprilFools, event.end) }
} }
} return state
return state }
} return null
return null }
}
override fun getWorldState(): Flow<WorldState> {
override fun getWorldState(): Flow<WorldState> { return localRepository.getWorldState()
return localRepository.getWorldState() }
} }
}

View file

@ -1,19 +1,27 @@
package com.habitrpg.android.habitica.data.implementation package com.habitrpg.android.habitica.data.implementation
import com.habitrpg.android.habitica.data.ApiClient import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.data.CustomizationRepository import com.habitrpg.android.habitica.data.CustomizationRepository
import com.habitrpg.android.habitica.data.local.CustomizationLocalRepository import com.habitrpg.android.habitica.data.local.CustomizationLocalRepository
import com.habitrpg.android.habitica.models.inventory.Customization import com.habitrpg.android.habitica.models.inventory.Customization
import com.habitrpg.android.habitica.modules.AuthenticationHandler import com.habitrpg.android.habitica.modules.AuthenticationHandler
import kotlinx.coroutines.flow.Flow 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,
return localRepository.getCustomizations(type, category, onlyAvailable) authenticationHandler,
} ),
} CustomizationRepository {
override fun getCustomizations(
type: String,
category: String?,
onlyAvailable: Boolean,
): Flow<List<Customization>> {
return localRepository.getCustomizations(type, category, onlyAvailable)
}
}

View file

@ -1,23 +1,23 @@
package com.habitrpg.android.habitica.data.implementation package com.habitrpg.android.habitica.data.implementation
import com.habitrpg.android.habitica.data.ApiClient import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.data.FAQRepository import com.habitrpg.android.habitica.data.FAQRepository
import com.habitrpg.android.habitica.data.local.FAQLocalRepository import com.habitrpg.android.habitica.data.local.FAQLocalRepository
import com.habitrpg.android.habitica.models.FAQArticle import com.habitrpg.android.habitica.models.FAQArticle
import com.habitrpg.android.habitica.modules.AuthenticationHandler import com.habitrpg.android.habitica.modules.AuthenticationHandler
import kotlinx.coroutines.flow.Flow 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> {
return localRepository.getArticle(position) return localRepository.getArticle(position)
} }
override fun getArticles(): Flow<List<FAQArticle>> { override fun getArticles(): Flow<List<FAQArticle>> {
return localRepository.articles return localRepository.articles
} }
} }

View file

@ -1,318 +1,391 @@
@file:OptIn(ExperimentalCoroutinesApi::class) @file:OptIn(ExperimentalCoroutinesApi::class)
package com.habitrpg.android.habitica.data.implementation package com.habitrpg.android.habitica.data.implementation
import com.habitrpg.android.habitica.data.ApiClient import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.data.InventoryRepository import com.habitrpg.android.habitica.data.InventoryRepository
import com.habitrpg.android.habitica.data.local.InventoryLocalRepository import com.habitrpg.android.habitica.data.local.InventoryLocalRepository
import com.habitrpg.android.habitica.helpers.AppConfigManager import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.android.habitica.models.inventory.Egg import com.habitrpg.android.habitica.models.inventory.Egg
import com.habitrpg.android.habitica.models.inventory.Equipment import com.habitrpg.android.habitica.models.inventory.Equipment
import com.habitrpg.android.habitica.models.inventory.Food import com.habitrpg.android.habitica.models.inventory.Food
import com.habitrpg.android.habitica.models.inventory.HatchingPotion import com.habitrpg.android.habitica.models.inventory.HatchingPotion
import com.habitrpg.android.habitica.models.inventory.Item import com.habitrpg.android.habitica.models.inventory.Item
import com.habitrpg.android.habitica.models.inventory.Mount import com.habitrpg.android.habitica.models.inventory.Mount
import com.habitrpg.android.habitica.models.inventory.Pet import com.habitrpg.android.habitica.models.inventory.Pet
import com.habitrpg.android.habitica.models.inventory.Quest import com.habitrpg.android.habitica.models.inventory.Quest
import com.habitrpg.android.habitica.models.inventory.QuestContent import com.habitrpg.android.habitica.models.inventory.QuestContent
import com.habitrpg.android.habitica.models.responses.BuyResponse import com.habitrpg.android.habitica.models.responses.BuyResponse
import com.habitrpg.android.habitica.models.shops.Shop import com.habitrpg.android.habitica.models.shops.Shop
import com.habitrpg.android.habitica.models.shops.ShopItem import com.habitrpg.android.habitica.models.shops.ShopItem
import com.habitrpg.android.habitica.models.user.Items import com.habitrpg.android.habitica.models.user.Items
import com.habitrpg.android.habitica.models.user.OwnedItem import com.habitrpg.android.habitica.models.user.OwnedItem
import com.habitrpg.android.habitica.models.user.OwnedMount import com.habitrpg.android.habitica.models.user.OwnedMount
import com.habitrpg.android.habitica.models.user.OwnedPet import com.habitrpg.android.habitica.models.user.OwnedPet
import com.habitrpg.android.habitica.models.user.User import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.modules.AuthenticationHandler import com.habitrpg.android.habitica.modules.AuthenticationHandler
import com.habitrpg.shared.habitica.models.responses.FeedResponse import com.habitrpg.shared.habitica.models.responses.FeedResponse
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
class InventoryRepositoryImpl( 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),
override fun getQuestContent(keys: List<String>) = localRepository.getQuestContent(keys) InventoryRepository {
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)
override fun getEquipment(searchedKeys: List<String>): Flow<List<Equipment>> {
return localRepository.getEquipment(searchedKeys) override fun getEquipment(searchedKeys: List<String>): Flow<List<Equipment>> {
} return localRepository.getEquipment(searchedKeys)
}
override fun getArmoireRemainingCount(): Flow<Int> {
return localRepository.getArmoireRemainingCount() override fun getArmoireRemainingCount(): Flow<Int> {
} return localRepository.getArmoireRemainingCount()
}
override fun getInAppRewards(): Flow<List<ShopItem>> {
return localRepository.getInAppRewards() override fun getInAppRewards(): Flow<List<ShopItem>> {
} return localRepository.getInAppRewards()
}
override fun getInAppReward(key: String): Flow<ShopItem> {
return localRepository.getInAppReward(key) override fun getInAppReward(key: String): Flow<ShopItem> {
} return localRepository.getInAppReward(key)
}
override suspend fun retrieveInAppRewards(): List<ShopItem>? {
val rewards = apiClient.retrieveInAppRewards() override suspend fun retrieveInAppRewards(): List<ShopItem>? {
if (rewards != null) { val rewards = apiClient.retrieveInAppRewards()
localRepository.saveInAppRewards(rewards) if (rewards != null) {
} localRepository.saveInAppRewards(rewards)
return rewards }
} return rewards
}
override fun getOwnedEquipment(type: String): Flow<List<Equipment>> {
return localRepository.getOwnedEquipment(type) override fun getOwnedEquipment(type: String): Flow<List<Equipment>> {
} return localRepository.getOwnedEquipment(type)
}
override fun getOwnedEquipment(): Flow<List<Equipment>> {
return localRepository.getOwnedEquipment() override fun getOwnedEquipment(): Flow<List<Equipment>> {
} return localRepository.getOwnedEquipment()
}
override fun getEquipmentType(type: String, set: String): Flow<List<Equipment>> {
return localRepository.getEquipmentType(type, set) override fun getEquipmentType(
} type: String,
set: String,
override fun getOwnedItems(itemType: String, includeZero: Boolean): Flow<List<OwnedItem>> { ): Flow<List<Equipment>> {
return authenticationHandler.userIDFlow.flatMapLatest { localRepository.getOwnedItems(itemType, it, includeZero) } return localRepository.getEquipmentType(type, set)
} }
override fun getOwnedItems(includeZero: Boolean): Flow<Map<String, OwnedItem>> { override fun getOwnedItems(
return authenticationHandler.userIDFlow.flatMapLatest { localRepository.getOwnedItems(it, includeZero) } itemType: String,
} includeZero: Boolean,
): Flow<List<OwnedItem>> {
override fun getItems(itemClass: Class<out Item>, keys: Array<String>): Flow<List<Item>> { return authenticationHandler.userIDFlow.flatMapLatest {
return localRepository.getItems(itemClass, keys) localRepository.getOwnedItems(
} itemType,
it,
override fun getItems(itemClass: Class<out Item>): Flow<List<Item>> { includeZero,
return localRepository.getItems(itemClass) )
} }
}
override fun getEquipment(key: String): Flow<Equipment> {
return localRepository.getEquipment(key) override fun getOwnedItems(includeZero: Boolean): Flow<Map<String, OwnedItem>> {
} return authenticationHandler.userIDFlow.flatMapLatest {
localRepository.getOwnedItems(
override suspend fun openMysteryItem(user: User?): Equipment? { it,
val item = apiClient.openMysteryItem() includeZero,
val equipment = localRepository.getEquipment(item?.key ?: "").firstOrNull() ?: return null )
val liveEquipment = localRepository.getLiveObject(equipment) }
localRepository.executeTransaction { }
liveEquipment?.owned = true
} override fun getItems(
localRepository.decrementMysteryItemCount(user) itemClass: Class<out Item>,
return equipment keys: Array<String>,
} ): Flow<List<Item>> {
return localRepository.getItems(itemClass, keys)
override fun saveEquipment(equipment: Equipment) { }
localRepository.save(equipment)
} override fun getItems(itemClass: Class<out Item>): Flow<List<Item>> {
return localRepository.getItems(itemClass)
override fun getMounts(): Flow<List<Mount>> { }
return localRepository.getMounts()
} override fun getEquipment(key: String): Flow<Equipment> {
return localRepository.getEquipment(key)
override fun getMounts(type: String?, group: String?, color: String?): Flow<List<Mount>> { }
return localRepository.getMounts(type, group, color)
} override suspend fun openMysteryItem(user: User?): Equipment? {
val item = apiClient.openMysteryItem()
override fun getOwnedMounts(): Flow<List<OwnedMount>> { val equipment = localRepository.getEquipment(item?.key ?: "").firstOrNull() ?: return null
return authenticationHandler.userIDFlow.flatMapLatest { localRepository.getOwnedMounts(it) } val liveEquipment = localRepository.getLiveObject(equipment)
} localRepository.executeTransaction {
liveEquipment?.owned = true
override fun getPets(): Flow<List<Pet>> { }
return localRepository.getPets() localRepository.decrementMysteryItemCount(user)
} return equipment
}
override fun getPets(type: String?, group: String?, color: String?): Flow<List<Pet>> {
return localRepository.getPets(type, group, color) override fun saveEquipment(equipment: Equipment) {
} localRepository.save(equipment)
}
override fun getOwnedPets(): Flow<List<OwnedPet>> {
return authenticationHandler.userIDFlow.flatMapLatest { localRepository.getOwnedPets(it) } override fun getMounts(): Flow<List<Mount>> {
} return localRepository.getMounts()
}
override fun updateOwnedEquipment(user: User) {
localRepository.updateOwnedEquipment(user) override fun getMounts(
} type: String?,
group: String?,
override suspend fun changeOwnedCount(type: String, key: String, amountToAdd: Int) { color: String?,
localRepository.changeOwnedCount(type, key, currentUserID, amountToAdd) ): Flow<List<Mount>> {
} return localRepository.getMounts(type, group, color)
}
override suspend fun sellItem(type: String, key: String): User? {
val item = localRepository.getOwnedItem(currentUserID, type, key, true).firstOrNull() ?: return null override fun getOwnedMounts(): Flow<List<OwnedMount>> {
return sellItem(item) return authenticationHandler.userIDFlow.flatMapLatest { localRepository.getOwnedMounts(it) }
} }
override suspend fun sellItem(item: OwnedItem): User? { override fun getPets(): Flow<List<Pet>> {
val itemData = localRepository.getItem(item.itemType ?: "", item.key ?: "").firstOrNull() ?: return null return localRepository.getPets()
return sellItem(itemData, item) }
}
override fun getPets(
override fun getLatestMysteryItem(): Flow<Equipment> { type: String?,
return localRepository.getLatestMysteryItem() group: String?,
} color: String?,
): Flow<List<Pet>> {
override fun getItem(type: String, key: String): Flow<Item> { return localRepository.getPets(type, group, color)
return localRepository.getItem(type, key) }
}
override fun getOwnedPets(): Flow<List<OwnedPet>> {
private suspend fun sellItem(item: Item, ownedItem: OwnedItem): User? { return authenticationHandler.userIDFlow.flatMapLatest { localRepository.getOwnedPets(it) }
localRepository.executeTransaction { }
val liveItem = localRepository.getLiveObject(ownedItem)
liveItem?.numberOwned = (liveItem?.numberOwned ?: 0) - 1 override fun updateOwnedEquipment(user: User) {
} localRepository.updateOwnedEquipment(user)
val user = apiClient.sellItem(item.type, item.key) ?: return null }
return localRepository.soldItem(currentUserID, user)
} override suspend fun changeOwnedCount(
type: String,
override suspend fun equipGear(equipment: String, asCostume: Boolean): Items? { key: String,
return equip(if (asCostume) "costume" else "equipped", equipment) amountToAdd: Int,
} ) {
localRepository.changeOwnedCount(type, key, currentUserID, amountToAdd)
override suspend fun equip(type: String, key: String): Items? { }
val liveUser = localRepository.getLiveUser(currentUserID)
override suspend fun sellItem(
if (liveUser != null) { type: String,
localRepository.modify(liveUser) { user -> key: String,
if (type == "mount") { ): User? {
user.items?.currentMount = key val item =
} else if (type == "pet") { localRepository.getOwnedItem(currentUserID, type, key, true).firstOrNull()
user.items?.currentPet = key ?: return null
} return sellItem(item)
val outfit = if (type == "costume") { }
user.items?.gear?.costume
} else { override suspend fun sellItem(item: OwnedItem): User? {
user.items?.gear?.equipped val itemData =
} localRepository.getItem(item.itemType ?: "", item.key ?: "").firstOrNull()
when (key.split("_").firstOrNull()) { ?: return null
"weapon" -> outfit?.weapon = key return sellItem(itemData, item)
"armor" -> outfit?.armor = key }
"shield" -> outfit?.shield = key
"eyewear" -> outfit?.eyeWear = key override fun getLatestMysteryItem(): Flow<Equipment> {
"head" -> outfit?.head = key return localRepository.getLatestMysteryItem()
"back" -> outfit?.back = key }
"headAccessory" -> outfit?.headAccessory = key
"body" -> outfit?.body = key override fun getItem(
} type: String,
} key: String,
} ): Flow<Item> {
val items = apiClient.equipItem(type, key) ?: return null return localRepository.getItem(type, key)
if (liveUser == null) return null }
localRepository.modify(liveUser) { liveUser ->
val newEquipped = items.gear?.equipped private suspend fun sellItem(
val oldEquipped = liveUser.items?.gear?.equipped item: Item,
val newCostume = items.gear?.costume ownedItem: OwnedItem,
val oldCostume = liveUser.items?.gear?.costume ): User? {
newEquipped?.let { equipped -> oldEquipped?.updateWith(equipped) } localRepository.executeTransaction {
newCostume?.let { costume -> oldCostume?.updateWith(costume) } val liveItem = localRepository.getLiveObject(ownedItem)
liveUser.items?.currentMount = items.currentMount liveItem?.numberOwned = (liveItem?.numberOwned ?: 0) - 1
liveUser.items?.currentPet = items.currentPet }
liveUser.balance = liveUser.balance val user = apiClient.sellItem(item.type, item.key) ?: return null
} return localRepository.soldItem(currentUserID, user)
return items }
}
override suspend fun equipGear(
override suspend fun feedPet(pet: Pet, food: Food): FeedResponse? { equipment: String,
val feedResponse = apiClient.feedPet(pet.key, food.key) ?: return null asCostume: Boolean,
localRepository.feedPet(food.key, pet.key, feedResponse.value ?: 0, currentUserID) ): Items? {
return feedResponse return equip(if (asCostume) "costume" else "equipped", equipment)
} }
override suspend fun hatchPet(egg: Egg, hatchingPotion: HatchingPotion, successFunction: () -> Unit): Items? { override suspend fun equip(
if (appConfigManager.enableLocalChanges()) { type: String,
localRepository.hatchPet(egg.key, hatchingPotion.key, currentUserID) key: String,
successFunction() ): Items? {
} val liveUser = localRepository.getLiveUser(currentUserID)
val items = apiClient.hatchPet(egg.key, hatchingPotion.key) ?: return null
localRepository.save(items, currentUserID) if (liveUser != null) {
if (!appConfigManager.enableLocalChanges()) { localRepository.modify(liveUser) { user ->
successFunction() if (type == "mount") {
} user.items?.currentMount = key
return items } else if (type == "pet") {
} user.items?.currentPet = key
}
override suspend fun inviteToQuest(quest: QuestContent): Quest? { val outfit =
val newQuest = apiClient.inviteToQuest("party", quest.key) if (type == "costume") {
localRepository.changeOwnedCount("quests", quest.key, currentUserID, -1) user.items?.gear?.costume
return newQuest } else {
} user.items?.gear?.equipped
}
override suspend fun buyItem(user: User?, id: String, value: Double, purchaseQuantity: Int): BuyResponse? { when (key.split("_").firstOrNull()) {
val buyResponse = apiClient.buyItem(id, purchaseQuantity) ?: return null "weapon" -> outfit?.weapon = key
val foundUser = user ?: localRepository.getLiveUser(currentUserID) ?: return buyResponse "armor" -> outfit?.armor = key
val copiedUser = localRepository.getUnmanagedCopy(foundUser) "shield" -> outfit?.shield = key
if (buyResponse.items != null) { "eyewear" -> outfit?.eyeWear = key
copiedUser.items = buyResponse.items "head" -> outfit?.head = key
} "back" -> outfit?.back = key
if (buyResponse.hp != null) { "headAccessory" -> outfit?.headAccessory = key
copiedUser.stats?.hp = buyResponse.hp "body" -> outfit?.body = key
} }
if (buyResponse.exp != null) { }
copiedUser.stats?.exp = buyResponse.exp }
} val items = apiClient.equipItem(type, key) ?: return null
if (buyResponse.mp != null) { if (liveUser == null) return null
copiedUser.stats?.mp = buyResponse.mp localRepository.modify(liveUser) { liveUser ->
} val newEquipped = items.gear?.equipped
if (buyResponse.gp != null) { val oldEquipped = liveUser.items?.gear?.equipped
copiedUser.stats?.gp = buyResponse.gp val newCostume = items.gear?.costume
} else { val oldCostume = liveUser.items?.gear?.costume
copiedUser.stats?.gp = (copiedUser.stats?.gp ?: 0.0) - (value * purchaseQuantity) newEquipped?.let { equipped -> oldEquipped?.updateWith(equipped) }
} newCostume?.let { costume -> oldCostume?.updateWith(costume) }
if (buyResponse.lvl != null) { liveUser.items?.currentMount = items.currentMount
copiedUser.stats?.lvl = buyResponse.lvl liveUser.items?.currentPet = items.currentPet
} liveUser.balance = liveUser.balance
localRepository.save(copiedUser) }
return buyResponse return items
} }
override fun getAvailableLimitedItems(): Flow<List<Item>> { override suspend fun feedPet(
return localRepository.getAvailableLimitedItems() pet: Pet,
} food: Food,
): FeedResponse? {
override suspend fun retrieveShopInventory(identifier: String): Shop? { val feedResponse = apiClient.feedPet(pet.key, food.key) ?: return null
return apiClient.retrieveShopIventory(identifier) localRepository.feedPet(food.key, pet.key, feedResponse.value ?: 0, currentUserID)
} return feedResponse
}
override suspend fun retrieveMarketGear(): Shop? {
return apiClient.retrieveMarketGear() override suspend fun hatchPet(
} egg: Egg,
hatchingPotion: HatchingPotion,
override suspend fun purchaseMysterySet(categoryIdentifier: String): Void? { successFunction: () -> Unit,
return apiClient.purchaseMysterySet(categoryIdentifier) ): Items? {
} if (appConfigManager.enableLocalChanges()) {
localRepository.hatchPet(egg.key, hatchingPotion.key, currentUserID)
override suspend fun purchaseHourglassItem(purchaseType: String, key: String): Void? { successFunction()
return apiClient.purchaseHourglassItem(purchaseType, key) }
} val items = apiClient.hatchPet(egg.key, hatchingPotion.key) ?: return null
localRepository.save(items, currentUserID)
override suspend fun purchaseQuest(key: String): Void? { if (!appConfigManager.enableLocalChanges()) {
return apiClient.purchaseQuest(key) successFunction()
} }
return items
override suspend fun purchaseSpecialSpell(key: String): Void? { }
return apiClient.purchaseSpecialSpell(key)
} override suspend fun inviteToQuest(quest: QuestContent): Quest? {
val newQuest = apiClient.inviteToQuest("party", quest.key)
override suspend fun purchaseItem(purchaseType: String, key: String, purchaseQuantity: Int): Void? { localRepository.changeOwnedCount("quests", quest.key, currentUserID, -1)
val response = apiClient.purchaseItem(purchaseType, key, purchaseQuantity) return newQuest
if (key == "gem") { }
val user = localRepository.getLiveUser(currentUserID)
localRepository.executeTransaction { override suspend fun buyItem(
user?.purchased?.plan?.gemsBought = purchaseQuantity + (user?.purchased?.plan?.gemsBought ?: 0) user: User?,
} id: String,
} value: Double,
return response purchaseQuantity: Int,
} ): BuyResponse? {
val buyResponse = apiClient.buyItem(id, purchaseQuantity) ?: return null
override suspend fun togglePinnedItem(item: ShopItem): List<ShopItem>? { val foundUser = user ?: localRepository.getLiveUser(currentUserID) ?: return buyResponse
if (item.isValid) { val copiedUser = localRepository.getUnmanagedCopy(foundUser)
apiClient.togglePinnedItem(item.pinType ?: "", item.path ?: "") if (buyResponse.items != null) {
} copiedUser.items = buyResponse.items
return retrieveInAppRewards() }
} if (buyResponse.hp != null) {
} copiedUser.stats?.hp = buyResponse.hp
}
if (buyResponse.exp != null) {
copiedUser.stats?.exp = buyResponse.exp
}
if (buyResponse.mp != null) {
copiedUser.stats?.mp = buyResponse.mp
}
if (buyResponse.gp != null) {
copiedUser.stats?.gp = buyResponse.gp
} else {
copiedUser.stats?.gp = (copiedUser.stats?.gp ?: 0.0) - (value * purchaseQuantity)
}
if (buyResponse.lvl != null) {
copiedUser.stats?.lvl = buyResponse.lvl
}
localRepository.save(copiedUser)
return buyResponse
}
override fun getAvailableLimitedItems(): Flow<List<Item>> {
return localRepository.getAvailableLimitedItems()
}
override suspend fun retrieveShopInventory(identifier: String): Shop? {
return apiClient.retrieveShopIventory(identifier)
}
override suspend fun retrieveMarketGear(): Shop? {
return apiClient.retrieveMarketGear()
}
override suspend fun purchaseMysterySet(categoryIdentifier: String): Void? {
return apiClient.purchaseMysterySet(categoryIdentifier)
}
override suspend fun purchaseHourglassItem(
purchaseType: String,
key: String,
): Void? {
return apiClient.purchaseHourglassItem(purchaseType, key)
}
override suspend fun purchaseQuest(key: String): Void? {
return apiClient.purchaseQuest(key)
}
override suspend fun purchaseSpecialSpell(key: String): Void? {
return apiClient.purchaseSpecialSpell(key)
}
override suspend fun purchaseItem(
purchaseType: String,
key: String,
purchaseQuantity: Int,
): Void? {
val response = apiClient.purchaseItem(purchaseType, key, purchaseQuantity)
if (key == "gem") {
val user = localRepository.getLiveUser(currentUserID)
localRepository.executeTransaction {
user?.purchased?.plan?.gemsBought =
purchaseQuantity + (user?.purchased?.plan?.gemsBought ?: 0)
}
}
return response
}
override suspend fun togglePinnedItem(item: ShopItem): List<ShopItem>? {
if (item.isValid) {
apiClient.togglePinnedItem(item.pinType ?: "", item.path ?: "")
}
return retrieveInAppRewards()
}
}

View file

@ -1,89 +1,212 @@
package com.habitrpg.android.habitica.data.implementation package com.habitrpg.android.habitica.data.implementation
import android.content.Context import android.content.Context
import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.data.SetupCustomizationRepository import com.habitrpg.android.habitica.data.SetupCustomizationRepository
import com.habitrpg.android.habitica.models.SetupCustomization import com.habitrpg.android.habitica.models.SetupCustomization
import com.habitrpg.android.habitica.models.user.User 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
constructor(private val context: Context) : SetupCustomizationRepository { @Inject
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(
private val glasses: List<SetupCustomization> SetupCustomization.createWheelchair("none", 0),
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)) SetupCustomization.createWheelchair("black", R.drawable.creator_chair_black),
SetupCustomization.createWheelchair("blue", R.drawable.creator_chair_blue),
private val flowers: List<SetupCustomization> SetupCustomization.createWheelchair("green", R.drawable.creator_chair_green),
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)) SetupCustomization.createWheelchair("pink", R.drawable.creator_chair_pink),
SetupCustomization.createWheelchair("red", R.drawable.creator_chair_red),
private val hairColors: List<SetupCustomization> SetupCustomization.createWheelchair("yellow", R.drawable.creator_chair_yellow),
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 glasses: 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(
private val skins: List<SetupCustomization> SetupCustomization.createGlasses("", R.drawable.creator_blank_face),
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)) SetupCustomization.createGlasses(
"eyewear_special_blackTopFrame",
override fun getCustomizations(type: String, user: User): List<SetupCustomization> { R.drawable.creator_eyewear_special_blacktopframe,
return getCustomizations(type, null, user) ),
} SetupCustomization.createGlasses(
"eyewear_special_blueTopFrame",
override fun getCustomizations(type: String, subtype: String?, user: User): List<SetupCustomization> { R.drawable.creator_eyewear_special_bluetopframe,
return when (type) { ),
SetupCustomizationRepository.CATEGORY_BODY -> { SetupCustomization.createGlasses(
when (subtype) { "eyewear_special_greenTopFrame",
SetupCustomizationRepository.SUBCATEGORY_SIZE -> sizes R.drawable.creator_eyewear_special_greentopframe,
SetupCustomizationRepository.SUBCATEGORY_SHIRT -> getShirts(user.preferences?.size ?: "slim") ),
else -> emptyList() SetupCustomization.createGlasses(
} "eyewear_special_pinkTopFrame",
} R.drawable.creator_eyewear_special_pinktopframe,
SetupCustomizationRepository.CATEGORY_SKIN -> skins ),
SetupCustomizationRepository.CATEGORY_HAIR -> { SetupCustomization.createGlasses(
when (subtype) { "eyewear_special_redTopFrame",
SetupCustomizationRepository.SUBCATEGORY_BANGS -> getBangs(user.preferences?.hair?.color ?: "") R.drawable.creator_eyewear_special_redtopframe,
SetupCustomizationRepository.SUBCATEGORY_PONYTAIL -> getHairBases(user.preferences?.hair?.color ?: "") ),
SetupCustomizationRepository.SUBCATEGORY_COLOR -> hairColors SetupCustomization.createGlasses(
else -> emptyList() "eyewear_special_yellowTopFrame",
} R.drawable.creator_eyewear_special_yellowtopframe,
} ),
SetupCustomizationRepository.CATEGORY_EXTRAS -> { SetupCustomization.createGlasses(
when (subtype) { "eyewear_special_whiteTopFrame",
SetupCustomizationRepository.SUBCATEGORY_FLOWER -> flowers R.drawable.creator_eyewear_special_whitetopframe,
SetupCustomizationRepository.SUBCATEGORY_GLASSES -> glasses ),
SetupCustomizationRepository.SUBCATEGORY_WHEELCHAIR -> wheelchairs )
else -> emptyList()
} private val flowers: List<SetupCustomization>
} get() =
else -> emptyList() 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),
private fun getHairBases(color: String): List<SetupCustomization> { SetupCustomization.createFlower("3", R.drawable.creator_hair_flower_3),
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"))) 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 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")))
} private val hairColors: List<SetupCustomization>
get() =
private fun getShirts(size: String): List<SetupCustomization> { listOf(
return if (size == "broad") { SetupCustomization.createHairColor("white", R.color.hair_white),
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)) SetupCustomization.createHairColor("brown", R.color.hair_brown),
} else { SetupCustomization.createHairColor("blond", R.color.hair_blond),
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)) SetupCustomization.createHairColor("red", R.color.hair_red),
} SetupCustomization.createHairColor("black", R.color.hair_black),
} )
private fun getResId(resName: String): Int { private val sizes: List<SetupCustomization>
return try { get() =
context.resources.getIdentifier(resName, "drawable", context.packageName) listOf(
} catch (e: Exception) { SetupCustomization.createSize(
-1 "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>
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> {
return getCustomizations(type, null, user)
}
override fun getCustomizations(
type: String,
subtype: String?,
user: User,
): List<SetupCustomization> {
return when (type) {
SetupCustomizationRepository.CATEGORY_BODY -> {
when (subtype) {
SetupCustomizationRepository.SUBCATEGORY_SIZE -> sizes
SetupCustomizationRepository.SUBCATEGORY_SHIRT ->
getShirts(
user.preferences?.size ?: "slim",
)
else -> emptyList()
}
}
SetupCustomizationRepository.CATEGORY_SKIN -> skins
SetupCustomizationRepository.CATEGORY_HAIR -> {
when (subtype) {
SetupCustomizationRepository.SUBCATEGORY_BANGS ->
getBangs(
user.preferences?.hair?.color ?: "",
)
SetupCustomizationRepository.SUBCATEGORY_PONYTAIL ->
getHairBases(
user.preferences?.hair?.color ?: "",
)
SetupCustomizationRepository.SUBCATEGORY_COLOR -> hairColors
else -> emptyList()
}
}
SetupCustomizationRepository.CATEGORY_EXTRAS -> {
when (subtype) {
SetupCustomizationRepository.SUBCATEGORY_FLOWER -> flowers
SetupCustomizationRepository.SUBCATEGORY_GLASSES -> glasses
SetupCustomizationRepository.SUBCATEGORY_WHEELCHAIR -> wheelchairs
else -> emptyList()
}
}
else -> emptyList()
}
}
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")),
)
}
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")),
)
}
private fun getShirts(size: String): List<SetupCustomization> {
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),
)
} 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),
)
}
}
private fun getResId(resName: String): Int {
return try {
context.resources.getIdentifier(resName, "drawable", context.packageName)
} catch (e: Exception) {
-1
}
}
}

View file

@ -1,359 +1,441 @@
@file:OptIn(ExperimentalCoroutinesApi::class) @file:OptIn(ExperimentalCoroutinesApi::class)
package com.habitrpg.android.habitica.data.implementation package com.habitrpg.android.habitica.data.implementation
import com.habitrpg.android.habitica.BuildConfig import com.habitrpg.android.habitica.BuildConfig
import com.habitrpg.android.habitica.data.ApiClient import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.data.SocialRepository import com.habitrpg.android.habitica.data.SocialRepository
import com.habitrpg.android.habitica.data.local.SocialLocalRepository import com.habitrpg.android.habitica.data.local.SocialLocalRepository
import com.habitrpg.android.habitica.models.Achievement import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.models.inventory.Quest import com.habitrpg.android.habitica.models.inventory.Quest
import com.habitrpg.android.habitica.models.members.Member import com.habitrpg.android.habitica.models.members.Member
import com.habitrpg.android.habitica.models.responses.PostChatMessageResult import com.habitrpg.android.habitica.models.responses.PostChatMessageResult
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.social.GroupMembership import com.habitrpg.android.habitica.models.social.GroupMembership
import com.habitrpg.android.habitica.models.social.InboxConversation import com.habitrpg.android.habitica.models.social.InboxConversation
import com.habitrpg.android.habitica.models.user.User import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.modules.AuthenticationHandler import com.habitrpg.android.habitica.modules.AuthenticationHandler
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import java.util.UUID 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(
group?.leaderID = userID groupID: String,
return group?.let { apiClient.updateGroup(groupID, it) } userID: String,
} ): Group? {
val group =
override suspend fun removeMemberFromGroup(groupID: String, userID: String): List<Member>? { localRepository.getGroup(groupID).first()?.let { localRepository.getUnmanagedCopy(it) }
apiClient.removeMemberFromGroup(groupID, userID) group?.leaderID = userID
return retrievePartyMembers(groupID, true) return group?.let { apiClient.updateGroup(groupID, it) }
} }
override suspend fun blockMember(userID: String): List<String>? { override suspend fun removeMemberFromGroup(
return apiClient.blockMember(userID) groupID: String,
} userID: String,
): List<Member>? {
override fun getMember(userID: String?): Flow<Member?> { apiClient.removeMemberFromGroup(groupID, userID)
return localRepository.getMember(userID) return retrievePartyMembers(groupID, true)
} }
override suspend fun updateMember( override suspend fun blockMember(userID: String): List<String>? {
memberID: String, return apiClient.blockMember(userID)
data: Map<String, Map<String, Boolean>> }
): Member? {
return apiClient.updateMember(memberID, data) override fun getMember(userID: String?): Flow<Member?> {
} return localRepository.getMember(userID)
}
override suspend fun retrievePartySeekingUsers(page: Int): List<Member>? {
return apiClient.retrievePartySeekingUsers(page) override suspend fun updateMember(
} memberID: String,
data: Map<String, Map<String, Boolean>>,
override fun getGroupMembership(id: String) = authenticationHandler.userIDFlow.flatMapLatest { localRepository.getGroupMembership(it, id) } ): Member? {
return apiClient.updateMember(memberID, data)
override fun getGroupMemberships(): Flow<List<GroupMembership>> { }
return authenticationHandler.userIDFlow.flatMapLatest { localRepository.getGroupMemberships(it) }
} override suspend fun retrievePartySeekingUsers(page: Int): List<Member>? {
return apiClient.retrievePartySeekingUsers(page)
override suspend fun retrieveGroupChat(groupId: String): List<ChatMessage>? { }
val messages = apiClient.listGroupChat(groupId)
messages?.forEach { it.groupId = groupId } override fun getGroupMembership(id: String) =
return messages authenticationHandler.userIDFlow.flatMapLatest {
} localRepository.getGroupMembership(
it,
override fun getGroupChat(groupId: String): Flow<List<ChatMessage>> { id,
return localRepository.getGroupChat(groupId) )
} }
override suspend fun markMessagesSeen(seenGroupId: String) { override fun getGroupMemberships(): Flow<List<GroupMembership>> {
apiClient.seenMessages(seenGroupId) return authenticationHandler.userIDFlow.flatMapLatest {
} localRepository.getGroupMemberships(
it,
override suspend fun flagMessage(chatMessageID: String, additionalInfo: String, groupID: String?): Void? { )
return when { }
chatMessageID.isBlank() -> return null }
currentUserID == BuildConfig.ANDROID_TESTING_UUID -> return null
else -> { override suspend fun retrieveGroupChat(groupId: String): List<ChatMessage>? {
val data = mutableMapOf<String, String>() val messages = apiClient.listGroupChat(groupId)
data["comment"] = additionalInfo messages?.forEach { it.groupId = groupId }
if (groupID?.isNotBlank() != true) { return messages
apiClient.flagInboxMessage(chatMessageID, data) }
} else {
apiClient.flagMessage(groupID, chatMessageID, data) override fun getGroupChat(groupId: String): Flow<List<ChatMessage>> {
} return localRepository.getGroupChat(groupId)
} }
}
} override suspend fun markMessagesSeen(seenGroupId: String) {
apiClient.seenMessages(seenGroupId)
override suspend fun reportMember(memberID: String, data: Map<String, String>): Void? { }
return apiClient.reportMember(memberID, data)
} override suspend fun flagMessage(
chatMessageID: String,
override suspend fun likeMessage(chatMessage: ChatMessage): ChatMessage? { additionalInfo: String,
if (chatMessage.id.isBlank()) { groupID: String?,
return null ): Void? {
} return when {
val message = apiClient.likeMessage(chatMessage.groupId ?: "", chatMessage.id) chatMessageID.isBlank() -> return null
message?.groupId = chatMessage.groupId currentUserID == BuildConfig.ANDROID_TESTING_UUID -> return null
message?.let { localRepository.save(it) } else -> {
return message val data = mutableMapOf<String, String>()
} data["comment"] = additionalInfo
if (groupID?.isNotBlank() != true) {
override suspend fun deleteMessage(chatMessage: ChatMessage): Void? { apiClient.flagInboxMessage(chatMessageID, data)
if (chatMessage.isInboxMessage) { } else {
apiClient.deleteInboxMessage(chatMessage.id) apiClient.flagMessage(groupID, chatMessageID, data)
} else { }
apiClient.deleteMessage(chatMessage.groupId ?: "", chatMessage.id) }
} }
localRepository.deleteMessage(chatMessage.id) }
return null
} override suspend fun reportMember(
memberID: String,
override suspend fun postGroupChat(groupId: String, messageObject: HashMap<String, String>): PostChatMessageResult? { data: Map<String, String>,
val result = apiClient.postGroupChat(groupId, messageObject) ): Void? {
result?.message?.groupId = groupId return apiClient.reportMember(memberID, data)
return result }
}
override suspend fun likeMessage(chatMessage: ChatMessage): ChatMessage? {
override suspend fun postGroupChat(groupId: String, message: String): PostChatMessageResult? { if (chatMessage.id.isBlank()) {
val messageObject = HashMap<String, String>() return null
messageObject["message"] = message }
return postGroupChat(groupId, messageObject) val message = apiClient.likeMessage(chatMessage.groupId ?: "", chatMessage.id)
} message?.groupId = chatMessage.groupId
message?.let { localRepository.save(it) }
override suspend fun retrieveGroup(id: String): Group? { return message
val group = apiClient.getGroup(id) }
group?.let { localRepository.saveGroup(it) }
retrieveGroupChat(id) override suspend fun deleteMessage(chatMessage: ChatMessage): Void? {
return group if (chatMessage.isInboxMessage) {
} apiClient.deleteInboxMessage(chatMessage.id)
} else {
override fun getGroup(id: String?): Flow<Group?> { apiClient.deleteMessage(chatMessage.groupId ?: "", chatMessage.id)
if (id?.isNotBlank() != true) { }
return emptyFlow() localRepository.deleteMessage(chatMessage.id)
} return null
return localRepository.getGroup(id) }
}
override suspend fun postGroupChat(
override suspend fun leaveGroup(id: String?, keepChallenges: Boolean): Group? { groupId: String,
if (id?.isNotBlank() != true) { messageObject: HashMap<String, String>,
return null ): PostChatMessageResult? {
} val result = apiClient.postGroupChat(groupId, messageObject)
result?.message?.groupId = groupId
apiClient.leaveGroup(id, if (keepChallenges) "remain-in-challenges" else "leave-challenges") return result
localRepository.updateMembership(currentUserID, id, false) }
return localRepository.getGroup(id).firstOrNull()
} override suspend fun postGroupChat(
groupId: String,
override suspend fun joinGroup(id: String?): Group? { message: String,
if (id?.isNotBlank() != true) { ): PostChatMessageResult? {
return null val messageObject = HashMap<String, String>()
} messageObject["message"] = message
val group = apiClient.joinGroup(id) return postGroupChat(groupId, messageObject)
group?.let { }
localRepository.updateMembership(currentUserID, id, true)
localRepository.save(group) override suspend fun retrieveGroup(id: String): Group? {
} val group = apiClient.getGroup(id)
return group group?.let { localRepository.saveGroup(it) }
} retrieveGroupChat(id)
return group
override suspend fun createGroup( }
name: String?,
description: String?, override fun getGroup(id: String?): Flow<Group?> {
leader: String?, if (id?.isNotBlank() != true) {
type: String?, return emptyFlow()
privacy: String?, }
leaderCreateChallenge: Boolean? return localRepository.getGroup(id)
): Group? { }
val group = Group()
group.name = name override suspend fun leaveGroup(
group.description = description id: String?,
group.type = type keepChallenges: Boolean,
group.leaderID = leader ): Group? {
group.privacy = privacy if (id?.isNotBlank() != true) {
val savedGroup = apiClient.createGroup(group) return null
savedGroup?.let { localRepository.save(it) } }
return savedGroup
} apiClient.leaveGroup(id, if (keepChallenges) "remain-in-challenges" else "leave-challenges")
localRepository.updateMembership(currentUserID, id, false)
override suspend fun updateGroup( return localRepository.getGroup(id).firstOrNull()
group: Group?, }
name: String?,
description: String?, override suspend fun joinGroup(id: String?): Group? {
leader: String?, if (id?.isNotBlank() != true) {
leaderCreateChallenge: Boolean? return null
): Group? { }
if (group == null) { val group = apiClient.joinGroup(id)
return null group?.let {
} localRepository.updateMembership(currentUserID, id, true)
val copiedGroup = localRepository.getUnmanagedCopy(group) localRepository.save(group)
copiedGroup.name = name }
copiedGroup.description = description return group
copiedGroup.leaderID = leader }
copiedGroup.leaderOnlyChallenges = leaderCreateChallenge ?: false
localRepository.save(copiedGroup) override suspend fun createGroup(
return apiClient.updateGroup(copiedGroup.id, copiedGroup) name: String?,
} description: String?,
leader: String?,
override fun getInboxConversations() = authenticationHandler.userIDFlow.flatMapLatest { localRepository.getInboxConversation(it) } type: String?,
privacy: String?,
override fun getInboxMessages(replyToUserID: String?) = authenticationHandler.userIDFlow.flatMapLatest { localRepository.getInboxMessages(it, replyToUserID) } leaderCreateChallenge: Boolean?,
): Group? {
override suspend fun retrieveInboxMessages(uuid: String, page: Int): List<ChatMessage>? { val group = Group()
val messages = apiClient.retrieveInboxMessages(uuid, page) ?: return null group.name = name
messages.forEach { group.description = description
it.isInboxMessage = true group.type = type
} group.leaderID = leader
localRepository.saveInboxMessages(currentUserID, uuid, messages, page) group.privacy = privacy
return messages val savedGroup = apiClient.createGroup(group)
} savedGroup?.let { localRepository.save(it) }
return savedGroup
override suspend fun retrieveInboxConversations(): List<InboxConversation>? { }
val conversations = apiClient.retrieveInboxConversations() ?: return null
localRepository.saveInboxConversations(currentUserID, conversations) override suspend fun updateGroup(
return conversations group: Group?,
} name: String?,
description: String?,
override suspend fun postPrivateMessage(recipientId: String, messageObject: HashMap<String, String>): List<ChatMessage>? { leader: String?,
apiClient.postPrivateMessage(messageObject) leaderCreateChallenge: Boolean?,
return retrieveInboxMessages(recipientId, 0) ): Group? {
} if (group == null) {
return null
override suspend fun postPrivateMessage(recipientId: String, message: String): List<ChatMessage>? { }
val messageObject = HashMap<String, String>() val copiedGroup = localRepository.getUnmanagedCopy(group)
messageObject["message"] = message copiedGroup.name = name
messageObject["toUserId"] = recipientId copiedGroup.description = description
return postPrivateMessage(recipientId, messageObject) copiedGroup.leaderID = leader
} copiedGroup.leaderOnlyChallenges = leaderCreateChallenge ?: false
localRepository.save(copiedGroup)
override suspend fun getPartyMembers(id: String) = localRepository.getPartyMembers(id) return apiClient.updateGroup(copiedGroup.id, copiedGroup)
override suspend fun getGroupMembers(id: String) = localRepository.getGroupMembers(id) }
override suspend fun retrievePartyMembers(id: String, includeAllPublicFields: Boolean): List<Member>? { override fun getInboxConversations() =
val members = apiClient.getGroupMembers(id, includeAllPublicFields) authenticationHandler.userIDFlow.flatMapLatest { localRepository.getInboxConversation(it) }
members?.let { localRepository.savePartyMembers(id, it) }
return members override fun getInboxMessages(replyToUserID: String?) =
} authenticationHandler.userIDFlow.flatMapLatest {
localRepository.getInboxMessages(
override suspend fun inviteToGroup(id: String, inviteData: Map<String, Any>) = apiClient.inviteToGroup(id, inviteData) it,
replyToUserID,
override suspend fun retrieveMember(userId: String?, fromHall: Boolean): Member? { )
return if (userId == null) { }
null
} else { override suspend fun retrieveInboxMessages(
if (fromHall) { uuid: String,
apiClient.getHallMember(userId) page: Int,
} else { ): List<ChatMessage>? {
try { val messages = apiClient.retrieveInboxMessages(uuid, page) ?: return null
val uuid = UUID.fromString(userId).toString() messages.forEach {
apiClient.getMember(uuid) it.isInboxMessage = true
} catch (_: IllegalArgumentException) { }
apiClient.getMemberWithUsername(userId) localRepository.saveInboxMessages(currentUserID, uuid, messages, page)
} return messages
} }
}
} override suspend fun retrieveInboxConversations(): List<InboxConversation>? {
val conversations = apiClient.retrieveInboxConversations() ?: return null
override suspend fun retrievegroupInvites(id: String, includeAllPublicFields: Boolean) = apiClient.getGroupInvites(id, includeAllPublicFields) localRepository.saveInboxConversations(currentUserID, conversations)
return conversations
override suspend fun findUsernames(username: String, context: String?, id: String?): List<FindUsernameResult>? { }
return apiClient.findUsernames(username, context, id)
} override suspend fun postPrivateMessage(
recipientId: String,
override suspend fun markPrivateMessagesRead(user: User?) { messageObject: HashMap<String, String>,
if (user?.isManaged == true) { ): List<ChatMessage>? {
localRepository.modify(user) { apiClient.postPrivateMessage(messageObject)
it.inbox?.hasUserSeenInbox = true return retrieveInboxMessages(recipientId, 0)
} }
}
return apiClient.markPrivateMessagesRead() override suspend fun postPrivateMessage(
} recipientId: String,
message: String,
override fun markSomePrivateMessagesAsRead(user: User?, messages: List<ChatMessage>) { ): List<ChatMessage>? {
if (user?.isManaged == true) { val messageObject = HashMap<String, String>()
val numOfUnseenMessages = messages.count { !it.isSeen } messageObject["message"] = message
localRepository.modify(user) { messageObject["toUserId"] = recipientId
val numOfNewMessagesFromInbox = it.inbox?.newMessages ?: 0 return postPrivateMessage(recipientId, messageObject)
if (numOfNewMessagesFromInbox > numOfUnseenMessages) { }
it.inbox?.newMessages = numOfNewMessagesFromInbox - numOfUnseenMessages
} else { override suspend fun getPartyMembers(id: String) = localRepository.getPartyMembers(id)
it.inbox?.newMessages = 0
} override suspend fun getGroupMembers(id: String) = localRepository.getGroupMembers(id)
}
} override suspend fun retrievePartyMembers(
for (message in messages.filter { it.isManaged && !it.isSeen }) { id: String,
localRepository.modify(message) { includeAllPublicFields: Boolean,
it.isSeen = true ): List<Member>? {
} val members = apiClient.getGroupMembers(id, includeAllPublicFields)
} members?.let { localRepository.savePartyMembers(id, it) }
} return members
}
override fun getUserGroups(type: String?) = authenticationHandler.userIDFlow.flatMapLatest { localRepository.getUserGroups(it, type) }
override suspend fun inviteToGroup(
override suspend fun acceptQuest(user: User?, partyId: String): Void? { id: String,
apiClient.acceptQuest(partyId) inviteData: Map<String, Any>,
user?.let { ) =
localRepository.updateRSVPNeeded(it, false) apiClient.inviteToGroup(id, inviteData)
}
return null override suspend fun retrieveMember(
} userId: String?,
fromHall: Boolean,
override suspend fun rejectQuest(user: User?, partyId: String): Void? { ): Member? {
apiClient.rejectQuest(partyId) return if (userId == null) {
user?.let { null
localRepository.updateRSVPNeeded(it, false) } else {
} if (fromHall) {
return null apiClient.getHallMember(userId)
} } else {
try {
override suspend fun leaveQuest(partyId: String): Void? { val uuid = UUID.fromString(userId).toString()
return apiClient.leaveQuest(partyId) apiClient.getMember(uuid)
} } catch (_: IllegalArgumentException) {
apiClient.getMemberWithUsername(userId)
override suspend fun cancelQuest(partyId: String): Void? { }
apiClient.cancelQuest(partyId) }
localRepository.removeQuest(partyId) }
return null }
}
override suspend fun retrievegroupInvites(
override suspend fun abortQuest(partyId: String): Quest? { id: String,
val quest = apiClient.abortQuest(partyId) includeAllPublicFields: Boolean,
localRepository.removeQuest(partyId) ) =
return quest apiClient.getGroupInvites(id, includeAllPublicFields)
}
override suspend fun findUsernames(
override suspend fun rejectGroupInvite(groupId: String): Void? { username: String,
apiClient.rejectGroupInvite(groupId) context: String?,
localRepository.rejectGroupInvitation(currentUserID, groupId) id: String?,
return null ): List<FindUsernameResult>? {
} return apiClient.findUsernames(username, context, id)
}
override suspend fun forceStartQuest(party: Group): Quest? {
val quest = apiClient.forceStartQuest(party.id, localRepository.getUnmanagedCopy(party)) override suspend fun markPrivateMessagesRead(user: User?) {
localRepository.setQuestActivity(party, true) if (user?.isManaged == true) {
return quest localRepository.modify(user) {
} it.inbox?.hasUserSeenInbox = true
}
override suspend fun getMemberAchievements(userId: String?): List<Achievement>? { }
return if (userId == null) { return apiClient.markPrivateMessagesRead()
null }
} else {
apiClient.getMemberAchievements(userId) override fun markSomePrivateMessagesAsRead(
} user: User?,
} messages: List<ChatMessage>,
) {
override suspend fun transferGems(giftedID: String, amount: Int): Void? { if (user?.isManaged == true) {
return apiClient.transferGems(giftedID, amount) val numOfUnseenMessages = messages.count { !it.isSeen }
} localRepository.modify(user) {
} val numOfNewMessagesFromInbox = it.inbox?.newMessages ?: 0
if (numOfNewMessagesFromInbox > numOfUnseenMessages) {
it.inbox?.newMessages = numOfNewMessagesFromInbox - numOfUnseenMessages
} else {
it.inbox?.newMessages = 0
}
}
}
for (message in messages.filter { it.isManaged && !it.isSeen }) {
localRepository.modify(message) {
it.isSeen = true
}
}
}
override fun getUserGroups(type: String?) =
authenticationHandler.userIDFlow.flatMapLatest { localRepository.getUserGroups(it, type) }
override suspend fun acceptQuest(
user: User?,
partyId: String,
): Void? {
apiClient.acceptQuest(partyId)
user?.let {
localRepository.updateRSVPNeeded(it, false)
}
return null
}
override suspend fun rejectQuest(
user: User?,
partyId: String,
): Void? {
apiClient.rejectQuest(partyId)
user?.let {
localRepository.updateRSVPNeeded(it, false)
}
return null
}
override suspend fun leaveQuest(partyId: String): Void? {
return apiClient.leaveQuest(partyId)
}
override suspend fun cancelQuest(partyId: String): Void? {
apiClient.cancelQuest(partyId)
localRepository.removeQuest(partyId)
return null
}
override suspend fun abortQuest(partyId: String): Quest? {
val quest = apiClient.abortQuest(partyId)
localRepository.removeQuest(partyId)
return quest
}
override suspend fun rejectGroupInvite(groupId: String): Void? {
apiClient.rejectGroupInvite(groupId)
localRepository.rejectGroupInvitation(currentUserID, groupId)
return null
}
override suspend fun forceStartQuest(party: Group): Quest? {
val quest = apiClient.forceStartQuest(party.id, localRepository.getUnmanagedCopy(party))
localRepository.setQuestActivity(party, true)
return quest
}
override suspend fun getMemberAchievements(userId: String?): List<Achievement>? {
return if (userId == null) {
null
} else {
apiClient.getMemberAchievements(userId)
}
}
override suspend fun transferGems(
giftedID: String,
amount: Int,
): Void? {
return apiClient.transferGems(giftedID, amount)
}
}

View file

@ -1,64 +1,63 @@
@file:OptIn(ExperimentalCoroutinesApi::class) @file:OptIn(ExperimentalCoroutinesApi::class)
package com.habitrpg.android.habitica.data.implementation package com.habitrpg.android.habitica.data.implementation
import com.habitrpg.android.habitica.data.ApiClient import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.data.TagRepository import com.habitrpg.android.habitica.data.TagRepository
import com.habitrpg.android.habitica.data.local.TagLocalRepository import com.habitrpg.android.habitica.data.local.TagLocalRepository
import com.habitrpg.android.habitica.models.Tag import com.habitrpg.android.habitica.models.Tag
import com.habitrpg.android.habitica.modules.AuthenticationHandler import com.habitrpg.android.habitica.modules.AuthenticationHandler
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flatMapLatest 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>> { return localRepository.getTags(userId)
return localRepository.getTags(userId) }
}
override suspend fun createTag(tag: Tag): Tag? {
override suspend fun createTag(tag: Tag): Tag? { val savedTag = apiClient.createTag(tag) ?: return null
val savedTag = apiClient.createTag(tag) ?: return null savedTag.userId = currentUserID
savedTag.userId = currentUserID localRepository.save(savedTag)
localRepository.save(savedTag) return savedTag
return savedTag }
}
override suspend fun updateTag(tag: Tag): Tag? {
override suspend fun updateTag(tag: Tag): Tag? { val savedTag = apiClient.updateTag(tag.id, tag) ?: return null
val savedTag = apiClient.updateTag(tag.id, tag) ?: return null savedTag.userId = currentUserID
savedTag.userId = currentUserID localRepository.save(savedTag)
localRepository.save(savedTag) return savedTag
return savedTag }
}
override suspend fun deleteTag(id: String): Void? {
override suspend fun deleteTag(id: String): Void? { apiClient.deleteTag(id)
apiClient.deleteTag(id) localRepository.deleteTag(id)
localRepository.deleteTag(id) return null
return null }
}
override suspend fun createTags(tags: Collection<Tag>): List<Tag> {
override suspend fun createTags(tags: Collection<Tag>): List<Tag> { return tags.mapNotNull {
return tags.mapNotNull { createTag(it)
createTag(it) }
} }
}
override suspend fun updateTags(tags: Collection<Tag>): List<Tag> {
override suspend fun updateTags(tags: Collection<Tag>): List<Tag> { return tags.mapNotNull {
return tags.mapNotNull { updateTag(it)
updateTag(it) }
} }
}
override suspend fun deleteTags(tagIds: Collection<String>): List<Void> {
override suspend fun deleteTags(tagIds: Collection<String>): List<Void> { return tagIds.mapNotNull {
return tagIds.mapNotNull { deleteTag(it)
deleteTag(it) }
} }
} }
}

View file

@ -1,405 +1,485 @@
package com.habitrpg.android.habitica.data.implementation package com.habitrpg.android.habitica.data.implementation
import com.habitrpg.android.habitica.data.ApiClient import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.data.TaskRepository import com.habitrpg.android.habitica.data.TaskRepository
import com.habitrpg.android.habitica.data.local.TaskLocalRepository import com.habitrpg.android.habitica.data.local.TaskLocalRepository
import com.habitrpg.android.habitica.helpers.Analytics import com.habitrpg.android.habitica.helpers.Analytics
import com.habitrpg.android.habitica.helpers.AppConfigManager import com.habitrpg.android.habitica.helpers.AppConfigManager
import com.habitrpg.android.habitica.helpers.EventCategory import com.habitrpg.android.habitica.helpers.EventCategory
import com.habitrpg.android.habitica.helpers.HitType import com.habitrpg.android.habitica.helpers.HitType
import com.habitrpg.android.habitica.interactors.ScoreTaskLocallyInteractor import com.habitrpg.android.habitica.interactors.ScoreTaskLocallyInteractor
import com.habitrpg.android.habitica.models.BaseMainObject import com.habitrpg.android.habitica.models.BaseMainObject
import com.habitrpg.android.habitica.models.responses.BulkTaskScoringData import com.habitrpg.android.habitica.models.responses.BulkTaskScoringData
import com.habitrpg.android.habitica.models.tasks.ChecklistItem import com.habitrpg.android.habitica.models.tasks.ChecklistItem
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
import com.habitrpg.android.habitica.models.user.User import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.modules.AuthenticationHandler import com.habitrpg.android.habitica.modules.AuthenticationHandler
import com.habitrpg.common.habitica.helpers.launchCatching import com.habitrpg.common.habitica.helpers.launchCatching
import com.habitrpg.shared.habitica.models.responses.TaskDirection import com.habitrpg.shared.habitica.models.responses.TaskDirection
import com.habitrpg.shared.habitica.models.responses.TaskDirectionData import com.habitrpg.shared.habitica.models.responses.TaskDirectionData
import com.habitrpg.shared.habitica.models.responses.TaskScoringResult import com.habitrpg.shared.habitica.models.responses.TaskScoringResult
import com.habitrpg.shared.habitica.models.tasks.TaskType import com.habitrpg.shared.habitica.models.tasks.TaskType
import com.habitrpg.shared.habitica.models.tasks.TasksOrder import com.habitrpg.shared.habitica.models.tasks.TasksOrder
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.UUID import java.util.UUID
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
class TaskRepositoryImpl( class TaskRepositoryImpl(
localRepository: TaskLocalRepository, localRepository: TaskLocalRepository,
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),
private var lastTaskAction: Long = 0 TaskRepository {
private var lastTaskAction: Long = 0
override fun getTasks(taskType: TaskType, userID: String?, includedGroupIDs: Array<String>): Flow<List<Task>> =
this.localRepository.getTasks(taskType, userID ?: authenticationHandler.currentUserID ?: "", includedGroupIDs) override fun getTasks(
taskType: TaskType,
override fun saveTasks(userId: String, order: TasksOrder, tasks: TaskList) { userID: String?,
localRepository.saveTasks(userId, order, tasks) includedGroupIDs: Array<String>,
} ): Flow<List<Task>> =
this.localRepository.getTasks(
override suspend fun retrieveTasks(userId: String, tasksOrder: TasksOrder): TaskList? { taskType,
val tasks = apiClient.getTasks() ?: return null userID ?: authenticationHandler.currentUserID ?: "",
this.localRepository.saveTasks(userId, tasksOrder, tasks) includedGroupIDs,
return tasks )
}
override fun saveTasks(
override suspend fun retrieveCompletedTodos(userId: String?): TaskList? { userId: String,
val taskList = this.apiClient.getTasks("completedTodos") ?: return null order: TasksOrder,
val tasks = taskList.tasks tasks: TaskList,
this.localRepository.saveCompletedTodos(userId ?: authenticationHandler.currentUserID ?: "", tasks.values) ) {
return taskList localRepository.saveTasks(userId, order, tasks)
} }
override suspend fun retrieveTasks(userId: String, tasksOrder: TasksOrder, dueDate: Date): TaskList? { override suspend fun retrieveTasks(
val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZZZZZ", Locale.US) userId: String,
val taskList = this.apiClient.getTasks("dailys", formatter.format(dueDate)) ?: return null tasksOrder: TasksOrder,
this.localRepository.saveTasks(userId, tasksOrder, taskList) ): TaskList? {
return taskList val tasks = apiClient.getTasks() ?: return null
} this.localRepository.saveTasks(userId, tasksOrder, tasks)
return tasks
@Suppress("ReturnCount") }
override suspend fun taskChecked(
user: User?, override suspend fun retrieveCompletedTodos(userId: String?): TaskList? {
task: Task, val taskList = this.apiClient.getTasks("completedTodos") ?: return null
up: Boolean, val tasks = taskList.tasks
force: Boolean, this.localRepository.saveCompletedTodos(
notifyFunc: ((TaskScoringResult) -> Unit)? userId ?: authenticationHandler.currentUserID ?: "",
): TaskScoringResult? { tasks.values,
val localData = if (user != null && appConfigManager.enableLocalTaskScoring()) { )
ScoreTaskLocallyInteractor.score(user, task, if (up) TaskDirection.UP else TaskDirection.DOWN) return taskList
} else { }
null
} override suspend fun retrieveTasks(
if (user != null && localData != null) { userId: String,
val stats = user.stats tasksOrder: TasksOrder,
val result = TaskScoringResult(localData, stats) dueDate: Date,
notifyFunc?.invoke(result) ): TaskList? {
val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZZZZZ", Locale.US)
handleTaskResponse(user, localData, task, up, 0f) val taskList = this.apiClient.getTasks("dailys", formatter.format(dueDate)) ?: return null
} this.localRepository.saveTasks(userId, tasksOrder, taskList)
val now = Date().time return taskList
val id = task.id }
if (lastTaskAction > now - 500 && !force || id == null) {
return null @Suppress("ReturnCount")
} override suspend fun taskChecked(
user: User?,
lastTaskAction = now task: Task,
val res = this.apiClient.postTaskDirection(id, (if (up) TaskDirection.UP else TaskDirection.DOWN).text) ?: return null up: Boolean,
// There are cases where the user object is not set correctly. So the app refetches it as a fallback force: Boolean,
val thisUser = user ?: localRepository.getUser(authenticationHandler.currentUserID ?: "").firstOrNull() ?: return null notifyFunc: ((TaskScoringResult) -> Unit)?,
// save local task changes ): TaskScoringResult? {
val localData =
Analytics.sendEvent( if (user != null && appConfigManager.enableLocalTaskScoring()) {
"task_scored", ScoreTaskLocallyInteractor.score(
EventCategory.BEHAVIOUR, user,
HitType.EVENT, task,
mapOf( if (up) TaskDirection.UP else TaskDirection.DOWN,
"type" to (task.type ?: ""), )
"scored_up" to up, } else {
"value" to task.value null
) }
) if (user != null && localData != null) {
if (res.lvl == 0) { val stats = user.stats
// Team tasks that require approval have weird data that we should just ignore. val result = TaskScoringResult(localData, stats)
return TaskScoringResult() notifyFunc?.invoke(result)
}
val result = TaskScoringResult(res, thisUser.stats) handleTaskResponse(user, localData, task, up, 0f)
if (localData == null) { }
notifyFunc?.invoke(result) val now = Date().time
} val id = task.id
handleTaskResponse(thisUser, res, task, up, localData?.delta ?: 0f) if (lastTaskAction > now - 500 && !force || id == null) {
return result return null
} }
override suspend fun bulkScoreTasks(data: List<Map<String, String>>): BulkTaskScoringData? { lastTaskAction = now
return apiClient.bulkScoreTasks(data) val res =
} this.apiClient.postTaskDirection(
id,
private fun handleTaskResponse( (if (up) TaskDirection.UP else TaskDirection.DOWN).text,
user: User, ) ?: return null
res: TaskDirectionData, // There are cases where the user object is not set correctly. So the app refetches it as a fallback
task: Task, val thisUser =
up: Boolean, user ?: localRepository.getUser(authenticationHandler.currentUserID ?: "").firstOrNull()
localDelta: Float ?: return null
) { // save local task changes
this.localRepository.executeTransaction {
val bgTask = localRepository.getLiveObject(task) ?: return@executeTransaction Analytics.sendEvent(
val bgUser = localRepository.getLiveObject(user) ?: return@executeTransaction "task_scored",
if (bgTask.type != TaskType.REWARD && (bgTask.value - localDelta) + res.delta != bgTask.value) { EventCategory.BEHAVIOUR,
bgTask.value = (bgTask.value - localDelta) + res.delta HitType.EVENT,
if (TaskType.DAILY == bgTask.type || TaskType.TODO == bgTask.type) { mapOf(
bgTask.completeForUser(authenticationHandler.currentUserID ?: "", up) "type" to (task.type ?: ""),
if (TaskType.DAILY == bgTask.type) { "scored_up" to up,
if (up) { "value" to task.value,
bgTask.streak = (bgTask.streak ?: 0) + 1 ),
} else { )
bgTask.streak = (bgTask.streak ?: 0) - 1 if (res.lvl == 0) {
} // Team tasks that require approval have weird data that we should just ignore.
} return TaskScoringResult()
} else if (TaskType.HABIT == bgTask.type) { }
if (up) { val result = TaskScoringResult(res, thisUser.stats)
bgTask.counterUp = (bgTask.counterUp ?: 0) + 1 if (localData == null) {
} else { notifyFunc?.invoke(result)
bgTask.counterDown = (bgTask.counterDown ?: 0) + 1 }
} handleTaskResponse(thisUser, res, task, up, localData?.delta ?: 0f)
} return result
}
if (bgTask.isGroupTask) {
val entry = bgTask.group?.assignedUsersDetail?.firstOrNull { it.assignedUserID == user.id } override suspend fun bulkScoreTasks(data: List<Map<String, String>>): BulkTaskScoringData? {
entry?.completed = up return apiClient.bulkScoreTasks(data)
if (up) { }
entry?.completedDate = Date()
} else { private fun handleTaskResponse(
entry?.completedDate = null user: User,
} res: TaskDirectionData,
} task: Task,
} up: Boolean,
res._tmp?.drop?.key?.let { key -> localDelta: Float,
val type = when (res._tmp?.drop?.type?.lowercase(Locale.US)) { ) {
"hatchingpotion" -> "hatchingPotions" this.localRepository.executeTransaction {
"egg" -> "eggs" val bgTask = localRepository.getLiveObject(task) ?: return@executeTransaction
else -> res._tmp?.drop?.type?.lowercase(Locale.US) val bgUser = localRepository.getLiveObject(user) ?: return@executeTransaction
} if (bgTask.type != TaskType.REWARD && (bgTask.value - localDelta) + res.delta != bgTask.value) {
var item = it.where(OwnedItem::class.java).equalTo("itemType", type).equalTo("key", key).findFirst() bgTask.value = (bgTask.value - localDelta) + res.delta
if (item == null) { if (TaskType.DAILY == bgTask.type || TaskType.TODO == bgTask.type) {
item = OwnedItem() bgTask.completeForUser(authenticationHandler.currentUserID ?: "", up)
item.key = key if (TaskType.DAILY == bgTask.type) {
item.itemType = type if (up) {
item.userID = user.id bgTask.streak = (bgTask.streak ?: 0) + 1
} else {
when (type) { bgTask.streak = (bgTask.streak ?: 0) - 1
"eggs" -> bgUser.items?.eggs?.add(item) }
"food" -> bgUser.items?.food?.add(item) }
"hatchingPotions" -> bgUser.items?.hatchingPotions?.add(item) } else if (TaskType.HABIT == bgTask.type) {
"quests" -> bgUser.items?.quests?.add(item) if (up) {
} bgTask.counterUp = (bgTask.counterUp ?: 0) + 1
} } else {
item.numberOwned += 1 bgTask.counterDown = (bgTask.counterDown ?: 0) + 1
} }
}
bgUser.stats?.hp = res.hp
bgUser.stats?.exp = res.exp if (bgTask.isGroupTask) {
bgUser.stats?.mp = res.mp val entry =
bgUser.stats?.gp = res.gp bgTask.group?.assignedUsersDetail?.firstOrNull { it.assignedUserID == user.id }
bgUser.stats?.lvl = res.lvl entry?.completed = up
bgUser.party?.quest?.progress?.up = ( if (up) {
bgUser.party?.quest?.progress?.up entry?.completedDate = Date()
?: 0F } else {
) + (res._tmp?.quest?.progressDelta?.toFloat() ?: 0F) entry?.completedDate = null
} }
} }
}
override suspend fun markTaskNeedsWork(task: Task, userID: String) { res._tmp?.drop?.key?.let { key ->
val savedTask = apiClient.markTaskNeedsWork(task.id ?: "", userID) val type =
if (savedTask != null) { when (res._tmp?.drop?.type?.lowercase(Locale.US)) {
savedTask.id = task.id "hatchingpotion" -> "hatchingPotions"
savedTask.position = task.position "egg" -> "eggs"
savedTask.group?.assignedUsersDetail?.firstOrNull { it.assignedUserID == userID }?.let { else -> res._tmp?.drop?.type?.lowercase(Locale.US)
it.completed = false }
it.completedDate = null var item =
} it.where(OwnedItem::class.java).equalTo("itemType", type).equalTo("key", key)
localRepository.save(savedTask) .findFirst()
} if (item == null) {
} item = OwnedItem()
item.key = key
override suspend fun taskChecked( item.itemType = type
user: User?, item.userID = user.id
taskId: String,
up: Boolean, when (type) {
force: Boolean, "eggs" -> bgUser.items?.eggs?.add(item)
notifyFunc: ((TaskScoringResult) -> Unit)? "food" -> bgUser.items?.food?.add(item)
): TaskScoringResult? { "hatchingPotions" -> bgUser.items?.hatchingPotions?.add(item)
val task = localRepository.getTask(taskId).firstOrNull() ?: return null "quests" -> bgUser.items?.quests?.add(item)
return taskChecked(user, task, up, force, notifyFunc) }
} }
item.numberOwned += 1
override suspend fun scoreChecklistItem(taskId: String, itemId: String): Task? { }
val task = apiClient.scoreChecklistItem(taskId, itemId)
val updatedItem: ChecklistItem? = task?.checklist?.lastOrNull { itemId == it.id } bgUser.stats?.hp = res.hp
if (updatedItem != null) { bgUser.stats?.exp = res.exp
localRepository.save(updatedItem) bgUser.stats?.mp = res.mp
} bgUser.stats?.gp = res.gp
return task bgUser.stats?.lvl = res.lvl
} bgUser.party?.quest?.progress?.up = (
bgUser.party?.quest?.progress?.up
override fun getTask(taskId: String) = localRepository.getTask(taskId) ?: 0F
) + (res._tmp?.quest?.progressDelta?.toFloat() ?: 0F)
override fun getTaskCopy(taskId: String) = localRepository.getTaskCopy(taskId) }
}
override suspend fun createTask(task: Task, force: Boolean): Task? {
val now = Date().time override suspend fun markTaskNeedsWork(
if (lastTaskAction > now - 500 && !force) { task: Task,
return null userID: String,
} ) {
lastTaskAction = now val savedTask = apiClient.markTaskNeedsWork(task.id ?: "", userID)
if (savedTask != null) {
task.isSaving = true savedTask.id = task.id
task.isCreating = true savedTask.position = task.position
task.hasErrored = false savedTask.group?.assignedUsersDetail?.firstOrNull { it.assignedUserID == userID }?.let {
task.ownerID = if (task.isGroupTask) { it.completed = false
task.group?.groupID ?: "" it.completedDate = null
} else { }
authenticationHandler.currentUserID ?: "" localRepository.save(savedTask)
} }
if (task.id == null) { }
task.id = UUID.randomUUID().toString()
} override suspend fun taskChecked(
localRepository.save(task) user: User?,
taskId: String,
val savedTask = if (task.isGroupTask) { up: Boolean,
apiClient.createGroupTask(task.group?.groupID ?: "", task) force: Boolean,
} else { notifyFunc: ((TaskScoringResult) -> Unit)?,
apiClient.createTask(task) ): TaskScoringResult? {
} val task = localRepository.getTask(taskId).firstOrNull() ?: return null
savedTask?.dateCreated = Date() return taskChecked(user, task, up, force, notifyFunc)
if (savedTask != null) { }
savedTask.tags = task.tags
localRepository.save(savedTask) override suspend fun scoreChecklistItem(
} else { taskId: String,
task.hasErrored = true itemId: String,
task.isSaving = false ): Task? {
localRepository.save(task) val task = apiClient.scoreChecklistItem(taskId, itemId)
} val updatedItem: ChecklistItem? = task?.checklist?.lastOrNull { itemId == it.id }
return savedTask if (updatedItem != null) {
} localRepository.save(updatedItem)
}
@Suppress("ReturnCount") return task
override suspend fun updateTask(task: Task, force: Boolean): Task? { }
val now = Date().time
if ((lastTaskAction > now - 500 && !force) || !task.isValid) { override fun getTask(taskId: String) = localRepository.getTask(taskId)
return task
} override fun getTaskCopy(taskId: String) = localRepository.getTaskCopy(taskId)
lastTaskAction = now
val id = task.id ?: return task override suspend fun createTask(
val unmanagedTask = localRepository.getUnmanagedCopy(task) task: Task,
unmanagedTask.isSaving = true force: Boolean,
unmanagedTask.hasErrored = false ): Task? {
localRepository.save(unmanagedTask) val now = Date().time
val savedTask = apiClient.updateTask(id, unmanagedTask) if (lastTaskAction > now - 500 && !force) {
savedTask?.position = task.position return null
savedTask?.id = task.id }
savedTask?.ownerID = task.ownerID lastTaskAction = now
if (savedTask != null) {
savedTask.tags = task.tags task.isSaving = true
localRepository.save(savedTask) task.isCreating = true
} else { task.hasErrored = false
unmanagedTask.hasErrored = true task.ownerID =
unmanagedTask.isSaving = false if (task.isGroupTask) {
localRepository.save(unmanagedTask) task.group?.groupID ?: ""
} } else {
return savedTask authenticationHandler.currentUserID ?: ""
} }
if (task.id == null) {
override suspend fun deleteTask(taskId: String): Void? { task.id = UUID.randomUUID().toString()
apiClient.deleteTask(taskId) ?: return null }
localRepository.deleteTask(taskId) localRepository.save(task)
return null
} val savedTask =
if (task.isGroupTask) {
override fun saveTask(task: Task) { apiClient.createGroupTask(task.group?.groupID ?: "", task)
localRepository.save(task) } else {
} apiClient.createTask(task)
}
override suspend fun createTasks(newTasks: List<Task>) = apiClient.createTasks(newTasks) savedTask?.dateCreated = Date()
if (savedTask != null) {
override fun markTaskCompleted(taskId: String, isCompleted: Boolean) { savedTask.tags = task.tags
localRepository.markTaskCompleted(taskId, isCompleted) localRepository.save(savedTask)
} } else {
task.hasErrored = true
override fun <T : BaseMainObject> modify(obj: T, transaction: (T) -> Unit) { task.isSaving = false
localRepository.modify(obj, transaction) localRepository.save(task)
} }
return savedTask
override fun swapTaskPosition(firstPosition: Int, secondPosition: Int) { }
localRepository.swapTaskPosition(firstPosition, secondPosition)
} @Suppress("ReturnCount")
override suspend fun updateTask(
override suspend fun updateTaskPosition(taskType: TaskType, taskID: String, newPosition: Int): List<String>? { task: Task,
val positions = apiClient.postTaskNewPosition(taskID, newPosition) ?: return null force: Boolean,
localRepository.updateTaskPositions(positions) ): Task? {
return positions val now = Date().time
} if ((lastTaskAction > now - 500 && !force) || !task.isValid) {
return task
override fun getUnmanagedTask(taskid: String) = getTask(taskid).map { localRepository.getUnmanagedCopy(it) } }
lastTaskAction = now
override fun updateTaskInBackground(task: Task, assignChanges: Map<String, MutableList<String>>) { val id = task.id ?: return task
MainScope().launchCatching { val unmanagedTask = localRepository.getUnmanagedCopy(task)
val updatedTask = updateTask(task) ?: return@launchCatching unmanagedTask.isSaving = true
handleAssignmentChanges(updatedTask, assignChanges) unmanagedTask.hasErrored = false
} localRepository.save(unmanagedTask)
} val savedTask = apiClient.updateTask(id, unmanagedTask)
savedTask?.position = task.position
override fun createTaskInBackground(task: Task, assignChanges: Map<String, MutableList<String>>) { savedTask?.id = task.id
MainScope().launchCatching { savedTask?.ownerID = task.ownerID
val createdTask = createTask(task) ?: return@launchCatching if (savedTask != null) {
handleAssignmentChanges(createdTask, assignChanges) savedTask.tags = task.tags
} localRepository.save(savedTask)
} } else {
unmanagedTask.hasErrored = true
private suspend fun handleAssignmentChanges(task: Task, assignChanges: Map<String, MutableList<String>>) { unmanagedTask.isSaving = false
val taskID = task.id ?: return localRepository.save(unmanagedTask)
assignChanges["assign"]?.let { assignments -> }
if (assignments.isEmpty()) return@let return savedTask
val savedTask = apiClient.assignToTask(taskID, assignments) ?: return@let }
savedTask.id = task.id
savedTask.ownerID = task.ownerID override suspend fun deleteTask(taskId: String): Void? {
savedTask.position = task.position apiClient.deleteTask(taskId) ?: return null
localRepository.save(savedTask) localRepository.deleteTask(taskId)
} return null
}
assignChanges["unassign"]?.let { unassignments ->
var savedTask: Task? = null override fun saveTask(task: Task) {
for (unassignment in unassignments) { localRepository.save(task)
savedTask = apiClient.unassignFromTask(taskID, unassignment) }
}
if (savedTask != null) { override suspend fun createTasks(newTasks: List<Task>) = apiClient.createTasks(newTasks)
savedTask.id = task.id
savedTask.position = task.position override fun markTaskCompleted(
savedTask.ownerID = task.ownerID taskId: String,
localRepository.save(savedTask) isCompleted: Boolean,
} ) {
} localRepository.markTaskCompleted(taskId, isCompleted)
} }
override fun getTaskCopies(): Flow<List<Task>> = authenticationHandler.userIDFlow.flatMapLatest { override fun <T : BaseMainObject> modify(
localRepository.getTasks(it) obj: T,
}.map { localRepository.getUnmanagedCopy(it) } transaction: (T) -> Unit,
) {
override fun getTaskCopies(tasks: List<Task>): List<Task> = localRepository.getUnmanagedCopy(tasks) localRepository.modify(obj, transaction)
}
override suspend fun retrieveDailiesFromDate(date: Date): TaskList? {
val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZZZZZ", Locale.US) override fun swapTaskPosition(
return apiClient.getTasks("dailys", formatter.format(date)) firstPosition: Int,
} secondPosition: Int,
) {
override suspend fun syncErroredTasks(): List<Task>? { localRepository.swapTaskPosition(firstPosition, secondPosition)
val tasks = localRepository.getErroredTasks(currentUserID ?: "").firstOrNull() }
return tasks?.map { localRepository.getUnmanagedCopy(it) }?.mapNotNull {
if (it.isCreating) { override suspend fun updateTaskPosition(
createTask(it, true) taskType: TaskType,
} else { taskID: String,
updateTask(it, true) newPosition: Int,
} ): List<String>? {
} val positions = apiClient.postTaskNewPosition(taskID, newPosition) ?: return null
} localRepository.updateTaskPositions(positions)
return positions
override suspend fun unlinkAllTasks(challengeID: String?, keepOption: String): Void? { }
return apiClient.unlinkAllTasks(challengeID, keepOption)
} override fun getUnmanagedTask(taskid: String) =
getTask(taskid).map { localRepository.getUnmanagedCopy(it) }
override fun getTasksForChallenge(challengeID: String?): Flow<List<Task>> {
return localRepository.getTasksForChallenge(challengeID, currentUserID ?: "") override fun updateTaskInBackground(
} task: Task,
} assignChanges: Map<String, MutableList<String>>,
) {
MainScope().launchCatching {
val updatedTask = updateTask(task) ?: return@launchCatching
handleAssignmentChanges(updatedTask, assignChanges)
}
}
override fun createTaskInBackground(
task: Task,
assignChanges: Map<String, MutableList<String>>,
) {
MainScope().launchCatching {
val createdTask = createTask(task) ?: return@launchCatching
handleAssignmentChanges(createdTask, assignChanges)
}
}
private suspend fun handleAssignmentChanges(
task: Task,
assignChanges: Map<String, MutableList<String>>,
) {
val taskID = task.id ?: return
assignChanges["assign"]?.let { assignments ->
if (assignments.isEmpty()) return@let
val savedTask = apiClient.assignToTask(taskID, assignments) ?: return@let
savedTask.id = task.id
savedTask.ownerID = task.ownerID
savedTask.position = task.position
localRepository.save(savedTask)
}
assignChanges["unassign"]?.let { unassignments ->
var savedTask: Task? = null
for (unassignment in unassignments) {
savedTask = apiClient.unassignFromTask(taskID, unassignment)
}
if (savedTask != null) {
savedTask.id = task.id
savedTask.position = task.position
savedTask.ownerID = task.ownerID
localRepository.save(savedTask)
}
}
}
override fun getTaskCopies(): Flow<List<Task>> =
authenticationHandler.userIDFlow.flatMapLatest {
localRepository.getTasks(it)
}.map { localRepository.getUnmanagedCopy(it) }
override fun getTaskCopies(tasks: List<Task>): List<Task> =
localRepository.getUnmanagedCopy(tasks)
override suspend fun retrieveDailiesFromDate(date: Date): TaskList? {
val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZZZZZ", Locale.US)
return apiClient.getTasks("dailys", formatter.format(date))
}
override suspend fun syncErroredTasks(): List<Task>? {
val tasks = localRepository.getErroredTasks(currentUserID).firstOrNull()
return tasks?.map { localRepository.getUnmanagedCopy(it) }?.mapNotNull {
if (it.isCreating) {
createTask(it, true)
} else {
updateTask(it, true)
}
}
}
override suspend fun unlinkAllTasks(
challengeID: String?,
keepOption: String,
): Void? {
return apiClient.unlinkAllTasks(challengeID, keepOption)
}
override fun getTasksForChallenge(challengeID: String?): Flow<List<Task>> {
return localRepository.getTasksForChallenge(challengeID, currentUserID)
}
}

View file

@ -1,21 +1,21 @@
package com.habitrpg.android.habitica.data.implementation package com.habitrpg.android.habitica.data.implementation
import com.habitrpg.android.habitica.data.ApiClient import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.data.TutorialRepository import com.habitrpg.android.habitica.data.TutorialRepository
import com.habitrpg.android.habitica.data.local.TutorialLocalRepository import com.habitrpg.android.habitica.data.local.TutorialLocalRepository
import com.habitrpg.android.habitica.models.TutorialStep import com.habitrpg.android.habitica.models.TutorialStep
import com.habitrpg.android.habitica.modules.AuthenticationHandler import com.habitrpg.android.habitica.modules.AuthenticationHandler
import kotlinx.coroutines.flow.Flow 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)
override fun getTutorialSteps(keys: List<String>): Flow<List<TutorialStep>> = override fun getTutorialSteps(keys: List<String>): Flow<List<TutorialStep>> =
localRepository.getTutorialSteps(keys) localRepository.getTutorialSteps(keys)
} }

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

@ -1,8 +1,12 @@
package com.habitrpg.android.habitica.data.local package com.habitrpg.android.habitica.data.local
import com.habitrpg.android.habitica.models.inventory.Customization 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

@ -1,70 +1,142 @@
package com.habitrpg.android.habitica.data.local package com.habitrpg.android.habitica.data.local
import com.habitrpg.android.habitica.models.inventory.Equipment import com.habitrpg.android.habitica.models.inventory.Equipment
import com.habitrpg.android.habitica.models.inventory.Item import com.habitrpg.android.habitica.models.inventory.Item
import com.habitrpg.android.habitica.models.inventory.Mount import com.habitrpg.android.habitica.models.inventory.Mount
import com.habitrpg.android.habitica.models.inventory.Pet import com.habitrpg.android.habitica.models.inventory.Pet
import com.habitrpg.android.habitica.models.inventory.QuestContent import com.habitrpg.android.habitica.models.inventory.QuestContent
import com.habitrpg.android.habitica.models.shops.ShopItem import com.habitrpg.android.habitica.models.shops.ShopItem
import com.habitrpg.android.habitica.models.user.Items import com.habitrpg.android.habitica.models.user.Items
import com.habitrpg.android.habitica.models.user.OwnedItem import com.habitrpg.android.habitica.models.user.OwnedItem
import com.habitrpg.android.habitica.models.user.OwnedMount import com.habitrpg.android.habitica.models.user.OwnedMount
import com.habitrpg.android.habitica.models.user.OwnedPet import com.habitrpg.android.habitica.models.user.OwnedPet
import com.habitrpg.android.habitica.models.user.User 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>>
fun getOwnedMounts(userID: String): Flow<List<OwnedMount>> fun getOwnedMounts(userID: String): Flow<List<OwnedMount>>
fun getPets(): Flow<List<Pet>> fun getPets(): Flow<List<Pet>>
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(keys: List<String>): Flow<List<QuestContent>> fun getQuestContent(key: String): Flow<QuestContent?>
fun getEquipment(searchedKeys: List<String>): Flow<List<Equipment>> fun getQuestContent(keys: List<String>): Flow<List<QuestContent>>
fun getOwnedEquipment(type: String): Flow<List<Equipment>> fun getEquipment(searchedKeys: List<String>): Flow<List<Equipment>>
fun getOwnedItems(itemType: String, userID: String, includeZero: Boolean): Flow<List<OwnedItem>> fun getOwnedEquipment(type: String): Flow<List<Equipment>>
fun getOwnedItems(userID: String, includeZero: Boolean): Flow<Map<String, OwnedItem>>
fun getEquipmentType(type: String, set: String): Flow<List<Equipment>> fun getOwnedItems(
itemType: String,
fun getEquipment(key: String): Flow<Equipment> userID: String,
fun getMounts(type: String?, group: String?, color: String?): Flow<List<Mount>> includeZero: Boolean,
fun getPets(type: String?, group: String?, color: String?): Flow<List<Pet>> ): Flow<List<OwnedItem>>
fun updateOwnedEquipment(user: User) fun getOwnedItems(
userID: String,
suspend fun changeOwnedCount(type: String, key: String, userID: String, amountToAdd: Int) includeZero: Boolean,
fun changeOwnedCount(item: OwnedItem, amountToAdd: Int?) ): Flow<Map<String, OwnedItem>>
fun getItem(type: String, key: String): Flow<Item> fun getEquipmentType(
fun getOwnedItem(userID: String, type: String, key: String, includeZero: Boolean): Flow<OwnedItem> type: String,
set: String,
fun decrementMysteryItemCount(user: User?) ): Flow<List<Equipment>>
fun saveInAppRewards(onlineItems: List<ShopItem>)
fun getEquipment(key: String): Flow<Equipment>
fun hatchPet(eggKey: String, potionKey: String, userID: String)
fun unhatchPet(eggKey: String, potionKey: String, userID: String) fun getMounts(
fun feedPet(foodKey: String, petKey: String, feedValue: Int, userID: String) type: String?,
fun getLatestMysteryItem(): Flow<Equipment> group: String?,
fun soldItem(userID: String, updatedUser: User): User color: String?,
fun getAvailableLimitedItems(): Flow<List<Item>> ): Flow<List<Mount>>
fun save(items: Items, userID: String) fun getPets(
type: String?,
fun getLiveObject(obj: OwnedItem): OwnedItem? group: String?,
fun getItems(itemClass: Class<out Item>): Flow<List<Item>> color: String?,
fun getItems(itemClass: Class<out Item>, keys: Array<String>): Flow<List<Item>> ): Flow<List<Pet>>
}
fun updateOwnedEquipment(user: User)
suspend fun changeOwnedCount(
type: String,
key: String,
userID: String,
amountToAdd: Int,
)
fun changeOwnedCount(
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 saveInAppRewards(onlineItems: List<ShopItem>)
fun hatchPet(
eggKey: 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 soldItem(
userID: String,
updatedUser: User,
): User
fun getAvailableLimitedItems(): Flow<List<Item>>
fun save(
items: Items,
userID: String,
)
fun getLiveObject(obj: OwnedItem): OwnedItem?
fun getItems(itemClass: Class<out Item>): Flow<List<Item>>
fun getItems(
itemClass: Class<out Item>,
keys: Array<String>,
): Flow<List<Item>>
}

View file

@ -1,55 +1,103 @@
package com.habitrpg.android.habitica.data.local package com.habitrpg.android.habitica.data.local
import com.habitrpg.android.habitica.models.members.Member import com.habitrpg.android.habitica.models.members.Member
import com.habitrpg.android.habitica.models.social.ChatMessage import com.habitrpg.android.habitica.models.social.ChatMessage
import com.habitrpg.android.habitica.models.social.Group import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.models.social.GroupMembership import com.habitrpg.android.habitica.models.social.GroupMembership
import com.habitrpg.android.habitica.models.social.InboxConversation import com.habitrpg.android.habitica.models.social.InboxConversation
import com.habitrpg.android.habitica.models.user.User import com.habitrpg.android.habitica.models.user.User
import io.realm.RealmResults 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,
fun getGroup(id: String): Flow<Group?> type: String?,
fun saveGroup(group: Group) ): Flow<List<Group>>
fun getGroupChat(groupId: String): Flow<List<ChatMessage>> fun getGroup(id: String): Flow<Group?>
fun deleteMessage(id: String) fun saveGroup(group: Group)
fun getPartyMembers(partyId: String): Flow<List<Member>> fun getGroupChat(groupId: String): Flow<List<ChatMessage>>
fun getGroupMembers(groupID: String): Flow<List<Member>>
fun deleteMessage(id: String)
fun updateRSVPNeeded(user: User?, newValue: Boolean)
fun getPartyMembers(partyId: String): Flow<List<Member>>
fun likeMessage(chatMessage: ChatMessage, userId: String, liked: Boolean)
fun getGroupMembers(groupID: String): Flow<List<Member>>
fun savePartyMembers(groupId: String?, members: List<Member>)
fun updateRSVPNeeded(
fun removeQuest(partyId: String) user: User?,
newValue: Boolean,
fun setQuestActivity(party: Group?, active: Boolean) )
fun saveChatMessages(groupId: String?, chatMessages: List<ChatMessage>) fun likeMessage(
chatMessage: ChatMessage,
fun doesGroupExist(id: String): Boolean userId: String,
fun updateMembership(userId: String, id: String, isMember: Boolean) liked: Boolean,
fun getGroupMembership(userId: String, id: String): Flow<GroupMembership?> )
fun getGroupMemberships(userId: String): Flow<List<GroupMembership>>
fun rejectGroupInvitation(userID: String, groupID: String) fun savePartyMembers(
groupId: String?,
fun getInboxMessages(userId: String, replyToUserID: String?): Flow<RealmResults<ChatMessage>> members: List<Member>,
)
fun getInboxConversation(userId: String): Flow<RealmResults<InboxConversation>>
fun saveGroupMemberships(userID: String?, memberships: List<GroupMembership>) fun removeQuest(partyId: String)
fun saveInboxMessages(
userID: String, fun setQuestActivity(
recipientID: String, party: Group?,
messages: List<ChatMessage>, active: Boolean,
page: Int )
)
fun saveInboxConversations(userID: String, conversations: List<InboxConversation>) fun saveChatMessages(
fun getMember(userID: String?): Flow<Member?> groupId: String?,
} chatMessages: List<ChatMessage>,
)
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 getInboxConversation(userId: String): Flow<RealmResults<InboxConversation>>
fun saveGroupMemberships(
userID: String?,
memberships: List<GroupMembership>,
)
fun saveInboxMessages(
userID: String,
recipientID: String,
messages: List<ChatMessage>,
page: Int,
)
fun saveInboxConversations(
userID: String,
conversations: List<InboxConversation>,
)
fun getMember(userID: String?): Flow<Member?>
}

View file

@ -1,35 +1,63 @@
package com.habitrpg.android.habitica.data.local package com.habitrpg.android.habitica.data.local
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.User import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.shared.habitica.models.tasks.TaskType import com.habitrpg.shared.habitica.models.tasks.TaskType
import com.habitrpg.shared.habitica.models.tasks.TasksOrder 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(
fun getTasks(taskType: TaskType, userID: String, includedGroupIDs: Array<String>): Flow<List<Task>> taskType: TaskType,
fun getTasks(userId: String): Flow<List<Task>> userID: String,
includedGroupIDs: Array<String>,
fun saveTasks(ownerID: String, tasksOrder: TasksOrder, tasks: TaskList) ): Flow<List<Task>>
fun deleteTask(taskID: String) fun getTasks(userId: String): Flow<List<Task>>
fun getTask(taskId: String): Flow<Task> fun saveTasks(
fun getTaskCopy(taskId: String): Flow<Task> ownerID: String,
tasksOrder: TasksOrder,
fun markTaskCompleted(taskId: String, isCompleted: Boolean) tasks: TaskList,
)
fun swapTaskPosition(firstPosition: Int, secondPosition: Int)
fun deleteTask(taskID: String)
fun getTaskAtPosition(taskType: String, position: Int): Flow<Task>
fun getTask(taskId: String): Flow<Task>
fun updateIsdue(daily: TaskList): TaskList
fun getTaskCopy(taskId: String): Flow<Task>
fun updateTaskPositions(taskOrder: List<String>)
fun saveCompletedTodos(userId: String, tasks: MutableCollection<Task>) fun markTaskCompleted(
fun getErroredTasks(userID: String): Flow<List<Task>> taskId: String,
fun getUser(userID: String): Flow<User> isCompleted: Boolean,
fun getTasksForChallenge(challengeID: String?, userID: String?): Flow<List<Task>> )
}
fun swapTaskPosition(
firstPosition: Int,
secondPosition: Int,
)
fun getTaskAtPosition(
taskType: String,
position: Int,
): Flow<Task>
fun updateIsdue(daily: TaskList): TaskList
fun updateTaskPositions(taskOrder: List<String>)
fun saveCompletedTodos(
userId: String,
tasks: MutableCollection<Task>,
)
fun getErroredTasks(userID: String): Flow<List<Task>>
fun getUser(userID: String): Flow<User>
fun getTasksForChallenge(
challengeID: String?,
userID: String?,
): Flow<List<Task>>
}

View file

@ -1,10 +1,10 @@
package com.habitrpg.android.habitica.data.local package com.habitrpg.android.habitica.data.local
import com.habitrpg.android.habitica.models.TutorialStep 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

@ -1,32 +1,40 @@
package com.habitrpg.android.habitica.data.local package com.habitrpg.android.habitica.data.local
import com.habitrpg.android.habitica.models.Achievement import com.habitrpg.android.habitica.models.Achievement
import com.habitrpg.android.habitica.models.QuestAchievement import com.habitrpg.android.habitica.models.QuestAchievement
import com.habitrpg.android.habitica.models.Skill import com.habitrpg.android.habitica.models.Skill
import com.habitrpg.android.habitica.models.TeamPlan import com.habitrpg.android.habitica.models.TeamPlan
import com.habitrpg.android.habitica.models.TutorialStep import com.habitrpg.android.habitica.models.TutorialStep
import com.habitrpg.android.habitica.models.social.ChatMessage import com.habitrpg.android.habitica.models.social.ChatMessage
import com.habitrpg.android.habitica.models.social.Group import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.models.user.User import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.android.habitica.models.user.UserQuestStatus import com.habitrpg.android.habitica.models.user.UserQuestStatus
import io.realm.RealmResults 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,
fun saveMessages(messages: List<ChatMessage>) overrideExisting: Boolean = true,
)
fun getSkills(user: User): Flow<List<Skill>>
fun saveMessages(messages: List<ChatMessage>)
fun getSpecialItems(user: User): Flow<List<Skill>>
fun getAchievements(): Flow<List<Achievement>> fun getSkills(user: User): Flow<List<Skill>>
fun getQuestAchievements(userID: String): Flow<List<QuestAchievement>>
fun getUserQuestStatus(userID: String): Flow<UserQuestStatus> fun getSpecialItems(user: User): Flow<List<Skill>>
fun getTeamPlans(userID: String): Flow<List<TeamPlan>>
fun getTeamPlan(teamID: String): Flow<Group?> fun getAchievements(): Flow<List<Achievement>>
}
fun getQuestAchievements(userID: String): Flow<List<QuestAchievement>>
fun getUserQuestStatus(userID: String): Flow<UserQuestStatus>
fun getTeamPlans(userID: String): Flow<List<TeamPlan>>
fun getTeamPlan(teamID: String): Flow<Group?>
}

View file

@ -1,128 +1,144 @@
package com.habitrpg.android.habitica.data.local.implementation package com.habitrpg.android.habitica.data.local.implementation
import com.habitrpg.android.habitica.data.local.BaseLocalRepository import com.habitrpg.android.habitica.data.local.BaseLocalRepository
import com.habitrpg.android.habitica.models.BaseMainObject import com.habitrpg.android.habitica.models.BaseMainObject
import com.habitrpg.android.habitica.models.BaseObject import com.habitrpg.android.habitica.models.BaseObject
import com.habitrpg.android.habitica.models.user.User import com.habitrpg.android.habitica.models.user.User
import io.realm.Realm import io.realm.Realm
import io.realm.RealmModel import io.realm.RealmModel
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.kotlin.deleteFromRealm import io.realm.kotlin.deleteFromRealm
import io.realm.kotlin.toFlow import io.realm.kotlin.toFlow
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter 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
override fun close() { override fun close() {
realm.close() realm.close()
} }
override fun executeTransaction(transaction: (Realm) -> Unit) { override fun executeTransaction(transaction: (Realm) -> Unit) {
pendingSaves.add(transaction) pendingSaves.add(transaction)
if (isSaving.compareAndSet(false, true)) { if (isSaving.compareAndSet(false, true)) {
process() process()
} }
} }
override fun <T : BaseObject> getUnmanagedCopy(managedObject: T): T { override fun <T : BaseObject> getUnmanagedCopy(managedObject: T): T {
return if (managedObject is RealmObject && managedObject.isManaged && managedObject.isValid) { return if (managedObject is RealmObject && managedObject.isManaged && managedObject.isValid) {
realm.copyFromRealm(managedObject) realm.copyFromRealm(managedObject)
} else { } else {
managedObject managedObject
} }
} }
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 realm.copyFromRealm(list) return emptyList()
} }
return realm.copyFromRealm(list)
companion object { }
private var isSaving = AtomicBoolean(false)
private var pendingSaves = mutableListOf<Any>() companion object {
} private var isSaving = AtomicBoolean(false)
private var pendingSaves = mutableListOf<Any>()
private fun <T : RealmModel> copy(realm: Realm, obj: T) { }
try {
realm.insertOrUpdate(obj) private fun <T : RealmModel> copy(
} catch (_: java.lang.IllegalArgumentException) { realm: Realm,
} obj: T,
} ) {
try {
private fun process() { realm.insertOrUpdate(obj)
if (isClosed) { return } } catch (_: java.lang.IllegalArgumentException) {
realm.executeTransaction { }
while (pendingSaves.isNotEmpty()) { }
val pending = pendingSaves.removeFirst()
@Suppress("UNCHECKED_CAST") private fun process() {
if (pending is RealmModel) { if (isClosed) {
copy(it, pending) return
} else if (pending as? List<BaseObject> != null) { }
it.insertOrUpdate(pending) realm.executeTransaction {
} else if (pending is Function0<*>) { while (pendingSaves.isNotEmpty()) {
pending.invoke() val pending = pendingSaves.removeFirst()
} else if (pending as? Function1<Realm, *> != null) { @Suppress("UNCHECKED_CAST")
pending.invoke(it) if (pending is RealmModel) {
} copy(it, pending)
} } else if (pending as? List<BaseObject> != null) {
isSaving.set(false) it.insertOrUpdate(pending)
} } else if (pending is Function0<*>) {
} pending.invoke()
} else if (pending as? Function1<Realm, *> != null) {
override fun <T : BaseObject> save(obj: T) { pending.invoke(it)
pendingSaves.add(obj) }
if (isSaving.compareAndSet(false, true)) { }
process() isSaving.set(false)
} }
} }
override fun <T : BaseObject> save(objects: List<T>) { override fun <T : BaseObject> save(obj: T) {
pendingSaves.add(objects) pendingSaves.add(obj)
if (isSaving.compareAndSet(false, true)) { if (isSaving.compareAndSet(false, true)) {
process() process()
} }
} }
override fun <T : BaseMainObject> modify(obj: T, transaction: (T) -> Unit) { override fun <T : BaseObject> save(objects: List<T>) {
if (isClosed) { return } pendingSaves.add(objects)
val liveObject = getLiveObject(obj) ?: return if (isSaving.compareAndSet(false, true)) {
executeTransaction { process()
transaction(liveObject) }
} }
}
override fun <T : BaseMainObject> modify(
override fun <T : BaseMainObject> delete(obj: T) { obj: T,
if (isClosed) { return } transaction: (T) -> Unit,
val liveObject = getLiveObject(obj) ?: return ) {
executeTransaction { if (isClosed) {
liveObject.deleteFromRealm() return
} }
} val liveObject = getLiveObject(obj) ?: return
executeTransaction {
override fun getLiveUser(id: String): User? { transaction(liveObject)
return realm.where(User::class.java).equalTo("id", id).findFirst() }
} }
override fun <T : BaseObject> getLiveObject(obj: T): T? { override fun <T : BaseMainObject> delete(obj: T) {
if (isClosed) return null if (isClosed) {
if (obj !is RealmObject || !obj.isManaged) return obj return
val baseObject = obj as? BaseMainObject ?: return null }
@Suppress("UNCHECKED_CAST") val liveObject = getLiveObject(obj) ?: return
return realm.where(baseObject.realmClass).equalTo(baseObject.primaryIdentifierName, baseObject.primaryIdentifier).findFirst() as? T executeTransaction {
} liveObject.deleteFromRealm()
}
fun queryUser(userID: String): Flow<User?> { }
return realm.where(User::class.java)
.equalTo("id", userID) override fun getLiveUser(id: String): User? {
.findAll() return realm.where(User::class.java).equalTo("id", id).findFirst()
.toFlow() }
.filter { it.isLoaded && it.isValid && !it.isEmpty() }
.map { it.firstOrNull() } override fun <T : BaseObject> getLiveObject(obj: T): T? {
} if (isClosed) return null
} if (obj !is RealmObject || !obj.isManaged) return obj
val baseObject = obj as? BaseMainObject ?: return null
@Suppress("UNCHECKED_CAST")
return realm.where(baseObject.realmClass)
.equalTo(baseObject.primaryIdentifierName, baseObject.primaryIdentifier)
.findFirst() as? T
}
fun queryUser(userID: String): Flow<User?> {
return realm.where(User::class.java)
.equalTo("id", userID)
.findAll()
.toFlow()
.filter { it.isLoaded && it.isValid && !it.isEmpty() }
.map { it.firstOrNull() }
}
}

View file

@ -15,30 +15,40 @@ 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),
ChallengeLocalRepository {
override fun isChallengeMember(
userID: String,
challengeID: String,
): Flow<Boolean> =
realm.where(ChallengeMembership::class.java)
.equalTo("userID", userID)
.equalTo("challengeID", challengeID)
.findAll()
.toFlow()
.filter { it.isLoaded }
.map { it.count() > 0 }
override fun isChallengeMember(userID: String, challengeID: String): Flow<Boolean> = realm.where(ChallengeMembership::class.java) override fun getChallengeMembership(
.equalTo("userID", userID) userId: String,
.equalTo("challengeID", challengeID) id: String,
.findAll() ) =
.toFlow() realm.where(ChallengeMembership::class.java)
.filter { it.isLoaded } .equalTo("userID", userId)
.map { it.count() > 0 } .equalTo("challengeID", id)
.findAll()
.toFlow()
.filter { it.isLoaded }
.map { it.first() }
.filterNotNull()
override fun getChallengeMembership(userId: String, id: String) = realm.where(ChallengeMembership::class.java) override fun getChallengeMemberships(userId: String) =
.equalTo("userID", userId) realm.where(ChallengeMembership::class.java)
.equalTo("challengeID", id) .equalTo("userID", userId)
.findAll() .findAll()
.toFlow() .toFlow()
.filter { it.isLoaded } .filter { it.isLoaded }
.map { it.first() }
.filterNotNull()
override fun getChallengeMemberships(userId: String) = realm.where(ChallengeMembership::class.java)
.equalTo("userID", userId)
.findAll()
.toFlow()
.filter { it.isLoaded }
override fun getChallenge(id: String): Flow<Challenge> { override fun getChallenge(id: String): Flow<Challenge> {
return realm.where(Challenge::class.java) return realm.where(Challenge::class.java)
@ -59,12 +69,13 @@ 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() =
.isNotNull("name") realm.where(Challenge::class.java)
.sort("official", Sort.DESCENDING, "createdAt", Sort.DESCENDING) .isNotNull("name")
.findAll() .sort("official", Sort.DESCENDING, "createdAt", Sort.DESCENDING)
.toFlow() .findAll()
.filter { it.isLoaded } .toFlow()
.filter { it.isLoaded }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override fun getUserChallenges(userId: String): Flow<List<Challenge>> { override fun getUserChallenges(userId: String): Flow<List<Challenge>> {
@ -74,9 +85,10 @@ class RealmChallengeLocalRepository(realm: Realm) : RealmBaseLocalRepository(rea
.toFlow() .toFlow()
.filter { it.isLoaded } .filter { it.isLoaded }
.flatMapLatest { it -> .flatMapLatest { it ->
val ids = it.map { val ids =
return@map it.challengeID it.map {
}.toTypedArray() return@map it.challengeID
}.toTypedArray()
realm.where(Challenge::class.java) realm.where(Challenge::class.java)
.isNotNull("name") .isNotNull("name")
.beginGroup() .beginGroup()
@ -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,26 +8,33 @@ 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(
.equalTo("type", type) type: String,
.equalTo("category", category) category: String?,
onlyAvailable: Boolean,
): Flow<List<Customization>> {
var query =
realm.where(Customization::class.java)
.equalTo("type", type)
.equalTo("category", category)
if (onlyAvailable) { if (onlyAvailable) {
val today = Date() val today = Date()
query = query query =
.beginGroup() query
.beginGroup() .beginGroup()
.lessThanOrEqualTo("availableFrom", today) .beginGroup()
.greaterThanOrEqualTo("availableUntil", today) .lessThanOrEqualTo("availableFrom", today)
.endGroup() .greaterThanOrEqualTo("availableUntil", today)
.or() .endGroup()
.beginGroup() .or()
.isNull("availableFrom") .beginGroup()
.isNull("availableUntil") .isNull("availableFrom")
.endGroup() .isNull("availableUntil")
.endGroup() .endGroup()
.endGroup()
} }
return query return query
.sort("customizationSet") .sort("customizationSet")

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,8 +23,9 @@ 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() =
.findAll() realm.where(FAQArticle::class.java)
.toFlow() .findAll()
.filter { it.isLoaded } .toFlow()
.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,17 +107,18 @@ 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 =
"eggs" -> it?.items?.eggs when (itemType) {
"hatchingPotions" -> it?.items?.hatchingPotions "eggs" -> it?.items?.eggs
"food" -> it?.items?.food "hatchingPotions" -> it?.items?.hatchingPotions
"quests" -> it?.items?.quests "food" -> it?.items?.food
"special" -> it?.items?.special "quests" -> it?.items?.quests
else -> emptyList() "special" -> it?.items?.special
} ?: emptyList() else -> emptyList()
} ?: emptyList()
if (includeZero) { if (includeZero) {
items items
} else { } else {
@ -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,9 +178,14 @@ 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?,
.sort("type", Sort.ASCENDING, if (color == null) "color" else "animal", Sort.ASCENDING) 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)
if (type != null) { if (type != null) {
query = query.equalTo("animal", type) query = query.equalTo("animal", type)
} }
@ -202,9 +217,14 @@ 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?,
.sort("type", Sort.ASCENDING, if (color == null) "color" else "animal", Sort.ASCENDING) 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)
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()
@ -271,7 +294,7 @@ class RealmInventoryLocalRepository(realm: Realm) :
"quests" -> it.items?.quests "quests" -> it.items?.quests
else -> emptyList() else -> emptyList()
} ?: emptyList() } ?: emptyList()
) )
items = items.filter { it.key == key } items = items.filter { it.key == key }
if (includeZero) { if (includeZero) {
items items
@ -283,15 +306,19 @@ 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,
"eggs" -> Egg::class.java key: String,
"hatchingPotions" -> HatchingPotion::class.java ): Flow<Item> {
"food" -> Food::class.java val itemClass: Class<out RealmObject> =
"quests" -> QuestContent::class.java when (type) {
"special" -> SpecialItem::class.java "eggs" -> Egg::class.java
else -> Egg::class.java "hatchingPotions" -> HatchingPotion::class.java
} "food" -> Food::class.java
"quests" -> QuestContent::class.java
"special" -> SpecialItem::class.java
else -> Egg::class.java
}
return realm.where(itemClass).equalTo("key", key) return realm.where(itemClass).equalTo("key", key)
.findAll() .findAll()
.toFlow() .toFlow()
@ -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,10 +465,14 @@ class RealmInventoryLocalRepository(realm: Realm) :
} }
} }
override fun soldItem(userID: String, updatedUser: User): User { override fun soldItem(
val user = realm.where(User::class.java) userID: String,
.equalTo("id", userID) updatedUser: User,
.findFirst() ?: return updatedUser ): User {
val user =
realm.where(User::class.java)
.equalTo("id", userID)
.findFirst() ?: return updatedUser
executeTransaction { executeTransaction {
val items = updatedUser.items val items = updatedUser.items
if (items != null) { if (items != null) {
@ -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,27 +18,39 @@ 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),
SocialLocalRepository {
override fun getGroupMembership(
userId: String,
id: String,
) =
realm.where(GroupMembership::class.java)
.equalTo("userID", userId)
.equalTo("groupID", id)
.findAll()
.toFlow()
.filter { it.isLoaded && it.isNotEmpty() }
.map { it.first() }
override fun getGroupMembership(userId: String, id: String) = realm.where(GroupMembership::class.java) override fun getGroupMemberships(userId: String): Flow<List<GroupMembership>> =
.equalTo("userID", userId) realm.where(GroupMembership::class.java)
.equalTo("groupID", id) .equalTo("userID", userId)
.findAll() .findAll()
.toFlow() .toFlow()
.filter { it.isLoaded && it.isNotEmpty() } .filter { it.isLoaded }
.map { it.first() }
override fun getGroupMemberships(userId: String): Flow<List<GroupMembership>> = realm.where(GroupMembership::class.java) override fun updateMembership(
.equalTo("userID", userId) userId: String,
.findAll() id: String,
.toFlow() isMember: Boolean,
.filter { it.isLoaded } ) {
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,19 +73,22 @@ 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 =
.equalTo("id", message.id) realm.where(ChatMessage::class.java)
.findAll() .equalTo("id", message.id)
.firstOrNull() .findAll()
.firstOrNull()
message.isSeen = existingMessage != null message.isSeen = existingMessage != null
} }
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,24 +151,28 @@ 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(
.equalTo("userID", userID) userID: String,
.findAll() type: String?,
.toFlow() ) =
.filter { it.isLoaded } realm.where(GroupMembership::class.java)
.flatMapLatest { memberships -> .equalTo("userID", userID)
realm.where(Group::class.java) .findAll()
.equalTo("type", type ?: "guild") .toFlow()
.`in`( .filter { it.isLoaded }
"id", .flatMapLatest { memberships ->
memberships.map { realm.where(Group::class.java)
return@map it.groupID .equalTo("type", type ?: "guild")
}.toTypedArray() .`in`(
) "id",
.sort("memberCount", Sort.DESCENDING) memberships.map {
.findAll() return@map it.groupID
.toFlow() }.toTypedArray(),
} )
.sort("memberCount", Sort.DESCENDING)
.findAll()
.toFlow()
}
override fun getGroup(id: String): Flow<Group?> { override fun getGroup(id: String): Flow<Group?> {
return realm.where(Group::class.java) return realm.where(Group::class.java)
@ -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) =
.equalTo("party.id", partyId) realm.where(Member::class.java)
.findAll() .equalTo("party.id", partyId)
.toFlow() .findAll()
.toFlow()
override fun getGroupMembers(groupID: String) = realm.where(GroupMembership::class.java) override fun getGroupMembers(groupID: String) =
.equalTo("groupID", groupID) realm.where(GroupMembership::class.java)
.findAll() .equalTo("groupID", groupID)
.toFlow() .findAll()
.map { memberships -> memberships.map { it.userID }.toTypedArray() } .toFlow()
.flatMapLatest { realm.where(Member::class.java).`in`("id", it).findAll().toFlow() } .map { memberships -> memberships.map { it.userID }.toTypedArray() }
.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,19 +329,24 @@ 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(
.equalTo("isInboxMessage", true) userId: String,
.equalTo("uuid", replyToUserID) replyToUserID: String?,
.equalTo("userID", userId) ) =
.sort("timestamp", Sort.DESCENDING) realm.where(ChatMessage::class.java)
.findAll() .equalTo("isInboxMessage", true)
.toFlow() .equalTo("uuid", replyToUserID)
.filter { it.isLoaded } .equalTo("userID", userId)
.sort("timestamp", Sort.DESCENDING)
.findAll()
.toFlow()
.filter { it.isLoaded }
override fun getInboxConversation(userId: String) = realm.where(InboxConversation::class.java) override fun getInboxConversation(userId: String) =
.equalTo("userID", userId) realm.where(InboxConversation::class.java)
.sort("timestamp", Sort.DESCENDING) .equalTo("userID", userId)
.findAll() .sort("timestamp", Sort.DESCENDING)
.toFlow() .findAll()
.filter { it.isLoaded } .toFlow()
.filter { it.isLoaded }
} }

View file

@ -1,254 +1,294 @@
package com.habitrpg.android.habitica.data.local.implementation package com.habitrpg.android.habitica.data.local.implementation
import com.habitrpg.android.habitica.data.local.TaskLocalRepository import com.habitrpg.android.habitica.data.local.TaskLocalRepository
import com.habitrpg.android.habitica.models.tasks.ChecklistItem import com.habitrpg.android.habitica.models.tasks.ChecklistItem
import com.habitrpg.android.habitica.models.tasks.RemindersItem import com.habitrpg.android.habitica.models.tasks.RemindersItem
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.User import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.shared.habitica.models.tasks.TaskType import com.habitrpg.shared.habitica.models.tasks.TaskType
import com.habitrpg.shared.habitica.models.tasks.TasksOrder import com.habitrpg.shared.habitica.models.tasks.TasksOrder
import io.realm.Realm import io.realm.Realm
import io.realm.RealmResults import io.realm.RealmResults
import io.realm.Sort import io.realm.Sort
import io.realm.kotlin.toFlow import io.realm.kotlin.toFlow
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter 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 {
if (realm.isClosed) return emptyFlow() override fun getTasks(
return findTasks(taskType, userID) taskType: TaskType,
.toFlow() userID: String,
.filter { it.isLoaded } includedGroupIDs: Array<String>,
} ): Flow<List<Task>> {
if (realm.isClosed) return emptyFlow()
private fun findTasks( return findTasks(taskType, userID)
taskType: TaskType, .toFlow()
ownerID: String .filter { it.isLoaded }
): RealmResults<Task> { }
return realm.where(Task::class.java)
.equalTo("typeValue", taskType.value) private fun findTasks(
.equalTo("ownerID", ownerID) taskType: TaskType,
.sort("position", Sort.ASCENDING, "dateCreated", Sort.DESCENDING) ownerID: String,
.findAll() ): RealmResults<Task> {
} return realm.where(Task::class.java)
.equalTo("typeValue", taskType.value)
override fun getTasks(userId: String): Flow<List<Task>> { .equalTo("ownerID", ownerID)
if (realm.isClosed) return emptyFlow() .sort("position", Sort.ASCENDING, "dateCreated", Sort.DESCENDING)
return realm.where(Task::class.java).equalTo("ownerID", userId) .findAll()
.sort("position", Sort.ASCENDING, "dateCreated", Sort.DESCENDING) }
.findAll()
.toFlow() override fun getTasks(userId: String): Flow<List<Task>> {
.filter { it.isLoaded } if (realm.isClosed) return emptyFlow()
} return realm.where(Task::class.java).equalTo("ownerID", userId)
.sort("position", Sort.ASCENDING, "dateCreated", Sort.DESCENDING)
override fun saveTasks(ownerID: String, tasksOrder: TasksOrder, tasks: TaskList) { .findAll()
val sortedTasks = mutableListOf<Task>() .toFlow()
sortedTasks.addAll(sortTasks(tasks.tasks, tasksOrder.habits)) .filter { it.isLoaded }
sortedTasks.addAll(sortTasks(tasks.tasks, tasksOrder.dailys)) }
sortedTasks.addAll(sortTasks(tasks.tasks, tasksOrder.todos))
sortedTasks.addAll(sortTasks(tasks.tasks, tasksOrder.rewards)) override fun saveTasks(
for (task in tasks.tasks.values) { ownerID: String,
task.position = (sortedTasks.lastOrNull { it.type == task.type }?.position ?: -1) + 1 tasksOrder: TasksOrder,
sortedTasks.add(task) tasks: TaskList,
} ) {
removeOldTasks(ownerID, sortedTasks) val sortedTasks = mutableListOf<Task>()
sortedTasks.addAll(sortTasks(tasks.tasks, tasksOrder.habits))
val allChecklistItems = ArrayList<ChecklistItem>() sortedTasks.addAll(sortTasks(tasks.tasks, tasksOrder.dailys))
val allReminders = ArrayList<RemindersItem>() sortedTasks.addAll(sortTasks(tasks.tasks, tasksOrder.todos))
sortedTasks.forEach { sortedTasks.addAll(sortTasks(tasks.tasks, tasksOrder.rewards))
if (it.ownerID.isBlank()) { for (task in tasks.tasks.values) {
it.ownerID = ownerID task.position = (sortedTasks.lastOrNull { it.type == task.type }?.position ?: -1) + 1
} sortedTasks.add(task)
it.checklist?.let { it1 -> allChecklistItems.addAll(it1) } }
it.reminders?.let { it1 -> allReminders.addAll(it1) } removeOldTasks(ownerID, sortedTasks)
}
removeOldReminders(allReminders) val allChecklistItems = ArrayList<ChecklistItem>()
removeOldChecklists(allChecklistItems) val allReminders = ArrayList<RemindersItem>()
sortedTasks.forEach {
executeTransaction { realm1 -> realm1.insertOrUpdate(sortedTasks) } if (it.ownerID.isBlank()) {
} it.ownerID = ownerID
}
override fun saveCompletedTodos(userId: String, tasks: MutableCollection<Task>) { it.checklist?.let { it1 -> allChecklistItems.addAll(it1) }
removeCompletedTodos(userId, tasks) it.reminders?.let { it1 -> allReminders.addAll(it1) }
executeTransaction { realm1 -> realm1.insertOrUpdate(tasks) } }
} removeOldReminders(allReminders)
removeOldChecklists(allChecklistItems)
private fun removeOldChecklists(onlineItems: List<ChecklistItem>) {
val localItems = realm.where(ChecklistItem::class.java).findAll().createSnapshot() executeTransaction { realm1 -> realm1.insertOrUpdate(sortedTasks) }
val itemsToDelete = localItems.filterNot { onlineItems.contains(it) } }
realm.executeTransaction {
for (item in itemsToDelete) { override fun saveCompletedTodos(
item.deleteFromRealm() userId: String,
} tasks: MutableCollection<Task>,
} ) {
} removeCompletedTodos(userId, tasks)
executeTransaction { realm1 -> realm1.insertOrUpdate(tasks) }
private fun removeOldReminders(onlineReminders: List<RemindersItem>) { }
val localReminders = realm.where(RemindersItem::class.java).findAll().createSnapshot()
val itemsToDelete = localReminders.filterNot { onlineReminders.contains(it) } private fun removeOldChecklists(onlineItems: List<ChecklistItem>) {
realm.executeTransaction { val localItems = realm.where(ChecklistItem::class.java).findAll().createSnapshot()
for (item in itemsToDelete) { val itemsToDelete = localItems.filterNot { onlineItems.contains(it) }
item.deleteFromRealm() realm.executeTransaction {
} for (item in itemsToDelete) {
} item.deleteFromRealm()
} }
}
private fun sortTasks(taskMap: MutableMap<String, Task>, taskOrder: List<String>): List<Task> { }
val taskList = ArrayList<Task>()
var position = 0 private fun removeOldReminders(onlineReminders: List<RemindersItem>) {
for (taskId in taskOrder) { val localReminders = realm.where(RemindersItem::class.java).findAll().createSnapshot()
val task = taskMap[taskId] val itemsToDelete = localReminders.filterNot { onlineReminders.contains(it) }
if (task != null) { realm.executeTransaction {
task.position = position for (item in itemsToDelete) {
taskList.add(task) item.deleteFromRealm()
position++ }
taskMap.remove(taskId) }
} }
}
return taskList private fun sortTasks(
} taskMap: MutableMap<String, Task>,
taskOrder: List<String>,
private fun removeOldTasks(ownerID: String, onlineTaskList: List<Task>) { ): List<Task> {
if (realm.isClosed) return val taskList = ArrayList<Task>()
val localTasks = realm.where(Task::class.java) var position = 0
.equalTo("ownerID", ownerID) for (taskId in taskOrder) {
.beginGroup() val task = taskMap[taskId]
.beginGroup() if (task != null) {
.equalTo("typeValue", TaskType.TODO.value) task.position = position
.equalTo("completed", false) taskList.add(task)
.endGroup() position++
.or() taskMap.remove(taskId)
.notEqualTo("typeValue", TaskType.TODO.value) }
.endGroup() }
.findAll() return taskList
.createSnapshot() }
val tasksToDelete = localTasks.filterNot { onlineTaskList.contains(it) }
executeTransaction { private fun removeOldTasks(
for (localTask in tasksToDelete) { ownerID: String,
localTask.deleteFromRealm() onlineTaskList: List<Task>,
} ) {
} if (realm.isClosed) return
} val localTasks =
realm.where(Task::class.java)
private fun removeCompletedTodos(userID: String, onlineTaskList: MutableCollection<Task>) { .equalTo("ownerID", ownerID)
val localTasks = realm.where(Task::class.java) .beginGroup()
.equalTo("ownerID", userID) .beginGroup()
.equalTo("typeValue", TaskType.TODO.value) .equalTo("typeValue", TaskType.TODO.value)
.equalTo("completed", true) .equalTo("completed", false)
.findAll() .endGroup()
.createSnapshot() .or()
val tasksToDelete = localTasks.filterNot { onlineTaskList.contains(it) } .notEqualTo("typeValue", TaskType.TODO.value)
executeTransaction { .endGroup()
for (localTask in tasksToDelete) { .findAll()
localTask.deleteFromRealm() .createSnapshot()
} val tasksToDelete = localTasks.filterNot { onlineTaskList.contains(it) }
} executeTransaction {
} for (localTask in tasksToDelete) {
localTask.deleteFromRealm()
override fun deleteTask(taskID: String) { }
val task = realm.where(Task::class.java).equalTo("id", taskID).findFirst() }
executeTransaction { }
if (task?.isManaged == true) {
task.deleteFromRealm() private fun removeCompletedTodos(
} userID: String,
} onlineTaskList: MutableCollection<Task>,
} ) {
val localTasks =
override fun getTask(taskId: String): Flow<Task> { realm.where(Task::class.java)
if (realm.isClosed) { .equalTo("ownerID", userID)
return emptyFlow() .equalTo("typeValue", TaskType.TODO.value)
} .equalTo("completed", true)
return realm.where(Task::class.java).equalTo("id", taskId).findAll().toFlow() .findAll()
.filter { realmObject -> realmObject.isLoaded && realmObject.isNotEmpty() } .createSnapshot()
.map { it.first() } val tasksToDelete = localTasks.filterNot { onlineTaskList.contains(it) }
.filterNotNull() executeTransaction {
} for (localTask in tasksToDelete) {
localTask.deleteFromRealm()
override fun getTaskCopy(taskId: String): Flow<Task> { }
return getTask(taskId) }
.map { task -> }
return@map if (task.isManaged && task.isValid) {
realm.copyFromRealm(task) override fun deleteTask(taskID: String) {
} else { val task = realm.where(Task::class.java).equalTo("id", taskID).findFirst()
task executeTransaction {
} if (task?.isManaged == true) {
} task.deleteFromRealm()
} }
}
override fun markTaskCompleted(taskId: String, isCompleted: Boolean) { }
val task = realm.where(Task::class.java).equalTo("id", taskId).findFirst()
executeTransaction { task?.completed = true } override fun getTask(taskId: String): Flow<Task> {
} if (realm.isClosed) {
return emptyFlow()
override fun swapTaskPosition(firstPosition: Int, secondPosition: Int) { }
val firstTask = realm.where(Task::class.java).equalTo("position", firstPosition).findFirst() return realm.where(Task::class.java).equalTo("id", taskId).findAll().toFlow()
val secondTask = realm.where(Task::class.java).equalTo("position", secondPosition).findFirst() .filter { realmObject -> realmObject.isLoaded && realmObject.isNotEmpty() }
if (firstTask != null && secondTask != null && firstTask.isValid && secondTask.isValid) { .map { it.first() }
executeTransaction { .filterNotNull()
firstTask.position = secondPosition }
secondTask.position = firstPosition
} override fun getTaskCopy(taskId: String): Flow<Task> {
} return getTask(taskId)
} .map { task ->
return@map if (task.isManaged && task.isValid) {
override fun getTaskAtPosition(taskType: String, position: Int): Flow<Task> { realm.copyFromRealm(task)
return realm.where(Task::class.java).equalTo("typeValue", taskType).equalTo("position", position) } else {
.findAll() task
.toFlow() }
.filter { realmObject -> realmObject.isLoaded && realmObject.isNotEmpty() } }
.map { it.first() } }
.filterNotNull()
} override fun markTaskCompleted(
taskId: String,
override fun updateIsdue(daily: TaskList): TaskList { isCompleted: Boolean,
val tasks = realm.where(Task::class.java).equalTo("typeValue", TaskType.DAILY.value).findAll() ) {
realm.beginTransaction() val task = realm.where(Task::class.java).equalTo("id", taskId).findFirst()
tasks.filter { daily.tasks.containsKey(it.id) }.forEach { it.isDue = daily.tasks[it.id]?.isDue } executeTransaction { task?.completed = true }
realm.commitTransaction() }
return daily
} override fun swapTaskPosition(
firstPosition: Int,
override fun updateTaskPositions(taskOrder: List<String>) { secondPosition: Int,
if (taskOrder.isNotEmpty()) { ) {
val tasks = realm.where(Task::class.java).`in`("id", taskOrder.toTypedArray()).findAll() val firstTask = realm.where(Task::class.java).equalTo("position", firstPosition).findFirst()
executeTransaction { _ -> val secondTask =
tasks.filter { taskOrder.contains(it.id) }.forEach { it.position = taskOrder.indexOf(it.id) } realm.where(Task::class.java).equalTo("position", secondPosition).findFirst()
} if (firstTask != null && secondTask != null && firstTask.isValid && secondTask.isValid) {
} executeTransaction {
} firstTask.position = secondPosition
secondTask.position = firstPosition
override fun getErroredTasks(userID: String): Flow<List<Task>> { }
return realm.where(Task::class.java) }
.equalTo("ownerID", userID) }
.equalTo("hasErrored", true)
.sort("position") override fun getTaskAtPosition(
.findAll() taskType: String,
.toFlow() position: Int,
.filter { it.isLoaded } ): Flow<Task> {
} return realm.where(Task::class.java).equalTo("typeValue", taskType)
.equalTo("position", position)
override fun getUser(userID: String): Flow<User> { .findAll()
return realm.where(User::class.java) .toFlow()
.equalTo("id", userID) .filter { realmObject -> realmObject.isLoaded && realmObject.isNotEmpty() }
.findAll() .map { it.first() }
.toFlow() .filterNotNull()
.filter { realmObject -> realmObject.isLoaded && realmObject.isValid && !realmObject.isEmpty() } }
.map { users -> users.first() }
.filterNotNull() override fun updateIsdue(daily: TaskList): TaskList {
} val tasks =
realm.where(Task::class.java).equalTo("typeValue", TaskType.DAILY.value).findAll()
override fun getTasksForChallenge(challengeID: String?, userID: String?): Flow<List<Task>> { realm.beginTransaction()
return realm.where(Task::class.java) tasks.filter { daily.tasks.containsKey(it.id) }
.equalTo("challengeID", challengeID) .forEach { it.isDue = daily.tasks[it.id]?.isDue }
.equalTo("ownerID", userID) realm.commitTransaction()
.findAll() return daily
.toFlow() }
.filter { it.isLoaded }
} override fun updateTaskPositions(taskOrder: List<String>) {
} if (taskOrder.isNotEmpty()) {
val tasks = realm.where(Task::class.java).`in`("id", taskOrder.toTypedArray()).findAll()
executeTransaction { _ ->
tasks.filter { taskOrder.contains(it.id) }
.forEach { it.position = taskOrder.indexOf(it.id) }
}
}
}
override fun getErroredTasks(userID: String): Flow<List<Task>> {
return realm.where(Task::class.java)
.equalTo("ownerID", userID)
.equalTo("hasErrored", true)
.sort("position")
.findAll()
.toFlow()
.filter { it.isLoaded }
}
override fun getUser(userID: String): Flow<User> {
return realm.where(User::class.java)
.equalTo("id", userID)
.findAll()
.toFlow()
.filter { realmObject -> realmObject.isLoaded && realmObject.isValid && !realmObject.isEmpty() }
.map { users -> users.first() }
.filterNotNull()
}
override fun getTasksForChallenge(
challengeID: String?,
userID: String?,
): Flow<List<Task>> {
return realm.where(Task::class.java)
.equalTo("challengeID", challengeID)
.equalTo("ownerID", userID)
.findAll()
.toFlow()
.filter { it.isLoaded }
}
}

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,11 +82,15 @@ 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 =
.equalTo("id", user.id) realm.where(User::class.java)
.findFirst() .equalTo("id", user.id)
.findFirst()
if (oldUser != null && oldUser.isValid) { if (oldUser != null && oldUser.isValid) {
if (user.needsCron && !oldUser.needsCron) { if (user.needsCron && !oldUser.needsCron) {
if (user.lastCron?.before(oldUser.lastCron) == true) { if (user.lastCron?.before(oldUser.lastCron) == true) {
@ -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 ->
res.getString(R.string.ago_1month) if (diffMonths == 1L) {
} else { res.getString(R.string.ago_1month)
res.getString(R.string.ago_months, diffMonths) } else {
} res.getString(R.string.ago_months, diffMonths)
diffWeeks != 0L -> if (diffWeeks == 1L) { }
res.getString(R.string.ago_1week)
} else { diffWeeks != 0L ->
res.getString(R.string.ago_weeks, diffWeeks) if (diffWeeks == 1L) {
} res.getString(R.string.ago_1week)
diffDays != 0L -> if (diffDays == 1L) { } else {
res.getString(R.string.ago_1day) res.getString(R.string.ago_weeks, diffWeeks)
} else { }
res.getString(R.string.ago_days, diffDays)
} diffDays != 0L ->
diffHours != 0L -> if (diffHours == 1L) { if (diffDays == 1L) {
res.getString(R.string.ago_1hour) res.getString(R.string.ago_1day)
} else { } else {
res.getString(R.string.ago_hours, diffHours) res.getString(R.string.ago_days, diffDays)
} }
diffHours != 0L ->
if (diffHours == 1L) {
res.getString(R.string.ago_1hour)
} else {
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 ->
res.getString(R.string.remaining_1month) if (diffMonths == 1L) {
} else { res.getString(R.string.remaining_1month)
res.getString(R.string.remaining_months, diffMonths) } else {
} res.getString(R.string.remaining_months, diffMonths)
diffWeeks != 0L -> if (diffWeeks == 1L) { }
res.getString(R.string.remaining_1week)
} else { diffWeeks != 0L ->
res.getString(R.string.remaining_weeks, diffWeeks) if (diffWeeks == 1L) {
} res.getString(R.string.remaining_1week)
diffDays != 0L -> if (diffDays == 1L) { } else {
res.getString(R.string.remaining_1day) res.getString(R.string.remaining_weeks, diffWeeks)
} else { }
res.getString(R.string.remaining_days, diffDays)
} diffDays != 0L ->
diffHours != 0L -> if (diffHours == 1L) { if (diffDays == 1L) {
res.getString(R.string.remaining_1hour) res.getString(R.string.remaining_1day)
} else { } else {
res.getString(R.string.remaining_hours, diffHours) res.getString(R.string.remaining_days, diffDays)
} }
diffHours != 0L ->
if (diffHours == 1L) {
res.getString(R.string.remaining_1hour)
} else {
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,11 +174,12 @@ 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 =
SimpleDateFormat("M/d/yy", locale) if (locale == Locale.US || locale == Locale.ENGLISH) {
} else { SimpleDateFormat("M/d/yy", locale)
SimpleDateFormat.getDateInstance(DateFormat.LONG, locale) } else {
} SimpleDateFormat.getDateInstance(DateFormat.LONG, locale)
}
return dateFormatter.format(this) return dateFormatter.format(this)
} }

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?,
start: Int,
count: Int,
after: Int,
) { // no-on
}
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
}
} }

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,17 +8,17 @@ 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 =
this, formatter().parseBest(
ZonedDateTime::from, this,
LocalDateTime::from ZonedDateTime::from,
) LocalDateTime::from,
)
return if (parsed is ZonedDateTime) { return if (parsed is ZonedDateTime) {
parsed parsed
} else { } else {
@ -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() {
@ -60,14 +61,14 @@ fun String.md5(): String? {
} }
class AdHandler(val activity: Activity, val type: AdType, val rewardAction: (Boolean) -> Unit) { class AdHandler(val activity: Activity, val type: AdType, val rewardAction: (Boolean) -> Unit) {
//private var rewardedAd: RewardedAd? = null // private var rewardedAd: RewardedAd? = null
companion object { companion object {
private enum class AdStatus { private enum class AdStatus {
UNINITIALIZED, UNINITIALIZED,
INITIALIZING, INITIALIZING,
READY, READY,
DISABLED DISABLED,
} }
private lateinit var sharedPreferences: SharedPreferences private lateinit var sharedPreferences: SharedPreferences
@ -100,15 +101,18 @@ 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") {
val androidId: String = val androidId: String =
Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
val deviceId: String = androidId.md5()?.uppercase() ?: "" val deviceId: String = androidId.md5()?.uppercase() ?: ""
//val configuration = RequestConfiguration.Builder().setTestDeviceIds(listOf(deviceId)).build() // val configuration = RequestConfiguration.Builder().setTestDeviceIds(listOf(deviceId)).build()
//MobileAds.setRequestConfiguration(configuration) // MobileAds.setRequestConfiguration(configuration)
} }
currentAdStatus = AdStatus.INITIALIZING currentAdStatus = AdStatus.INITIALIZING
@ -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,24 +199,27 @@ 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
} }
} }
} }
//private fun configureReward() { // private fun configureReward() {
//rewardedAd?.run { } // rewardedAd?.run { }
//} // }
private fun showRewardedAd() { private fun showRewardedAd() {
if (nextAdAllowedDate(type)?.after(Date()) == true) { if (nextAdAllowedDate(type)?.after(Date()) == true) {

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,17 +39,18 @@ 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 =
"eventAction" to eventAction, mutableMapOf<String, Any?>(
"eventCategory" to category?.key, "eventAction" to eventAction,
"hitType" to hitType?.key, "eventCategory" to category?.key,
"status" to "displayed" "hitType" to hitType?.key,
) "status" to "displayed",
)
if (additionalData != null) { if (additionalData != null) {
data.putAll(additionalData) data.putAll(additionalData)
} }
@ -74,18 +75,20 @@ object Analytics {
} }
fun initialize(context: Context) { fun initialize(context: Context) {
amplitude = Amplitude( amplitude =
Configuration( Amplitude(
context.getString(R.string.amplitude_app_id), Configuration(
context context.getString(R.string.amplitude_app_id),
context,
),
) )
)
firebase = FirebaseAnalytics.getInstance(context) firebase = FirebaseAnalytics.getInstance(context)
} }
fun identify(sharedPrefs: SharedPreferences) { fun identify(sharedPrefs: SharedPreferences) {
val identify = Identify() val identify =
.setOnce("androidStore", BuildConfig.STORE) Identify()
.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,46 +74,47 @@ 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 =
Notification.Type.ACHIEVEMENT_PARTY_UP.type -> true when (it.type) {
Notification.Type.ACHIEVEMENT_PARTY_ON.type -> true Notification.Type.ACHIEVEMENT_PARTY_UP.type -> true
Notification.Type.ACHIEVEMENT_BEAST_MASTER.type -> true Notification.Type.ACHIEVEMENT_PARTY_ON.type -> true
Notification.Type.ACHIEVEMENT_MOUNT_MASTER.type -> true Notification.Type.ACHIEVEMENT_BEAST_MASTER.type -> true
Notification.Type.ACHIEVEMENT_TRIAD_BINGO.type -> true Notification.Type.ACHIEVEMENT_MOUNT_MASTER.type -> true
Notification.Type.ACHIEVEMENT_GUILD_JOINED.type -> true Notification.Type.ACHIEVEMENT_TRIAD_BINGO.type -> true
Notification.Type.ACHIEVEMENT_CHALLENGE_JOINED.type -> true Notification.Type.ACHIEVEMENT_GUILD_JOINED.type -> true
Notification.Type.ACHIEVEMENT_INVITED_FRIEND.type -> true Notification.Type.ACHIEVEMENT_CHALLENGE_JOINED.type -> true
Notification.Type.ACHIEVEMENT_INVITED_FRIEND.type -> true
Notification.Type.ACHIEVEMENT_ALL_YOUR_BASE.type -> true Notification.Type.ACHIEVEMENT_ALL_YOUR_BASE.type -> true
Notification.Type.ACHIEVEMENT_BACK_TO_BASICS.type -> true Notification.Type.ACHIEVEMENT_BACK_TO_BASICS.type -> true
Notification.Type.ACHIEVEMENT_JUST_ADD_WATER.type -> true Notification.Type.ACHIEVEMENT_JUST_ADD_WATER.type -> true
Notification.Type.ACHIEVEMENT_LOST_MASTERCLASSER.type -> true Notification.Type.ACHIEVEMENT_LOST_MASTERCLASSER.type -> true
Notification.Type.ACHIEVEMENT_MIND_OVER_MATTER.type -> true Notification.Type.ACHIEVEMENT_MIND_OVER_MATTER.type -> true
Notification.Type.ACHIEVEMENT_DUST_DEVIL.type -> true Notification.Type.ACHIEVEMENT_DUST_DEVIL.type -> true
Notification.Type.ACHIEVEMENT_ARID_AUTHORITY.type -> true Notification.Type.ACHIEVEMENT_ARID_AUTHORITY.type -> true
Notification.Type.ACHIEVEMENT_MONSTER_MAGUS.type -> true Notification.Type.ACHIEVEMENT_MONSTER_MAGUS.type -> true
Notification.Type.ACHIEVEMENT_UNDEAD_UNDERTAKER.type -> true Notification.Type.ACHIEVEMENT_UNDEAD_UNDERTAKER.type -> true
Notification.Type.ACHIEVEMENT_PRIMED_FOR_PAINTING.type -> true Notification.Type.ACHIEVEMENT_PRIMED_FOR_PAINTING.type -> true
Notification.Type.ACHIEVEMENT_PEARLY_PRO.type -> true Notification.Type.ACHIEVEMENT_PEARLY_PRO.type -> true
Notification.Type.ACHIEVEMENT_TICKLED_PINK.type -> true Notification.Type.ACHIEVEMENT_TICKLED_PINK.type -> true
Notification.Type.ACHIEVEMENT_ROSY_OUTLOOK.type -> true Notification.Type.ACHIEVEMENT_ROSY_OUTLOOK.type -> true
Notification.Type.ACHIEVEMENT_BUG_BONANZA.type -> true Notification.Type.ACHIEVEMENT_BUG_BONANZA.type -> true
Notification.Type.ACHIEVEMENT_BARE_NECESSITIES.type -> true Notification.Type.ACHIEVEMENT_BARE_NECESSITIES.type -> true
Notification.Type.ACHIEVEMENT_FRESHWATER_FRIENDS.type -> true Notification.Type.ACHIEVEMENT_FRESHWATER_FRIENDS.type -> true
Notification.Type.ACHIEVEMENT_GOOD_AS_GOLD.type -> true Notification.Type.ACHIEVEMENT_GOOD_AS_GOLD.type -> true
Notification.Type.ACHIEVEMENT_ALL_THAT_GLITTERS.type -> true Notification.Type.ACHIEVEMENT_ALL_THAT_GLITTERS.type -> true
Notification.Type.ACHIEVEMENT_BONE_COLLECTOR.type -> true Notification.Type.ACHIEVEMENT_BONE_COLLECTOR.type -> true
Notification.Type.ACHIEVEMENT_SKELETON_CREW.type -> true Notification.Type.ACHIEVEMENT_SKELETON_CREW.type -> true
Notification.Type.ACHIEVEMENT_SEEING_RED.type -> true Notification.Type.ACHIEVEMENT_SEEING_RED.type -> true
Notification.Type.ACHIEVEMENT_RED_LETTER_DAY.type -> true Notification.Type.ACHIEVEMENT_RED_LETTER_DAY.type -> true
Notification.Type.ACHIEVEMENT_GENERIC.type -> true Notification.Type.ACHIEVEMENT_GENERIC.type -> true
Notification.Type.ACHIEVEMENT_ONBOARDING_COMPLETE.type -> true Notification.Type.ACHIEVEMENT_ONBOARDING_COMPLETE.type -> true
Notification.Type.LOGIN_INCENTIVE.type -> true Notification.Type.LOGIN_INCENTIVE.type -> true
Notification.Type.NEW_MYSTERY_ITEMS.type -> true Notification.Type.NEW_MYSTERY_ITEMS.type -> true
Notification.Type.FIRST_DROP.type -> true Notification.Type.FIRST_DROP.type -> true
else -> false else -> false
} }
if (notificationDisplayed) { if (notificationDisplayed) {
readNotification(it) readNotification(it)

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,32 +160,37 @@ class PurchaseHandler(
return return
} }
billingClientState = BillingClientState.CONNECTING billingClientState = BillingClientState.CONNECTING
billingClient.startConnection(object : BillingClientStateListener { billingClient.startConnection(
override fun onBillingSetupFinished(billingResult: BillingResult) { object : BillingClientStateListener {
when (billingResult.responseCode) { override fun onBillingSetupFinished(billingResult: BillingResult) {
BillingClient.BillingResponseCode.OK -> { when (billingResult.responseCode) {
billingClientState = BillingClientState.READY BillingClient.BillingResponseCode.OK -> {
MainScope().launchCatching { billingClientState = BillingClientState.READY
queryPurchases() MainScope().launchCatching {
queryPurchases()
}
}
BillingClient.BillingResponseCode.SERVICE_DISCONNECTED -> {
retryListening()
}
BillingClient.BillingResponseCode.SERVICE_TIMEOUT -> {
retryListening()
}
else -> {
billingClientState = BillingClientState.UNAVAILABLE
} }
} }
BillingClient.BillingResponseCode.SERVICE_DISCONNECTED -> {
retryListening()
}
BillingClient.BillingResponseCode.SERVICE_TIMEOUT -> {
retryListening()
}
else -> {
billingClientState = BillingClientState.UNAVAILABLE
}
} }
}
override fun onBillingServiceDisconnected() { override fun onBillingServiceDisconnected() {
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 =
QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS) billingClient.queryPurchasesAsync(
.build() QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS)
) .build(),
)
processPurchases(subResponse.billingResult, subResponse.purchasesList) processPurchases(subResponse.billingResult, subResponse.purchasesList)
val iapResponse = billingClient.queryPurchasesAsync( val iapResponse =
QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.INAPP) billingClient.queryPurchasesAsync(
.build() QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.INAPP)
) .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,29 +242,40 @@ 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 =
skus.map { QueryProductDetailsParams.newBuilder().setProductList(
Product.newBuilder().setProductId(it).setProductType(type).build() skus.map {
Product.newBuilder().setProductId(it).setProductType(type).build()
},
).build()
val skuDetailsResult =
withContext(Dispatchers.IO) {
billingClient.queryProductDetails(params)
} }
).build()
val skuDetailsResult = withContext(Dispatchers.IO) {
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,12 +471,13 @@ class PurchaseHandler(
} }
suspend fun checkForSubscription(): Purchase? { suspend fun checkForSubscription(): Purchase? {
val result = withContext(Dispatchers.IO) { val result =
val params = withContext(Dispatchers.IO) {
QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS) val params =
.build() QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS)
billingClient.queryPurchasesAsync(params) .build()
} billingClient.queryPurchasesAsync(params)
}
val fallback: Purchase? = null val fallback: Purchase? = null
if (result.billingResult.responseCode == BillingClient.BillingResponseCode.OK) { if (result.billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
return findMostRecentSubscription(result.purchasesList) return findMostRecentSubscription(result.purchasesList)
@ -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,52 +544,57 @@ 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.applicationContext as? HabiticaBaseApplication) ?: return@launchCatching (context as? HabiticaBaseApplication)
?: (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 =
PurchaseTypes.allSubscriptionNoRenewTypes.contains(sku) -> { when {
title = context.getString(R.string.gift_confirmation_title) PurchaseTypes.allSubscriptionNoRenewTypes.contains(sku) -> {
context.getString( title = context.getString(R.string.gift_confirmation_title)
R.string.gift_confirmation_text_sub,
giftedTo,
durationString(sku)
)
}
PurchaseTypes.allSubscriptionTypes.contains(sku) -> {
if (sku == PurchaseTypes.Subscription1Month) {
context.getString(R.string.subscription_confirmation)
} else {
context.getString( context.getString(
R.string.subscription_confirmation_multiple, R.string.gift_confirmation_text_sub,
durationString(sku) giftedTo,
durationString(sku),
) )
} }
}
PurchaseTypes.allGemTypes.contains(sku) && giftedTo != null -> { PurchaseTypes.allSubscriptionTypes.contains(sku) -> {
title = context.getString(R.string.gift_confirmation_title) if (sku == PurchaseTypes.SUBSCRIPTION_1_MONTH) {
context.getString( context.getString(R.string.subscription_confirmation)
R.string.gift_confirmation_text_gems_new, } else {
giftedTo, context.getString(
gemAmountString(sku) R.string.subscription_confirmation_multiple,
) durationString(sku),
} )
}
}
PurchaseTypes.allGemTypes.contains(sku) && giftedTo == null -> { PurchaseTypes.allGemTypes.contains(sku) && giftedTo != null -> {
context.getString(R.string.gem_purchase_confirmation, gemAmountString(sku)) title = context.getString(R.string.gift_confirmation_title)
} context.getString(
R.string.gift_confirmation_text_gems_new,
giftedTo,
gemAmountString(sku),
)
}
else -> null PurchaseTypes.allGemTypes.contains(sku) && giftedTo == null -> {
} context.getString(R.string.gem_purchase_confirmation, gemAmountString(sku))
}
else -> null
}
application.currentActivity?.get()?.let { activity -> application.currentActivity?.get()?.let { activity ->
val alert = HabiticaAlertDialog(activity) val alert = HabiticaAlertDialog(activity)
alert.setTitle(title) alert.setTitle(title)
@ -559,17 +612,19 @@ 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.applicationContext as? HabiticaBaseApplication) ?: return@launch (context as? HabiticaBaseApplication)
?: (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 =
context.getString(R.string.jubilant_gryphatrice_confirmation_gift) if (giftedTo != null) {
} else { context.getString(R.string.jubilant_gryphatrice_confirmation_gift)
context.getString(R.string.jubilant_gryphatrice_confirmation) } else {
} context.getString(R.string.jubilant_gryphatrice_confirmation)
}
application.currentActivity?.get()?.let { activity -> application.currentActivity?.get()?.let { activity ->
val alert = HabiticaAlertDialog(activity) val alert = HabiticaAlertDialog(activity)
alert.setTitle(title) alert.setTitle(title)
@ -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 Subscription3MonthNoRenew = "com.habitrpg.android.habitica.norenew_subscription.3month" const val SUBSCRIPTION_1_MONTH_NORENEW =
const val Subscription6MonthNoRenew = "com.habitrpg.android.habitica.norenew_subscription.6month" "com.habitrpg.android.habitica.norenew_subscription.1month"
const val Subscription12MonthNoRenew = "com.habitrpg.android.habitica.norenew_subscription.12month" const val SUBSCRIPTION_3_MONTH_NORENEW =
var allSubscriptionNoRenewTypes = listOf( "com.habitrpg.android.habitica.norenew_subscription.3month"
Subscription1MonthNoRenew, const val SUBSCRIPTION_6_MONTH_NORENEW =
Subscription3MonthNoRenew, "com.habitrpg.android.habitica.norenew_subscription.6month"
Subscription6MonthNoRenew, const val SUBSCRIPTION_12_MONTH_NORENEW =
Subscription12MonthNoRenew "com.habitrpg.android.habitica.norenew_subscription.12month"
) 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,10 +36,11 @@ 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 =
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) AudioAttributes.Builder()
.setLegacyStreamType(AudioManager.STREAM_MUSIC) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build() .setLegacyStreamType(AudioManager.STREAM_MUSIC)
.build()
player?.setAudioAttributes(attributes) player?.setAudioAttributes(attributes)
player?.prepare() player?.prepare()

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,64 +6,66 @@ 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>()
soundFiles.add(SoundFile(soundTheme, SoundAchievementUnlocked))
soundFiles.add(SoundFile(soundTheme, SoundChat))
soundFiles.add(SoundFile(soundTheme, SoundDaily))
soundFiles.add(SoundFile(soundTheme, SoundDeath))
soundFiles.add(SoundFile(soundTheme, SoundItemDrop))
soundFiles.add(SoundFile(soundTheme, SoundLevelUp))
soundFiles.add(SoundFile(soundTheme, SoundMinusHabit))
soundFiles.add(SoundFile(soundTheme, SoundPlusHabit))
soundFiles.add(SoundFile(soundTheme, SoundReward))
soundFiles.add(SoundFile(soundTheme, SoundTodo))
MainScope().launchCatching {
soundFileLoader.download(soundFiles)
}
}
fun loadAndPlayAudio(type: String) {
if (soundTheme == SoundThemeOff) {
return
}
if (loadedSoundFiles.containsKey(type)) {
loadedSoundFiles[type]?.play()
} else {
val soundFiles = ArrayList<SoundFile>() val soundFiles = ArrayList<SoundFile>()
soundFiles.add(SoundFile(soundTheme, SOUND_ACHIEVEMENT_UNLOCKED))
soundFiles.add(SoundFile(soundTheme, type)) soundFiles.add(SoundFile(soundTheme, SOUND_CHAT))
soundFiles.add(SoundFile(soundTheme, SOUND_DAILY))
soundFiles.add(SoundFile(soundTheme, SOUND_DEATH))
soundFiles.add(SoundFile(soundTheme, SOUND_ITEM_DROP))
soundFiles.add(SoundFile(soundTheme, SOUND_LEVEL_UP))
soundFiles.add(SoundFile(soundTheme, SOUND_MINUS_HABIT))
soundFiles.add(SoundFile(soundTheme, SOUND_PLUS_HABIT))
soundFiles.add(SoundFile(soundTheme, SOUND_REWARD))
soundFiles.add(SoundFile(soundTheme, SOUND_TODO))
MainScope().launchCatching { MainScope().launchCatching {
val newFiles = soundFileLoader.download(soundFiles) soundFileLoader.download(soundFiles)
val file = newFiles[0]
loadedSoundFiles[type] = file
file.play()
} }
} }
}
companion object { fun loadAndPlayAudio(type: String) {
const val SoundAchievementUnlocked = "Achievement_Unlocked" if (soundTheme == SOUND_THEME_OFF) {
const val SoundChat = "Chat" return
const val SoundDaily = "Daily" }
const val SoundDeath = "Death"
const val SoundItemDrop = "Item_Drop" if (loadedSoundFiles.containsKey(type)) {
const val SoundLevelUp = "Level_Up" loadedSoundFiles[type]?.play()
const val SoundMinusHabit = "Minus_Habit" } else {
const val SoundPlusHabit = "Plus_Habit" val soundFiles = ArrayList<SoundFile>()
const val SoundReward = "Reward"
const val SoundTodo = "Todo" soundFiles.add(SoundFile(soundTheme, type))
const val SoundThemeOff = "off" MainScope().launchCatching {
val newFiles = soundFileLoader.download(soundFiles)
val file = newFiles[0]
loadedSoundFiles[type] = file
file.play()
}
}
}
companion object {
const val SOUND_ACHIEVEMENT_UNLOCKED = "Achievement_Unlocked"
const val SOUND_CHAT = "Chat"
const val SOUND_DAILY = "Daily"
const val SOUND_DEATH = "Death"
const val SOUND_ITEM_DROP = "Item_Drop"
const val SOUND_LEVEL_UP = "Level_Up"
const val SOUND_MINUS_HABIT = "Minus_Habit"
const val SOUND_PLUS_HABIT = "Plus_Habit"
const val SOUND_REWARD = "Reward"
const val SOUND_TODO = "Todo"
const val SOUND_THEME_OFF = "off"
}
} }
}

View file

@ -1,287 +1,322 @@
package com.habitrpg.android.habitica.helpers package com.habitrpg.android.habitica.helpers
import android.app.AlarmManager import android.app.AlarmManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.habitrpg.android.habitica.data.TaskRepository import com.habitrpg.android.habitica.data.TaskRepository
import com.habitrpg.android.habitica.extensions.withImmutableFlag import com.habitrpg.android.habitica.extensions.withImmutableFlag
import com.habitrpg.android.habitica.models.tasks.RemindersItem import com.habitrpg.android.habitica.models.tasks.RemindersItem
import com.habitrpg.android.habitica.models.tasks.Task import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.android.habitica.modules.AuthenticationHandler import com.habitrpg.android.habitica.modules.AuthenticationHandler
import com.habitrpg.android.habitica.receivers.NotificationPublisher import com.habitrpg.android.habitica.receivers.NotificationPublisher
import com.habitrpg.android.habitica.receivers.TaskReceiver import com.habitrpg.android.habitica.receivers.TaskReceiver
import com.habitrpg.common.habitica.helpers.ExceptionHandler import com.habitrpg.common.habitica.helpers.ExceptionHandler
import com.habitrpg.shared.habitica.HLogger import com.habitrpg.shared.habitica.HLogger
import com.habitrpg.shared.habitica.LogLevel import com.habitrpg.shared.habitica.LogLevel
import com.habitrpg.shared.habitica.models.tasks.TaskType import com.habitrpg.shared.habitica.models.tasks.TaskType
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.DateTimeException import java.time.DateTimeException
import java.time.ZoneId import java.time.ZoneId
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.Calendar import java.util.Calendar
import java.util.Date 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. *
* * This method iterates through all reminders of a task and schedules multiple upcoming alarms for each.
* This method iterates through all reminders of a task and schedules multiple upcoming alarms for each. * It determines the upcoming reminder times based on the reminder's configuration (like frequency, repeat days, etc.)
* It determines the upcoming reminder times based on the reminder's configuration (like frequency, repeat days, etc.) * and schedules an alarm for each of these times.
* and schedules an alarm for each of these times. *
* * For each reminder, it updates the reminder's time to each upcoming occurrence and then calls
* For each reminder, it updates the reminder's time to each upcoming occurrence and then calls * `setAlarmForRemindersItem` to handle the actual alarm scheduling. This ensures that each reminder
* `setAlarmForRemindersItem` to handle the actual alarm scheduling. This ensures that each reminder * is scheduled accurately according to its specified rules and times.
* is scheduled accurately according to its specified rules and times. *
* * This method is particularly useful for reminders that need to be repeated at regular intervals
* This method is particularly useful for reminders that need to be repeated at regular intervals * (e.g., daily, weekly) or on specific days, as it schedules multiple occurrences in advance.
* (e.g., daily, weekly) or on specific days, as it schedules multiple occurrences in advance. *
* * @param task The task for which the alarms are being set. Each reminder in the task's reminder list
* @param task The task for which the alarms are being set. Each reminder in the task's reminder list * is processed to schedule the upcoming alarms.
* is processed to schedule the upcoming alarms. */
*/ private fun setAlarmsForTask(task: Task) {
private fun setAlarmsForTask(task: Task) { CoroutineScope(Dispatchers.IO).launch {
CoroutineScope(Dispatchers.IO).launch { val reminderOccurencesToSchedule =
val reminderOccurencesToSchedule = if (task.type == TaskType.TODO) { 1 } else { if (task.type == TaskType.TODO) {
// For dailies, we schedule multiple reminders in advance 1
upcomingReminderOccurrencesToSchedule } else {
} // For dailies, we schedule multiple reminders in advance
task.reminders?.let { reminders -> upcomingReminderOccurrencesToSchedule
for (reminder in reminders) { }
try { task.reminders?.let { reminders ->
val upcomingReminders = for (reminder in reminders) {
task.getNextReminderOccurrences(reminder, reminderOccurencesToSchedule) try {
upcomingReminders?.forEachIndexed { index, reminderNextOccurrenceTime -> val upcomingReminders =
reminder?.time = task.getNextReminderOccurrences(reminder, reminderOccurencesToSchedule)
reminderNextOccurrenceTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) upcomingReminders?.forEachIndexed { index, reminderNextOccurrenceTime ->
setAlarmForRemindersItem(task, reminder, index) reminder?.time =
} reminderNextOccurrenceTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
} catch (_: DateTimeException) { setAlarmForRemindersItem(task, reminder, index)
// code accidentally generated an invalid date }
} } catch (_: DateTimeException) {
} // code accidentally generated an invalid date
} }
} }
} }
}
}
fun removeAlarmsForTask(task: Task) {
CoroutineScope(Dispatchers.IO).launch { fun removeAlarmsForTask(task: Task) {
task.reminders?.let { reminders -> CoroutineScope(Dispatchers.IO).launch {
// Remove not only the immediate reminder, but also the next however many (upcomingReminderOccurrencesToSchedule) reminders task.reminders?.let { reminders ->
reminders.forEachIndexed { index, reminder -> // Remove not only the immediate reminder, but also the next however many (upcomingReminderOccurrencesToSchedule) reminders
removeAlarmForRemindersItem(reminder, index) reminders.forEachIndexed { index, reminder ->
} removeAlarmForRemindersItem(reminder, index)
} }
} }
} }
}
// This function is used from the TaskReceiver since we do not have access to the task
// We currently only use this function to schedule the next reminder for dailies // This function is used from the TaskReceiver since we do not have access to the task
// We may be able to use repeating alarms instead of this in the future // We currently only use this function to schedule the next reminder for dailies
fun addAlarmForTaskId(taskId: String) { // We may be able to use repeating alarms instead of this in the future
MainScope().launch(ExceptionHandler.coroutine()) { fun addAlarmForTaskId(taskId: String) {
val task = taskRepository.getTaskCopy(taskId) MainScope().launch(ExceptionHandler.coroutine()) {
.filter { task -> task.isValid && task.isManaged && TaskType.DAILY == task.type } val task =
.first() taskRepository.getTaskCopy(taskId)
setAlarmsForTask(task) .filter { task -> task.isValid && task.isManaged && TaskType.DAILY == task.type }
} .first()
} setAlarmsForTask(task)
}
suspend fun scheduleAllSavedAlarms(preventDailyReminder: Boolean) { }
val tasks = taskRepository.getTaskCopies().firstOrNull()
tasks?.forEach { this.setAlarmsForTask(it) } suspend fun scheduleAllSavedAlarms(preventDailyReminder: Boolean) {
val tasks = taskRepository.getTaskCopies().firstOrNull()
if (!preventDailyReminder) { tasks?.forEach { this.setAlarmsForTask(it) }
scheduleDailyReminder(context)
} if (!preventDailyReminder) {
} scheduleDailyReminder(context)
}
fun scheduleAlarmsForTask(task: Task) { }
setAlarmsForTask(task)
} fun scheduleAlarmsForTask(task: Task) {
setAlarmsForTask(task)
/** }
* Schedules an alarm for a given reminder associated with a task.
* /**
* This method takes a task and its associated reminder item to schedule an alarm. * Schedules an alarm for a given reminder associated with a task.
* It first checks if the reminder time is valid and not in the past. If the reminder time *
* is valid, it prepares an intent for the alarm, uniquely identified by combining the reminder's ID * This method takes a task and its associated reminder item to schedule an alarm.
* and its scheduled time. This unique identifier ensures that each reminder occurrence is distinctly handled. * It first checks if the reminder time is valid and not in the past. If the reminder time
* * is valid, it prepares an intent for the alarm, uniquely identified by combining the reminder's ID
* If an alarm with the same identifier already exists, it is cancelled and replaced with the new one. * and its scheduled time. This unique identifier ensures that each reminder occurrence is distinctly handled.
* This ensures that reminders are always up to date with their latest scheduled times. *
* * If an alarm with the same identifier already exists, it is cancelled and replaced with the new one.
* The alarm is scheduled to trigger at the exact time specified in the reminder. Upon triggering, * This ensures that reminders are always up to date with their latest scheduled times.
* it will send a broadcast to `TaskReceiver`, which should handle the reminder notification. *
* * The alarm is scheduled to trigger at the exact time specified in the reminder. Upon triggering,
* @param reminderItemTask The task associated with the reminder. * it will send a broadcast to `TaskReceiver`, which should handle the reminder notification.
* @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. * @param reminderItemTask The task associated with the reminder.
*/ * @param remindersItem The reminder item containing details like ID and the time for the reminder.
private fun setAlarmForRemindersItem(reminderItemTask: Task, remindersItem: RemindersItem?, occurrenceIndex: Int) { * If this is null, the method returns immediately without scheduling an alarm.
if (remindersItem == null) return */
private fun setAlarmForRemindersItem(
val now = ZonedDateTime.now().withZoneSameLocal(ZoneId.systemDefault())?.toInstant() reminderItemTask: Task,
val reminderZonedTime = remindersItem.getLocalZonedDateTimeInstant() remindersItem: RemindersItem?,
occurrenceIndex: Int,
if (reminderZonedTime == null || reminderZonedTime.isBefore(now)) { ) {
return if (remindersItem == null) return
}
val now = ZonedDateTime.now().withZoneSameLocal(ZoneId.systemDefault())?.toInstant()
val reminderZonedTime = remindersItem.getLocalZonedDateTimeInstant()
val intent = Intent(context, TaskReceiver::class.java)
intent.action = remindersItem.id if (reminderZonedTime == null || reminderZonedTime.isBefore(now)) {
intent.putExtra(TASK_NAME_INTENT_KEY, reminderItemTask.text) return
intent.putExtra(TASK_ID_INTENT_KEY, reminderItemTask.id) }
// Create a unique identifier based on remindersItem.id and the occurrence index val intent = Intent(context, TaskReceiver::class.java)
val intentId = (remindersItem.id?.hashCode() ?: 0) + occurrenceIndex intent.action = remindersItem.id
intent.putExtra(TASK_NAME_INTENT_KEY, reminderItemTask.text)
// Cancel alarm if already exists intent.putExtra(TASK_ID_INTENT_KEY, reminderItemTask.id)
val previousSender = PendingIntent.getBroadcast(
context, // Create a unique identifier based on remindersItem.id and the occurrence index
intentId, val intentId = (remindersItem.id?.hashCode() ?: 0) + occurrenceIndex
intent,
withImmutableFlag(PendingIntent.FLAG_NO_CREATE) // Cancel alarm if already exists
) val previousSender =
if (previousSender != null) { PendingIntent.getBroadcast(
previousSender.cancel() context,
am?.cancel(previousSender) intentId,
} intent,
withImmutableFlag(PendingIntent.FLAG_NO_CREATE),
val sender = PendingIntent.getBroadcast( )
context, if (previousSender != null) {
intentId, previousSender.cancel()
intent, am?.cancel(previousSender)
withImmutableFlag(PendingIntent.FLAG_CANCEL_CURRENT) }
)
val sender =
PendingIntent.getBroadcast(
CoroutineScope(Dispatchers.IO).launch { context,
setAlarm(context, reminderZonedTime.toEpochMilli(), sender) intentId,
} intent,
} withImmutableFlag(PendingIntent.FLAG_CANCEL_CURRENT),
)
private fun removeAlarmForRemindersItem(remindersItem: RemindersItem, occurrenceIndex: Int? = null) {
val intent = Intent(context, TaskReceiver::class.java) CoroutineScope(Dispatchers.IO).launch {
intent.action = remindersItem.id setAlarm(context, reminderZonedTime.toEpochMilli(), sender)
val intentId = if (occurrenceIndex != null) (remindersItem.id?.hashCode() ?: (0 and 0xfffffff)) + occurrenceIndex else (remindersItem.id?.hashCode() ?: (0 and 0xfffffff)) }
val sender = PendingIntent.getBroadcast( }
context,
intentId, private fun removeAlarmForRemindersItem(
intent, remindersItem: RemindersItem,
withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT) occurrenceIndex: Int? = null,
) ) {
val am = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager val intent = Intent(context, TaskReceiver::class.java)
sender.cancel() intent.action = remindersItem.id
am?.cancel(sender) val intentId =
} if (occurrenceIndex != null) {
(
companion object { remindersItem.id?.hashCode()
const val TASK_ID_INTENT_KEY = "TASK_ID" ?: (0 and 0xfffffff)
const val TASK_NAME_INTENT_KEY = "TASK_NAME" ) + occurrenceIndex
} else {
fun scheduleDailyReminder(context: Context?) { (
if (context == null) return remindersItem.id?.hashCode()
val prefs = PreferenceManager.getDefaultSharedPreferences(context) ?: (0 and 0xfffffff)
if (prefs.getBoolean("use_reminder", false)) { )
val timeval = prefs.getString("reminder_time", "19:00") }
val sender =
val pieces = PendingIntent.getBroadcast(
timeval?.split(":".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray() context,
?: return intentId,
val hour = Integer.parseInt(pieces[0]) intent,
val minute = Integer.parseInt(pieces[1]) withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT),
val cal = Calendar.getInstance() )
cal.set(Calendar.HOUR_OF_DAY, hour) val am = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
cal.set(Calendar.MINUTE, minute) sender.cancel()
cal.set(Calendar.SECOND, 0) am?.cancel(sender)
if (cal.timeInMillis < Date().time) { }
cal.set(Calendar.DAY_OF_YEAR, cal.get(Calendar.DAY_OF_YEAR) + 1)
} companion object {
val triggerTime = cal.timeInMillis const val TASK_ID_INTENT_KEY = "TASK_ID"
const val TASK_NAME_INTENT_KEY = "TASK_NAME"
val notificationIntent = Intent(context, NotificationPublisher::class.java)
notificationIntent.putExtra(NotificationPublisher.NOTIFICATION_ID, 1) fun scheduleDailyReminder(context: Context?) {
notificationIntent.putExtra(NotificationPublisher.CHECK_DAILIES, false) if (context == null) return
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager if (prefs.getBoolean("use_reminder", false)) {
val previousSender = PendingIntent.getBroadcast( val timeval = prefs.getString("reminder_time", "19:00")
context,
0, val pieces =
notificationIntent, timeval?.split(":".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray()
withImmutableFlag(PendingIntent.FLAG_NO_CREATE) ?: return
) val hour = Integer.parseInt(pieces[0])
if (previousSender != null) { val minute = Integer.parseInt(pieces[1])
previousSender.cancel() val cal = Calendar.getInstance()
alarmManager?.cancel(previousSender) cal.set(Calendar.HOUR_OF_DAY, hour)
} cal.set(Calendar.MINUTE, minute)
cal.set(Calendar.SECOND, 0)
val pendingIntent = PendingIntent.getBroadcast( if (cal.timeInMillis < Date().time) {
context, cal.set(Calendar.DAY_OF_YEAR, cal.get(Calendar.DAY_OF_YEAR) + 1)
0, }
notificationIntent, val triggerTime = cal.timeInMillis
withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT)
) val notificationIntent = Intent(context, NotificationPublisher::class.java)
notificationIntent.putExtra(NotificationPublisher.NOTIFICATION_ID, 1)
setAlarm(context, triggerTime, pendingIntent) notificationIntent.putExtra(NotificationPublisher.CHECK_DAILIES, false)
}
} val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
val previousSender =
fun removeDailyReminder(context: Context?) { PendingIntent.getBroadcast(
val notificationIntent = Intent(context, NotificationPublisher::class.java) context,
val alarmManager = context?.getSystemService(Context.ALARM_SERVICE) as? AlarmManager 0,
val displayIntent = notificationIntent,
PendingIntent.getBroadcast(context, 0, notificationIntent, withImmutableFlag(0)) withImmutableFlag(PendingIntent.FLAG_NO_CREATE),
alarmManager?.cancel(displayIntent) )
} if (previousSender != null) {
previousSender.cancel()
private fun setAlarm(context: Context, time: Long, pendingIntent: PendingIntent?) { alarmManager?.cancel(previousSender)
HLogger.log(LogLevel.INFO, "TaskAlarmManager", "Scheduling for $time") }
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
val pendingIntent =
if (pendingIntent == null) { PendingIntent.getBroadcast(
return context,
} 0,
notificationIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT),
// For SDK >= Android 12, allows batching of reminders )
try {
alarmManager?.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pendingIntent) setAlarm(context, triggerTime, pendingIntent)
Log.d("TaskAlarmManager", "setAlarm: Scheduling for $time using setAndAllowWhileIdle") }
} catch (ex: Exception) { }
when (ex) {
is IllegalStateException, is SecurityException -> { fun removeDailyReminder(context: Context?) {
alarmManager?.setWindow(AlarmManager.RTC_WAKEUP, time, 600000, pendingIntent) val notificationIntent = Intent(context, NotificationPublisher::class.java)
} val alarmManager = context?.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
else -> { val displayIntent =
throw ex PendingIntent.getBroadcast(context, 0, notificationIntent, withImmutableFlag(0))
} alarmManager?.cancel(displayIntent)
}
}
} private fun setAlarm(
} else { context: Context,
alarmManager?.setWindow(AlarmManager.RTC_WAKEUP, time, 600000, pendingIntent) time: Long,
} pendingIntent: PendingIntent?,
} ) {
} HLogger.log(LogLevel.INFO, "TaskAlarmManager", "Scheduling for $time")
} val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
if (pendingIntent == null) {
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// For SDK >= Android 12, allows batching of reminders
try {
alarmManager?.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pendingIntent)
Log.d(
"TaskAlarmManager",
"setAlarm: Scheduling for $time using setAndAllowWhileIdle",
)
} catch (ex: Exception) {
when (ex) {
is IllegalStateException, is SecurityException -> {
alarmManager?.setWindow(
AlarmManager.RTC_WAKEUP,
time,
600000,
pendingIntent,
)
}
else -> {
throw ex
}
}
}
} else {
alarmManager?.setWindow(AlarmManager.RTC_WAKEUP, time, 600000, pendingIntent)
}
}
}
}

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 ->
R.string.habit_summary_description, context.getString(
describeHabitDirections(task.up ?: false, task.down ?: false), R.string.habit_summary_description,
describeDifficulty(task.priority) describeHabitDirections(task.up ?: false, task.down ?: false),
) 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(
R.string.daily_summary_description, TaskType.DAILY ->
describeDifficulty(task.priority), context.getString(
describeRepeatInterval(task.frequency, task.everyX ?: 1), R.string.daily_summary_description,
describeRepeatDays(task) describeDifficulty(task.priority),
) describeRepeatInterval(task.frequency, task.everyX ?: 1),
describeRepeatDays(task),
)
else -> "" else -> ""
} }
} }
@ -59,48 +63,61 @@ class TaskDescriptionBuilder(private val context: Context) {
} }
return when (task.frequency) { return when (task.frequency) {
Frequency.WEEKLY -> { Frequency.WEEKLY -> {
" " + if (task.repeat?.isEveryDay == true) { " " +
context.getString(R.string.on_every_day_of_week) if (task.repeat?.isEveryDay == true) {
} else { context.getString(R.string.on_every_day_of_week)
if (task.repeat?.isOnlyWeekdays == true) {
context.getString(R.string.on_weekdays)
} else if (task.repeat?.isOnlyWeekends == true) {
context.getString(R.string.on_weekends)
} else { } else {
val dayStrings = task.repeat?.dayStrings(context) ?: listOf() if (task.repeat?.isOnlyWeekdays == true) {
joinToCount(dayStrings) context.getString(R.string.on_weekdays)
} else if (task.repeat?.isOnlyWeekends == true) {
context.getString(R.string.on_weekends)
} else {
val dayStrings = task.repeat?.dayStrings(context) ?: listOf()
joinToCount(dayStrings)
}
} }
}
} }
Frequency.MONTHLY -> {
" " + if (task.getDaysOfMonth()?.isNotEmpty() == true) {
val dayList = task.getDaysOfMonth()?.map {
withOrdinal(it)
}
context.getString(R.string.on_the_x, joinToCount(dayList))
} else if (task.getWeeksOfMonth()?.isNotEmpty() == true) {
val occurrence = when (task.getWeeksOfMonth()?.first()) {
0 -> context.getString(R.string.first)
1 -> context.getString(R.string.second)
2 -> context.getString(R.string.third)
3 -> context.getString(R.string.fourth)
4 -> context.getString(R.string.fifth)
else -> return ""
}
val dayStrings = task.repeat?.dayStrings(context) ?: listOf()
context.getString(R.string.on_the_x_of_month, occurrence, joinToCount(dayStrings)) Frequency.MONTHLY -> {
} else { " " +
"" if (task.getDaysOfMonth()?.isNotEmpty() == true) {
} val dayList =
task.getDaysOfMonth()?.map {
withOrdinal(it)
}
context.getString(R.string.on_the_x, joinToCount(dayList))
} else if (task.getWeeksOfMonth()?.isNotEmpty() == true) {
val occurrence =
when (task.getWeeksOfMonth()?.first()) {
0 -> context.getString(R.string.first)
1 -> context.getString(R.string.second)
2 -> context.getString(R.string.third)
3 -> context.getString(R.string.fourth)
4 -> context.getString(R.string.fifth)
else -> return ""
}
val dayStrings = task.repeat?.dayStrings(context) ?: listOf()
context.getString(
R.string.on_the_x_of_month,
occurrence,
joinToCount(dayStrings),
)
} else {
""
}
} }
Frequency.YEARLY -> " " + context.getString(
R.string.on_x, Frequency.YEARLY ->
task.startDate?.let { " " +
val flags = DateUtils.FORMAT_SHOW_DATE + DateUtils.FORMAT_NO_YEAR context.getString(
DateUtils.formatDateTime(context, it.time, flags) R.string.on_x,
} ?: "" task.startDate?.let {
) val flags = DateUtils.FORMAT_SHOW_DATE + DateUtils.FORMAT_NO_YEAR
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,
R.plurals.repeat_monthly, everyX,
everyX, everyX,
everyX )
)
Frequency.YEARLY -> context.resources.getQuantityString(R.plurals.repeat_yearly, everyX, everyX) Frequency.WEEKLY ->
context.resources.getQuantityString(
R.plurals.repeat_weekly,
everyX,
everyX,
)
Frequency.MONTHLY ->
context.resources.getQuantityString(
R.plurals.repeat_monthly,
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

@ -1,142 +1,149 @@
package com.habitrpg.android.habitica.helpers package com.habitrpg.android.habitica.helpers
import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.models.inventory.Equipment import com.habitrpg.android.habitica.models.inventory.Equipment
import com.habitrpg.android.habitica.models.user.Stats 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 { var labelId: Int = 0
var labelId: Int = 0 var strVal: Float = 0.toFloat()
var strVal: Float = 0.toFloat() var intVal: Float = 0.toFloat()
var intVal: Float = 0.toFloat() var conVal: Float = 0.toFloat()
var conVal: Float = 0.toFloat() var perVal: Float = 0.toFloat()
var perVal: Float = 0.toFloat() var roundDown: Boolean = false
var roundDown: Boolean = false var summary: Boolean = false
var summary: Boolean = false }
}
inner class EquipmentRow : StatsRow {
inner class EquipmentRow : StatsRow { var gearKey: String? = null
var gearKey: String? = null var text: String? = null
var text: String? = null var stats: String? = null
var stats: String? = null }
}
fun computeClassBonus(
fun computeClassBonus(equipmentList: List<Equipment>?, user: Avatar): List<StatsRow> { equipmentList: List<Equipment>?,
val skillRows = ArrayList<StatsRow>() user: Avatar,
): List<StatsRow> {
var strAttributes = 0f val skillRows = ArrayList<StatsRow>()
var intAttributes = 0f
var conAttributes = 0f var strAttributes = 0f
var perAttributes = 0f var intAttributes = 0f
var conAttributes = 0f
var strClassBonus = 0f var perAttributes = 0f
var intClassBonus = 0f
var conClassBonus = 0f var strClassBonus = 0f
var perClassBonus = 0f var intClassBonus = 0f
var conClassBonus = 0f
// Summarize stats and fill equipment table var perClassBonus = 0f
for (i in equipmentList ?: emptyList()) {
val strength = i.str // Summarize stats and fill equipment table
val intelligence = i._int for (i in equipmentList ?: emptyList()) {
val constitution = i.con val strength = i.str
val perception = i.per val intelligence = i.intelligence
val constitution = i.con
strAttributes += strength.toFloat() val perception = i.per
intAttributes += intelligence.toFloat()
conAttributes += constitution.toFloat() strAttributes += strength.toFloat()
perAttributes += perception.toFloat() intAttributes += intelligence.toFloat()
conAttributes += constitution.toFloat()
val sb = StringBuilder() perAttributes += perception.toFloat()
if (strength != 0) { val sb = StringBuilder()
sb.append("STR ").append(strength).append(", ")
} if (strength != 0) {
if (intelligence != 0) { sb.append("STR ").append(strength).append(", ")
sb.append("INT ").append(intelligence).append(", ") }
} if (intelligence != 0) {
if (constitution != 0) { sb.append("INT ").append(intelligence).append(", ")
sb.append("CON ").append(constitution).append(", ") }
} if (constitution != 0) {
if (perception != 0) { sb.append("CON ").append(constitution).append(", ")
sb.append("PER ").append(perception).append(", ") }
} if (perception != 0) {
sb.append("PER ").append(perception).append(", ")
// remove the last comma }
if (sb.length > 2) {
sb.delete(sb.length - 2, sb.length) // remove the last comma
} if (sb.length > 2) {
sb.delete(sb.length - 2, sb.length)
val equipmentRow = EquipmentRow() }
equipmentRow.gearKey = i.key
equipmentRow.text = i.text val equipmentRow = EquipmentRow()
equipmentRow.stats = sb.toString() equipmentRow.gearKey = i.key
skillRows.add(equipmentRow) equipmentRow.text = i.text
equipmentRow.stats = sb.toString()
// Calculate class bonus skillRows.add(equipmentRow)
var itemClass: String? = i.klass
val itemSpecialClass = i.specialClass // Calculate class bonus
var itemClass: String? = i.klass
val classDoesNotExist = itemClass.isNullOrEmpty() val itemSpecialClass = i.specialClass
val specialClassDoesNotExist = itemSpecialClass.isEmpty()
val classDoesNotExist = itemClass.isNullOrEmpty()
if (classDoesNotExist && specialClassDoesNotExist) { val specialClassDoesNotExist = itemSpecialClass.isEmpty()
continue
} if (classDoesNotExist && specialClassDoesNotExist) {
continue
var classBonus = 0.5f }
val userClassMatchesGearClass = !classDoesNotExist && itemClass == user.stats?.habitClass
val userClassMatchesGearSpecialClass = !specialClassDoesNotExist && itemSpecialClass == user.stats?.habitClass var classBonus = 0.5f
val userClassMatchesGearClass =
if (!userClassMatchesGearClass && !userClassMatchesGearSpecialClass) classBonus = 0f !classDoesNotExist && itemClass == user.stats?.habitClass
val userClassMatchesGearSpecialClass =
if (itemClass.isNullOrEmpty() || itemClass == "special") { !specialClassDoesNotExist && itemSpecialClass == user.stats?.habitClass
itemClass = itemSpecialClass
} if (!userClassMatchesGearClass && !userClassMatchesGearSpecialClass) classBonus = 0f
when (itemClass) { if (itemClass.isNullOrEmpty() || itemClass == "special") {
Stats.ROGUE -> { itemClass = itemSpecialClass
strClassBonus += strength * classBonus }
perClassBonus += perception * classBonus
} when (itemClass) {
Stats.HEALER -> { Stats.ROGUE -> {
conClassBonus += constitution * classBonus strClassBonus += strength * classBonus
intClassBonus += intelligence * classBonus perClassBonus += perception * classBonus
} }
Stats.WARRIOR -> {
strClassBonus += strength * classBonus Stats.HEALER -> {
conClassBonus += constitution * classBonus conClassBonus += constitution * classBonus
} intClassBonus += intelligence * classBonus
Stats.MAGE -> { }
intClassBonus += intelligence * classBonus
perClassBonus += perception * classBonus Stats.WARRIOR -> {
} strClassBonus += strength * classBonus
} conClassBonus += constitution * classBonus
} }
val attributeRow = AttributeRow() Stats.MAGE -> {
attributeRow.labelId = R.string.battle_gear intClassBonus += intelligence * classBonus
attributeRow.strVal = strAttributes perClassBonus += perception * classBonus
attributeRow.intVal = intAttributes }
attributeRow.conVal = conAttributes }
attributeRow.perVal = perAttributes }
attributeRow.roundDown = true
attributeRow.summary = false val attributeRow = AttributeRow()
skillRows.add(attributeRow) attributeRow.labelId = R.string.battle_gear
attributeRow.strVal = strAttributes
val attributeRow2 = AttributeRow() attributeRow.intVal = intAttributes
attributeRow2.labelId = R.string.profile_class_bonus attributeRow.conVal = conAttributes
attributeRow2.strVal = strClassBonus attributeRow.perVal = perAttributes
attributeRow2.intVal = intClassBonus attributeRow.roundDown = true
attributeRow2.conVal = conClassBonus attributeRow.summary = false
attributeRow2.perVal = perClassBonus skillRows.add(attributeRow)
attributeRow2.roundDown = false
attributeRow2.summary = false val attributeRow2 = AttributeRow()
skillRows.add(attributeRow2) attributeRow2.labelId = R.string.profile_class_bonus
attributeRow2.strVal = strClassBonus
return skillRows attributeRow2.intVal = intClassBonus
} attributeRow2.conVal = conClassBonus
} attributeRow2.perVal = perClassBonus
attributeRow2.roundDown = false
attributeRow2.summary = false
skillRows.add(attributeRow2)
return skillRows
}
}

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,12 +4,13 @@ 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 =
.setBigContentTitle(title) NotificationCompat.BigTextStyle()
.bigText(message) .setBigContentTitle(title)
.bigText(message)
return super.configureNotificationBuilder(data) return super.configureNotificationBuilder(data)
.setStyle(style) .setStyle(style)
} }

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 =
.setGroupConversation(true) NotificationCompat.MessagingStyle(user)
.setConversationTitle(data["groupName"]) .setGroupConversation(true)
.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
notificationManager?.activeNotifications?.filter { it.id == getNotificationID(data) } val existingNotifications =
} else { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
null notificationManager?.activeNotifications?.filter { it.id == getNotificationID(data) }
} } else {
val oldMessages = existingNotifications?.firstOrNull()?.notification?.extras?.getBundle("messages")?.get("messages") as? ArrayList<Map<String, String>> ?: arrayListOf() null
}
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,20 +62,24 @@ 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 =
setLabel(replyLabel) RemoteInput.Builder(actionName).run {
build() setLabel(replyLabel)
} build()
}
val intent = Intent(context, LocalNotificationActionReceiver::class.java) val intent = Intent(context, LocalNotificationActionReceiver::class.java)
intent.action = actionName intent.action = actionName
intent.putExtra("groupID", groupID) intent.putExtra("groupID", groupID)
@ -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,24 +29,26 @@ 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 =
context, PendingIntent.getBroadcast(
groupID.hashCode(), context,
acceptInviteIntent, groupID.hashCode(),
withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT) acceptInviteIntent,
) withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT),
)
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_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 =
context, PendingIntent.getBroadcast(
groupID.hashCode() + 1, context,
rejectInviteIntent, groupID.hashCode() + 1,
withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT) rejectInviteIntent,
) withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT),
)
notificationBuilder.addAction(0, "Reject", pendingIntentReject) notificationBuilder.addAction(0, "Reject", pendingIntentReject)
} }
} }

View file

@ -1,28 +1,27 @@
package com.habitrpg.android.habitica.helpers.notifications package com.habitrpg.android.habitica.helpers.notifications
import com.google.firebase.messaging.FirebaseMessaging import com.google.firebase.messaging.FirebaseMessaging
import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage import com.google.firebase.messaging.RemoteMessage
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class HabiticaFirebaseMessagingService : FirebaseMessagingService() { class HabiticaFirebaseMessagingService : FirebaseMessagingService() {
@Inject
@Inject internal lateinit var pushNotificationManager: PushNotificationManager
internal lateinit var pushNotificationManager: PushNotificationManager
override fun onMessageReceived(remoteMessage: RemoteMessage) {
override fun onMessageReceived(remoteMessage: RemoteMessage) { PushNotificationManager.displayNotification(remoteMessage, applicationContext)
PushNotificationManager.displayNotification(remoteMessage, applicationContext) }
}
override fun onNewToken(s: String) {
override fun onNewToken(s: String) { super.onNewToken(s)
super.onNewToken(s) FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
FirebaseMessaging.getInstance().token.addOnCompleteListener { task -> val refreshedToken = task.result
val refreshedToken = task.result if (refreshedToken != null && this::pushNotificationManager.isInitialized) {
if (refreshedToken != null && this::pushNotificationManager.isInitialized) { pushNotificationManager.refreshedToken = refreshedToken
pushNotificationManager.refreshedToken = refreshedToken }
} }
} }
} }
}

View file

@ -18,16 +18,16 @@ 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 =
.setSmallIcon(R.drawable.ic_gryphon_white) NotificationCompat.Builder(context, "default")
.setAutoCancel(true) .setSmallIcon(R.drawable.ic_gryphon_white)
.setAutoCancel(true)
open fun configureNotificationBuilder(data: MutableMap<String, String>): NotificationCompat.Builder { open fun configureNotificationBuilder(data: MutableMap<String, String>): NotificationCompat.Builder {
val path = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) val path = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
@ -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,16 +69,20 @@ 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 =
context, PendingIntent.getActivity(
3000, context,
intent, 3000,
withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT) intent,
) 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,24 +24,26 @@ 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 =
context, PendingIntent.getBroadcast(
groupID.hashCode(), context,
acceptInviteIntent, groupID.hashCode(),
withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT) acceptInviteIntent,
) withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT),
)
notificationBuilder.addAction(0, context.getString(R.string.accept), pendingIntentAccept) notificationBuilder.addAction(0, context.getString(R.string.accept), pendingIntentAccept)
val rejectInviteIntent = Intent(context, LocalNotificationActionReceiver::class.java) val rejectInviteIntent = Intent(context, LocalNotificationActionReceiver::class.java)
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 =
context, PendingIntent.getBroadcast(
groupID.hashCode() + 1, context,
rejectInviteIntent, groupID.hashCode() + 1,
withImmutableFlag(PendingIntent.FLAG_UPDATE_CURRENT) rejectInviteIntent,
) 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

@ -1,174 +1,179 @@
package com.habitrpg.android.habitica.helpers.notifications package com.habitrpg.android.habitica.helpers.notifications
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.edit import androidx.core.content.edit
import com.google.firebase.messaging.FirebaseMessaging import com.google.firebase.messaging.FirebaseMessaging
import com.google.firebase.messaging.RemoteMessage import com.google.firebase.messaging.RemoteMessage
import com.habitrpg.android.habitica.data.ApiClient import com.habitrpg.android.habitica.data.ApiClient
import com.habitrpg.android.habitica.helpers.Analytics import com.habitrpg.android.habitica.helpers.Analytics
import com.habitrpg.android.habitica.helpers.EventCategory import com.habitrpg.android.habitica.helpers.EventCategory
import com.habitrpg.android.habitica.helpers.HitType import com.habitrpg.android.habitica.helpers.HitType
import com.habitrpg.android.habitica.models.user.User import com.habitrpg.android.habitica.models.user.User
import com.habitrpg.common.habitica.helpers.launchCatching import com.habitrpg.common.habitica.helpers.launchCatching
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import java.io.IOException 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()) { return
return }
}
field = value
field = value sharedPreferences.edit {
sharedPreferences.edit { putString(DEVICE_TOKEN_PREFERENCE_KEY, value)
putString(DEVICE_TOKEN_PREFERENCE_KEY, value) }
} }
} private var user: User? = null
private var user: User? = null
fun setUser(user: User) {
fun setUser(user: User) { this.user = user
this.user = user }
}
/**
/** * New installs on Android 13 require
* New installs on Android 13 require * Notification permissions be approved.
* Notification permissions be approved. * Devices on Android 12L or lower with previously
* Devices on Android 12L or lower with previously * allowed notification permissions that update to 13
* allowed notification permissions that update to 13 * will have notification permissions enabled by default.
* will have notification permissions enabled by default. */
*/ fun notificationPermissionEnabled(): Boolean {
fun notificationPermissionEnabled(): Boolean { val notificationManager = NotificationManagerCompat.from(context)
val notificationManager = NotificationManagerCompat.from(context) return notificationManager.areNotificationsEnabled()
return notificationManager.areNotificationsEnabled() }
}
fun addPushDeviceUsingStoredToken() {
fun addPushDeviceUsingStoredToken() { if (refreshedToken.isNotBlank()) {
if (refreshedToken.isNotBlank()) { addRefreshToken()
addRefreshToken() } else {
} else { try {
try { FirebaseMessaging.getInstance().token.addOnCompleteListener {
FirebaseMessaging.getInstance().token.addOnCompleteListener { try {
try { refreshedToken = it.result
refreshedToken = it.result addRefreshToken()
addRefreshToken() } catch (_: IOException) {
} catch (_: IOException) { // catchy catch-catch
// catchy catch-catch } catch (_: Exception) {
} catch (_: Exception) { // catchy catch-catch-cat, I'm out of breath.
// catchy catch-catch-cat, I'm out of breath. }
} }
} } catch (_: Exception) {
} catch (_: Exception) { // catchy catch
// catchy catch }
} }
} }
}
private fun addRefreshToken() {
private fun addRefreshToken() { if (this.refreshedToken.isEmpty() || this.user == null || this.userHasPushDevice()) {
if (this.refreshedToken.isEmpty() || this.user == null || this.userHasPushDevice()) { return
return }
} val pushDeviceData = HashMap<String, String>()
val pushDeviceData = HashMap<String, String>() pushDeviceData["regId"] = this.refreshedToken
pushDeviceData["regId"] = this.refreshedToken pushDeviceData["type"] = "android"
pushDeviceData["type"] = "android" MainScope().launchCatching {
MainScope().launchCatching { apiClient.addPushDevice(pushDeviceData)
apiClient.addPushDevice(pushDeviceData) }
} }
}
suspend fun removePushDeviceUsingStoredToken() {
suspend fun removePushDeviceUsingStoredToken() { if (this.refreshedToken.isEmpty() || !userHasPushDevice()) {
if (this.refreshedToken.isEmpty() || !userHasPushDevice()) { return
return }
} apiClient.deletePushDevice(refreshedToken)
apiClient.deletePushDevice(refreshedToken) }
}
private fun userHasPushDevice(): Boolean {
private fun userHasPushDevice(): Boolean { for (pushDevice in this.user?.pushDevices ?: emptyList()) {
for (pushDevice in this.user?.pushDevices ?: emptyList()) { if (pushDevice.regId == this.refreshedToken) {
if (pushDevice.regId == this.refreshedToken) { return true
return true }
} }
} return this.user?.pushDevices == null
return this.user?.pushDevices == null }
}
private fun userIsSubscribedToNotificationType(type: String?): Boolean {
private fun userIsSubscribedToNotificationType(type: String?): Boolean { val key =
val key = when { 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"
type?.contains(RECEIVED_SUBSCRIPTION_GIFT_PUSH_NOTIFICATION_KEY) == true -> "preference_push_gifted_subscription" type?.contains(RECEIVED_SUBSCRIPTION_GIFT_PUSH_NOTIFICATION_KEY) == true -> "preference_push_gifted_subscription"
type?.contains(GUILD_INVITE_PUSH_NOTIFICATION_KEY) == true -> "preference_push_invited_to_guild" type?.contains(GUILD_INVITE_PUSH_NOTIFICATION_KEY) == true -> "preference_push_invited_to_guild"
type?.contains(QUEST_INVITE_PUSH_NOTIFICATION_KEY) == true -> "preference_push_invited_to_quest" type?.contains(QUEST_INVITE_PUSH_NOTIFICATION_KEY) == true -> "preference_push_invited_to_quest"
type?.contains(QUEST_BEGUN_PUSH_NOTIFICATION_KEY) == true -> "preference_push_your_quest_has_begun" type?.contains(QUEST_BEGUN_PUSH_NOTIFICATION_KEY) == true -> "preference_push_your_quest_has_begun"
type?.contains(WON_CHALLENGE_PUSH_NOTIFICATION_KEY) == true -> "preference_push_you_won_challenge" type?.contains(WON_CHALLENGE_PUSH_NOTIFICATION_KEY) == true -> "preference_push_you_won_challenge"
type?.contains(CONTENT_RELEASE_NOTIFICATION_KEY) == true -> "preference_push_content_release" type?.contains(CONTENT_RELEASE_NOTIFICATION_KEY) == true -> "preference_push_content_release"
else -> return true else -> return true
} }
return sharedPreferences.getBoolean(key, true) return sharedPreferences.getBoolean(key, true)
} }
companion object { companion object {
const val PARTY_INVITE_PUSH_NOTIFICATION_KEY = "invitedParty" const val PARTY_INVITE_PUSH_NOTIFICATION_KEY = "invitedParty"
const val RECEIVED_PRIVATE_MESSAGE_PUSH_NOTIFICATION_KEY = "newPM" const val RECEIVED_PRIVATE_MESSAGE_PUSH_NOTIFICATION_KEY = "newPM"
const val RECEIVED_GEMS_PUSH_NOTIFICATION_KEY = "giftedGems" const val RECEIVED_GEMS_PUSH_NOTIFICATION_KEY = "giftedGems"
const val RECEIVED_SUBSCRIPTION_GIFT_PUSH_NOTIFICATION_KEY = "giftedSubscription" const val RECEIVED_SUBSCRIPTION_GIFT_PUSH_NOTIFICATION_KEY = "giftedSubscription"
const val GUILD_INVITE_PUSH_NOTIFICATION_KEY = "invitedGuild" const val GUILD_INVITE_PUSH_NOTIFICATION_KEY = "invitedGuild"
const val QUEST_INVITE_PUSH_NOTIFICATION_KEY = "questInvitation" const val QUEST_INVITE_PUSH_NOTIFICATION_KEY = "questInvitation"
const val QUEST_BEGUN_PUSH_NOTIFICATION_KEY = "questStarted" const val QUEST_BEGUN_PUSH_NOTIFICATION_KEY = "questStarted"
const val WON_CHALLENGE_PUSH_NOTIFICATION_KEY = "wonChallenge" const val WON_CHALLENGE_PUSH_NOTIFICATION_KEY = "wonChallenge"
const val CHANGE_USERNAME_PUSH_NOTIFICATION_KEY = "changeUsername" const val CHANGE_USERNAME_PUSH_NOTIFICATION_KEY = "changeUsername"
const val GIFT_ONE_GET_ONE_PUSH_NOTIFICATION_KEY = "gift1get1" const val GIFT_ONE_GET_ONE_PUSH_NOTIFICATION_KEY = "gift1get1"
const val CHAT_MENTION_NOTIFICATION_KEY = "chatMention" const val CHAT_MENTION_NOTIFICATION_KEY = "chatMention"
const val GROUP_ACTIVITY_NOTIFICATION_KEY = "groupActivity" const val GROUP_ACTIVITY_NOTIFICATION_KEY = "groupActivity"
const val CONTENT_RELEASE_NOTIFICATION_KEY = "contentRelease" const val CONTENT_RELEASE_NOTIFICATION_KEY = "contentRelease"
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(
val remoteMessageIdentifier = remoteMessage.data["identifier"] remoteMessage: RemoteMessage,
context: Context,
if (pushNotificationManager?.userIsSubscribedToNotificationType(remoteMessageIdentifier) != false) { pushNotificationManager: PushNotificationManager? = null,
if (remoteMessage.data.containsKey("sendAnalytics")) { ) {
val additionalData = HashMap<String, Any>() val remoteMessageIdentifier = remoteMessage.data["identifier"]
additionalData["identifier"] = remoteMessageIdentifier ?: ""
Analytics.sendEvent( if (pushNotificationManager?.userIsSubscribedToNotificationType(remoteMessageIdentifier) != false) {
"receive notification", if (remoteMessage.data.containsKey("sendAnalytics")) {
EventCategory.BEHAVIOUR, val additionalData = HashMap<String, Any>()
HitType.EVENT, additionalData["identifier"] = remoteMessageIdentifier ?: ""
additionalData Analytics.sendEvent(
) "receive notification",
} EventCategory.BEHAVIOUR,
HitType.EVENT,
val notificationFactory = HabiticaLocalNotificationFactory() additionalData,
val localNotification = notificationFactory.build( )
remoteMessageIdentifier, }
context
) val notificationFactory = HabiticaLocalNotificationFactory()
localNotification.setExtras(remoteMessage.data) val localNotification =
val notification = remoteMessage.notification notificationFactory.build(
if (notification != null) { remoteMessageIdentifier,
localNotification.notifyLocally( context,
notification.title ?: remoteMessage.data["title"], )
notification.body ?: remoteMessage.data["body"], localNotification.setExtras(remoteMessage.data)
remoteMessage.data val notification = remoteMessage.notification
) if (notification != null) {
} else { localNotification.notifyLocally(
localNotification.notifyLocally( notification.title ?: remoteMessage.data["title"],
remoteMessage.data["title"], notification.body ?: remoteMessage.data["body"],
remoteMessage.data["body"], remoteMessage.data,
remoteMessage.data )
) } else {
} localNotification.notifyLocally(
} remoteMessage.data["title"],
} remoteMessage.data["body"],
} 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,41 +10,47 @@ 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 =
PendingIntent.FLAG_UPDATE_CURRENT + PendingIntent.FLAG_IMMUTABLE if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
} else { PendingIntent.FLAG_UPDATE_CURRENT + PendingIntent.FLAG_IMMUTABLE
PendingIntent.FLAG_UPDATE_CURRENT } else {
} PendingIntent.FLAG_UPDATE_CURRENT
val pendingIntentAccept = PendingIntent.getBroadcast( }
context, val pendingIntentAccept =
3001, PendingIntent.getBroadcast(
acceptInviteIntent, context,
flags 3001,
) acceptInviteIntent,
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 =
context, PendingIntent.getBroadcast(
2001, context,
rejectInviteIntent, 2001,
flags rejectInviteIntent,
) 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